Building a blog in Next.js

Journey into building a modern blog with Next.js and MDX. Explore how code, content, and creativity come together to craft a dynamic, developer-friendly publishing platform.

Blog Post Image

Setting goals

In many years of software development there is one simple truth that has become critical to getting the best outcomes when writing code, writing a paper or just succeeding in life in general: be clear on what your goals are. When I was younger I can remember teachers in high school encouraging goal setting as a way of focusing on the right things to achieve your objectives - it never stuck at the time. I was young and didn't listen very well - my wife would say not much has changed!

Throughout my technology career however this has become a critical part of my process, and no less so than for building a blog. Aside from ensuring that I don't wander off down too many rabbitholes, its also useful to keep your goals front of mind so you can check your progress and know when you're "done".

When coming up with a set of goals for my site, I had a key question in my head...

What do I need to have a simple, solid web platform to produce articles that people might like to read?

  1. A highly performant, mostly statically rendered site.
    There are future ideas I have that will require some server-side rendering (edge would be even better), so I needed to also ensure I kept options like SSR open.
  2. A simple, markdown approach to writing blog posts.
    Writing content in React/HTML is fine, but I know that I would get frustrated with this over time and probably spend more time building React components than writing articles. I want to keep the writing as simple as possible without sacrificing look and feel.
  3. Modern, sleek styling, with the option for dark mode if I want.
    Dark mode is well supported across the web these days. I will probably start simple with a single theme with no specific dark option, but lets keep the possibilities open for the future.
  4. Cheap, simple hosting.
    No fancy hosting and as cheap as possible. I'm not sure whether I'll monetise in the future to offset any hosting costs but for now it must be free or as cheap as possible.
  5. A minimum number of posts already written.
    That number I've since decided is four based on my current layout.

This I feel is a good start, however I have since added the following to my list as secondary, but minimum required to launch goals:

  1. Incremental rendering.
    Building a performant site is one thing, but ensuring that as I write and publish posts the whole site isn't being redeployed and refetched will improve the experience for readers.
  2. Search Engine Optimisation (SEO)
    While I'm not neccessarily worried about being top of the list, I want to at least be SEO-friendly.
  3. Analytics and observability.
    I want to know what is and isn't working on my site.

Choosing the right framework

