Building This Site: A Modern Web Development Journey
Building a personal website is one of the best ways to learn modern web development. In this post, I'll walk you through exactly how I built this site, explaining each technology choice and the reasoning behind it.
The Foundation: Why These Technologies?
Next.js 14 with App Router
I chose Next.js because it's the most popular React framework, offering:
- Server-Side Rendering (SSR) and Static Site Generation (SSG) out of the box
- App Router - the new file-based routing system that's more intuitive
- Built-in optimization for images, fonts, and JavaScript bundles
- Great developer experience with hot reloading and TypeScript support
The App Router (vs the older Pages Router) uses a app/ directory where:
layout.tsxdefines shared UI elements across pagespage.tsxfiles automatically become routes- Folders create nested routes (like
app/writing/page.tsx→/writing)
TypeScript for Type Safety
TypeScript adds static type checking to JavaScript, which means:
interface PostFrontmatter {
title: string
summary: string
date: string
tags: string[]
published: boolean
}
This prevents bugs by catching errors at compile time rather than runtime. For example, if I try to access post.titel (misspelled), TypeScript will immediately flag this error.
Tailwind CSS for Styling
Tailwind CSS is a utility-first CSS framework that lets you style directly in your HTML:
<div className="max-w-3xl mx-auto px-4 py-8">
<h1 className="text-3xl font-semibold text-neutral-900 dark:text-neutral-100">
My Title
</h1>
</div>
Benefits:
- No CSS files to manage - styles are co-located with components
- Consistent design system - predefined spacing, colors, and typography
- Dark mode support built-in with
dark:prefix - Responsive design with
sm:,md:,lg:prefixes
Project Structure and Architecture
Here's how I organized the codebase:
├── app/ # Next.js App Router pages
│ ├── layout.tsx # Root layout with header/footer
│ ├── page.tsx # Homepage
│ └── writing/ # Blog section
│ ├── page.tsx # Blog index
│ └── [slug]/ # Dynamic blog post pages
├── components/ # Reusable UI components
├── content/ # Blog posts and data
│ └── writing/ # MDX blog posts
├── lib/ # Utility functions
│ ├── content.ts # Content loading logic
│ └── mdx.tsx # MDX processing
└── public/ # Static assets
This separation of concerns makes the code:
- Easier to maintain - each file has a single responsibility
- Reusable - components can be used across multiple pages
- Scalable - easy to add new sections or features
The Content Management System
Why MDX Over a Traditional CMS?
I chose MDX (Markdown + JSX) for blog posts because:
- Git-based workflow - content lives in the repository
- No database required - perfect for static sites
- Rich content - can embed React components in markdown
- Developer-friendly - write in your favorite editor
Content Loading with Gray Matter
Instead of using Contentlayer (which has Next.js 14 compatibility issues), I built a custom content system:
export function getAllPosts(): Post[] {
const writingDir = path.join(contentDirectory, 'writing')
const filenames = fs.readdirSync(writingDir)
const posts = filenames
.filter((name) => name.endsWith('.mdx'))
.map((name) => {
const slug = name.replace(/\.mdx$/, '')
const fullPath = path.join(writingDir, name)
const fileContents = fs.readFileSync(fullPath, 'utf8')
const { data, content } = matter(fileContents)
return {
slug,
frontmatter: data as PostFrontmatter,
content,
readingTime: readingTime(content).text,
}
})
.filter((post) => post.frontmatter.published)
.sort((a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime())
return posts
}
This function:
- Reads all MDX files from the content directory
- Parses frontmatter (title, date, tags) using gray-matter
- Calculates reading time using the reading-time library
- Filters published posts and sorts by date
- Returns typed data that's safe to use throughout the app
MDX Processing Pipeline
For rich content rendering, I set up an MDX processing pipeline:
export const mdxOptions = {
mdxOptions: {
remarkPlugins: [remarkGfm], // GitHub Flavored Markdown
rehypePlugins: [rehypeSlug], // Auto-generate heading IDs
},
}
- remarkGfm adds support for tables, strikethrough, task lists
- rehypeSlug automatically generates IDs for headings (for table of contents)
- next-mdx-remote renders the processed MDX on the client
Component Architecture
Layout System
The root layout (app/layout.tsx) wraps all pages:
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1 container max-w-3xl mx-auto px-4 py-8">
{children}
</main>
<Footer />
</div>
</ThemeProvider>
</body>
</html>
)
}
Key concepts:
- Flexbox layout ensures footer stays at bottom
- Container with max-width creates readable line lengths
- ThemeProvider enables dark mode functionality
- suppressHydrationWarning prevents theme-related hydration mismatches
Dark Mode Implementation
Using next-themes for dark mode:
'use client'
import { useTheme } from 'next-themes'
export function ThemeToggle() {
const { setTheme, theme } = useTheme()
return (
<button
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
className="p-2 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800"
>
{/* Theme icons */}
</button>
)
}
Benefits of this approach:
- System preference detection - respects user's OS theme
- Persistent choice - remembers user preference in localStorage
- No flash of unstyled content - prevents theme flickering on page load
Dynamic Routing and Static Generation
Blog Post Pages
The [slug] directory creates dynamic routes:
// app/writing/[slug]/page.tsx
export async function generateStaticParams() {
const posts = getAllPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
export default function PostPage({ params }: { params: { slug: string } }) {
const post = getPostBySlug(params.slug)
if (!post || !post.frontmatter.published) {
notFound()
}
return (
<article className="prose prose-neutral dark:prose-invert max-w-none">
<MDXContent content={post.content} />
</article>
)
}
generateStaticParams tells Next.js which pages to pre-build at build time. This means:
- Faster page loads - HTML is pre-generated
- Better SEO - search engines can crawl static HTML
- Reduced server load - no server rendering needed
Production Optimization and Deployment
Next.js Configuration for Performance
// next.config.mjs
const nextConfig = {
output: 'export', // Generate static files
trailingSlash: true, // Consistent URLs
images: { unoptimized: true }, // For static hosting
compiler: {
removeConsole: process.env.NODE_ENV === 'production', // Remove console.logs
},
swcMinify: true, // Fast JavaScript minification
}
Each setting has a purpose:
- output: 'export' generates a static site that can be hosted anywhere
- trailingSlash: true ensures consistent URLs for better SEO
- removeConsole keeps production builds clean
- swcMinify uses Rust-based SWC for faster builds
Cloudflare Pages Deployment
I chose Cloudflare Pages for hosting because:
- Global CDN - fast loading worldwide
- Automatic HTTPS - secure by default
- Branch previews - test changes before going live
- Edge computing - run code closer to users
- Free tier - generous limits for personal sites
The deployment process:
- Git integration - automatically deploys on push to main branch
- Build optimization - Cloudflare minifies and compresses assets
- Cache headers - static assets cached for optimal performance
Performance Optimizations
The final site achieves excellent performance through:
- Static generation - all pages pre-built as HTML
- Tree shaking - unused code automatically removed
- Code splitting - JavaScript loaded on-demand
- Optimal caching - static assets cached for 1 year
- Gzip compression - reduced file sizes
SEO and Accessibility
Metadata Generation
Each page generates appropriate metadata:
export async function generateMetadata({ params }: PostPageProps) {
const post = getPostBySlug(params.slug)
return {
title: post.frontmatter.title,
description: post.frontmatter.summary,
}
}
This creates proper <title> and <meta> tags for each blog post, improving search engine visibility.
Semantic HTML and Accessibility
The site uses semantic HTML elements:
<article>for blog posts<nav>for navigation<main>for primary content- Proper heading hierarchy (
h1,h2,h3)
This helps screen readers and search engines understand the content structure.
Development Workflow
Tools for Code Quality
- ESLint - catches common JavaScript errors
- Prettier - automatically formats code consistently
- TypeScript - prevents type-related bugs
- GitHub Actions - automatically tests and deploys
Bundle Analysis
The @next/bundle-analyzer helps optimize performance:
npm run build:analyze
This shows which parts of your JavaScript bundle are largest, helping identify optimization opportunities.
What's Next?
This foundation supports future enhancements:
- Search functionality - add blog post search
- Analytics - integrate privacy-friendly analytics
- Newsletter - add email subscription
- Comments - integrate with GitHub Discussions
- More content types - projects portfolio, case studies
Key Takeaways
Building this site taught me several important web development principles:
- Start simple - get a working version first, then optimize
- Choose technologies wisely - each tool should solve a specific problem
- Plan for scale - organize code to handle future features
- Performance matters - optimize for both developers and users
- Documentation is crucial - document decisions for future reference
The combination of Next.js, TypeScript, Tailwind CSS, and Cloudflare Pages creates a modern, fast, and maintainable website that's a joy to work with and performs excellently for users.
Whether you're building your first website or your hundredth, these technologies and patterns provide a solid foundation for any web project.