Building an MDX Blog from Scratch with Next.js - How This Blog Was Made
How I built a blog that runs on nothing but markdown files, no CMS, using the Next.js 16 App Router. Folder structure, MDX setup, and SEO, walked through with the actual code.
TL;DR
Keep posts as .mdx files under src/content/posts and render them with @next/mdx - no CMS needed. The file is the URL, and a meta export replaces frontmatter for listings and metadata.
On this page
The blog you're reading right now has no database and no admin panel. Every post is just a single markdown file. Here's exactly how I built it.
Why file-based
There are plenty of blogging tools out there, but most are overkill for a solo developer.
- CMS: comes with an admin panel, integrations, and a bill
- Notion integration: convenient, but you hit walls with rendering, speed, and customization
- Markdown files: posts are version-controlled with the code, it's free, and
git pushis publishing
I picked the third. Least friction.
Folder structure
Boiled down to the essentials, it looks like this.
src/
├── app/
│ ├── page.tsx # 홈 - 글 목록
│ ├── posts/[slug]/page.tsx # 글 상세
│ └── sitemap.ts # /sitemap.xml
├── content/posts/*.mdx # ← 글은 여기에만 추가
└── lib/posts.ts # 목록·메타 로딩
The filename becomes the URL. hello.mdx opens at /posts/hello.
MDX setup
To use MDX with the App Router, you wire up @next/mdx. Next 16 uses Turbopack by default, which means remark/rehype plugins must be passed as string names (you can't pass function references).
const withMDX = createMDX({
options: {
remarkPlugins: ["remark-gfm"], // 표·취소선 지원
rehypePlugins: ["rehype-slug"], // 제목 앵커
},
})
meta export instead of frontmatter
You'd normally put the title and date in a --- block (frontmatter) at the top of a markdown file, but MDX can hold JavaScript, so there's a cleaner way: just export an object.
export const meta = {
title: "글 제목",
date: "2026-07-06",
category: "dev",
}
## 본문 시작
The listing page dynamically imports each file and reads only this meta.
const mod = await import(`@/content/posts/${slug}.mdx`)
return mod.meta // 본문 렌더 없이 메타만
No parsing library, no separate index.
SEO from day one
If the point of the blog is traffic, SEO belongs at the start, not the end. I baked in per-post metadata, Open Graph, JSON-LD structured data, plus sitemap.xml, robots.txt, and rss.xml from the beginning. I'll go deeper on that in the next post.
Wrapping up
- Each post is a single
.mdxfile, and the filename is the URL @next/mdxwith string plugins (Turbopack)export const metainstead of frontmatter- SEO from the first commit
With this same structure, you can have your own blog up in 30 minutes. I'm using it to keep building up stories about products like Daily Wave.
Frequently asked questions
Why MDX files instead of a CMS?
Posts are version-controlled alongside the code, it's free, and deploying is publishing - the least friction for a solo developer. You can also freely embed images, code, and components.
How do you build a post list without frontmatter?
Export a meta object at the top of each .mdx file, then dynamically import the file on the listing page and read just that meta. No separate parsing library needed.
Related posts
- 💻 Dev
AI Writes the Code Now. So Why Does Git Matter More Than Ever?
If AI writes all the code, do I still need to learn Git? The question has it backwards: you need Git precisely because AI writes the code. A solo developer who builds almost everything with AI explains four reasons to get comfortable with Git and GitHub.
- 💻 Dev
I Blocked Double Execution - Then One Crash Locked It Forever
A status guard meant to stop duplicate background jobs ended up trapping a job in 'generating' forever when the process died mid-deploy. Every lock needs a way out, shipped in the same commit.
- 💻 Dev
5 Apps in One Repo - A Solo Developer's Turborepo Monorepo
How to manage multiple apps and websites in a single repository with a Turborepo monorepo. Sharing common configs, saving builds with caching, and per-app deploys, from a hands-on perspective.