Making MDX blog with Nextjs - Part 2

Jashn Maloo / October 19, 2020

11 minutes readUpdated: June 27, 2021

In the last post we finished with adding mdx files, getting slug and other details for index page and completed index page for posts. I recommend you to get going by completing the part-1 first if you haven't done it. It build the base for what we're going to do now.

Part 1: Making MDX blog with Next.js - Part 1

All we gotta do now is to add that dynamic page we talked in the last post. I know I'm moving directly in the building part without discussing anything right now, but it's better this way. So let's get going.

1. Adding post fetching logic

Before we start making our dynamic page for posts, we've to add some logic regarding how and what are we fetching in that file. So in the end of

/lib/posts.js
file, we'll be adding two functions, one for fetching slugs to attact to each page and one for all the content for each page we are fetching in the first function.

//Get slugs
// ./lib/posts.js
//...
export const getSortedPosts = () => {
//...
};
//Get Slugs
export const getAllPostSlugs = () => {
const fileNames = fs.readdirSync(postDirectory);
return fileNames.map((filename) => {
return {
params: {
slug: filename.replace(".mdx", "")
}
};
});
};
//Get Post based on Slug
export const getPostdata = async (slug) => {
const fullPath = path.join(postDirectory, `${slug}.mdx`);
const postContent = fs.readFileSync(fullPath, "utf8");
return postContent;
};

Here,

  • getAllPostSlugs
    is creating and fetching slugs from all the posts
  • getPostData
    is used to find content of the post by navigating to the file using slug it gets as parameter and returns
    post content
    .

These two functions are the master functions because using these two functions only, we'll be getting all our content and pages.

2. Making [slug].js page

Now that we've the logic to get slug and post from that slug, let's finally build the

[slug].js
page.

If you're familiar with dynamic routing is react, we use

:id
or something like that for dynamic pages, and render page by matching URL parameters with all the data available. Once data is found, it dispatches to the page according to the page design. Nextjs has a better way to handle this(atleast what I feel). As you know nextjs has file based routing, wouldn't it be challenging to make a different page for each posts with similar styles and components? That's where the
[dynamicPageName].js
types of files come in action. Such file name tells next that the content of this file depends on the URL parameter user is visiting so next handles it that way only.

In the

/blog
directory make a file named
[slug].js
and add the following content to it -

// ./blog/[slug].js
/** @jsx jsx */
import { getAllPostSlugs, getPostdata } from "../../lib/posts";
import { Box, jsx, Text, Divider, Flex } from "theme-ui";
import matter from "gray-matter";
export default function Posts({ source, frontMatter }) {
return (
<Box sx={{ variant: "containers.page" }}>
<Box sx={{ mt: "4rem" }}>
<h1>{frontMatter.title}</h1>
<Divider color="muted" />
<Box>{source}</Box>
</Box>
</Box>
);
}
export async function getStaticPaths() {
const paths = getAllPostSlugs();
return {
paths,
fallback: false
};
}
export async function getStaticProps({ params }) {
const postContent = await getPostdata(params.slug);
const { data, content } = matter(postContent);
return {
props: {
source: content,
frontMatter: data
}
};
}

Isn't this mostly like the index page we built earlier? So what makes it dynamic apart from the filename? This time we've another function provided by nextjs,

getStaticPaths
and the role this plays is simple but quite important.

As we know that all the posts will be created at build time, so that means our dynamic page will be statically generated, interesting right? So

getStaticPaths
returns an array of all the URL paramaters possible for our dynamic page based on the data/posts we've created. Here, it fetches all the slugs from the
getAllPostSlugs
function we added in
./lib/posts.js
file and returns an array of it. Now all the URL parameters in thsi array are pre-rendered by nextjs. That means Next.js will generate all the posts route in the build time only. And fallback here is false to give 404 error for paths not returned by
getStaticPaths
. You can read more about it in official documentation.

For all the paths pre-rendered, URL parameter is passed into

getStaticProps
, which fetches
post content
belonging to that param, thus pre-rendering all the paths and pages with their content statically. Here, We are collecting front-matter details in
data
variable and post content in
content
variable with
gray-matter
. And as usual, all this data is passed onto the page component above.

Simple MDX post rendering

Messy, right?

3. Adding Components to MDX

One of the main aspect differring mdx with md is using components within itself. So let's create two simple custom components. Make a

components
folder in the root directory and add the following two components-

