Create your own blog with MDX and NextJS
Tags: nextjs, mdx, javascript
Published: 2022-12-05 14:30:05

When we decided to implement a blog feature to the Zone 2 Technologies webpage, our team conculded it would be best to use MDX for writing the content of articles as it brings together the ease of use of regular Markdown language and allows the use of custom components.

In this article we walk you through the process of creating a simple blog app using the popular React framework [NextJS](https://nextjs.org), [`gray-matter`](https://www.npmjs.com/package/gray-matter) and [`next-mdx-remote`](https://www.npmjs.com/package/next-mdx-remote).

## Table of Contents

- [What is MDX?](#mdx)
- [Setup a simple NextJS app](#create-nextjs-app)
- [Use gray-matter to extract metadata from mdx file](#use-graymatter)
- [Use next-mdx-remote to compile html](#next-mdx-remote)
- [Add code highlighting with rehype-highlight](#code-highlight)
- [Improve the SEO of your app](#improve-seo)


### What is MDX?

MDX is a format which combines JSX and Markdown. Markdown is easier to write than HTML and is therefore the preferred option for writing content such as blog posts.
JSX, on the other hand, is an extension of JS which looks like html and allows the reuse of components.

### Setup NextJS app


1.0. To create a NextJS app run the following command in your terminal:

```shell
npx create-next-app@latest
```

2.0. Create a `/src` folder at the root of the project and move the folder `/pages` inside it, so the project structure is as follows:

```
 ┣ node_modules
 ┣ public
 ┣ src
 ┃  ┣ pages
 ┃  ┃ ┣ _app.js
 ┃  ┃ ┗ index.js
 ┣ .gitignore
 ┣ next.config.js
 ┣ package-lock.json
 ┣ package.json
 ┣ README.md
```

3.1. Create a `/posts` folder and add a few articles inside it:

```
 ┣ node_modules
 ┣ public
 ┣ src
 ┃  ┣ pages
 ┃  ┃ ┣ [id].jsx
 ┃  ┃ ┣ _app.js
 ┃  ┃ ┗ index.js
 ┃  ┣ posts
 ┃  ┃ ┣ article-1.mdx
 ┃  ┃ ┗ article-2.mdx
 ┣ .gitignore
 ┣ next.config.js
 ┣ package-lock.json
 ┣ package.json
 ┣ README.md
```

3.2. Example content for the `article-1.mdx` and `article-2.mdx` :

```md
---
title: "Article 1"
id: "article-1"
---

## This is article 1
```

**Note:** Make sure that the `id` meta tag matches the name of the mdx file as it will be used in dynamic routing later on.

4.0. Create a `/services` folder in `/src` and add a JavaScript file `blog-services.js`:

```
 ┣ node_modules
 ┣ public
 ┣ src
 ┃  ┣ pages
 ┃  ┃ ┣ [id].jsx
 ┃  ┃ ┣ _app.js
 ┃  ┃ ┗ index.js
 ┃  ┣ posts
 ┃  ┃ ┣ article-1.mdx
 ┃  ┃ ┗ article-2.mdx
 ┃  ┗ services
 ┃    ┗ blog-services.js
 ┣ .gitignore
 ┣ next.config.js
 ┣ package-lock.json
 ┣ package.json
 ┣ README.md
```


### Use gray-matter to extract metadata from mdx file

5.0. Now that we have the project structure let's install the packages we need to compile html from the mdx:

- [gray-matter](https://www.npmjs.com/package/gray-matter) : Used to separate the metadata and content of markdown
- [next-mdx-remote](https://www.npmjs.com/package/next-mdx-remote) : Used to compile html and display it on the page
- [rehype-highlight](https://www.npmjs.com/package/rehype-highlight) : Used to add highlight to code blocks

```shell
npm i gray-matter next-mdx-remote rehype-highlight
```

6.0. In `/src/services/blog-services.js` write a function which will receive the filename (id) of an article, read it and return its metadata and content.
To achieve this use the `matter()` function from the `gray-matter` package

```js
import matter from "gray-matter";
import { join } from "path";
import * as fs from "fs";

export async function getArticleById(fileId) {
  const postsDirectory = join(process.cwd(), "./src/posts");
  const fullPath = join(postsDirectory, `${fileId}.mdx`);

  const fileContents = fs.readFileSync(fullPath, "utf8");

  const { data, content } = matter(fileContents);
  return { ...data, content };
}
```

7.0. Now that we have extracted the content of the article, we need another function to list all of the articles stored in the `/posts` directory. In the same file add the following:

```js
export async function getAllArticles() {
  const articlesList = [];
  const postsDirectory = join(process.cwd(), "./src/posts");
  const filesList = fs.readdirSync(postsDirectory);

  for (let fname of filesList) {
    const id = fname.replace(/\.mdx$/, "");

    const articleInfo = await getArticleById(id);
    articlesList.push({ ...articleInfo });
  }
  return articlesList;
}
```

8.0. We can now access the metadata and read the content of each article. Awesome!
Let's display the articles on our homepage. What we have to do is use the `getStaticProps()` function to load all available articles and pass them down to the component as props.
In the `/src/pages/index.js` write the following:

```js
import { getAllArticles } from "../services/blog-services";

export async function getStaticProps() {
  const articles = await getAllArticles();
  return { props: { articles } };
}

export default function Home({ articles }) {
  return (
    <>
      <h1>Blog Articles:</h1>

      {articles.map((article, key) => (
        <div key={key}>
          <p>{article.title}</p>
          <a href={`/${article.id}`}> Read More</a>
        </div>
      ))}
    </>
  );
}
```

**Note:** Don't forget to add the `key` attribute when looping through elements!

### Use next-mdx-remote to compile html

9.0. In order to access each article inividually, we will use dynamic routing. If you are not familiar with dynamic routing I advise you to look it up in the [NextJS Documentation](https://nextjs.org/docs/routing/dynamic-routes).

9.1. Create a `/src/pages/[id].jsx` file and export a component which will be used as a template for each article. The component must receive the article as props, so that let's begin with
the `getStaticProps()` function. The id of the article is accessible through the `context`. In order to display the content from mdx we need to compile it to html first. To do so, use the `serialize()` function from the `next-mdx-remote` package.

```js
import { getArticleById } from "../services/blog-services";
import { getAllArticles } from "../services/blog-services";

import { serialize } from "next-mdx-remote/serialize";
import { MDXRemote } from "next-mdx-remote";

export async function getStaticProps(context) {
  const { id } = context.params;
  const articleInfo = await getArticleById(id);

  const serializedPost = await serialize(articleInfo.content);
  return {
    props: {
      ...articleInfo,
      source: serializedPost,
    },
  };
}
```

9.2. When using dynamic routing we need to use the `getStaticPaths()` function to generate a route for each article. So, let's add one:

```js
export async function getStaticPaths() {
  const allPosts = await getAllArticles();
  let allPostIds = allPosts.map((post) => `/${post.id}`);

  return {
    paths: allPostIds,
    fallback: false,
  };
}
```

9.3. And finally, the `Article` component itself. Since we are using the `next-mdx-remote` package, we have to import the `MDXRemote` component and pass down the serialized content to it like shown:

```js
const Article = (article) => {
  return (
    <>
      <h1>{article.title}</h1>
      <MDXRemote {...article.source} components={{}}></MDXRemote>
    </>
  );
};

export default Article;
```

9.4 It is **important to note** that if there are any **imported components inside the mdx file** you have to pass them down the `MDXRemote` through the **`components` attribute**. To make it clear let's add a custom image component.

10. Add a `/src/components/ImageCard.jsx` with the following code:

```js
export default function ImageCard({
  imageSrc,
  altText,
  width = "200px",
  height,
}) {
  console.log(imageSrc);
  return height ? (
    <img src={imageSrc} alt={altText} width={width} height={height} />
  ) : (
    <img src={imageSrc} alt={altText} width={width} />
  );
}
```

11. To use the component inside the mdx file, simply add it as a JSX tag, e.g.:

```md
---
title: "Article 1"
id: "article-1"
---

## This is article 1

<ImageCard imageSrc="https://images.pexels.com/photos/1001682/pexels-photo-1001682.jpeg" altText="sea">
```

**Note:** Notice that you don't have to explicitly import the component in the mdx file.

12. In order to use the `<ImageCard>` you have to import it in the `[id].jsx` file and pass it down the `MDXRemote` component through the `components` attribute (i.e. `components={{ ImageCard }}`):

```js
import { getArticleById } from "../services/blog-services";
import { getAllArticles } from "../services/blog-services";

import { serialize } from "next-mdx-remote/serialize";
import { MDXRemote } from "next-mdx-remote";
import { ImageCard } from "../components/ImageCard";

export async function getStaticProps(context) {
  const { id } = context.params;
  const articleInfo = await getArticleById(id);

  const serializedPost = await serialize(articleInfo.content);
  return {
    props: {
      ...articleInfo,
      source: serializedPost,
    },
  };
}

export async function getStaticPaths() {
  const allPosts = await getAllArticles();
  let allPostIds = allPosts.map((post) => `/${post.id}`);

  return {
    paths: allPostIds,
    fallback: false,
  };
}

const Article = (article) => {
  return (
    <>
      <h1>{article.title}</h1>
      <MDXRemote {...article.source} components={{ ImageCard }}></MDXRemote>
    </>
  );
};

export default Article;
```


### Add code highlighting with rehype-highlight

13.1. Congratulations! Now our blog is fully functional. However, if we want it to
look better, we can add code highlighting theme with the `rehype-highlight` package.

We already inastalled the package in step 5.0., so what remains to be done is select
a theme and import it in `/src/pages/[id].jsx`. You can check out the available themes on
[https://highlightjs.org/static/demo](https://highlightjs.org/static/demo/). Our theme of choice is Agate:

```js
import rehypeHighlight from "rehype-highlight";
import "highlight.js/styles/agate.css";
```

13.2. Lastly, add the `rehypeHighlight` as plugin to the serialize function.

```js
const serializedPost = await serialize(articleInfo.content, {
  mdxOptions: {
    rehypePlugins: [rehypeHighlight],
  },
});
```

With this final step, the complete `[id].jsx` file looks like this:

```js
import { getArticleById } from "../services/blog-services";
import { getAllArticles } from "../services/blog-services";

import { serialize } from "next-mdx-remote/serialize";
import { MDXRemote } from "next-mdx-remote";
import ImageCard from "../components/ImageCard";

import rehypeHighlight from "rehype-highlight";
import "highlight.js/styles/agate.css";

export async function getStaticProps(context) {
  const { id } = context.params;
  const articleInfo = await getArticleById(id);

  const serializedPost = await serialize(articleInfo.content, {
    mdxOptions: {
      rehypePlugins: [rehypeHighlight],
    },
  });
  return {
    props: {
      ...articleInfo,
      source: serializedPost,
    },
  };
}

export async function getStaticPaths() {
  const allPosts = await getAllArticles();
  let allPostIds = allPosts.map((post) => `/${post.id}`);

  return {
    paths: allPostIds,
    fallback: false,
  };
}

const Article = (article) => {
  return (
    <>
      <h1>{article.title}</h1>
      <MDXRemote {...article.source} components={{ ImageCard }}></MDXRemote>
    </>
  );
};

export default Article;
```


### Improve the SEO of your blog

SEO (Search Engine Optimisation) is the process of improving the visibility of a page on search engines.

Search Engines use bots to crawl the web and collect information about each page and store it for further reference,
so that it can be used to retrieve the respctive webpage when it is being searched.

SEO is critical part of digital marketing as people often conduct search with commercial intent - to obtain information about a product/service. Ranking higher in search results can have an imense impact on the success of a business.

Fortunately, NextJS supports a component `<Head>`, which allows you to pass `<meta>` tags to your pages.

In our blog app we can improve the SEO by adding some meta tags describing the article.

First, let's add more information to the metadata of the article, which we will use in the meta tags:

```md
---
title: "Article 1"
description: "This is a very interesting and informative article"
author: "John Doe"
img: "https://images.pexels.com/photos/1001682/pexels-photo-1001682.jpeg"
id: "article-1"
---

## This is article 1

<ImageCard imageSrc="https://images.pexels.com/photos/1001682/pexels-photo-1001682.jpeg" altText="sea"/>
```

Finally, in the `/src/pages/[id].jsx` import the `<Head>` tag and populate it with the metadata from props:

```js
import Head from "next/head";

const Article = (article) => {
  return (
    <>
      <Head>
        <meta property="og:title" content={article.title} key="ogtitle" />
        <meta
          property="og:description"
          content={article.description}
          key="ogdesc"
        />
        <meta property="og:image" content={article.img} key="ogimage" />
        <meta
          property="og:url"
          content={`https://www.my-blog.com/${article.id}`}
          key="ogurl"
        />
        <meta property="og:type" content="article" key="ogtype" />
        <title>{`Blog | ${article.title}`}</title>
      </Head>
      <h1>{article.title}</h1>
      <MDXRemote {...article.source} components={{ ImageCard }}></MDXRemote>
    </>
  );
};

export default Article;
```

With this our blog app is complete.

#### Thank you for reading this article. I hope it has been helpful to you.