September 3rd, 2021
  • 6 mins
  • 900 words, 6.2k chars
static site generator, react, server-side rendering, mdx, gatsby

DIY Gatsby – MDX

This is Part III of a series where we'll look into how kimmo.blog works. Part III covers MDX rendering.

Motivation has been covered in Writing, or coding.

What is MDX

MDX is an amazing markup format. It allows familiar authoring experience using markdown syntax – with the capability of adding custom React components along with the text.

For example this source

**Here's** an _example_ of a [toast](https://ux.stackexchange.com/questions/11998/what-is-a-toast-notification):
<ButtonLink onClick={() => info('This is amazing!')}>Spawn a toast</ButtonLink>

would render:

Here's an example of a toast:

All this could be achieved with good old React components, but it's just not as smooth of a writing experience. Paragraphs would need to be wrapped in <p> elements, links would become cumbersome to type, and blah.

Writing blog posts in React would make the site generator simpler, but we're already on the over-engineering train so let's continue.

How MDX works

While MDX is an incredible authoring experience, it comes at the price of complexity when done right. By right, I mean with good performance.

It is possible to greatly simplify the rendering, if performance is not a concern. MDX text could be sent as a raw string to the browser and it would do the heavy rendering with e.g. @mdx-js/runtime. It would make the build pipeline simpler, but other aspects worse:

"warning: this is not the preferred way to use MDX since it introduces a substantial amount of overhead and dramatically increases bundle sizes. It must not be used with user input that isn’t sandboxed."

This blog uses next-mdx-remote library to deal with MDX rendering. It's been designed for Next.js, but it also works well for our use case.

To speed things up in the browser, next-mdx-remote compiles the MDX to minified JavaScript server-side. The serialize method does this using @mdx-js/mdx and esbuild under the hood.

This is how the previous example would be serialized:

import { serialize } from "next-mdx-remote/serialize";
const content = `
**Here's** an _example_ of a [toast](https://ux.stackexchange.com/questions/11998/what-is-a-toast-notification):
<ButtonLink onClick={() => info('This is amazing!')}>Spawn a toast</ButtonLink>
`;
const { compiledSource } = await serialize(content);

The compiledSource is a string that contains the compiled JavaScript. Here's the code unminified and simplified:

function MDXContent(n) {
var e = n,
{
components: o
} = e,
t = m(e, ["components"]);
return mdx(MDXLayout, c(a(a({}, layoutProps), t), {
components: o,
mdxType: "MDXLayout"
}), mdx("p", null, mdx("strong", {
parentName: "p"
}, "Here's"), " an ", mdx("em", {
parentName: "p"
}, "example"), " of a ", mdx("a", a({
parentName: "p"
}, {
href: "https://ux.stackexchange.com/questions/11998/what-is-a-toast-notification"
}), "a toast"), ":"), mdx(ButtonLink, {
onClick: () => info("This is amazing!"),
mdxType: "ButtonLink"
}, "Spawn a toast"))
}

In the browser, next-mdx-remote evals the code with Reflect.construct(), and renders the MDXContent:

<MDX.MDXProvider components={components}>
<MDXContent />
</MDX.MDXProvider>

How the blog renders posts

The high-level flow of rendering .mdx pages is similar to React pages, but has more steps:

We'll soon understand why .txt extension is used. Let's go through the templates one by one.

PostPage.tsx.template

The template looks like this:

import { PostLayout } from "src/components/PostLayout";
import { MDXRemote } from "src/components/MDXRemote";
// Will be replaced with "./compiledSource.txt"
import compiledSource from "{{{ compiledSourcePath }}}";
export default function PageComponent(props) {
return (
<PostLayout siteData={props.siteData} data={props.pageData}>
<MDXRemote compiledSource={compiledSource} />
</PostLayout>
);
}

The purpose of the rendered file is to represent the React component for a blog post page. The template will be rendered to a verbosely named file: output/posts/<slug>/<slug>-post.tsx.

It's easier to debug the build process when unique names are used instead of tens of "post.tsx" files.

pageHydrate.tsx.template

The same template that we used to render React pages in Part II. In React pages, the import refers to a page component in the source code.

import PageComponent from "src/pages/NotFound404";

With MDX pages on the other hand, the PageComponent is imported from the dynamically rendered post page file:

import PageComponent from "./1-writing-or-coding-post";

The template is saved to output/posts/<slug>/<slug>-post-hydrate.tsx.

page.html.template

Again, the same template as for React pages. The MDX content will be rendered as HTML in <body>. Blog post title, description, and tags from the front matter are rendered into <head>:

<html>
<head>
<title>Writing, or coding - kimmo.blog</title>
<meta name="description" content="Do I want to write content, or do I want to code a blog?" />
<meta name="keywords" content="meta,blog,content creation" />
</head>
<body>
<div id="react-root"><!-- MDX blog page as HTML --></div>
<script type="module" src="./1-writing-or-coding-post-hydrate.js"></script>
</body>
</html>

compiledSource.txt

As mentioned, this file contains the MDX as minified JavaScript.

Why does it have a .txt extension then?

We need to be able to transfer the code exactly as-is to the browser, so that it can be passed as a string to MDXRemote. A good way to do this is using a rollup string plugin. The plugin has been configured so that a TypeScript code can import any .txt file, and the exact contents will be assigned to a variable.

This functionality used to be just a simple text replacement directly into code, but that broke with special characters and line breaks.

To recap

MDX rendering

Each .mdx blog post file ends up creating quite some files in the output directory. First TypeScript files and compileSource.txt are rendered, then Rollup transpiles the source code into JS bundles, and finally static assets are copied.

Thank you!

You should get a confirmation email soon. I'll keep the content coming up!

- Kimmo

Like the content?

Let me know by subscribing to new posts.