Next MDX vs Next Contentlayer

Vaibhav Yadav's avatar

Vaibhav Yadav

If you're stepping into the world of web development, you might be pondering over a crucial question:

What is MDX?

MDX, short for Markdown eXtended, is a versatile markup language that combines the simplicity of Markdown with the power of React components. This amalgamation empowers developers to create dynamic and interactive content, such as engaging blog posts.

With MDX, you can craft blog posts in Markdown while seamlessly embedding React components within them. This flexibility opens the door to rendering charts, images, videos, interactive forms, and custom styling within your content. Notably, MDX is supported by popular static site generators like Next.js and Gatsby, making it an excellent choice for building aesthetically pleasing and functional blogs.

Now that we've clarified MDX, let's dive into our comparison!

Installation

Next MDX

npm install @next/mdx @mdx-js/loader @mdx-js/react

Next Contentlayer

npm install contentlayer next-contentlayer

Configuration

Next MDX

To enable MDX compilation with @next/mdx, we need to tweak our next.config.js file as below:

// next.config.js
 
const withMDX = require('@next/mdx')({
  extension: /\.mdx?$/,
  options: {
    // If you use remark-gfm, you'll need to use next.config.mjs
    // as the package is ESM only
    // https://github.com/remarkjs/remark-gfm#install
    remarkPlugins: [],
    rehypePlugins: [],
    // If you use `MDXProvider`, uncomment the following line.
    // providerImportSource: "@mdx-js/react",
  },
});
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Configure pageExtensions to include md and mdx
  pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
  // Optionally, add any other Next.js config below
  reactStrictMode: true,
};
 
// Merge MDX config with Next.js config
module.exports = withMDX(nextConfig);

Next, we create a new MDX page within the /pages directory:

your-project
├── pages
 └── my-mdx-page.mdx
└── package.json

After that, we can import a React component directly into our MDX page:

import { MyComponent } from 'components/MyComponent';
 
My MDX page
 
This is a list in markdown:
 
- One
- Two
- Three
 
Checkout my React component:
 
<MyComponent />

Next Contentlayer

To compile MDX using next-contentlayer, we make adjustments to our next.config.js file like so:

// next.config.js
import { withContentlayer } from 'next-contentlayer';
export default withContentlayer({});

Unlike @next/mdx, we don't need to specify extensions in next.config.js because next-contentlayer intelligently detects them. However, next-contentlayer requires a dedicated configuration file, like this:

// contentlayer.config.ts
import { defineDocumentType, makeSource } from 'contentlayer/source-files';
 
const Post = defineDocumentType(() => ({
  name: 'Post',
  filePathPattern: `**/*.mdx`,
  contentType: 'mdx',
  fields: {
    title: {
      type: 'string',
      description: 'The title of the post',
      required: true,
    },
    date: {
      type: 'date',
      description: 'The date of the post',
      required: true,
    },
  },
  computedFields: {
    url: {
      type: 'string',
      resolve: (doc) => `/posts/${doc._raw.flattenedPath}`,
    },
  },
}));
 
export default makeSource({
  contentDirPath: 'posts',
  documentTypes: [Post],
});

Next, we create a new MDX document within the /posts directory. For example, let's consider the file posts/hello-world.mdx:

---
title: Hello world
date: 2022-02-28
---
 
Hello world!

Upon starting or building the server, contentlayer generates a .contentlayer directory in the root of our project. The directory structure looks like this:

your-project
├── .contentlayer
 └── generated
     └── Post
         └──index.d.ts
├── posts
├── index.d.ts
├── index.mjs
├── types.d.ts
└── package.json

Now, we can import the generated types and posts in our page component, as shown below:

// app/posts/[slug]/page.tsx
 
import { format, parseISO } from 'date-fns';
import { allPosts } from 'contentlayer/generated';
import { getMDXComponent } from 'next-contentlayer/hooks';
 
export const generateStaticParams = async () =>
  allPosts.map((post) => ({ slug: post._raw.flattenedPath }));
 
export const generateMetadata = ({ params }) => {
  const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
  return { title: post.title };
};
 
