Create your own blog with MDX and NextJS

by Tifani Dermendzhieva

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, gray-matter and next-mdx-remote.

Table of Contents

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. To create a NextJS app run the following command in your terminal:
npx create-next-app@latest
  1. 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
  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
  1. Example content for the article-1.mdx and article-2.mdx :
---
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.

  1. 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

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

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

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

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 };
}

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:

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;
}

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:

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

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.

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.

import { getArticleById } from "../services/blog-services";
import { serialize } from "next-mdx-remote/serialize";

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,
    },
  };
}

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

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

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

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:

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

export default Article;

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.

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

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} />
  );
}

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

---
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.

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 }}):

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

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. Our theme of choice is Agate:

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

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

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

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

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:

---
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:

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.

How can we help?

We're passionate about solving challenges and turning exciting ideas into reality, together with you. If you have any questions or need assistance with your projects, we're here to help. Don't hesitate to get in touch!

Book a Call
or send a message