← Back to Writing

Building This Site: A Modern Web Development Journey

8 min read
web-developmentnextjstypescripttailwindcloudflare

How I built my personal website using Next.js, TypeScript, and Tailwind CSS - from initial setup to production deployment on Cloudflare Pages.

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.tsx defines shared UI elements across pages
  • page.tsx files 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:

  1. Git-based workflow - content lives in the repository
  2. No database required - perfect for static sites
  3. Rich content - can embed React components in markdown
  4. 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:

  1. Reads all MDX files from the content directory
  2. Parses frontmatter (title, date, tags) using gray-matter
  3. Calculates reading time using the reading-time library
  4. Filters published posts and sorts by date
  5. 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:

  1. Global CDN - fast loading worldwide
  2. Automatic HTTPS - secure by default
  3. Branch previews - test changes before going live
  4. Edge computing - run code closer to users
  5. Free tier - generous limits for personal sites

The deployment process:

  1. Git integration - automatically deploys on push to main branch
  2. Build optimization - Cloudflare minifies and compresses assets
  3. 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:

  1. Search functionality - add blog post search
  2. Analytics - integrate privacy-friendly analytics
  3. Newsletter - add email subscription
  4. Comments - integrate with GitHub Discussions
  5. More content types - projects portfolio, case studies

Key Takeaways

Building this site taught me several important web development principles:

  1. Start simple - get a working version first, then optimize
  2. Choose technologies wisely - each tool should solve a specific problem
  3. Plan for scale - organize code to handle future features
  4. Performance matters - optimize for both developers and users
  5. 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.