Building a blog with Next.js
- react
- nextjs
- typescript
I’ve been building with React for years now, and somewhere along that journey, I stumbled into the wonderful chaos of static blogs, SEO, and content workflows. I used to think that making a high-performance blog meant juggling endless build tools, patching together plugins, and writing more config than content. Then I met Next.js.
It wasn’t love at first sight — I came from the land of create-react-app, and server-side rendering sounded like black magic. But the more I used it, the more I realized how much complexity it quietly handled for me.
Fast-forward to today: if you asked me how to build a fast, SEO-friendly blog without drowning in boilerplate, I’d tell you — without hesitation — use Next.js.
Why Next.js for blogs just works
SEO has always been a bit of a frenemy to developers. We know it’s important, but we’d rather be building features than fiddling with meta tags. Next.js makes SEO-friendly static generation (SSG) painless. You write content in MDX, sprinkle in your React components, and ship a site that’s production-ready before your coffee gets cold.
That’s the experience I wanted to share in this post: how I built a static-generated, production-ready blog with Next.js, MDX, and a handful of plugins.
Spinning up the project
It started with one command:
npx create-next-app@latest site --typescript
From there, I laid out the folder structure the way I like to think about content and code:
site
├─ lib
│ ├─ mdx.ts
│ └─ types.ts
├─ pages
│ ├─ blog
│ │ └─ [slug].tsx
│ ├─ _app.tsx
│ ├─ _document.tsx
│ └─ blog.tsx
└─ posts
└─ 🎉.mdx
If you’ve been around static site generators before, you’ll recognize the pattern:
lib/
for logic that transforms contentpages/
for routesposts/
for… well, the actual posts.
The magic sauce: MDX
For me, MDX is like Markdown but on steroids — it lets me drop React components right into my writing. To make it work with syntax highlighting, heading anchors, and other niceties, I wired up a small helper in lib/mdx.ts
:
import { serialize } from 'next-mdx-remote/serialize';
import remarkGfm from 'remark-gfm';
import rehypeSlug from 'rehype-slug';
import rehypeCodeTitles from 'rehype-code-titles';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypePrism from 'rehype-prism-plus';
export async function mdxToHtml(source: string) {
const mdxSource = await serialize(source, {
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
rehypeSlug,
rehypeCodeTitles,
rehypePrism,
[
rehypeAutolinkHeadings,
{
properties: { className: ['anchor'] }
}
]
],
format: 'mdx'
}
});
return { html: mdxSource };
}
I still remember the first time I saw my headings turn into clickable links automatically. That’s rehypeSlug
+ rehypeAutolinkHeadings
working their quiet magic.
Defining types early (thank me later)
In lib/types.ts
, I wrote my TypeScript interfaces so my editor could stop nagging me later:
import { MDXRemoteSerializeResult } from 'next-mdx-remote';
export interface PostPageType {
slug: string;
frontmatter: {
title: string;
description: string;
date: string;
};
content: MDXRemoteSerializeResult;
}
export interface BlogPostType {
[key: string]: Array<PostPageType>;
}
The first post
I wrote my very first .mdx
file like this:
---
title: party
description: We'll celebrate now.
date: 2022-08-31
---
# 🎉 Party Time
Because why not.
Nothing fancy — but it was enough to wire up the reading and rendering logic.
Listing posts
In pages/blog.tsx
, I used gray-matter
to pull the front matter from each post and list them:
export async function getStaticProps() {
const files = readdirSync(join(process.cwd(), 'posts'));
const posts = files.map((filename) => {
const slug = filename.replace('.mdx', '');
const markdownWithMeta = readFileSync(
join(process.cwd(), 'posts', filename),
'utf-8'
);
const { data: frontmatter } = matter(markdownWithMeta);
return { slug, frontmatter };
});
return { props: { posts } };
}
When I first saw my blog list render, I felt like I had just unlocked the “blogger achievement” in developer life.
Rendering a single post
Finally, pages/blog/[slug].tsx
tied it all together. getStaticPaths
maps out every post route, getStaticProps
loads the MDX, and MDXRemote
renders it.
That was the moment it hit me: I now had a fully static, blazing-fast, SEO-friendly blog — and I didn’t fight with webpack once.
Final thoughts
If there’s one takeaway from this build, it’s that Next.js removes the mental tax from shipping a blog. You focus on the content, sprinkle in a few plugins for nice-to-haves, and you’re done.
And the best part? You can always grow it — add tags, RSS feeds, or even drop in live components. But the foundation stays the same: simple, fast, and production-ready.