Render Posts with MDX

10 min read

Now that we've bootstrapped our Next app, configured TailwindCSS, and created some common layout with navigation and some styling, it's time time shift our focus to loading in the MDX content. In this article, we'll put together some sample content, install some dependencies, create the page for individual blog posts, and populate the page with MDX content based on the current URL slug. Let's do this thing!

Create Some Sample Content

In order to make sense of the steps we're taking and to verify things work when we're done, let's start by adding some sample content to our placeholder files.

We could use some standard "lorem ipsum" here, but I like to use the Bob Ross variant. Let's start with the content/hello-world.md file. We're going to add some frontmatter at the top of our file. Our frontmatter goes between two --- lines and will be in YAML format. We can put any meta data we want in here, but for now we'll stick with a title and a publish date.

---
title: 'Hello, World!'
date: '01/21/2022'
---

Below the frontmatter block, let's add some content from your favorite "lorem ipsum" generator.

Maybe there's a **happy little bush** that lives right there. There's nothing wrong with having a tree as a friend. Mountains are so simple, they're hard.

Trees get lonely too, so we'll give him a little friend. This is where you take out all your _hostilities and frustrations_. It's better than kicking the puppy dog around and all that so. Only think about one thing at a time. Don't get greedy. No pressure. Just relax and watch it happen.

If we're going to have animals around we all have to be concerned about them and take care of them. A [little happy sunlight](https://unsplash.com/photos/PsLrgT55DeM) shining through there. Let your imagination be your guide. They say everything looks better with odd numbers of things. But sometimes I put even numbers—just to upset the critics.

It takes dark in order to show light. Nothing wrong with washing your brush. You can do anything here - the only pre-requisite is that it makes you happy. You're meant to have fun in life.

You could sit here for weeks with your one hair brush trying to do that - or you could do it with one stroke with an almighty brush. It's a very cold picture, I may have to go get my coat. It’s about to freeze me to death. We wash our brush with odorless thinner. Maybe there's a little something happening right here. Don't be afraid to make these big decisions. Once you start, they sort of just make themselves. When you buy that first tube of paint it gives you an artist license.

We'll make some happy little bushes here. Now, we're going to fluff this cloud. This is probably the greatest thing that's ever happened in my life.

I'm gonna add just a tiny little amount of Prussian Blue. Let's put some happy trees and bushes back in here. Now let's put some happy little clouds in here. I guess I'm a little weird. I like to talk to trees and animals. That's okay though; I have more fun than most people. Don't fight it, use what happens. This is a happy place, little squirrels live here and play.
Tip
I've also added a bit of markdown formatting to mine so I can verify proper rendering.

Now let's repeat the process with the other sample files. Give each a title that corresponds to the slug and a date of your choosing. Throw some placeholder text in each file and we'll move on to making this all work.

Install Dependencies

We'll use the gray-matter module to process our frontmatter and to grab our raw MDX content. We'll also be using next-mdx-remote to serialize our MDX content and to render it on the page.

npm i -D gray-matter next-mdx-remote

Extract Post Data From the Markdown

We want to take a "slug" value from the url, map that back to the corresponding markdown file in our content directory, and then pull out both the frontmatter and the raw MDX content. We'll do that with a new utility function in our utils.ts file.

Let's create a new, async function called getPostBySlug that accepts a string argument called slug. We'll use the slug value to create the full file, then we'll read the file contents into an identifier called source.

export async function getPostBySlug(slug: string) {
  const filePath = join(postDirectory, `${slug}.md`)
  const source = fs.readFileSync(filePath)
}

We're going to pass this source data into front-matter so let's add the import for that at the top of our file.

 import fs from 'fs'
 import { join } from 'path'
+import matter from 'gray-matter'

Then back in our getPostBySlug function, we'll pass source into matter and destructure the response into content and data.

const { content, data } = matter(source)

The data value here will contain our frontmatter, and content is the raw MDX content from the file.

We want to serialize the content, so we'll add another import at the top of our file to pull in the serialize function from next-mdx-remote

 import fs from 'fs'
 import { join } from 'path'
 import matter from 'gray-matter'
+import { serialize } from 'next-mdx-remote/serialize'

Then back in getPostBySlug we'll take our content value and serialize it into mdxSource.

const mdxSource = serialize(content)

Then we'll return an object containing mdxSource and data.

return { mdxSource, data }

The whole utility should look like this at this stage.

export async function getPostBySlug(slug: string) {
  const filePath = join(postDirectory, `${slug}.md`)
  const source = fs.readFileSync(filePath)
  const { content, data } = matter(source)
  const mdxSource = await serialize(content)
  return { mdxSource, data }
}

Load Post Data on the Page

With our utility in place, let's get that data loaded up for our post route.

Let's start by importing our new utility function into pages/blog/[slug].tsx.

-import { getAllPostSlugs } from '../../utils'
+import { getAllPostSlugs, getPostBySlug } from '../../utils'

Update getStaticProps

Now we'll use getPostBySlug down in our getStaticProps function and pass the result along as props for our component.

