This write-up covers the technical decisions behind my personal website. A breakdown of why I chose each piece of the stack, what tradeoffs I considered, and how the pieces fit together. The site serves two functions: a blog and a portfolio with a lightweight admin panel for managing content.
Architecture overview
The site follows a headless architecture pattern with clear separation between the content layer, the data layer, and the presentation layer.
Next.js on Vercel handles rendering and API routes
Contentful is the CMS for blog content
Supabase handles portfolio data, image storage, and authentication
Resend handles transactional email via Supabase Auth's custom SMTP configuration
GitHub + Claude Code power the development and CI/CD loop
Every commit to main triggers a Vercel build pipeline that tests, scans, and deploys automatically. Content updates in Contentful trigger on-demand revalidation via webhook, so blog posts go live without a full redeploy.
Next.js and the App Router
I chose Next.js 14 with the App Router for a few specific reasons.
The App Router's React Server Components model means I can fetch data at the component level on the server without shipping that logic to the client. For a portfolio site, most pages are read-heavy and don't need client-side state. Server Components let me keep the client bundle lean while still having interactive islands where I actually need them.
Rendering strategy varies by route:
Blog post pages use Incremental Static Regeneration (ISR) with on-demand revalidation. Pages are statically generated at build time and revalidated via a webhook when I publish in Contentful. This means fast load times without stale content.
Portfolio pages use dynamic rendering with server-side data fetching from Supabase, since that content updates more frequently.
Admin routes are fully dynamic and protected behind Supabase Auth middleware.
Next.js API routes also give me a lightweight backend without needing a separate server. Webhook handlers, revalidation triggers, and form submissions all live as route handlers inside the Next.js project.
Contentful as the content layer
The decision to use a headless CMS instead of storing posts as MDX files in the repo came down to one thing: I wanted to be able to publish without touching code.
MDX in the repo works, but it couples writing to the development workflow. You need a local environment to preview, a commit to publish, and a deploy to go live. That friction is fine for documentation but wrong for a blog.
With Contentful, I define a content model (title, slug, body in rich text, cover image, tags, publish date) and write in their editor. Vercel fetches from the Contentful Delivery API. When I publish a post, Contentful fires a webhook to a Next.js route handler that calls revalidatePath, and the page updates within seconds. No deploy, no terminal.
The other benefit is a clean separation of concerns. Contentful owns editorial content. Supabase owns structured application data. If I ever need to migrate CMSes, the rest of the stack is untouched.
Supabase as the data and auth layer
Supabase handles three distinct responsibilities.
Portfolio data. Projects, case studies, and work entries are stored in Postgres tables. Querying from Next.js Server Components using the Supabase client is straightforward, and because it's real Postgres, I can use joins, filtered queries, and views rather than being constrained by a document model. Row Level Security (RLS) policies ensure that public routes can only read published entries.
Image storage. Cover images and portfolio thumbnails are stored in Supabase Storage. It provides a CDN-backed object store with bucket-level access policies. Rather than managing an S3 bucket and CloudFront distribution separately, this gives me the same outcome with zero additional configuration.
Authentication. The admin panel uses Supabase Auth with a magic link flow. I'm the only user, so there's no need for a user management system. I enter my email, receive a time-limited link, and get a JWT-backed session. Supabase middleware in Next.js handles session validation on protected routes. No passwords to manage or rotate.
Resend for transactional email
Supabase Auth handles the magic link email flow, but by default it routes through Supabase's own SMTP, which is rate-limited and gives you no control over sender domain or deliverability.
The fix is straightforward: configure a custom SMTP provider in Supabase Auth settings. I use Resend, which is purpose-built for developer-facing transactional email. It takes about ten minutes to set up with a verified domain, and from that point the auth emails send reliably with proper SPF and DKIM alignment. The Resend dashboard also gives visibility into delivery status, which matters when the email in question is your only way into the admin panel.
CI/CD and the development workflow
The deployment pipeline is fully automated via GitHub and Vercel.
Every push to a feature branch creates a Vercel preview deployment with its own URL. Merging to main triggers a production build. Vercel runs the build, checks for type errors and lint issues, and deploys if everything passes. There's no manual step between committing code and a live update.
For development, I use Claude Code as the primary environment. On a solo project, the biggest productivity loss is context-switching: coming back to a codebase after a few days and spending time reconstructing what you were doing and why. Claude Code holds the full codebase context across sessions, which meaningfully cuts that ramp-up time. It also catches things that would normally require a second set of eyes, like inconsistencies across components or edge cases in data fetching logic.
Tradeoffs and what I would reconsider
No stack decision is free of tradeoffs.
Contentful adds a vendor dependency for content. If Contentful's pricing changes significantly, migrating content is a real effort. The mitigation is keeping the content model simple and making sure the Contentful-specific logic is isolated behind a data-fetching layer, not scattered across components.
Supabase for everything dynamic is convenient but means a single point of failure for data, storage, and auth. For a personal site, this is an acceptable tradeoff. At larger scale, I would consider separating these concerns further.
ISR is powerful, but the revalidation logic needs to be reliable. If the webhook fails, content can sit stale. I have a manual revalidation endpoint as a fallback, but a more robust setup would include retry logic on the Contentful webhook side.
