DEV Community

Jashn Maloo
Jashn Maloo

Posted on • Updated on • Originally published at jashn.xyz

Making MDX blog with Nextjs - Part 2

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's the base for what we're going to do now.

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.

Repo: mdx-blog-with-nextjs

Live: Next MDX Blog

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;
};
Enter fullscreen mode Exit fullscreen mode

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
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
// ./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;
Enter fullscreen mode Exit fullscreen mode

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"/>

Enter fullscreen mode Exit fullscreen mode
// some-random-points.mdx

---
//...
---
import MyButton from "../components/MyButton.js"


//...

<MyButton check text="Click"/>

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 }) {
  //...
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 ✌

Top comments (3)

Collapse
 
muuhoffman profile image
Matthew Hoffman

With the current version of next-mdx-remote you have to use

// ./blog/[slug].js

export async function getStaticProps({ params }) {
  ...
  const mdxSource = await serialize(content, {
    components,
    scope: data,
  });
  ...
}

export default function Posts({ source, frontMatter }) {
  return (
    <>
      ...
      <MDXRemote {...source} components={components} />
      ...
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
parkadzedev profile image
Michael Parkadze

Hey, did you get code highlighting for the md files? currently having problems with that.

Collapse
 
jashnm profile image
Jashn Maloo

Yeah. I'm using this extension for syntax highlighting.