MDX, Tailwind & Syntax Highlighting in NextJS Pages-Routing

February 06, 2023 - 11min - #mdx #tailwindtypography #rehype #webdev #tutorial

Setting up MDX in NextJS can be a bit of a struggle. Especially when you mix it with Tailwind and Rehype Pretty Code, which at times interfere. Here is how you can set them up together in near harmony.

Table of Contents

  1. Prerequisites
  2. MDX Setup
  3. Tailwind Typography
  4. Rehype Pretty Code
  5. MDX Custom Elements
  6. Custom Colortheme

0. Prerequisites

You must have already either followed the steps of this guide:

How to Code a Blog in 2025 (with MDX in Next 14 Pages-Routing)

Or have scanned through it to make sure the state of your project lines up with it:

1. MDX Setup

Start with the basic installation:

zsh / bash
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

Now to the config file, with a little bummer. The NextJS docs show an outdated version of the configuration settings:

/next.config.JS
const withMDX = require('@next/mdx')();
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
};
 
module.exports = withMDX(nextConfig);

All other plugin configs that you need to make MDX working use ESM module imports. So you have to translate it into ESM syntax like this:

/next.config.Mjs
// This imports a higher-order function that configures Next.js and MDX to work together.
// In some tutorials it gets imported as createMDX.
import nextMDX from '@next/mdx';
 
// This is the NextJS-specific config object.
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
  reactStrictMode: true,
};
 
// In nextMDX, you can set additional MDX options and plugins, which will be covered later in the article.
// It then returns a function with the final mdx settings to later extend nextConfig.
// withMDX captures the returned function.
const withMDX = nextMDX({ options: {} });
 
// Finally, wrap the extension function around nextConfig.
// This exports "nextConfig extended with MDX".
export default withMDX(nextConfig);

Note: For NextJS to recognize the config to be in "module mode", you have to also change the file-ending to .mjs.

A big part in making the whole setup work, is to understand how the config file works, so you can consciously re-adjust from there.

If you should get stuck anyway, I can recommend to follow the steps again in a from-scratch NextJS project.

Note for beginners: You can set that up really fast in a separate IDE workspace and run both your main and your experimentation project at the same time.

Remark Frontmatter Plugin

Now you should be able to see your .mdx pages via localhost:3000/posts/blog-post-title.mdx. However, the frontmatter metadata section gets rendered aswell and ruins the start of the article. To hide it, install remark-frontmatter:

zsh / bash
npm install remark-frontmatter

And update the config:

/next.config.mjs
import nextMDX from '@next/mdx';
import remarkFrontmatter from 'remark-frontmatter'; // Import.
 
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
  reactStrictMode: true,
};
 
const withMDX = nextMDX({
  options: {
    remarkPlugins: [remarkFrontmatter], // Set.
  },
});
 
export default withMDX(nextConfig);

2. Tailwind Typography Setup

Now the metadata is hidden and you should only see the content of the markdown page.

The next thing to notice is that there is no markdown formating: ##h2 - li >blockquote etc aren't showing as they should. That's because Tailwind resets all styles, including markdown elements.

You have to install the Tailwind Typography Plugin and apply its prose-classes to your .mdx pages, to revive the markdown functionality.

Install via npm:

zsh / bash
npm install -D @tailwindcss/typography

And add the plugin to your tailwind.config.js file:

/tailwind.config.js
const tailwindConfig = {
  content: [
    './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
    './src/components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      fontWeight: {
        // ...
      },
      // ...
    },
    // ...
  },
  plugins: [require('@tailwindcss/typography')],
};
export default tailwindConfig;

Now, to actually apply the typography plugin styles (className="prose") to your mdx page, you need to set up an MDXPage component, and an MdxLayout component.

/src/components/MdxLayout.js
export default function MdxLayout({ children }) {
  return (
    // Place the main tag for your blog pages in here.
    <main
      // Set styling layout here. Explanation of classes below codeblock.
      className="prose prose-neutral mx-auto w-full max-w-2xl grow px-4 pt-12 dark:prose-invert hover:prose-a:underline prose-pre:-mb-4"
    >
      {children}
    </main>
  );
}

Explanation of classes (line by line):

  1. general <main> settings related to height, width, positioning etc
  2. "activate" prose for all posts, set grayscale tone, enable darkmode
  3. customizations for markdown elements

Check out the Tailwind Typography Docs for all styling options.

Now, its time to mount MdxLayout. Below the frontmatter, and above your content of each .mdx file insert this MDXPage component:

/src/pages/posts/each-blog-post-title.mdx
---
title: ""
date: "2023-00-00"
readingTime: "00min"
preview: ""
---
import MdxLayout from '../components/MdxLayout'; // Import.
import Head from 'next/head';
 
