DEV Community

Cover image for Create your own blog with MDX and NextJS
Tifani Dermendzhieva for Zone 2 technologies Ltd.

Posted on • Updated on • Originally published at zone2.tech

Create your own blog with MDX and NextJS

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.0. To create a NextJS app run the following command in your terminal:

npx create-next-app@latest
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

## This is article 1
Enter fullscreen mode Exit fullscreen mode

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

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:

npm i gray-matter next-mdx-remote rehype-highlight
Enter fullscreen mode Exit fullscreen mode

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

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

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:

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

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:

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

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.

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.

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

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:

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

  return {
    paths: allPostIds,
    fallback: false,
  };
}
Enter fullscreen mode Exit fullscreen mode

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:

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

export default Article;
Enter fullscreen mode Exit fullscreen mode

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.

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

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

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

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

import rehypeHighlight from "rehype-highlight";
import "highlight.js/styles/agate.css";
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

With this our blog app is complete.

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

Top comments (0)