const PostLayout = ({ params }: { params: { slug: string } }) => {
  const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
 
  const Content = getMDXComponent(post.body.code);
 
  return (
    <article className="py-8 mx-auto max-w-xl">
      <div className="mb-8 text-center">
        <time dateTime={post.date} className="mb-1 text-xs text-gray-600">
          {format(parseISO(post.date), 'LLLL d, yyyy')}
        </time>
        <h1>{post.title}</h1>
      </div>
      <Content />
    </article>
  );
};
 
export default PostLayout;

NOTE: It's advisable to add .contentlayer to .gitignore since it's generated during the build.

Remote MDX

Next MDX

If your Markdown or MDX files aren't stored within your application, you can dynamically fetch them from a server. This is particularly useful for obtaining content from a Content Management System (CMS) or other data sources.

import { MDXRemote } from 'next-mdx-remote/rsc';
 
export default async function Home() {
  const res = await fetch('https://...');
  const markdown = await res.text();
  return <MDXRemote source={markdown} />;
}

Next Contentlayer (Experimental)

The remote file source in next-contentlayer is currently in an experimental phase and not recommended for production usage. It operates similarly to the static file source but allows content files to reside outside the website folder. Contentlayer automatically syncs these remote content files to your local website folder and processes them using the standard file source.

Common remote file sources include other Git repositories, databases, APIs, or any location where your content originates.

See full example here.

// NOTE we're using the `defineDocumentType` from the regular files source
import { spawn } from 'node:child_process';
import { makeSource } from '@contentlayer/source-remote-files';
import { defineDocumentType } from '@contentlayer/source-files';
 
const Post = defineDocumentType(() => ({
  name: 'Post',
  filePathPattern: `docs/**/*.md`,
  fields: {
    title: { type: 'string', required: false },
  },
}));
 
const syncContentFromGit = async (contentDir: string) => {
  const syncRun = async () => {
    const gitUrl = 'https://github.com/vercel/next.js.git';
    // TODO: git pull or git clone (see full example for working code)
  };
 
  let wasCancelled = false;
  let syncInterval;
 
  const syncLoop = async () => {
    await syncRun();
 
    if (wasCancelled) return;
 
    syncInterval = setTimeout(syncLoop, 1000 * 60);
  };
 
  // Block until the first sync is done
  await syncLoop();
 
  return () => {
    wasCancelled = true;
    clearTimeout(syncInterval);
  };
};
 
export default makeSource({
  syncFiles: syncContentFromGit,
  contentDirPath: 'nextjs-repo',
  contentDirInclude: ['docs'],
  documentTypes: [Post],
  disableImportAliasWarning: true,
});

NOTE: You can combine static source files with remote source files by using the same root contentDirPath and storing content in different subdirectories.

Layouts

Next MDX

To add a layout or custom styles to our top-level MDX page, we can create a new component and import it into the MDX page. We can wrap the MDX page with the layout component like this:

import HelloWorld from './hello.mdx';
import { MyLayoutComponent } from 'my-components';
 
export const metadata = {
  author: 'Rich Haines',
};
 
export default Page({ children }) => (
  <MyLayoutComponent meta={metadata}><HelloWorld /></MyLayoutComponent>
);

Next Contentlayer

When using next-contentlayer, we don't create an MDX page directly under the pages directory as with @next/mdx. Instead, we create a page component and import the generated types and posts, like this:

// app/posts/[slug]/page.tsx
import PostLayot from 'components/PostLayout';
import { allPosts } from 'contentlayer/generated';
import { getMDXComponent } from 'next-contentlayer/hooks';
 
export const generateStaticParams = async () =>
  allPosts.map((post) => ({ slug: post._raw.flattenedPath }));
 
export const generateMetadata = ({ params }) => {
  const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
  return { title: post.title };
};
 
const PostLayout = ({ params }: { params: { slug: string } }) => {
  const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
 
  const Content = getMDXComponent(post.body.code);
 
  return (
      <PostLayot>
        <Content />
      </PostLayout >
  );
};
 
export default PostLayout;

Remark & Rehype Plugins

Next MDX

To transform MDX content using remark and rehype plugins, we can use the options object in the createMDX function. Since the remark and rehype ecosystem is ESM only, use next.config.mjs for configuration, as shown in this example:

import createMDX from '@next/mdx';
import remarkGfm from 'remark-gfm';
 