export default function MDXPage({ children }) {
  const headtitle = 'Headtitle For The Post'; // Here you can set the Head title for your current post.
  return (
    <MdxLayout>
      {' '}
      // Mount.
      <Head>
        <title>{headtitle}</title>
      </Head>
      <article className="text-justify">{children}</article> // Set styling
      specific for this post. Include article tag for SEO.
    </MdxLayout>
  );
}
 
// Blog post content...
Tip: You can save a post-template.mdx file containing empty frontmatter and this component.

Now you should finally see markdown behaviour as usual. Congratulations!

3. Rehype Pretty Code

Every dev blog needs proper codeblocks. A great syntax highlighter for this projects tech stack is Rehype Pretty Code.

Install via npm:

zsh / bash
npm install rehype-pretty-code shiki

And setup in next.config.mjs:

/next.config.mjs
import nextMDX from '@next/mdx';
import remarkFrontmatter from 'remark-frontmatter';
import rehypePrettyCode from 'rehype-pretty-code'; // Import.
 
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
  reactStrictMode: true,
};
 
// See docs for more options and pre-packaged themes.
const prettyCodeOptions = {
  theme: 'synthwave-84',
};
 
const withMDX = nextMDX({
  options: {
    remarkPlugins: [remarkFrontmatter],
    rehypePlugins: [[rehypePrettyCode, prettyCodeOptions]], // Set.
  },
});
 
export default withMDX(nextConfig);

Now your codeblocks should be pretty, let's go!

4. MDX Custom Elements

There is a lot you can customize via Tailwind Typography, but for some markdown components you might want extra control.

Just create an mdx-components.js file like this in your root folder, and NextJS will recognize it:

/mdx-components.js
export function useMDXComponents(components) {
  return {
    h2: ({ children }) => <h2 className="text-4xl text-red-500">{children}</h2>, // Tailwind Classes do not always work here.
    hr: () => <hr style={{ height: '1.7px', backgroundColor: 'white' }} />, // Inline styles did always work so far.
    pre: ({ children }) => <pre>{children}</pre>, // Why is this unstyled? See explanation further below.
    ...components,
  };
}

Even if you don't have customizations in mind yet, make sure to set up at least the empty component already, you will likely run in to trouble without it later.

At this point we have TailwindCSS, Tailwind Typography, Vanilla CSS and Markdown (and Prettycode) intermingling. I am pretty sure that's the reason, why sometimes, Tailwind classes don't work in mdx-components.js.

I have not found a direct solution for that, but you can cope by using inline-styles CSS (see <hr/>) or by customizing MDX components via Tailwind Typography in MdxLayout.js.

Codeblock Customization: You might notice, that some codeblock customizations via prose-pre: in MdxLayout do not work. I have not found the reason why, but I found a way to make it work: Insert pre: ({children})=> <pre>{children}</pre>, as an unstyled custom component. You should now be able to use all prose-pre: customizations.

MDXProvider: Some tutorials also tell you to use an <MDXProvider> for custom components, but that is not neccessary and critizised by the MDX Docs. Instead, just set up the mdx-components.js file like explained, and have <MDXProvider> (@mdx-js/react) NOT installed.

5. Custom Colortheme

Parallel to the blog, I created a custom colortheme to use in VS Codium. I used this buggy, but cool themegenerator on vscode.one.

a screenshot of the theme generator tool

Unfortunately I found out afterwards, that it is not compatible with the newest versions of VS Code. However, I could at least use it for this blog, by importing it via fs:

/next.config.mjs
import nextMDX from '@next/mdx';
import remarkFrontmatter from 'remark-frontmatter';
import rehypePrettyCode from 'rehype-pretty-code';
import fs from 'fs'; // Import filesystem to access locally stored custom colortheme.
 
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
  reactStrictMode: true,
};
 
const prettyCodeOptions = {
  // Set local theme like this.
  theme: JSON.parse(fs.readFileSync('./public/LSD-Classics.json', 'utf-8')),
};
 
const withMDX = nextMDX({
  options: {
    remarkPlugins: [remarkFrontmatter],
    rehypePlugins: [[rehypePrettyCode, prettyCodeOptions]], // Set plugin.
  },
});
 
export default withMDX(nextConfig);

Be careful when creating your own theme with that tool: Saving only works every once in a while...

Also, changes that you make manually in the .json after loading into rehype only show up after replacing it with another theme eg. rehypePlugins: [[rehypePrettyCode,{theme:'one-dark-pro'}]], and then reimporting your updated custom theme again.


I hope this saves you from having days of frustration. If it does - feel free to subscribe below:)

Let’s stay connected.
High-Signal-Only Email Updates.