Building a blog with MDX, Next.js and Tailwind: why markdown is never too much
While writing a post packed with interactive components, I noticed I was spending more time on the post's code than on the text. That's when I decided to rebuild the blog with MDX. In this post, I walk through how to put together a blog with Next.js, MDX and Tailwind.
ยท5 min read
On this page
While writing a post packed with interactive components, I noticed I was spending more time on the components' code than on the actual text. That got me wondering: "isn't there a better way to mix code and content?"
After trying a few options, I decided to rebuild the blog with MDX. Fitting it into my stack took more work than I expected. I banged my head against a few config issues, but in the end I got to the version you're reading right now.
In this post, I'll go through the whole process, from the initial setup to the final tweaks, so you can build your own blog with Next.js, MDX and Tailwind CSS without the same headaches. But first, why MDX?
Why MDX?
If you've ever written in Markdown, you know how much easier it makes things. The syntax is simple, and you can format text without touching HTML or fighting a visual editor full of bells and whistles. Now imagine mixing Markdown with React components. That's exactly what MDX does.
MDX (Markdown + JSX) lets you write posts in Markdown and drop React components right in the middle of the text. Besides paragraphs, lists and images, you can add buttons, animated charts, alerts or any other component you like. That flexibility was what I needed to explain things better and make the posts more fun to read.
Setting up the project
Let's start by creating a Next.js project. For MDX support, we'll use @next/mdx, the official Next.js package for it.
Creating the project
In your terminal, run:
bash
npx create-next-app@latest
shell
What is your project named? mdx-blog
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like your code inside a `src/` directory? Yes
Would you like to use App Router? (recommended) Yes
Would you like to use Turbopack for `next dev`? No
Would you like to customize the import alias (`@/*` by default)? No
Why so many dependencies? (damn you, JavaScript ๐)
@next/mdx, @mdx-js/loader, @mdx-js/react, @types/mdx: let Next.js handle MDX files, which mix Markdown and JSX.
gray-matter: pulls metadata out of Markdown/MDX files from the frontmatter, a YAML (or JSON/TOML) block at the top of the file.
remark-frontmatter, remark-mdx-frontmatter: make the MDX parser recognize the frontmatter block and turn that data into an object you can access inside the MDX.
Hooking MDX into Next.js
With the project created, let's plug MDX into Next.js:
next.config.ts
import createMDX from "@next/mdx";
import remarkFrontmatter from "remark-frontmatter";
import remarkMDXFrontmatter from "remark-mdx-frontmatter";
Enables frontmatter parsing with the remark-frontmatter and remark-mdx-frontmatter plugins.
Sets the page extensions (.mdx, .md, .js, .jsx, .ts, .tsx) so files in those formats can be treated as pages by Next.js.
Dynamic post pages
With everything set up, we can start structuring the blog. We'll use this folder layout:
text
โฃ ๐ public/
โฃ ๐ posts/ # posts and pages written in MDX
โ โฃ example.mdx
โฃ ๐ src/
โ โฃ ๐ app/
โ โ โฃ ๐ blog/ # main blog page (post list)
โ โ โ โฃ ๐ [slug]/ # dynamic route that renders each post
โ โ โ โ โฃ page.tsx
โ โ โ โฃ page.tsx
โ โ โฃ globals.css
โ โ โฃ layout.tsx
โ โ โฃ page.tsx
โ โฃ ๐ components/
โ โฃ ๐ widgets/ # components used inside MDX
โ โฃ ๐ utils/ # helper functions, like frontmatter parsing
โ โฃ ๐ lib/ # MDX config, file loading, etc.
โ โฃ mdx-components.tsx # more on this one later
โฃ .gitignore # files and folders ignored by git
โฃ package.json # project dependencies and scripts
โฃ next.config.ts # Next.js config, including MDX support
โฃ README.md # project docs
What's [slug]?
In Next.js, [slug] is a dynamic route, which generates pages automatically from an identifier. In our case, the identifier is the MDX file name. Instead of creating a page by hand for every post, Next.js loads and renders the right content based on the URL.
In the page.tsx file of the [slug] route, we'll create a component that grabs the post name from the URL (for example, /blog/example-post), finds the matching file (example-post.mdx) and renders it.
The code looks roughly like this:
src/app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
export default async function PostPage({
params,
}: {
params: { slug: string };
}) {
try {
// Dynamically import the post based on the URL slug
const { default: Post } = await import(
`../../../../posts/${params.slug}.mdx`
);
return (
<article>
<div>
<span>{/* Title */}</span>
<span>{/* PublishedOn */}</span>
</div>
<Post />
</article>
);
} catch (error) {
// If the file doesn't exist, show the 404 page
notFound();
}
}
This way, every new post you drop into posts/ shows up at /blog/post-name, with no extra setup.
There's still one thing missing: how do we get to the posts' metadata? The page needs to show at least the title and publish date, and that information lives in the frontmatter.
Creating posts-helpers.ts
To keep the code organized, let's create a posts-helpers.ts file inside the utils/ folder. It'll hold the helper functions for working with posts, like listing all of them and loading a specific one.
src/utils/posts-helpers.ts
import fs from "fs/promises";
import path from "path";
import matter from "gray-matter";
type BlogPost = {
slug: string;
title: string;
abstract: string;
publishedOn: string; // Could be `Date` if you'd rather work with date objects
};
/**
* Returns the list of blog posts sorted by publish date (newest first).
*/
export async function getBlogPostList(): Promise<BlogPost[]> {
const fileNames = await readDirectory("posts"); // Lists the files in `posts/`
The big win with MDX is being able to use React components inside your posts. For that to work in Next.js, we need a special file called mdx-components.tsx, which defines which components are available in MDX files.
src/mdx-components.tsx
import type { MDXComponents } from "mdx/types";
import ChromaticCircle from "@/widgets/chromatic-circle";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
ChromaticCircle, // Makes the component available in MDX files
};
}
With the component registered, you can use it in an MDX post:
posts/example.mdx
## The color wheel
If you work with color ...
<ChromaticCircle type="selector" />
One detail: Next.js only finds mdx-components.tsx if it sits at the root of the src/ folder. The structure looks like this:
text
๐ src/
โฃ ๐ components/
โฃ ๐ widgets/ <-- components for MDX
โ โ ๐ chromatic-circle.tsx
โ ๐ mdx-components.tsx
Next steps
From here, just create your .mdx files in the posts/ folder and start writing. What we built is the skeleton: a list page, a post page and component support. Styling, tags, an RSS feed and everything else are up to you.
If you build yours, send me the link. I want to see what you end up putting in the middle of your text.