How I Built a Fully-Custom Blog with MDX and Next.js

Published: (April 24, 2026 at 06:33 AM EDT)
5 min read
Source: Dev.to

Source: Dev.to

A Little Back Story

I’ve always wanted to post the things I find interesting—things I’ve learned, topics that intrigue me, lessons, educational content, or just goofy random stuff. I told myself that if I ever did it, I would publish it on my own blog.

Why a personal blog?

Current platforms are highly limited in terms of customization and features. Some require a subscription just to access certain features (or even to read an article). I wanted full control over how the content is delivered: the design, layout, fonts, colour scheme, etc.


Day Job

I recently graduated from college and was lucky enough to get hired right out of school. After a few months on the job, I had to adjust to the demanding nature of my role, especially once my probation period ended and I needed to understand highly complex parts of the system.

That pushed the blog idea further to the back of my mind. Now that I’m more comfortable with my role, can handle the stress and pressure better, and have a clearer view of my responsibilities, I’m finally able to return to the unfinished idea of building my own blog (that can’t be that hard, right?).

Note: I’ve done a lot of cool things at work that I’d love to share, but that’s a story for another day.

I still haven’t reached the level of “full customization” I envision for this application. I had many ideas, but because I kept delaying the build, I lost sight of what I wanted. After creating a simple version, I can now refocus on the original vision.

Transitioning from student to full‑time employee deserves its own post—maybe later.

Now, let’s get a bit technical.


Tech Stack

The blog is part of a monorepo that also hosts my portfolio (yes, a bit much). It’s built with Next.js and uses MDX for the blog posts. The posts are written in Markdown and compiled to React components using remark.

  • Styling: Tailwind CSS with the @tailwindcss/typography plugin for headings, paragraphs, lists, etc.
  • MDX: The core of the system—without it, getting the blog up and running would have required a lot more work. Huge thanks to the contributors of this wonderful tool.
  • Deployment: Vercel.

Database (or lack thereof)

I did not use a database to store blog‑post metadata (definitely not because I wanted to avoid paying for one). Some might argue that SQLite or a free hosted DB would be appropriate, but I didn’t want to over‑complicate the project; I just wanted to move it out of the “wishlist” bucket.

Instead, I created a custom script that:

  1. Loops over the MDX files.
  2. Extracts the front‑matter using gray‑matter.
  3. Saves the collected data to a TypeScript file.

The generated file is imported by the application at build time, so no heavy runtime processing (like reading files) is required. The data only changes when I rebuild the app, making this approach sensible.

Example of the generated object

// THIS FILE IS AUTO‑GENERATED BY compile‑mdx‑data SCRIPT, DO NOT EDIT

export const blogPostsObject: Record = {
  "mdx-nextjs-blog-setup": {
    title: "My MDX + Next.js Blog Setup",
    description: "This is a description",
    tags: ["nextjs", "mdx", "tailwind"],
    date: "2025-11-21",
    readingTime: "5 min",
  },
};

The script runs before the build script.


Blog Main Page

The main page that lists all posts consumes blogPostsObject. This keeps everything consistent and eliminates the need to manually update post metadata in multiple places.


Code Snippets

I use Expressive Code to highlight code blocks. It’s a powerful plugin that adds syntax highlighting, file tabs, line numbers, and even diff views.

  • File tab example

    // file: src/components/Button.tsx
    export const Button = () => Click me;
  • Show line numbers & highlight a line

    // file: server.ts
    const port = 3000; //  and let me know what you think!

Generating a Table of Contents with withTocExport

The withTocExport function is what triggers exporting the TOC data.

import withToc from "@stefanprobst/rehype-extract-toc";
import withTocExport from "@stefanprobst/rehype-extract-toc/mdx";

const withMdx = nextMdx({
  options: {
    // …
    rehypePlugins: [
      withToc,
      [withTocExport, { name: "toc" }],
    ],
  },
});

The toc variable is then exported from the MDX import itself, which we pass to the Toc component to render the table‑of‑contents tree:

const { default: Post, toc } = await import(`@/app/(blog)/mdx/${slug}.mdx`);

return (
  <>
    {/* render Post and Toc here */}
  </>
);
  • Post – the component that will be rendered as the blog post.
  • toc – the variable that contains the generated table of contents.
  • Toc – a component responsible for rendering the TOC UI.

Closing Thoughts

I’m really happy with how this blog turned out. Even though there is still much work to be done, what I have finished deserves a moment to appreciate the hard work that went into it.

I’m excited for what comes next in this little project, and I hope you enjoyed reading this post! Sorry for any mistakes or typos—I’m still learning how to write this kind of stuff 😅

0 views
Back to Blog

Related posts

Read more »

Building a Markdown editor (Markflow)

I’ve been working with Markdown editors both as a user. At some point I wanted to better understand how they actually behave under the hood, especially when doc...