Overview
My portfolio is not just a static page — it is a full CMS that I manage entirely from an admin panel. Projects, blog posts, and services are all stored in a PostgreSQL database (hosted on Supabase) and editable through a secure admin dashboard.
Image Hosting with Cloudinary
Every project thumbnail and blog cover image is stored on Cloudinary. I use the Cloudinary Node SDK in a Next.js API route to handle uploads:
import { v2 as cloudinary } from 'cloudinary';
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
const result = await cloudinary.uploader.upload(filePath, {
folder: 'portfolio/projects',
transformation: [{ width: 1200, height: 630, crop: 'fill' }],
});
The returned secure_url is then saved to the database and used in <Image> tags via Next.js's built-in image optimization.
Contact Emails with Nodemailer
Whenever someone submits the contact form, two emails fire:
- Admin notification — sent to my Gmail, containing the visitor's name, email, and message.
- Auto-reply — sent to the visitor confirming their message was received.
I use a Gmail App Password (16-character code) so I never expose my real password in environment variables.
Admin Dashboard
The admin area is protected by NextAuth.js with a custom credentials provider. Only users with role: "ADMIN" in the database can access /admin routes. Everything else redirects to the login page via middleware.
Deployment
The entire project — frontend, API routes, database connections — deploys as a single unit to Vercel. Environment variables are set in the Vercel dashboard, and every git push triggers an automatic preview deployment.
Takeaways
Building your own portfolio as a real product (not just a static page) teaches you more about full-stack development than any tutorial. Own your stack, own your data.