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 content
  • pages/ for routes
  • posts/ 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.

Building a blog with Next.js - Mostafa Waleed