Return to notes
2025.12.15

How I Built My Blog with MDX and Next.js

nextjsmdxreactweb
DATE
EST. READ
2 min read

Why MDX?

When I set out to add a blog to my portfolio, I had a few non-negotiables:

  • Write in Markdown — I want to focus on content, not HTML
  • Embed React components — Interactive demos, custom callouts, and styled code blocks
  • Static generation — Every post should be pre-rendered at build time for maximum performance
  • Syntax highlighting — Beautiful, accurate code blocks with zero client-side JavaScript

MDX checks every box. It's Markdown with superpowers — you write prose in .mdx files and sprinkle in React components wherever you need them.

The Stack

Here's what powers the blog you're reading right now:

typescript
// The core dependencies
const stack = {
  framework: "Next.js 15 (App Router)",
  content: "MDX via next-mdx-remote",
  frontmatter: "gray-matter",
  highlighting: "rehype-pretty-code + shiki",
  styling: "Tailwind CSS v4",
  animations: "Framer Motion",
};

Content Loading

All posts live in a content/blog/ directory as .mdx files. A utility module reads them at build time:

typescript
// src/lib/posts.ts
import fs from "fs";
import path from "path";
import matter from "gray-matter";
import readingTime from "reading-time";
 
const POSTS_DIR = path.join(process.cwd(), "content", "blog");
 
export function getAllPosts(): PostMeta[] {
  return fs
    .readdirSync(POSTS_DIR)
    .filter((f) => f.endsWith(".mdx"))
    .map((filename) => {
      const raw = fs.readFileSync(
        path.join(POSTS_DIR, filename),
        "utf-8"
      );
      const { data, content } = matter(raw);
      return {
        slug: filename.replace(/\.mdx$/, ""),
        title: data.title,
        excerpt: data.excerpt,
        date: data.date,
        tags: data.tags ?? [],
        readingTime: readingTime(content).text,
      };
    })
    .sort(
      (a, b) =>
        new Date(b.date).getTime() - new Date(a.date).getTime()
    );
}

The beauty of this approach is simplicity — no database, no CMS, no API calls. Just files on disk, parsed at build time.

Syntax Highlighting

I'm using rehype-pretty-code powered by shiki for syntax highlighting. It runs at build time, so there's zero client-side JavaScript for code blocks:

typescript
// src/lib/mdx.ts
const { content } = await compileMDX({
  source,
  options: {
    mdxOptions: {
      rehypePlugins: [
        [rehypePrettyCode, { theme: "github-dark-default" }],
      ],
    },
  },
});

The result is pre-rendered HTML with inline styles — fast, accessible, and beautiful out of the box.

What's Next

I plan to add:

  • View count tracking with a lightweight analytics setup
  • Series support for multi-part posts
  • RSS feed and sitemap generation (already done!)

If you're building your own blog, I'd highly recommend this stack. It's simple, fast, and gives you full control over the reading experience.

© 2026 Prince