Series Background: This is a follow-up to Shipping a Developer Portfolio: Next.js, Tailwind v4 & shadcn/ui, where I built a minimal portfolio with Next.js 15, TypeScript, and Tailwind v4 in a weekend. This series documents the production-hardening work I did after launch.
After shipping my portfolio, I realized that "working" and "production-ready" are two very different things. Over the next few weeks, I systematically addressed security vulnerabilities, performance bottlenecks, and developer experience gaps.
This series documents everything I learned along the way.
Content Security Policy (CSP) Implementation
The site had zero CSP headers, making it vulnerable to XSS attacks and clickjacking. I implemented a defense-in-depth approach using Next.js middleware and Vercel configuration.
What I Built:
src/middleware.ts) for HTML routesvercel.json fallbackKey Learnings:
Technical Highlights:
// Generate unique nonce per request
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
// Build strict CSP
const csp = `
default-src 'self';
script-src 'self' 'nonce-${nonce}' https://va.vercel-scripts.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
frame-ancestors 'none';
base-uri 'self';
Results:
frame-ancestors 'none'Rate Limiting Implementation
The contact form API endpoint had no rate limiting, making it vulnerable to spam and abuse. I implemented a zero-dependency, in-memory rate limiter.
What I Built:
src/lib/rate-limit.ts (146 lines)X-Forwarded-For)X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset)scripts/test-rate-limit.mjs)Key Learnings:
Technical Highlights:
// Rate limiter with automatic cleanup
export class RateLimiter {
private requests = new Map<string, RequestRecord>();
async check(identifier: string): Promise<RateLimitResult> {
const
Results:
npm run test:rate-limitINP (Interaction to Next Paint) Improvements
Google's Core Web Vitals showed poor INP scores (664ms+) on navigation links. The culprit: Next.js hover prefetching blocking the main thread.
What I Fixed:
<Link> components (prefetch={false})will-change-auto, contain: layout style)transform: translateZ(0))useTransitionKey Learnings:
useTransition keeps UI responsive during state updatesTechnical Highlights:
// Non-blocking theme toggle
export function ThemeToggle() {
const [isPending, startTransition] = useTransition();
const { setTheme, theme } = useTheme();
const toggleTheme = () => {
startTransition(() => {
Results:
GitHub Contributions Heatmap
I added a live GitHub contributions heatmap to the homepage to showcase activity.
What I Built:
/api/github-contributions)react-calendar-heatmapGITHUB_TOKEN env var for higher rate limitsKey Learnings:
Technical Highlights:
// Fetch with caching
const fetchContributions = async (): Promise<ContributionResponse> => {
// Check cache first
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const data = JSON.parse(cached);
Results:
AI Contributor Guide
I created comprehensive instructions for AI coding assistants (GitHub Copilot, Cursor, etc.) to maintain code quality and architectural consistency.
What I Built:
.github/copilot-instructions.mdKey Learnings:
Results:
Future improvements on my radar:
Taking a project from "works on my machine" to "production-ready" requires intentional hardening. Security, performance, and developer experience all need attention.
The good news? Modern frameworks like Next.js make it easier than ever. Middleware for CSP, server actions for rate limiting, and React's concurrent features for performance—the tools are there.
Now it's your turn. What's your "weekend project" that needs production hardening?