export const getStaticProps: GetStaticProps<PostProps> = async (context) => {
  const slug = context.params?.slug as string
+ const { mdxSource, data } = await getPostBySlug(slug)
  return {
    props: {
      slug,
+     mdxSource,
+     data,
    },
  }
}

Update Props

Now we can update the PostProps interface and our Post signature.

import type { GetStaticPaths, GetStaticProps } from 'next'
import { getAllPostSlugs, getPostBySlug } from '../../utils'
+import { MDXRemoteSerializeResult } from 'next-mdx-remote/dist/types'

interface PostProps {
  slug: string
+ mdxSource: MDXRemoteSerializeResult
+ data: { [key: string]: any }
}

-export default function Post({ slug }: PostProps) {
+export default function Post({ slug, mdxSource, data }: PostProps) {
  return (
    <div>
      <h1>Post Page</h1>
      <h2>{slug}</h2>
    </div>
  )
}

Render Post Data

And let's update our h1 to show the title from our frontmatter (data).

export default function Post({ slug, mdxSource, data }: PostProps) {
  return (
    <div>
      <h1>{data?.title}</h1>
      <h2>{slug}</h2>
    </div>
  )
}

When we load up one of our pages, we should now see the title value from the frontmatter! Let's get the rest of the content in the page.

Let's import the MDXRemote component from next-mdx-remote.

import type { GetStaticPaths, GetStaticProps } from 'next'
import { getAllPostSlugs, getPostBySlug } from '../../utils'
import { MDXRemoteSerializeResult } from 'next-mdx-remote/dist/types'
+import { MDXRemote } from 'next-mdx-remote'

Now down in the return for our Post component, let's remove the h2 that displays the slug and replace it with an <article> tag and an instance of the MDXRemote component, spreading the mdxSource prop into it.

export default function Post({ slug, mdxSource, data }: PostProps) {
  return (
    <div>
      <h1>{data?.title}</h1>
      <article>
        <MDXRemote {...mdxSource} />
      </article>
    </div>
  )
}

And we have content! 🎉🎉🎉

Let's add the date in here while we're at it. We'll pull the date from the data prop, pass it into the Date constructor and convert it to a date string to get a displayDate.

const displayDate = new Date(data?.date).toDateString()

Now we can add a little markup to render our date.

export default function Post({ slug, mdxSource, data }: PostProps) {
  const displayDate = new Date(data?.date).toDateString()
  return (
    <div>
      <h1>{data?.title}</h1>
+     <div>
+       <time dateTime={displayDate}>{displayDate}</time>
+     </div>
      <article>
        <MDXRemote {...mdxSource} />
      </article>
    </div>
  )
}

Now we have all our post data rendering on the page. If you go back to any of your sample data and add some markdown formatting, you will see that applied to the content when it's rendered. We can't use React components in our content quite yet, but we'll get there soon.

Style the Page

Let's start styling this page a bit. We'll just do some baseline styling and I'll leave it as an exercise for you to take it the rest of the way and make the design your own.

We'll start by throwing a little top margin (mt-4) on the root div to push it down away from the header. We'll make our h1 more pronounced with a large, bold font and we'll center it (text-5xl text-center font-black).

return (
  <div className="mt-4">
    <h1 className="text-5xl text-center font-black">{data?.title}</h1>
    <div>
      <time dateTime={displayDate}>{displayDate}</time>
    </div>
    <article>
      <MDXRemote {...mdxSource} />
    </article>
  </div>
)

Let's also style the date a bit. We'll start by centering the date, and giving it a little breathing room with a top margin. We'll also bump up the text size, make the font semibold and give it a dark gray color.

<div className="text-center">
  <time className="text-lg text-gray-800 font-semibold" dateTime={displayDate}>
    {displayDate}
  </time>
</div>

Styling MDX Content

We can continue to style the elements we have access to, but what do we do about the content we've loaded from markdown? We can't style the tags that are rendered from markdown directly. We could fall back to standard CSS, but we're using tailwind and it would be nice to continue using the same colors, spacing scales, font sizes, etc.

This problem has been addressed for us by the awesome team behind Tailwind through the official Typography plugin.

Let's install the plugin.

npm install -D @tailwindcss/typography

Then we can update our tailwind.config.js to use the plugin.

module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
    './content/**/*.md',
  ],
  theme: {
    extend: {
      gridTemplateRows: {
        3: 'auto 1fr auto',
      },
    },
  },
- plugins: [],
+ plugins: [require('@tailwindcss/typography')],
}

Now we can update our Post component (in pages/blog/[slug].tsx) to apply some classes exposed by the plugin we just installed.

<article className="prose prose-xl prose-sky mx-auto mt-2 py-4">
  <MDXRemote {...mdxSource} />
</article>

We need the prose class to apply the baseline styling for our elements. The prose-xl bumps the font size up, and the prose-sky class will apply colors from the sky color palate to elements like links. The use of mx-auto will center the content area which has a max width applied to it by the prose class. The mt-2 py-4 will just give us some space between the date and our content as well as keeping the end of the text from being right up against the footer. Of course, you can take it from here and change up the styling until you're happy with it.

Hot Tip
Check back next week for the next article in the series, Include React Components in MDX Content!