// ./components/MyButton.js
/** @jsx jsx */
import { Button, jsx } from "theme-ui";
const MyButton = ({ text, check }) => {
return (
<Button sx={{ width: "100%", bg: check ? "steelblue" : "coral", my: 2 }}>
{text}
</Button>
);
};
export default MyButton;
// ./components/MyBackground.js
/** @jsx jsx */
import { jsx } from "theme-ui";
const MyBackground = ({ children }) => {
return <div sx={{ bg: "cornsilk", py: 1 }}>{children}</div>;
};
export default MyBackground;

Let's add these components to our posts.

// getting-started-with-mdx.mdx
---
//...front-matter
---
import MyButton from "../components/MyButton.js"
import MyBackground from "../components/MyBackground.js";
//...rest of the content
<MyBackground>
[MDX](https://mdxjs.com) is markdown for component era.
</MyBackground>
<MyButton text="Click"/>
// some-random-points.mdx
---
//...
---
import MyButton from "../components/MyButton.js"
//...
<MyButton check text="Click"/>

And this is how the post will look now

Post with component

Unable to understand what's written in the post? Yeah, ditto! If it would've been a markdown file, we could've used remark, remark-html or react-markdown to convert markdown to html. But it's an mdx file and we're using components in it, how can we show our file the way it is meant to be shown?

And that's where a problem arises. Natively we can render mdx files with components easily, but first, we're rendering them through a dynamic route to save ourselves from repitition and drastic memory usage. Secondly, we've front-matter in it, and MDX does not support rendering of front-matter natively. So what's the solution now, we want our mdx files to show content, components and front-matter. This is where I got lost for few days, but you don't have to.

There are two workarounds for this -

  1. next-mdx-enhanced
    : It overcomes with some of the problems of
    @next/mdx
    and renders MDX files with a common layout, provides a way to get components and front-matter render in the post and few extra features that we probably don't need. But it does require little bit of extra config for a super smooth rendering experience.
  2. next-mdx-remote
    : By the same developer, but ~50% faster, more flexible and easier to use. It refines some of the issues of
    next-mdx-enhanced
    . But this is what we'll be using.

Although

next-mdx-remote
is awesome, it does have one caveat which we'll understand once we start using it.

4. Using next-mdx-remote

Install

next-mdx-remote

npm i next-mdx-remote

And now it's time to modify our champ

[slug].js
. We'll be adding and modifying a good amount of code, so let's just rebuild it

// ./blog/[slug].js
/** @jsx jsx */
import Head from "next/head";
import { getAllPostSlugs, getPostdata } from "../../lib/posts";
import { Box, jsx, Text } from "theme-ui";
import renderToString from "next-mdx-remote/render-to-string";
import hydrate from "next-mdx-remote/hydrate";
import matter from "gray-matter";
import MyBackground from "../../components/MyBackground";
import MyButton from "../../components/MyButton";
const components = { MyBackground, MyButton };
export default function Posts({ source, frontMatter }) {
const content = hydrate(source, { components });
return (
<>
<Head>
<title>{frontMatter.title}</title>
</Head>
<Box sx={{ variant: "containers.page" }}>
<Box sx={{ mt: "4rem", textAlign: "center" }}>
<h1>{frontMatter.title}</h1>
<Text
sx={{
width: ["80%", "50%"],
mx: "auto"
}}
>
{frontMatter.author}
{" / "}
<span>{frontMatter.date}</span>
</Text>
</Box>
<Box sx={{ mt: "4rem" }}>
<Box>{content}</Box>
</Box>
</Box>
</>
);
}
export async function getStaticPaths() {
const paths = getAllPostSlugs();
return {
paths,
fallback: false
};
}
export async function getStaticProps({ params }) {
const postContent = await getPostdata(params.slug);
const { data, content } = matter(postContent);
const mdxSource = await renderToString(content, {
components,
scope: data
});
return {
props: {
source: mdxSource,
frontMatter: data
}
};
}

We added

next-mdx-remote
and two functions from it,
renderToString
and
hydrate
.

  • renderrToString
    runs at build time, so it's included in
    getStaticProps
    . It returns an object of MDX content with components it utilizes.
  • The object returned by
    renderToString
    now gets passed into
    hydrate
    along with the location of components we're using inside our MDX.This
    hydrate
    function initially renders static content and hydrate it when browser's not busy with other tasks.

If you now visit your

http://localhost:3000/blog/getting-started-with-mdx
route, you'll get an error,
require is not definer error

It is pointing that error is in our

[slug].js
file in line 52. And that's because it is the line that preapres MDX file for rendering and for determining components in it. So that means we've a problem in our MDX files? Hell Yeah. And this is where we discuess the limitations of
next-mdx-remote
.

next-mdx-remote
does not allow adding
import
inside MDX files, therefore to use components, we've to pass them in second argument in
hydrate
and
renderToString
functions and that's what we did in the code above. So if we remove the
import
lines from our MDX files, and visit our post, we'll have this -
MDX rendered with front-matter and components

Pretty amazing, right?

  • Front-matter ✔️
  • Formatted content ✔️
  • Components rendering ✔️

So we've completed our blog? Kind of, but there's one problem left. Remember how we cannot add

import
in MDX file while working with
next-mdx-remote
and that we've to import components while we're rendering it. According to the official docs of
next-mdx-remote
, while adding components to
hydrate
function, components should be the exact same components that were passed to
renderToString
. And in that case, if we've to make different pages for each post to render, what's the point of doing all this hard work? I totally get you, and so I've a workaround here, it works decently with the things we've setup in 2 lengthy posts.

Currently, we're passing the components of

getting-started-with-mdx
post in the
hydrate
function by importing them in
[slug].js
, now suppose you've few more components being used by several posts. So what simple step we're gonna take is, create
AllComponents.js
file in
components
folder and add all the components in there. Once exported,
AllComponents
will pass required components to the posts which utilize them.

// ./components/AllComponents.js
import MyBackground from "./MyBackground";
import MyButton from "./MyButton";
//import as many components you're using collectively in all your posts
const AllComponents = {
MyButton,
MyBackground
// Any other component you want
};
export default AllComponents;

And now, replace the components you added in

[slug].js
with
AllComponents

// ./blog/[slug].js
//... Other import statements
//Replace MyButton, Mybackground import with AllComponents
import AllComponents from "../../components/AllComponents";
//Replace {MyButton, MyBackground} with AllComponents
const components = AllComponents;
//Rest of the file remains same
export default function Posts({ source, frontMatter }) {
//...
}

Voila! our blog is ready. You're good to go. Use n number of components in your MDX, all you gotta do is to add that component in your

AllComponents
file and wuhoo!, you can render n number of posts without any issue.



Optional



Apart from the whole process we just completed, if you want to provide custom styles/components to native markdown components like H1, H2, lists, link, Image, etc. You can use

MDXProvider
.

Working with MDXProvider

npm i @mdx-js/react

Because I'm using

theme-ui
, I'll be using it to provide custom styling to my markdown components. In your components folder, add
MDXCompProvider.js
and add the following

// ./components/MDXProvider.js
/** @jsx jsx */
import { MDXProvider } from "@mdx-js/react";
import { Heading, Text, jsx, Box, Link, Flex } from "theme-ui";
export default function MDXCompProvider(props) {
const state = {
h1: (props) => <Heading as="h1" sx={{ mt: "3", mb: "2" }} {...props} />,
h2: (props) => <Heading as="h2" sx={{ mt: "3", mb: "2" }} {...props} />,
h3: (props) => <Heading as="h3" sx={{ mt: "3", mb: "2" }} {...props} />,
h4: (props) => <Heading as="h4" sx={{ mt: "3", mb: "2" }} {...props} />,
p: (props) => <Text as="p" sx={{ mb: "2", lineHeight: "2" }} {...props} />,
a: (props) => (
<Link as="a" sx={{ color: "secondary", fontWeight: "bold" }} {...props} />
)
};
return (
<MDXProvider components={state}>
<Box {...props} />
</MDXProvider>
);
}

Here we are providing our components to be used instead of native markdown h1, h2, p, etc. You can do a lot of customizations here according to your need.

Wrapping blog with MDXProvider

Last step, We just need to wrap our Next.js blog with MDXProvider so that it can be applied automatically to our MDX files. Open

_app.js
and wrap
<Component {...pageProps} />
with the
MDXCompProvider
we just created`.

// ./pages/_app.js
import "../styles/globals.css";
import { ThemeProvider } from "theme-ui";
import theme from "../theme";
import MDXProvider from "../components/MDXProvider";
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider theme={theme}>
<MDXProvider>
<Component {...pageProps} />
</MDXProvider>
</ThemeProvider>
);
}
export default MyApp;

So we're finally done with creating our MDX blog with Next.js.

It's a lengthy process if you're new to it. Once you know the stuff, it'll be smooth af!


It's my first tutorial/technical blog, hope you like it.

Peace ✌