Building My Portfolio with Next.js 14
Building My Portfolio with Next.js 14
When I decided to rebuild my portfolio, I wanted to use the latest technologies while keeping things simple and maintainable. Here's a detailed look at how I built it.
Tech Stack Overview
The portfolio is built with:
- Next.js 14 with App Router for server components and routing
- TypeScript for type safety
- CSS Modules for scoped styling
- MDX for this blog you're reading right now
- PostHog for analytics and event tracking
- Upstash Redis for article likes and view counts
Project Structure
Here's how I organized the codebase:
src/
โโโ app/ # Next.js App Router pages
โ โโโ api/ # API routes (likes, views)
โโโ components/ # Reusable UI components
โโโ content/ # MDX blog posts
โโโ images/ # Static images
โโโ lib/ # Utility functions (blog, redis)
Each component follows a consistent pattern:
// ComponentName/
// โโโ ComponentName.tsx
// โโโ ComponentName.module.css
// โโโ index.ts
import styles from "./ComponentName.module.css";
interface Props {
children: React.ReactNode;
variant?: "primary" | "secondary";
}
export function ComponentName({ children, variant = "primary" }: Props) {
return (
<div className={styles[variant]}>
{children}
</div>
);
}Design System
I created a simple design system using CSS custom properties:
:root {
/* Neutral colors */
--N0: #ffffff;
--N50: #f8f9fa;
--N100: #e9ecef;
--N900: #212529;
/* Accent colors */
--B300: #3b82f6; /* Blue */
--G300: #22c55e; /* Green */
--R300: #ff5630; /* Red - for likes */
--Y300: #eab308; /* Yellow */
}Adding the Blog
The blog feature uses MDX with next-mdx-remote. Here's the setup:
// lib/blog.ts
import matter from "gray-matter";
import readingTime from "reading-time";
export function getPostBySlug(slug: string) {
const filePath = path.join(BLOG_DIR, `${slug}.mdx`);
const fileContents = fs.readFileSync(filePath, "utf8");
const { data, content } = matter(fileContents);
return {
...data,
content,
readingTime: readingTime(content).text,
};
}Custom MDX Components
I created custom components specifically for the blog:
- ArticleImage โ Enhanced images with zoom and fullwidth support
- Callout โ Highlighted tip/info/warning boxes
- CodeBlock โ Syntax-highlighted code with copy button
- LinkCard โ Rich link previews
- Mention โ Social media mentions with platform icons
- LikeButton โ Interactive like button with PostHog tracking
- ViewCounter โ Page view counter
Article Engagement Features
Each article includes a footer with engagement features:
Like Button
The like button uses Upstash Redis for persistent storage and PostHog for analytics:
// LikeButton.tsx
const handleLike = async () => {
if (hasLiked) return;
setHasLiked(true);
localStorage.setItem(`liked:${slug}`, 'true');
// Track in PostHog
posthog.capture('article_liked', { slug });
// Persist to Upstash
await fetch('/api/likes', {
method: 'POST',
body: JSON.stringify({ slug }),
});
};View Counter
Views are tracked automatically when users visit an article:
// ViewCounter.tsx
useEffect(() => {
const hasViewed = sessionStorage.getItem(`viewed:${slug}`);
if (!hasViewed) {
fetch('/api/views', {
method: 'POST',
body: JSON.stringify({ slug }),
}).then(() => {
sessionStorage.setItem(`viewed:${slug}`, 'true');
});
}
}, [slug]);Analytics with PostHog
PostHog provides insight into how users interact with the site:
// providers.tsx
'use client';
import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react';
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
});
}
export function PHProvider({ children }: { children: React.ReactNode }) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}Upstash Redis Integration
For serverless-friendly data persistence, I use Upstash Redis:
// lib/redis.ts
import { Redis } from '@upstash/redis';
export const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});The API routes are simple and efficient:
// app/api/likes/route.ts
export async function POST(request: NextRequest) {
const { slug } = await request.json();
const likes = await redis.incr(`likes:${slug}`);
return NextResponse.json({ likes });
}Performance Optimizations
Here are some optimizations I implemented:
- Static Generation โ All pages are pre-rendered at build time
- Image Optimization โ Using Next.js Image with blur placeholders
- Font Optimization โ Using the Geist font with
next/font - Code Splitting โ Components are loaded only when needed
- Optimistic UI โ Like and view counts update immediately
Useful Resources
If you're building your own portfolio, check out these resources:
Conclusion
Building this portfolio was a great learning experience. The combination of Next.js 14, TypeScript, CSS Modules, PostHog, and Upstash provides a solid foundation that's easy to maintain and extend.
The engagement features (likes and views) add a nice interactive element while providing valuable insights into which content resonates with readers.
If you're interested in the source code, it's available on @a1exalexander.