/** @type {import('next').NextConfig} */
const nextConfig = {};
 
const withMDX = createMDX({
  options: {
    extension: /\.mdx?$/,
    remarkPlugins: [remarkGfm],
    rehypePlugins: [],
    // If you use `MDXProvider`, uncomment the following line.
    // providerImportSource: "@mdx-js/react",
  },
});
export default withMDX(nextConfig);

Next Contentlayer

Achieving the same behavior in next-contentlayer is straightforward. Use the remarkPlugins and rehypePlugins options in the makeSource function, like this:

import highlight from 'rehype-highlight';
import { makeSource } from '@contentlayer/source-files';
 
export default makeSource({
  // ...
  mdx: { rehypePlugins: [highlight] },
});

Frontmatter Or Metadata

Next MDX

@next/mdx doesn't natively support frontmatter. To include it, you can use external solutions like gray-matter.

Although we can achieve the same behavior using @next/mdx by exporting a meta object from within the MDX file as given in the example below:

export const meta = {
  author: 'Rich Haines',
}
 
# My MDX page

Next Contentlayer

next-contentlayer provides built-in support for frontmatter. You can define your fields in the contentlayer.config.ts file, as demonstrated earlier in this article.

---
title: Hello world
date: 2022-02-28
author: Vaibhav Yadav
---
// contentlayer.config.ts
import { defineDocumentType, makeSource } from 'contentlayer/source-files';
 
const Post = defineDocumentType(() => ({
  name: 'Post',
  filePathPattern: `**/*.mdx`,
  contentType: 'mdx',
  fields: {
    title: {
      type: 'string',
      description: 'The title of the post',
      required: true,
    },
    date: {
      type: 'date',
      description: 'The date of the post',
      required: true,
    },
    author: {
      type: 'string',
      description: 'The author of the post',
      required: true,
    },
  },
  computedFields: {
    url: {
      type: 'string',
      resolve: (doc) => `/posts/${doc._raw.flattenedPath}`,
    },
  },
}));
 
export default makeSource({
  contentDirPath: 'posts',
  documentTypes: [Post],
});

With this configuration, next-contentlayer will automatically extract frontmatter data from your MDX files and make it available in the generated types.

Custom Elements

An advantage of using Markdown is that it maps to native HTML elements, making it faster and more intuitive to write HTML in Markdown format. For instance, the following Markdown:

These are lists in markdown:
 
- One
- Two
- Three
 
1. One
2. Two
3. Three

Compiles to the following HTML:

<p>These are lists in markdown:</p>
 
<ul>
  <li>One</li>
  <li>Two</li>
  <li>Three</li>
</ul>
 
<ol type="1">
  <li>One</li>
  <li>Two</li>
  <li>Three</li>
</ol>

However, when you want to style your own elements to give a custom feel to your website or application, you can introduce custom components known as "shortcodes." These shortcodes map to HTML elements and can be included in your Markdown or MDX content to achieve custom styling and functionality.

Next MDX

To enable the use of custom elements in Next MDX, you need to set up the MDXProvider and pass a components object as a prop. Each key in the components object maps to an HTML element name.

Here's an example of how to set up custom elements in Next MDX:

const withMDX = require('@next/mdx')({
  // ...
  options: {
    providerImportSource: '@mdx-js/react',
  },
});
import Image from 'next/image';
import { MDXProvider } from '@mdx-js/react';
import { Heading, InlineCode, Pre, Table, Text } from 'my-components';
 
const ResponsiveImage = (props) => (
  <Image
    alt={props.alt}
    sizes="100vw"
    style={{ width: '100%', height: 'auto' }}
    {...props}
  />
);
 
const components = {
  p: Text,
  pre: Pre,
  h1: Heading.H1,
  h2: Heading.H2,
  code: InlineCode,
  img: ResponsiveImage,
};
 
export default function Post(props) {
  return (
    <MDXProvider components={components}>
      <main {...props} />
    </MDXProvider>
  );
}

In this example, the components object maps custom component names (p, pre, h1, h2, code, img) to their respective components, allowing you to style and customize these elements in your MDX content.

NOTE: If we have to use it across the site then we should add the provider to _app.ts.

Next Contentlayer

Using custom components with Next Contentlayer is similar to Next MDX. You can define a components object and pass it to your MDX content.

Here's an example of how to set up custom elements with Next Contentlayer:

import Link from 'next/link';
import type { MDXComponents } from 'mdx/types';
import { allPosts } from 'contentlayer/generated';
import { useMDXComponent } from 'next-contentlayer/hooks';
 
// Define your custom MDX components.
const mdxComponents: MDXComponents = {
  // Override the default <a> element to use the next/link component.
  a: ({ href, children }) => <Link href={href as string}>{children}</Link>,
  // Add a custom component.
  MyComponent: () => <div>Hello World!</div>,
};
 
export default async function Page({ params }: { params: { slug: string } }) {
  // Find the post for the current page.
  const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
 
  // 404 if the post does not exist.
  if (!post) notFound();
 
  // Parse the MDX file via the useMDXComponent hook.
  const MDXContent = useMDXComponent(post.body.code);
 
  return (
    <div>
      {/* Some code ... */}
      <MDXContent components={mdxComponents} /> {/* <= Include your custom MDX components here */}
    </div>
  );
}

In this example, the mdxComponents object maps custom component names (a, MyComponent) to their respective components. You can also override default HTML elements, such as the <a> element, to use custom behavior, like the Next.js Link component.

Handling Markdown

Natively React does not understand Markdown. We need to transform the markdown plain text into HTML.

It can be achieved using remark and rehype. Remark is an ecosystem of tools around markdown. Rehype is the same, but for HTML. For example, the following code snippet transforms markdown into HTML:

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeSanitize from 'rehype-sanitize';
import rehypeStringify from 'rehype-stringify';
 
main();
 
async function main() {
  const file = await unified()
    .use(remarkParse) // Convert into markdown AST
    .use(remarkRehype) // Transform to HTML AST
    .use(rehypeSanitize) // Sanitize HTML input
    .use(rehypeStringify) // Convert AST into serialized HTML
    .process('Hello, Next.js!');
 
  console.log(String(file)); // <p>Hello, Next.js!</p>
}

Next MDX

With @next/mdx we don't need to use remark or rehype directly, as it is handled for us by the package itself. Although you'd still need a utility to parse markdown as below:

import html from 'remark-html';
import { remark } from 'remark';
 
export default async function markdownToHtml(markdown: string) {
  const result = await remark().use(html).process(markdown);
  return result.toString();
}

Learn more about using Markdown with Next-MDX

Next Contentlayer

By default next-contentlayer treats your document as markdown so to handle markdown files instead of MDX with it all we need to do it update the contentlayer.config.ts file as below:

// contentlayer.config.ts
// required imports
 
const Post = defineDocumentType(() => ({
  name: 'Post',
  // Update the filePathPattern to match markdown files
  filePathPattern: '**/*.md',
  // Remove the contentType property
  // contentType: 'mdx',
  //...
}));

And for syntax highlighting when using markdown we can use rehype-highlight as below:

import highlight from 'rehype-highlight';
import { makeSource } from '@contentlayer/source-files';
 
export default makeSource({
  // ...
  markdown: { rehypePlugins: [highlight] },
});

Bonus

Next MDX

Next.js now supports a new MDX compiler in Rust. It is not recommended to use it in production yet since it is still in experimental stage. To use the Rust compiler, we need to configure next.config.js when we pass it to withMDX as below:

module.exports = withMDX({
  experimental: {
    mdxRs: true,
  },
});

Next Contentlayer

next-contentlayer is working towards supporting various CMS out there like notion, contentful, sanity as well as the platform like stackbit. All the mentioned integrations are still in the experimental stage and not recommended to use in production yet. But since contentlayer is still in it's early stage, it's a good sign that it's working towards supporting various CMS and platforms.

Conclusion

Next Contentlayer Pros Over Next MDX

  • Typesafe content support out of the box
  • Reduces the line of code with a dedicated configuration file

Next MDX Pros Over Next Contentlayer

  • Supports remote files source

Ultimately, the choice between Next MDX and Next Contentlayer will depend on your familiarity and ease of use with the tools. Both options empower you to create dynamic and engaging MDX-powered websites with Next.js.

Happy coding!

Useful Links: