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 (
...

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: