DEV Community

Cover image for Managing Markdown content doesn't have to be hard!
Hector Sosa
Hector Sosa

Posted on

Managing Markdown content doesn't have to be hard!

I've created this project starter that uses MDX to power your Next.js content only using next-mdx-remote and @tailwindcss/typography packages.

NOTHING else is required, but nice to have are these plugins for your parser: rehype-autolink-headings, rehype-prism-plus and rehype-slug to make your life easier (more on these later on).

Here's the link to the GitHub repo and here's the live demo.

Don't forget to visit the demo's Blog page and view the sample entries. They are full-written articles also featured on my website.

First let's review what I'm including in this project starter:

  • Next.js setup for TypeScript and TailwindCSS
  • Layout (for navs and footers) and Meta (for dynamic SEO) components
  • Project directory for storing local blog files
  • Utility functions to parse your blog content

How to use

  1. From your terminal clone the repo,
  2. Navigate to the project's directory and install all its npm dependencies
$ git clone <FOLDER_DIRECTORY>
$ npm install
Enter fullscreen mode Exit fullscreen mode

That's all you need. You can change the <Meta /> and remove the GitHubCorner component to your own and write your own content in the /content and /blog directories, everything else is already set.

How does it work?

The magic happens within the /util folder. getMdx and getPaths. They both illustrate how we can use next-mdx-remote to process our MDX files in a Node.js context to statically serve our content. Keep in mind this is the ONLY required package for this. All the others after it are just to jazz up our content.

Let's break down each of these util functions, starting with getMdx:

import fs from "fs";
import path from "path";
import { serialize } from "next-mdx-remote/serialize";
import rehypeSlug from "rehype-slug";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypePrism from "rehype-prism-plus";
Enter fullscreen mode Exit fullscreen mode
  • We are using fs from Node.js which enables us to interact with the file system. The fs.readFileSync method returns the files content.
  • Similarly, path allows us to work with files and directory paths, the path.joins() method joins all the given segments.
  • serialize runs on the server-side or/and at build time to generate an object that can be passed directly into our frontend <MDXRemote /> component.
  • Then we are left with the rehypePlugins which ARE NOT REQUIRED but as an added bonus helps us do the following:
    • rehypeSlug generates ids to all headings that do not yet have one.
    • rehypeAutolinkHeadings looks through all headings that have ids and injects a link to them.
    • rehypePrism provides classes to your code for syntax highlighting and line numbers functionalities (does require additional CSS).

How do we use all of these packages?

The function takes two parameters: (a) the directory's path you are targeting and (b) the slug or filename you want to read. Then it goes ahead and gets the file's content and parses it with the options provided. Here's how the function looks:

export default async function getMdx(dirPath: string, slug: string) {
    const source = fs.readFileSync(path.join(dirPath, slug + ".mdx"), "utf8");

    return await serialize(source, {
        parseFrontmatter: true,
        mdxOptions: {
            rehypePlugins: [
                        properties: {
                            className: ["anchor"],
            format: "mdx",
Enter fullscreen mode Exit fullscreen mode

How do we render it on the client-side?

It's all downhill from here. You just need to call the util getMdx from the server side and pass it as a prop.

import { MDXRemote } from "next-mdx-remote";

export async function getStaticProps() {
    const mdxSource = await getMdx("content", "index");
    return { props: { source: mdxSource } };

export default Home ({ source }){
        <article className='prose'>
            <MDXRemote {...source} />
Enter fullscreen mode Exit fullscreen mode

Notice a couple of things:

  • Having the utility function really cleans up your code. You can choose to use Markdown files to power content blocks from any given page OR generate an entire page (such as a blog post) with it too.
  • TailwindCSS comes into play here, by installing the pluging @tailwindcss/typography you have access to the prose class name, which formats the entire content more information on it, here.

Generating your project's paths

Now changing gears let's review how to generate your project's paths using the getPaths function:

import fs from "fs";
import path from "path";

export default function getPaths(dirPath: string) {
    const files = fs.readdirSync(path.join(dirPath));
    return => ({
        params: {
            slug: file.replace(".mdx", ""),
Enter fullscreen mode Exit fullscreen mode

Once again, we use the same Node.js functions and provide the paths for our getStaticPaths() data fetching function.


Powering your project's content with MDX cannot be any easier. There are multiple solutions and libraries out there for this, however. I've found that this is the most flexible solution I've been able to come up with. Clone the GitHub repo and experiment with it to create your own solutions. A couple of final thoughts:

  • Using the additional libraries to enable the plugins aren't requireq but a great addition for your content
  • Syntax highlighting is subject to configuring CSS classess to a desired theme
  • If your MDX files have custom component you would need to map those components when invoking <MDXRemote /> from the client-side
  • With this setup you cannot use .mdx as standalone pages

Thanks for reading!

Top comments (0)