Elijah Agbonze I am a full-stack software developer from Nigeria. I love coding and writing about coding.

Create a Next.js and MDX blog

24 min read 6858

Create a Next.js blog with MDX

Markdown is a language that is widely used by developers to write different types of content, like posts, blogs, and documents. If you haven’t used it, chances are you’ve already seen it in action. For example, the README.md files in GitHub and npm are written in Markdown.

Markdown provides an easy syntax for writing and structuring content compared to regular HTML or other writing tools. For example, in Markdown we can say

## Hello World!

I am here to see [John Doe](https://johndoe.com)

But in HTML, the same text would be:

<h2>Hello World!</h2>
<p>I am here to see <a href="https://johndoe.com">John Doe</a></p>

Writing in Markdown feels like writing plain text, with a tiny bit of extra detail to structure your content. This is why Markdown is mostly used by developers to write documentation and articles.

But, there are limitations to Markdown, one of which is writing interactive content.

Oftentimes in our articles or docs, we may want to add an interactive one-off widget to demonstrate something to the reader, but we can’t in Markdown.

This is where MDX comes in. MDX is a language that makes this possible because it allows you to use JSX in Markdown.

So, in this article, we will look at how we can use this tool to build this blog in Next.js with no Gatsby or Strapi — all from scratch. We’ll cover:

If you’d like to follow along, here is the code on my GitHub.

What is MDX?

Before we jump into building this blog, let’s talk a little about what MDX is and what we can do with it.

It is necessary to be familiar with both Markdown and JSX before working with MDX because MDX is simply a combination of them.

MDX syntax

MDX doesn’t have a new or special syntax because it’s just a combination of Markdown and JSX in one place. Also, it is possible to save an MDX file with the .mdx extension.

You can use Markdown dynamically and interactively, like importing components or writing XML-like syntax in your Markdown:

import Head from 'next/head';

<Head>
  <title>Hello World - My Blog</title>
</Head>

# Hello World

In the example above, we import a component Head from Next.js and use it to specify a title for our article. You can also notice the “Hello World” message with an # (which is an h1 in markdown). This is Markdown and JSX working together.

Writing expressions in MDX

The v2 release of MDX makes it possible to write expressions in MDX, just like you would in JSX. A simple example is the following:

# Hello world

I think 2 + 2 is {2+2}

This can be useful in docs or articles when you want to refer to a JavaScript variable, function expression, object, array — you name it.



Writing ESM in MDX

MDX also supports importing and exporting. For instance, the BlogTitle component below can be used as any other JSX component:

import Head from 'next/head';

export const BlogTitle = () => <h3>Hello World</h3>

<Head>
  <title>Hello World - My Blog</title>
</Head>

# Hello World

<BlogTitle />

Apart from components, non-components like objects, variables, and functions can be exported and imported:

import { year } from '../utils/date.js'

# Top Ten Languages To Learn In {year}

Since we can use expressions in MDX, we can just pass in a year into our header easily. Think of any data you pass as expressions in JSX; now you can do it in your Markdown too:

import { posts } from '../posts/node.js'

# A List of My NodeJS Posts

<ul>
  {posts.map(post => <li>{post.title}</li>)}
</ul>

It is also possible to create a variable in MDX like we saw with a functional component (BlogTitle), but we need to prefix it with the export keyword:

export const languages = ['Java', 'JavaScript', 'Python']
export const year = new Date().getFullYear();

# Top Ten Languages in {year}

<ul>
  {languages.map(language => <li>{language}</li>)}
</ul>

Prefixing every declaration with export can be weird, but the good news is you don’t have to — you can simply declare it once elsewhere and import it.

Using prefixes with metadata

There is one exception where you may need to declare and use an object in your Markdown. That is when you use metadata:

export const meta = {
  title: "Your article doesn't beat this",
  publishedOn: "January, 6th. 2022",
  slug: "your-article-doesnt-beat-this"
}

# {meta.title}

While it’s not compulsory, it helps organize your content. You can also simply use Front Matter (which we’ll use in our blog later).

One interesting thing about MDX is that its files are compiled into JSX components. Their compilers (integrations from MDX) compile both MDX files (files in .mdx) and Markdown files (files in .md) into JSX components that can be used as any other components.

Say we have the example below saved as article.mdx:

import Head from 'next/head';

export const BlogTitle = () => <h3>Hello World</h3>

<Head>
  <title>Hello World - My Blog</title>
</Head>

# Hello World

We can import it as such:

import Article, { BlogTitle } from 'article.mdx';

const MyBlog = () => {
  <Article />;
  <BlogTitle />;
};

export default MyBlog;

The article.mdx file is compiled into a JSX component and exported by default, so we can import it into a .js/.jsx/.mdx file as default. Also, recall that the BlogTitle component was also exported from article.mdx.

Passing props in MDX

“If they can use it as any other JSX component, then they should also be able to pass props, right?” so I presume the MDX developers thought.

Well, they must have, as it is possible to pass data through props to any component, including the ones you create and the ones that compile to components by default.

We can refactor our last code block to look like the following:

import Article, { BlogTitle } from 'article.mdx';
import { year } from '../utils/date.js';

const MyBlog = () => {
  <Article year={year} />;
  <BlogTitle />;
};

export default MyBlog;

Our article.mdx now looks like the following:

import Head from 'next/head';

export const BlogTitle = () => <h3>Hello World</h3>

<Head>
  <title>Hello World - My Blog</title>
</Head>

# Hello World

This year is {props.year}

Note that not all JSX syntax is allowed in MDX. There are exceptions between them, and you can find them on GitHub.

There are also extensions that provide language support like syntax highlighting for MDX for different text editors and IDEs. If you’re using VS Code, simply install MDX, and you’ll be good to go.

Enough with the introductions, let’s jump into code and see MDX in action.


More great articles from LogRocket:


Creating a blog with MDX

While creating this blog, we will do the following:

  • Use a widget in one of the articles
  • Display articles on the home page using Next.js getStaticProps and api
  • Develop each article as an MDX file
  • Use Front Matter for our article metadata
  • Apply styles to our article content using only CSS
  • Use highlight.js for syntax highlighting
  • Apply pagination to the home page

If you have never used Next.js before, you will still be able to follow along as long as you know React, Markdown, and JSX.

Creating and configuring the Next.js app

Pardon me if this is obvious to you, but I want to try to carry everyone along.

To create your Next.js app, run the command below:

npx [email protected]

You’ll be prompted to name your app; you can give yours any name, but for this tutorial, I will name the project PressBlog.

Installing dependencies

Let’s install all our dependencies here and now. To begin, run the command below:

npm install @mdx-js/[email protected] @mdx-js/[email protected] gray-matter remark-frontmatter rehype-highlight @reach/tooltip @reach/disclosure

You must use @next to ensure you’re using the current version of mdx-js. At the time of this writing, the current version is 2.0.0. Now, let’s review what each of these dependencies does.

  1. @mdx-js/[email protected] is the integration, provided by MDX for webpack bundlers, that compiles Markdown and MDX into JavaScript. We will configure it in our next.config.js file
  2. @mdx-js/[email protected] provides the context for our app which we can wrap our components with and easily style our markdown contents
  3. gray-matter: we will use Front Matter in this blog, and gray-matter will parse it into an object of metadata
  4. remark-frontmatter: MDX does not provide support for Front Matter, so we need to use this package to ignore Front Matter when we view our MDX files as pages in the browser
  5. rehype-highlight allows us to use highlight.js to apply syntax highlighting to any code block
  6. @reach/tooltip provides a component we will use to create a tooltip in our blog article
  7. @reach/disclosure provides a component we will use for writing a disclosure in our blog article.

Configuring Next.js

To configure the Next.js app, copy the following into your next.config.js:

import remarkFrontmatter from 'remark-frontmatter';
import rehypeHighlight from 'rehype-highlight';

export default {
  webpack: (config, options) => {
    config.module.rules.push({
      test: /\.mdx?$/,
      use: [
        options.defaultLoaders.babel,
        {
          loader: '@mdx-js/loader',
          options: {
            providerImportSource: '@mdx-js/react',
            remarkPlugins: [remarkFrontmatter],
            rehypePlugins: [rehypeHighlight],
          },
        },
      ],
    });

    return config;
  },
  reactStrictMode: true,
  pageExtensions: ['js', 'jsx', 'md', 'mdx'],
};

Notice that we use the ES2015 import and export statement because we can only import remark-frontmatter, instead of using the require statement. So with that, you can rename next.config.js to next.config.mjs.

The code above simply configures Next.js so we can use .mdx files as pages in Next.js. The options parameter is where we can input our plugins (remark and rehype). They are different and work separately: remark plugins parses Markdown, while rehype plugins parses HTML.

Also, in the options parameter, we have the providerImportSource to parse our React context provider from @mdx-js/react, which we installed earlier.

Just at the end of the webpack method, we passed in some other Next.js configurations. But, let’s add one more configuration for our Next.js Image loader:

export default {
  //..
  pageExtensions: ['js', 'jsx', 'md', 'mdx'],
  images: {
    loader: 'imgix',
    path: 'https://images.unsplash.com/',
  },
  //...
};

Creating your first Next.js page in MDX

Under the pages directory, create a new file called about.mdx. Now, you can write any Markdown or/and JSX content, or simply copy-paste the following:

import Head from 'next/head';

<Head>
  <title>Hello World - PressBlog</title>
</Head>

# Hello World

Start your Next.js server with npm run dev and visit your new /about page in the browser. You should receive the “Hello World” message with a “Hello World – PressBlog” as the title.

This usually would throw a 404 error, but thanks to the configuration we added in next.config.mjs, we can now create pages with MDX.

Now that we have everything set up, let’s start organizing our blog, shall we?

Organizing our MDX blog file structure

How would you organize and structure your files? We just created a page in MDX, right? So, should we create all of our pages in MDX, and then create custom components (widgets, you might say) in JavaScript? But, if we can create custom components in MDX too, then what’s stopping us from just creating everything and anything in MDX?

Well, the truth is, organizing your blog with MDX can be confusing at first. Now that you have two tools that can do almost the same thing, the question you should be asking is, which should you use and when should you use it?

To answer this, we need to first understand that the purpose of MDX is not to replace JSX in all use cases. The purpose of MDX is to help us easily produce interactive and dynamically rich content in Markdown.

So, although MDX can be used to make a header component that only contains the name and logo of the blog with some links, for example, we shouldn’t use it because that would just be a waste of a tool.

When you have lots of content to write, you should use MDX rather than JSX. In a blog like this, we only have lots of content in our articles. That’s why, in most MDX use cases, you can hardly find an .mdx file in the components folder — it’s because they don’t need it there.

Now that we decided to only use MDX for our articles, we must now decide how our articles will be structured. We’ve seen earlier that it is possible to create a page in MDX. Wouldn’t it be simpler to have all of our posts/articles as a page in MDX?

What this means is we can have a posts directory under the pages directory in our Next.js app, and we can simply write all our posts in MDX in this directory.

For example, our articles would look like this:

  • pages/posts/what-is-react-native.mdx
  • pages/posts/where-to-learn-react-native.mdx

Traditionally, and alternatively, in Next.js, you would have a posts directory (that contains your articles in Markdown) outside of the pages directory, and another posts directory inside the pages directory with only one dynamic file (route).

That is, for example, pages/posts/[slug].js and fetching the required post in [slug].js based on the slug.

This is still possible, but it kind of kills MDX’s usefulness. In this article, we will use the first approach only, but I will also show you how you can fetch and parse MDX files from a directory, which is basically how the second approach works.

At the end of this tutorial, this is how the structure of our project will look (not including unchanged directories and files that come with Next.js by default):

PressBlog
|
|___ next.config.mjs
|
|___ components
|     |___ Header.js
|     |___ MDXComponents.js
|     |___ MeetMe.js
|     |___ Meta.js
|     |___ PostItem.js
|
|___ layouts
|     |___ Layout.js
|
|___ api
|     |___ posts.js
|
|___ pages
|     |___ index.js
|     |___ about.mdx
|     |___ _app.js
|     |___ _document.js
|     |___ posts
|           |___ learn-react-navigation.mdx
|           |___ what-is-react-native.mdx
|
|___ scripts
|     |___ utils.js
|
|___ styles
|     |___ globals.css
|     |___ Header.module.css
|     |___ Home.module.css
|     |___ Markdown.module.css

Using CSS for styling

In this tutorial, we will only use the Next.js styling approach; no Tailwind, no Bootstrap. But, it is possible to use styled-components or any utility-first framework.

Let’s apply all used styles (which are minimal) for this blog here and now.

First, let’s add Header.module.css, which applies to our header only. So, copy and paste the following styles into it:

.header {
  padding: 10px 0;
  background-color: rgb(164, 233, 228);
}

.header > div {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.header li {
  list-style: none;
  display: inline-block;
  margin: 0 10px;
}

Next, we’ll update globals.css, which already exists, so you don’t have to create it, just add the following styles to it:

a {
  color: inherit;
  text-decoration: underline transparent;
  transition: 0.25s ease-in-out;
}

a:hover {
  text-decoration-color: currentColor;
}

.max-width-container {
  max-width: 680px;
  margin-left: auto;
  margin-right: auto;
  padding: 0 10px;
}

.max-width-container.main {
  padding: 50px 30px;
}

p {
  line-height: 1.6;
}

Then, apply the Home.module.css style (replace any available styles), which will only apply to the home page:

.mainContainer {
  padding: 15px 30px;
  padding-top: 45px;
}

.articleList {
  margin-top: 55px;
}

.desc {
  color: #3b3b3b;
  font-style: italic;
  font-size: 0.85rem;
}

.postItem {
  margin-bottom: 40px;
}

.img {
  max-width: 100%;
  height: auto;
  border-radius: 50%;
}
.button {
  display: block;
  max-width: 300px;
  width: 100%;
  cursor: pointer;
  border-radius: 4px;
  margin: 0 auto;
  padding: 10px;
  border: none;
  background-color: #3b3b3b;
  color: #fff;
}

Finally, use Markdown.module.css, which will apply only to our articles’ contents:

.postTitle {
  border-bottom: 2px solid rgb(164, 233, 228);
  padding-bottom: 13px;
}

.link {
  color: rgb(0, 110, 255);
}

.tooltipText {
  background: hsla(0, 0%, 0%, 0.75);
  color: white;
  border: none;
  border-radius: 4px;
  padding: 0.5em 1em;
}

Creating the About page

For now, let’s work on our About page. Recall that we already created an about.mdx page; now, we can insert Lorem ipsum text into it. But before that, let’s first create a component called MeetMe.js. This component will contain an image and a short description of the author.

By creating it as a component, we can use it on the home page as well. So, go ahead and create a components directory in the root directory of the app. Then, create a file called MeetMe.js and copy and paste the following into it:

import Image from 'next/image';
import styles from '../styles/Home.module.css';

const MeetMe = () => {
  return (
    <div>
      <Image
        src='photo-1618077360395-f3068be8e001?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NHx8bWFufGVufDB8MnwwfHw%3D&auto=format&fit=crop&w=500&q=60'
        alt='john doe avatar'
        width={150}
        height={150}
        className={styles.img}
      />
      <p className={styles.p}>
        Hey, I am <strong>John Doe</strong>. I love coding. Lorem ipsum dolor
        sit, amet consectetur adipisicing elit. Reiciendis commodi numquam
        incidunt blanditiis quibusdam atque natus inventore sunt autem iusto.
      </p>
    </div>
  );
};

export default MeetMe;

Not a lot is happening here; we are simply importing the Image component from next/image and also applying styles to the paragraph and image using the Home.module.css styles, which we already created.

Now that we have the MeetMe component set up, let’s use it in about.mdx:

import MeetMe from '../components/MeetMe.js';

<MeetMe />

#### Let's dive into more of me

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ea deserunt ab maiores eligendi nemo, ipsa, pariatur blanditiis, ullam exercitationem beatae incidunt deleniti ut sit est accusantium dolorum temporibus ipsam quae.

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ea deserunt ab maiores eligendi nemo, ipsa, pariatur blanditiis, ullam exercitationem beatae incidunt deleniti ut sit est accusantium dolorum temporibus ipsam quae.

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ea deserunt ab maiores eligendi nemo, ipsa, pariatur blanditiis, ullam exercitationem beatae incidunt deleniti ut sit est accusantium dolorum temporibus ipsam quae.

Lastly, before we check out of this page, let’s create a Meta component that will contain our dynamic meta tags and title. So, head up to the components directory, create a new file called Meta.js, and copy and paste the following:

import Head from 'next/head';

const Meta = ({ title }) => {
  return (
    <Head>
      <title>{title}</title>
      <meta
        name='keywords'
        content='react native, blog, John Doe, tutorial, react navigation'
      />
    </Head>
  );
};

export default Meta;

// let's set a default title
Meta.defaultProps = {
  title: 'PressBlog - Your one stop blog for everything React Native',
};

To keep things simple, we will only use static meta keywords and a dynamic title. In a real blog, you would need to optimize for search engines. next-seo is a good tool for that.

Let’s import our new component Meta.js into about.mdx, and use as such

{
  /* .. */
}
import Meta from '../components/Meta.js';

<Meta title='About John Doe - PressBlog' />;

{
  /* .. */
}

With that, we can save our files and test our about page.

Creating the layout

The next thing we’ll work on is the layout of the blog. Before we proceed, we need a header component, so let’s create it. Head up to the components directory and create a new file called Header.js and copy and paste the following:

import Link from 'next/link';
import styles from '../styles/Header.module.css';

const Header = () => {
  return (
    <header className={styles.header}>
      <div className='max-width-container'>
        <h2>
          <Link href='/'>PressBlog</Link>
        </h2>
        <ul>
          <li>
            <Link href='/about'>About</Link>
          </li>
        </ul>
      </div>
    </header>
  );
};

export default Header;

Here, we use the Link component from next/link. Also, we use the Header.module.css styles, which we created earlier.

There is a tiny bit of change for the styles, however. Notice that we used the class name max-width-container, which is not from Header.module.css. It is not a typo; the class name was previously used in globals.css, this is because we need it globally.

Next, let’s create a directory called layouts in the root directory and create a file in it called Layout.js:

import Header from '../components/Header';

const Layout = ({ children }) => {
  return (
    <div>
      <Header />
      <main className='max-width-container main'>{children}</main>
    </div>
  );
};

export default Layout;

Here, we import the Header component we just created and pass it as the header of our blog layout. You can do the same with a footer, but we are not using a footer for this blog.

Now, to apply the Layout component to our app, head up to pages/_app.js and wrap the Layout component around the Component component:

//..
return (
  <Layout>
    <Component {...pageProps} />
  </Layout>
);

//..

Adding the articles

In this section, we will create two articles. We will use these articles later on the home page., but for now, we will just create and test them as single pages.

Let’s create a new directory called posts under the pages directory. In this new directory, create the following MDX files.

We’ll start with learn-react-navigation.mdx:

---
title: React Navigation Tutorial
publishedOn: January, 6th. 2022
excerpt: React Navigation is a number one tool to learn in React Native. You would always use them; these are some of the best practices in using React Navigation
---

import Meta from '../../components/Meta';
import { Disclosure, DisclosureButton, DisclosurePanel } from "@reach/disclosure";

<Meta title='React Navigation Tutorial - PressBlog' />

# React Navigation Tutorial

React Navigation is one of the best things that happened to [**React Native**](https://reactnative.org)

## Benefits of using React Navigation

1. It is cool to use
2. It is simple to learn
3. It has an extensive community
4. And more and more

<Disclosure>
  <DisclosureButton as='div'>React Navigation won't be here forever. Click to find why</DisclosureButton>
  <DisclosurePanel>Nothing lasts forever, yeah! That's all I got to say</DisclosurePanel>
</Disclosure>

The Disclosure components and their counterparts only demonstrate using components and widgets in MDX, and how useful they can be for your articles. Our next article uses a tooltip, so let’s check it out by using what-is-react-native.mdx:

---
title: What is React Native?
publishedOn: January, 7th. 2022
excerpt: Lorem, ipsum dolor sit amet consectetur adipisicing elit. Ea deserunt ab maiores eligendi nemo, ipsa, pariatur
---

import Meta from '../../components/Meta.js';
import Tooltip from '@reach/tooltip';
import '@reach/tooltip/styles.css';

<Meta title='What is React Native - PressBlog' />

# What is React Native?

React Native is a lovely language

<Tooltip label="It's not actually a language though">
  <button>Fact about React native</button>
</Tooltip>
### React Native App Example:

```js
const App = () => {
  return (
    <View>
      <Text>Hello World Example</Text>
    </View>
  );
};

export default App;
```

You can test each of your articles in the browser to see if it works. To test, use the URL /posts/what-is-react-native or /posts/learn-react-navigation. Notice that the part that contains our metadata doesn’t show up in the browser, which is a good thing because we don’t want our readers to see this.

This is possible with remark-frontmatter, which we installed and configured in next.config.mjs. The JavaScript code we wrote is not styled, though we have configured rehype-highlight — this is because we would need a highlight.js theme. We can also use the GitHub dark theme available on cdnjs by adding it to our app with the HTML link tag.

We can add global and static HTML link tags in our Next.js app by updating the _document.js file. There isn’t any yet, so let’s create one. Head up to the pages directory and create a new file _document.js, then paste the following:

import { Html, Head, Main, NextScript } from 'next/document';

const MyDocument = () => {
  return (
    <Html lang='en'>
      <Head>
        <link
          rel='stylesheet'
          href='https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.4.0/styles/github-dark.min.css'
        />
      </Head>
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
};

export default MyDocument;

We have created a custom document that renders every page in Next.js. Now when you check the article at the URL, the code should be highlighted and styled.

Adding article-specific styles

Let’s add styles that are specific to our blog articles only. With the approach we will use, we will be able to style any element in MDX.

Note that when I say element, I mean when it compiles to JSX, for example, # Hello world in MDX compiles to h1 in JSX. So, with this approach, we can style all h1 elements in our MDX.

The way this is done is by wrapping our app components with the MDXProvider from @mdx-js/react, and providing an object of components for mapping as many elements as needed.

So, head up to the components directory, create a new file called MDXComponents.js, and copy and paste the following code into it:

import styles from '../styles/Markdown.module.css';

const MDXComponents = {
  p: (props) => <p {...props} className={styles.p} />,
  a: (props) => <a {...props} className={styles.link} />,
  h1: (props) => <h1 {...props} className={styles.postTitle} />,
};

export default MDXComponents;

Here, we import the styles we created earlier for Markdown contents only. Each key of the MDXComponents object is a component that corresponds to whatever MDX compiles to.

You can now create custom components for any JSX elements:

import { Sparky } from 'sparky-text'; // a dummy module - does not exist
import Link from 'next/link';
import styles from '../styles/Markdown.module.css';

const MDXComponents = {
  strong: ({ children }) => <Sparky color='gold'>{children}</Sparky>,
  p: (props) => <p {...props} className={styles.p} />,
  a: (props) => <Link {...props} className={styles.link} />,
  h1: (props) => <h2 {...props} className={styles.postTitle} />, // for some reasons I want h1 to be mapped, styled and displayed as h2
};

// please do not use this example, this is only a demonstration

Right now, our MDXComponents.js has no effect. So, to fix that, head up to pages/_app.js and import MDXProvider from @mdx-js/react, and the MDXComponents component we just created.

Wrap the MDXProvider around the Layout component and pass MDXComponents as a component prop for MDXProvider. In summary, your _app.js should look like this:

import '../styles/globals.css';
import { MDXProvider } from '@mdx-js/react';
import MDXComponents from '../components/MDXComponents';
import Layout from '../layouts/Layout';

function MyApp({ Component, pageProps }) {
  return (
    <MDXProvider components={MDXComponents}>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </MDXProvider>
  );
}

export default MyApp;

Creating the home page

The home page contains all of our articles. We will have ten articles by duplicating the available articles by increments of 5 each. This will be useful for demonstrating pagination.

Let’s begin by fetching the two available articles using the Node.js file system and path module. Let’s head up to the pages/api directory and create a new file posts.js.

The api directory is where we can build API routes for our app. We only need one route to fetch all posts (available in the pages/posts directory) in MDX.

We will not make any requests to this route in Next.js’ getStaticProps function, the reason being that the page it’s used on will not prerender in the production build. Next.js recommends that server-side code written in an api route and meant to be used in the getStaticProp function should be written in the getStaticProp function. We will eventually need the posts api route for pagination.

This will all be clearer soon. For now, just know that fetching the articles will be done in both an api route and a getStaticProps function.

Create a scripts directory in the root directory and create a utils.js file in it. This file will contain a getPosts function (and some other functions) that fetches the articles.

This is how we can fetch all posts in the getPosts function:

  1. Get .mdx files in pages/posts using the Node.js system module’s readdirSync and readFileSync methods
  2. Parse each file’s Front Matter into an object of metadata using gray-matter
  3. Generate a slug for each file using the filename

Now, let’s see the code. In scripts/utils.js, copy and paste the code below:

import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';

export const getPosts = (pageIndex) => {
  const dirFiles = fs.readdirSync(path.join(process.cwd(), 'pages', 'posts'), {
    withFileTypes: true,
  });

  const posts = dirFiles
    .map((file) => {
      if (!file.name.endsWith('.mdx')) return;

      const fileContent = fs.readFileSync(
        path.join(process.cwd(), 'pages', 'posts', file.name),
        'utf-8'
      );
      const { data, content } = matter(fileContent);

      const slug = file.name.replace(/.mdx$/, '');
      return { data, content, slug };
    })
    .filter((post) => post);

  return posts
};

This is quite a lot, but let’s go through each line.

The first thing we do is import our modules, which are fs (from Node.js), path (from Node.js), and matter (from gray-matter).

The readdirSync gets all files in the pages/posts directory with their file extension and is assigned to the dirFiles variable.

Then we loop through the dirFiles to filter out all files that aren’t MDX. While in the loop, we can read each file’s content using the readFileSync method, and pass the content as a parameter for the matter function.

The matter function returns an object in which we only need data and content. data is the metadata that we used Front Matter to write in each of our articles, and content is the remaining content of the articles.

Finally, in the loop, we generate a slug from the filename (without the extension) and return an object of data, content, and slug from our loop function.

You want to make sure you’re not getting all of your posts asynchronously, which is the reason for the readdirSync and readFileSync because they are both synchronous.

Now, let’s head back to api/posts.js and create a function to handle the request to this route. Copy and paste this in api/posts.js:

import { getPosts } from '../../scripts/utils';

export default function handler(req, res) {
  const posts = getPosts(2); // argument will change

  res.status(200).json(posts);
}

The handler function is just like the one in Express.js — the req and res parameters are used for viewing request parameters and handling responses, respectively. Save this file, we will need it later.

For now, we can open up pages/index.js and clear out all tags (in the return statement) and import statements (they come by default for every Next.js app). Create a new function, getStaticProps, outside the Home function, and copy and paste the code below into it:

export const getStaticProps = () => {
  const posts = getPosts(1); // the argument has no effect yet

  return {
    props: {
      posts,
    },
  };
};

If you’ve worked with Next.js, this is very straightforward. But if you haven’t, it is a function used for data fetching in Next.js. Next.js uses the props returned from the exported getStaticProps function to prerender our index.js page at build time. This process is called static site generation.

You can perform lots of filtering and sorting here. For instance, if you want to get the last ten published articles — not just any ten — you can sort it out with the publishedOn metadata returned from each post.

However, note that you must export the getStaticProps function, or else it would just behave like any other function. In this function, you can fetch data from a CMS database because, technically, it runs in the server, not in the client.

Next.js gives us the comfort of using the props returned from the getStaticProps in our Home function. What this means is our Home function on this page (pages/index.js) will have posts as props passed into it:

const Home = ({ posts }) => {
  return (
    <>
      <Meta />
      <MeetMe />
      <Link href='/about'>More about me</Link>

      <div className={styles.articleList}>
        <p className={styles.desc}>Newly Published</p>
        {posts.map((post) => (
          <p key={post.slug}>{post.data.title}</p>
        ))}
      </div>
    </>
  );
};
export default Home;

We are using the Meta component to add meta tags to our page. The MeetMe component displays a short description of the author, in which there is a link to a full description on the About page.

Lastly, we loop through the posts array coming as a prop from our Home function. You should import the components and functions used in the Home function including styles from Home.module.css.

As for now, we are only displaying each article’s title in a paragraph, which is far from what we want. Let’s go ahead and create a new component called PostItem.

To do this, create a file in the components directory called PostItem.js, and copy and paste in the code below:

import Link from 'next/link';
import styles from '../styles/Home.module.css';

const PostItem = ({ post }) => {
  return (
    <div className={styles.postItem}>
      <h3>
        <Link href={`/posts/${post.slug}`}>{post.data.title}</Link>
      </h3>
      <p>{post.data.excerpt}</p>
      <Link href={`/posts/${post.slug}`}>Read more</Link>
    </div>
  );
};

export default PostItem;

Now, head back to pages/index.js and import the PostItem component we just created. Then, replace the paragraph that displays the title of each post with <PostItem key={post.slug} post={post} />:

//..
<div className={styles.articleList}>
  <p className={styles.desc}>Newly Published</p>
  {posts.map((post) => (
    <PostItem key={post.slug} post={post} />
  ))}
</div>
//..

Finally, save the files and test the home page in your browser. This is the same principle we will apply to the blog page.

Adding pagination to our home page

Loading all articles at once is really not a good practice for large datasets. Having a large amount of data on a page can affect the performance of your app, so it’d be best if we load them in chunks. For example if we had ten articles, we could load five per page.

Let’s demonstrate this in our blog. Create ten articles by creating a function in scripts/utils.js to duplicate our existing articles. Paste the code below before or after the getPosts function.

const createMultiplePosts = (posts) => {
  const multiplePosts = [];

  posts.forEach((post) => {
    for (let i = 0; i < 5; i++) {
      multiplePosts.push(post);
    }
  });

  return multiplePosts;
};

We loop through any available article and produce five of each article.

The function we require to handle the pagination will also go here, in utils.js, so paste the code below, before or after the createMultiplePosts function.

const filterPostsByPageIndex = (posts, pageIndex) => {
  const postPerPage = 5;
  // get the total posts from page 1 to current page
  const totalPagePosts = +pageIndex * postPerPage;

  // get the total posts from page 1 to previous page
  const prevPagePosts = totalPagePosts - postPerPage;

  return posts.filter(
    (post, index) => index < totalPagePosts && index >= prevPagePosts
  );
};

This function will help us incrementally add five articles to the page whenever a request is made to it.

Now, let’s update the getPosts function to return a duplicated and filtered post. Recall that we already have a pageIndex parameter passed into the function, so we can replace the return statement with this:

return filterPostsByPageIndex(createMultiplePosts(posts), pageIndex);

We also have to update the posts API route to request a page index as a query whenever a request is made to it. Get the page query and add this to the beginning of the api/posts.js handler function:

  const { page } = req.query;

So you would have:

export default function handler(req, res) {
  const { page } = req.query;

//...

Lastly, change the getPosts argument from 2 to page, i.e.:

//..
const posts = getPosts(page);
//..

Here is how the above works:

  1. Load first five articles in the getStaticProps function and prerender
  2. Incrementally load and display five more articles to the page based on the user’s request. For this blog, the user can make such a request with a “Load more” button.

With that said, there are going to be a ton of changes to the Home function in pages/index.js. Paste the code below to replace the Home function:

const Home = ({ posts }) => {
  const [filteredPosts, setFilteredPosts] = useState(posts);
  const [currentPageIndex, setCurrentPageIndex] = useState(1);

  const loadMorePosts = async () => {
    const res = await fetch(`/api/posts?page=${currentPageIndex + 1}`); // absolute url is supported here
    const posts = await res.json();

    setFilteredPosts((_posts) => [..._posts, ...posts]);
    setCurrentPageIndex((_pageIndex) => _pageIndex + 1);
  };

  return (
    <>
      <Meta title='PressBlog - Your one stop blog for anything React Native' />
      <MeetMe />
      <Link href='/about'>More about me</Link>

      <div className={styles.articleList}>
        <p className={styles.desc}>Newly Published</p>
        {filteredPosts.map((post, index) => (
          <PostItem key={index} post={post} />
        ))}
        <button onClick={loadMorePosts} className={styles.button}>
          Load more
        </button>
      </div>
    </>
  );
};

export default Home;

When the loadMorePosts function is clicked, it will make a get request to our API posts route and fetch the next five articles. So in summary, this is the pages/index.js page:

import MeetMe from '../components/MeetMe.js';
import Link from 'next/link';
import PostItem from '../components/PostItem';
import styles from '../styles/Home.module.css';
import Meta from '../components/Meta';
import { useState } from 'react';

const index = ({ posts }) => {
  const [filteredPosts, setFilteredPosts] = useState(posts);
  const [currentPageIndex, setCurrentPageIndex] = useState(1);

  const loadMorePosts = async () => {
    const res = await fetch(`/api/posts?page=${currentPageIndex + 1}`); // absolute url is supported here
    const posts = await res.json();

    setFilteredPosts((_posts) => [..._posts, ...posts]);
    setCurrentPageIndex((_pageIndex) => _pageIndex + 1);
  };

  return (
    <>
      <Meta title='PressBlog - Your one stop blog for anything React Native' />
      <MeetMe />
      <Link href='/about'>More about me</Link>

      <div className={styles.articleList}>
        <p className={styles.desc}>Newly Published</p>
        {filteredPosts.map((post, index) => (
          <PostItem key={index} post={post} />
        ))}
        <button onClick={loadMorePosts} className={styles.button}>
          Load more
        </button>
      </div>
    </>
  );
};

export default index;

export const getStaticProps = () => {
  const posts = getPosts(1);

  return {
    props: {
      posts,
    },
  };
}

Now, save the file and test the new blog page (visit / in your browser). With that, we’ve successfully built a simple blog with Next.js and MDX.

You might be wondering why we used the getPosts function in the loadMorePosts function, rather than creating an API route. Well, this is because the getPosts function is server-side code, and it will lead to an error if used on the client side. And, of course, you can’t use any internal API route in getStaticProps in production.
As a result, this is the only simple approach to achieving pagination with static site generation.

Conclusion

Creating an interactive blog or documentation has never been this easy with Markdown. If you followed along, you are one step away from having a blog of your own. You can decide whether to add more features before deploying.

If you hope to build documentation, this tutorial can help you as well, or you can simply use Nextra. Nextra uses Next.js and MDX to create a website for your documentation, and of course, you get to write all your content in MDX.

To round it all up, in this article we learned about an interesting tool called MDX, what we can do with it, like writing expressions and passing props, and creating a blog with MDX and Next.js.

In building this blog, we saw how we can configure MDX to work in Next.js, how we should structure our apps, and how we can fetch and parse MDX files into an object of metadata (from Front Matter) and content.

So, that’s it. Thanks for reading.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Elijah Agbonze I am a full-stack software developer from Nigeria. I love coding and writing about coding.

One Reply to “Create a Next.js and MDX blog”

Leave a Reply