How I Built My Blog with MDX and Next.js
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:
// 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:
// 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:
// 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.