Build a Blog with Next.js #1: Getting Started and Design
We’re putting the Server Components / Server Actions from the Modern React + Next.js series into your hands with a real project. This series builds a personal blog from scratch — and an interesting twist is that the site you’re reading right now (schoolofweb.net) has nearly the same structure. A great topic for dogfooding-driven learning.
We’ll build it up gradually across 5 posts.
- #1 Getting Started and Design ← this post
- #2 Post list and detail page
- #3 Tags and search
- #4 Comments (Server Actions)
- #5 SEO and deployment (wrap-up)
Define the requirements #
First, write down what our blog needs to be able to do.
Core features
- Post list page (newest first)
- Post detail page (rendered Markdown body)
- Per-tag post list page
- Search (title/body)
- Comments per post
Tech decisions (pin these down up front to avoid wavering)
- Posts are written as MDX files (no DB, stored in the filesystem)
- Comments use an in-memory store (a real service would need a DB, but we’re simplifying for learning)
- Data fetching: Server Components read directly with fs
- Mutations: use Server Actions
- Deploy on Vercel
These choices anchor the entire series and keep you from drifting on questions like “where does this go?” or “how do I solve this?”.
Why MDX-file-based? #
For storing blog content, there are three main options:
- DB (PostgreSQL, SQLite, Supabase, etc.) — strong for multi-author, admin pages, dynamic post creation
- MDX files — Git workflow, simplicity, embedded JSX components
- External CMS (Contentful, Sanity, etc.) — strong for non-technical editor collaboration
This series picks MDX files. Reasons:
- Git is the backup — full history is preserved
- Edit locally — write in your favorite editor
- No DB setup — lower learning overhead
- Server Components shine naturally — code that reads files with
fs.readFileSyncjust works - Embed React components — use custom components like
<YouTube />or<Tip>inside posts
This site uses the same approach, and it’s how many personal tech blogs run.
Designing the folder structure #
Sketching the folder structure before coding helps you avoid getting stuck later.
my-blog/
├── posts/ ← where MDX posts live
│ ├── hello-world.mdx
│ ├── about-rsc.mdx
│ └── learning-react.mdx
├── src/
│ └── app/
│ ├── layout.js ← shared site layout (header/footer)
│ ├── page.js ← '/' post list (newest first)
│ ├── posts/
│ │ └── [slug]/
│ │ └── page.js ← '/posts/[slug]' post detail
│ ├── tags/
│ │ ├── page.js ← '/tags' tag list
│ │ └── [tag]/
│ │ └── page.js ← '/tags/[tag]' posts by tag
│ ├── search/
│ │ └── page.js ← '/search?q=...' search
│ └── lib/
│ └── posts.js ← MDX read/parse utility
├── public/
└── package.jsonEach route’s role:
| Route | Page |
|---|---|
/ | Latest posts |
/posts/[slug] | Post detail (body + comments) |
/tags | All tags |
/tags/[tag] | Posts with a specific tag |
/search?q=... | Search results |
Utility files like src/app/lib/posts.js collect shared logic such as reading MDX files — a common pattern for keeping page files from getting bloated.
Pick the post data shape #
Pinning down what each MDX file contains keeps the code clean.
---
title: "Hello, blog"
date: 2026-05-01
description: "First post."
tags: ["life", "announcement"]
published: true
---
## First section
This is the body, written in **Markdown**.
Lists work too:
- Item 1
- Item 2
- Item 3The ----delimited block on top is frontmatter (metadata); below is the body. Frontmatter is YAML, and our code parses it into a JavaScript object.
What each field means:
| Field | Type | Description |
|---|---|---|
title | string | Post title |
date | string (YYYY-MM-DD) | Publish date |
description | string | One-line summary (used in lists and meta) |
tags | string[] | Tag array |
draft | boolean | If true, excluded from lists (work in progress) |
You can add more fields like image or keywords later; let’s keep it minimal for now.
Slug #
The slug in /posts/[slug] is the post’s URL identifier. We’ll derive the slug from the filename.
posts/hello-world.mdx→/posts/hello-worldposts/about-rsc.mdx→/posts/about-rsc
A pleasingly simple rule — no separate ID assignment or URL mapping per post.
Start the project #
Let’s get into real code. Create a new Next.js project.
npx create-next-app@latest my-blog
cd my-blogChoose the same options as the previous series (App Router, JavaScript, src/ directory recommended).
Required dependencies #
Install libraries to parse and compile MDX.
npm install gray-matter next-mdx-remoteWhat each does:
gray-matter— splits frontmatter from the body in.mdxnext-mdx-remote— compiles the Markdown body into a React component
Optional packages worth adding:
remark-gfm— GitHub Flavored Markdown (tables, checkboxes, etc.)rehype-pretty-code— code block syntax highlighting
For this post we install only the two essentials and add more plugins in #2 when we compile the body.
Create the first post #
Make a my-blog/posts/ folder (outside Next.js — it’s data, not a route) and add the first post.
posts/hello-world.mdx:
---
title: "Hello, blog"
date: 2026-05-01
description: "My first blog post built with Next.js."
tags: ["announcement", "react"]
published: true
---
# Hello!
This post was written in **MDX**. A Next.js Server Component reads the file directly and renders it.
## Bold, italics, code
Most basic Markdown syntax works.
- List item 1
- List item 2
- List item 3
`Inline code` works too.
\```js
// Code blocks too
function hello() {
console.log("hello");
}
\```(Note: the closing \``` of the last code block is actually `````` in the real file — it’s escaped here for inline display.)
posts/learning-react.mdx:
---
title: "React learning notes"
date: 2026-05-10
description: "Key points I picked up while learning React."
tags: ["react", "study"]
published: true
---
Started studying React!
## Server Components
Default to Server Components, only use Client Components where needed.posts/draft-not-shown.mdx:
---
title: "Still drafting"
date: 2026-05-15
description: "This is a draft."
tags: ["memo"]
published: false
---
This post has draft: true and shouldn't appear in the list.All three follow the same structure. The post with draft: true will be excluded from the list we’ll build in #2.
MDX parsing utility — first pass #
In src/app/lib/posts.js we sketch the first version of the functions to read and parse MDX files (we’ll put them to real use in #2).
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const POSTS_DIR = path.join(process.cwd(), 'posts');
export function getAllSlugs() {
return fs.readdirSync(POSTS_DIR)
.filter(file => file.endsWith('.mdx'))
.map(file => file.replace(/\.mdx$/, ''));
}
export function getPostBySlug(slug) {
const fullPath = path.join(POSTS_DIR, `${slug}.mdx`);
if (!fs.existsSync(fullPath)) return null;
const fileContent = fs.readFileSync(fullPath, 'utf-8');
const { data, content } = matter(fileContent);
return {
slug,
frontmatter: data,
content,
};
}
export function getAllPosts() {
const slugs = getAllSlugs();
const posts = slugs
.map(slug => getPostBySlug(slug))
.filter(post => post && !post.frontmatter.draft);
return posts.sort((a, b) => a.frontmatter.date < b.frontmatter.date ? 1 : -1);
}What each function does:
getAllSlugs()— return slugs derived from.mdxfilenames inposts/getPostBySlug(slug)— read the file matching the slug and split frontmatter and bodygetAllPosts()— get all posts excluding drafts, sorted by publish date descending
These functions can only be called from Server Components (because they use fs). Trying to call them from a Client Component will fail the build — a built-in safety net.
This site’s app/lib/posts-util.ts is structured almost identically. We’re keeping ours simpler for learning, and we’ll extend it as needed.
Verify the setup #
There are no pages yet, so spinning up the dev server won’t show much. Still, it’s worth confirming everything runs.
npm run devIf http://localhost:3000 shows the default Next.js screen, you’re good. The next post starts building real pages.
Wrap-up #
This post laid the foundation for the build series.
- Wrote down requirements clearly (list / detail / tags / search / comments)
- Decided how to store data (MDX files)
- Sketched folder structure and routing
- Defined the post frontmatter
- Created the first MDX posts
- Drafted the first version of the
posts.jsutility
Real screen work begins next. In “Build a Blog with Next.js #2 Post list and detail page,” we’ll use the getAllPosts we wrote to render the home list, then move on to compiling MDX bodies in the /posts/[slug] dynamic route.