Nextjs 14 MDX Blog

Effortless Blogging with Markdown: A Practical Reference with Tips and Tricks

Starting Point

My current setup includes an NX monorepo with Next.js apps and libraries. I was contemplating how to establish a blog that balances convenience with simplicity. Then, I recalled MDX—a markdown format that supports components, offering a simple yet rich solution for a mostly static blog.

I created a Next.js app using NX.

Miscellaneous Configurations

Key Files

  • next.config.js

next.config.js

First, install the MDX package:

npm i @next/mdx

Then, integrate the withMDX plugin:

const withMDX = require('@next/mdx')();

MDX Directory

Create an articles directory within src to house your markdown articles.

Sample article content:

---
title: 'Nextjs 14 MDX Blog'
date: '2024-04-15'
updated: '2024-04-15'
---

# Where am I starting from?

From here

Next I added global.css with this:

pre:has(> code) {
  background-color: #f5f5f5;
  padding: 1rem;
  border-radius: 0.5rem;
}

:not(pre) > code {
  background-color: #f5f5f5;
  padding: 0.25rem;
  border-radius: 0.25rem;
}

Dependencies for formatting

npm i gray-matter react-markdown

Gotchas

Since we no longe have getStaticPaths we must use generateStaticParams

const generateStaticParams = async () => {
  const slugs = await getArticleSlugs();
  return slugs.map(async ({ slug }) => ({
    params: {
      slug,
    },
  }));
};

Never used it before, but its a funny setup.

I needed 2 methods:

getArticleSlugs

const getArticleSlugs = async () => {
  const articlesDirectory = path.join(process.cwd(), 'src/articles');
  const files = fs.readdirSync(articlesDirectory);
  const articles = files.map((filename) => {
    const slug = filename.replace('.mdx', '');
    return { slug };
  });
  return articles;
};

just to read the slugs at build time, and getArticleBySlug, to retrieve the whole article.

const getArticleBySlug = async (slug: string) => {
  const articlesDirectory = path.join(process.cwd(), 'src/articles');
  const filePath = path.join(articlesDirectory, `${slug}.mdx`);
  const markdownWithMetadata = fs.readFileSync(filePath, 'utf-8');
  const { data: frontMatter, content } = matter(markdownWithMetadata);
  const article = {
    frontMatter,
    slug,
    href: `/article/${slug}`,
    content,
  };
  return article;
};

Then you have to stitch it together in the actual page

...
const ArticlePage = async (props: ArticleProps) => {
  const { frontMatter, content } = await getArticleBySlug(
    props.params.slug
  );
  return (
...

For the root of the blog you just need to get some of the data from each article, like for example:

const getArticles = async () => {
  const articlesDirectory = path.join(process.cwd(), './src/articles');
  const files = await fs.readdirSync(articlesDirectory);
  const articles = await files.map((filename) => {
    const filePath = path.join(articlesDirectory, filename);
    const markdownWithMetadata = fs.readFileSync(filePath, 'utf-8');
    const { data: frontMatter } = matter(markdownWithMetadata);
    const slug = filename.split('.')[0];
    return {
      frontMatter,
      slug,
      href: `/article/${slug}`,
    };
  });
  return articles;
};

And just use it from the page component

...
const Home = async () => {
  const articles = await getArticles();
  return (
...
← Back to posts

We use cookies to improve your experience and analyze our traffic. By using our site, you consent to our use of cookies. You can manage your preferences below: