Viewing docs for an older release. View latest
On this page


While we believe that a strong separation of data and display is important, we understand that formats that mix the two such as MDX (Markdown with embedded JSX components) have become a popular and powerful authoring format for developers.

Rather than compiling your content at build-time like this document demonstrates, it's typically better UX and DX if you do this at runtime via something like mdx-bundler. It's also much more customizable and powerful. However, if you prefer to do this compilation at build-time, continue reading.

Remix has built-in support for using MDX at build-time in two ways:

  • You can use a .mdx file as one of your route modules
  • You can import a .mdx file into one of your route modules (in app/routes)


The simplest way to get started with MDX in Remix is to create a route module. Just like .js and .ts files in your app/routes directory, .mdx (and .md) files will participate in automatic file system based routing.

MDX routes allow you to define both meta and headers as if they were a code based route:

  title: My First Post
  description: Isn't this awesome?
  Cache-Control: no-cache

# Hello Content!

The lines in the document above between the --- are called "frontmatter". You can think of them like metadata for your document, formatted as YAML.

You can reference your frontmatter fields through the global attributes variable in your MDX:

  label: Hello, World!

import SomeComponent from "~/components/some-component";

# Hello MDX!

<SomeComponent {...attributes.componentData} />


By creating a app/routes/posts/first-post.mdx we can start writing a blog post:

  title: My First Post
  description: Isn't this just awesome?

# Example Markdown Post

You can reference your frontmatter data through "attributes". The title of this post is {attributes.meta.title}!

Advanced Example

You can even export all the other things in this module that you can in regular route modules in your mdx files like loader, action, and handle:

  title: My First Post
  description: Isn't this awesome?

  Cache-Control: no-cache

  someData: abc

import styles from "./first-post.css";

export const links = () => [
  { rel: "stylesheet", href: styles },

import { json } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";

export const loader = async () => {
  return json({ mamboNumber: 5 });

export function ComponentUsingData() {
  const { mamboNumber } = useLoaderData<typeof loader>();
  return <div id="loader">Mambo Number: {mamboNumber}</div>;

# This is some markdown!

<ComponentUsingData />


Besides just route level MDX, you can also import these files anywhere yourself as if it were a regular JavaScript module.

When you import a .mdx file, the exports of the module are:

  • default: The react component for consumption
  • attributes: The frontmatter data as an object
  • filename: The basename of the source file (e.g. "first-post.mdx")
import Component, {
} from "./first-post.mdx";

Example Blog Usage

The following example demonstrates how you might build a simple blog with MDX, including individual pages for the posts themselves and an index page that shows all posts.

import { json } from "@remix-run/node"; // or cloudflare/deno
import { Link, useLoaderData } from "@remix-run/react";

// Import all your posts from the app/routes/posts directory. Since these are
// regular route modules, they will all be available for individual viewing
// at /posts/a, for example.
import * as postA from "./posts/a.mdx";
import * as postB from "./posts/";
import * as postC from "./posts/";

function postFromModule(mod) {
  return {
    slug: mod.filename.replace(/\.mdx?$/, ""),

export async function loader() {
  // Return metadata about each of the posts for display on the index page.
  // Referencing the posts here instead of in the Index component down below
  // lets us avoid bundling the actual posts themselves in the bundle for the
  // index page.
  return json([

export default function Index() {
  const posts = useLoaderData<typeof loader>();

  return (
      { => (
        <li key={post.slug}>
          <Link to={post.slug}>{post.title}</Link>
          {post.description ? (
          ) : null}

Clearly this is not a scalable solution for a blog with thousands of posts. Realistically speaking, writing is hard, so if your blog starts to suffer from too much content, that's an awesome problem to have. If you get to 100 posts (congratulations!), we suggest you rethink your strategy and turn your posts into data stored in a database so that you don't have to rebuild and redeploy your blog every time you fix a typo. You can even keep using MDX with MDX Bundler.

Advanced Configuration

If you wish to configure your own remark plugins you can do so through the remix.config.js's mdx export:

const {
} = require("remark-mdx-frontmatter");

// can be an sync / async function or an object
exports.mdx = async (filename) => {
  const [rehypeHighlight, remarkToc] = await Promise.all([
    import("rehype-highlight").then((mod) => mod.default),
    import("remark-toc").then((mod) => mod.default),

  return {
    remarkPlugins: [remarkToc],
    rehypePlugins: [rehypeHighlight],
Docs and examples licensed under MIT