Build a Blog with Next.js #1: Getting Started and Design

7 min read

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:

  1. DB (PostgreSQL, SQLite, Supabase, etc.) — strong for multi-author, admin pages, dynamic post creation
  2. MDX files — Git workflow, simplicity, embedded JSX components
  3. 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.readFileSync just 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.

project structure
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.json

Each route’s role:

RoutePage
/Latest posts
/posts/[slug]Post detail (body + comments)
/tagsAll 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.

posts/hello-world.mdx example
---
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 3

The ----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:

FieldTypeDescription
titlestringPost title
datestring (YYYY-MM-DD)Publish date
descriptionstringOne-line summary (used in lists and meta)
tagsstring[]Tag array
draftbooleanIf 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-world
  • posts/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.

create the project
npx create-next-app@latest my-blog
cd my-blog

Choose the same options as the previous series (App Router, JavaScript, src/ directory recommended).

Required dependencies #

Install libraries to parse and compile MDX.

MDX dependencies
npm install gray-matter next-mdx-remote

What each does:

  • gray-matter — splits frontmatter from the body in .mdx
  • next-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:

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 .mdx filenames in posts/
  • getPostBySlug(slug) — read the file matching the slug and split frontmatter and body
  • getAllPosts() — 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 dev

If 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.js utility

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.

X