·5 min read·Next.jsMDXBuild in Public

Scaffolding an MDX Blog into a Next.js Portfolio

A meta-post on how this very blog was built. File by file, dependency by dependency, with infographics that animate inside the prose.

I wanted a blog. The kind that lives next to my portfolio, looks like the rest of the site, ships fast, and lets me drop a React component into the middle of a paragraph when prose isn't enough.

This post is meta. You're reading the very thing it describes. The file tree below is real. The numbers ticking up are real. Here's how it came together.

Why MDX won

MDX gives you Markdown for the boring parts and React for the fun parts. The post you're reading is one .mdx file, and the animations below are real components, not screenshots.

MDX = Markdown + JSX. Write prose like a normal person. When you need an animation, chart, or interactive demo, drop a <Component /> and keep writing.

The tradeoffs I weighed:

  • Headless CMS (Sanity, Hashnode). Nice GUI, but I'd have to fight to make posts feel native to my brand.
  • Notion as CMS. I already live in Notion, but the API is slow and the formatting is opinionated.
  • MDX in repo. Every post is a git commit, infra cost is zero, full styling control, React inside.

Free + fast + native = no contest.

The architecture

The whole thing is six files plus content. That's it.

project structure
vevaar-portfolio/
content/
blog/
scaffolding-mdx-blog.mdx← you are here
src/
app/blog/
page.tsx
[slug]/page.tsx
components/blog/
mdx-components.tsx
infographics/
FileTree.tsx
InstallSteps.tsx
StatCounter.tsx
Callout.tsx
lib/blog.ts

content/blog/ is plain MDX. No database, no admin panel, just text files. lib/blog.ts reads the directory and parses frontmatter. app/blog/ holds two routes: the index and the dynamic post page. components/blog/ holds the custom components I want available inside posts.

The four moves

01
Install
npm i next-mdx-remote gray-matter

Two deps. That's the whole pipeline.

02
Folder
content/blog/*.mdx

Posts live in git, not a database.

03
Helper
src/lib/blog.ts

Reads files, parses frontmatter, exports list.

04
Routes
/blog + /blog/[slug]

Index page + dynamic post page.

next-mdx-remote/rsc gives me a server-component-first MDX compiler. Works directly in async pages, no client bundle needed for the prose. gray-matter parses the frontmatter at the top of each file. No webpack config. No plugin chain. Two dependencies and we're done.

What it costs

0dependencies added
0$monthly infra
0 minempty folder to first post

Custom components inside prose

The juice of MDX is exactly this: when you need to show something instead of describing it, you reach for a component.

// inside any .mdx file, just use it:
<StatCounter value={42} label="some stat" />

The component lives in components/blog/infographics/. The .mdx file uses it by name, no imports needed, because the components are wired into the renderer:

import { compileMDX } from "next-mdx-remote/rsc";

const { content, frontmatter } = await compileMDX({
  source,
  components: { StatCounter, FileTree, InstallSteps, Callout },
  options: { parseFrontmatter: true },
});

That components map is where the magic is. Anything I put there is available inside every post. No per-post imports, no boilerplate.

Frontmatter as the source of truth

Every post starts with this:

---
title: "Scaffolding an MDX Blog..."
description: "A meta-post about..."
date: "2026-05-03"
readingTime: "5 min read"
tags: ["Next.js", "MDX"]
---

gray-matter parses it once on the server and exposes the metadata to three surfaces simultaneously.

One source, three surfaces. The same frontmatter renders the index card, the post header, and the <head> SEO tags. Change a field once, all three update. No drift.

What's next

A list of things I'll add as the blog grows up:

  • rehype-pretty-code for proper syntax highlighting (Shiki under the hood)
  • An RSS feed at /feed.xml
  • Reading-progress bar pinned to the top
  • Per-post OG image generation via @vercel/og
  • A /notes route for shorter, scrappier posts

If you're reading this and thinking "I should ship a blog", you can be at first-post in under an hour. Two dependencies, six files. Go.