React has dominated the web framework space for a number of years now and it is still an obvious choice when commencing a web build. Before React, there was Angular (v1 🤮), KnockoutJS (which isn't a complete framework itself) and others. Static websites have been around forever, ever since the first websites written in raw HTML but these day you want a framework to wrestle that beast, the engineer in me always wants to avoid duplication and a good framework will let you build rapidly with hopefully a high amount of reuse.

Post the ASP.NET days with KnockoutJS/SammyJS/custom, React became a mainstay, especially when we were chasing the dream of JAMstack and a big simplication in the channel build. ASP.NET was heavy. Aside from the quirks of cshtml, web engineers just weren't cut out for writing in both javscript/typescript AND dotnet. While a few of us did well, most - I'd say the majority - didn't. You were either good at one or the other. React was beautiful because early on we wrote javascript node APIs (backends for frontends - BFFs) in the same codebase and as long as you remembered the browser and node had important differences we got the best of engineers sticking to mostly one platform AND rapid development.

At some point however we needed static compilation and I discovered Gatsby. It solved for static compilation and a community sprung up around it rapidly producing plugins that mostly solved all the big problems for you. Need to pull in dynamic content from Contentful? There is a plugin for that. Need search-engine optimisation (SEO)? There is a plugin for that. You get the story. But where are we at now?

Interestingly Next.js was incubating right around the same time as Gatsby, however I don't even remember coming across it when I was searching in 2017 - it had some support for server-side rendering (SSR) but no static site generation (SSG) so its no real surprise it never came up. Since then however it has become the defacto-standard for React build anywhere one needs any amount of SSG and/or SSR. It is now a rich unified React platform with a whole bunch of goodies:

  1. All the best of React layouts and component semantics
  2. Automatic code splitting
  3. Server-Side Rendering (SSR) & Static Site Generation (SSG)
  4. Incremental Static Regeneration (ISR)
  5. Client-Side Rendering
  6. Edge runtime (and route handlers)
  7. ...and more

Have I sold you yet? Gatsby probably remains a notable option in the React universe, and outside you'll see people building with Astro or SvelteKit, but you'll find everything you need in Next, and if you can't find something, you can roll your own pretty quickly. For a brief moment I did consider Docusaurus for the simplicity, I would probably have had an easier time with MDX if I did, however Next is familiar and hasn't let me down so far.

So for better or for worse...

Project setup & structure

Setting up Next, like React is very easy. Assuming you know your way around some basic typescript (you could go javascript but I highly recommend the type safety of typescript), there are a few prerequisites to get started:

  1. nvm to install nodejs.
  2. corepack to manage the package manager.
  3. pnpm as our preferred package manager.
  4. VSCode as a basic IDE.

Installing nvm is the only manual part of this whole process. Follow the instructions at nvm based on your platform. Once you have installed nvm.. you'll want to install the latest LTS version of node...

nvm install --lts

You will then want to enable corepack and install pnpm...

corepack enable
corepack install -g pnpm

We can then create our Next.js application.

pnpm create next-app@latest

Posts in MDX

Because I want to create posts in markdown, I need a way of rendering markdown to html. MDX provides a way of writing markdown in JSX-based projects so that you can seamlessly integrate them into your site. It also provides some interesting extra benefits such as:

  1. Blending markdown and JSX so that you can enrich markdown with JSX components.
  2. Treat .mdx files as React components.
  3. Styling customisation such as the code rendering customisation you see on my site that uses Shiki for syntax highlighting.
  4. Extensibility via remark and rehype processors.

An example of mdx looks like this...

# Hello

<MyComponent id="123" />

You can also use objects with components, such as the `thisOne` component on
the `myComponents` object: <myComponents.thisOne />

<Component
  open
  x={1}
  label={"this is a string, *not* markdown!"}
  icon={<Icon />}
/>

{/* From https://mdxjs.com/docs/what-is-mdx/#mdx-syntax */}

See MDX to render markdown in Next.js for a technical deep dive on how to setup an MDX pipeline in Next.js.

Routing & slugs

I want to ensure that posts are proper routes, with clean slugs and static generation where possible. With the Next.js App Router, you typically:

  • Keep your posts in something like src/posts/*.mdx with frontmatter (title, date, tags, slug).
  • Create a dynamic route to render posts at /blog/[...slug].
  • Use generateStaticParams to prebuild pages for known posts and enable ISR to avoid full rebuilds when adding new posts.

A minimal shape looks like:

app/
  blog/
    [...slug]/
      page.tsx
lib/
  blog.ts           # loads frontmatter & content
  mdx.ts            # MDX/remark/rehype pipeline
posts/
  building-a-blog.mdx
  another-post.mdx

Example dynamic route:

// app/blog/[...slug]/page.tsx
import { getAllPosts, getPostBySlug } from "@/lib/blog";
import Post from "@/components/post";

export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map((p) => ({ slug: p.slug.split("/") })); // supports nested slugs
}

export const revalidate = 60; // ISR: rebuild at most once per minute

export default async function BlogPostPage({
  params,
}: {
  params: { slug: string[] };
}) {
  const slug = params.slug.join("/");
  const post = await getPostBySlug(slug);
  return <Post post={post} />;
}

This keeps URLs tidy (e.g., /blog/building-a-blog-in-nextjs) and lets you add new posts without redeploying the entire site, thanks to ISR.

Tooling

A little tooling goes a long way:

  • TypeScript strict mode for safer refactors.
  • ESLint with the Next.js preset and a couple of custom rules that fit your style.
  • Prettier for consistent formatting (add a .prettierrc and ignore compiled output).
  • Tailwind CSS for fast, consistent styling—dark mode can come later, but starting with utility classes keeps you productive.
  • Optional, but nice: shadcn/ui or a small component kit for buttons, cards, and typography; contentlayer (or your own loader) for generating typed post metadata.

We won't go into other obvious web tooling such as biomejs or turbopack right now, we'll leave them for a future post I'm planning on monorepos and build speed. One heads-up, though: I’ve temporarily disabled Turbopack for this blog because its Rust-based MDX pipeline isn’t compatible with my remark/rehype plugins yet. I’ll cover the details in a separate deep-dive on MDX in Next.js.

MDX under Turbopack uses a Rust compiler (mdxjs-rs) when experimental.mdxRs is enabled. That pipeline is fast, but doesn’t support remark/rehype plugins right now (things like remark-gfm, rehype-pretty-code, custom transforms) - it cannot directly handle javascript functions. If your content pipeline depends on those, they generally silently fail.

See MDX: Using Plugins with Turbopack for more detail.

What's next?

  1. Hosting Next.js on Cloudflare.
  2. MDX to render markdown in Next.js.
  3. Styling components in MDX.