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?
- Setup a simple NextJS app
- Use gray-matter to extract metadata from mdx file
- Use next-mdx-remote to compile html
- Add code highlighting with rehype-highlight
- Improve the SEO of your app
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
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
:
---
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 : Used to separate the metadata and content of markdown
- next-mdx-remote : Used to compile html and display it on the page
- rehype-highlight : Used to add highlight to code blocks
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
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:
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:
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.
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,
},
};
}
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,
};
}
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;
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.
- 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 theMDXRemote
component through thecomponents
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
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";
13.2. 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.
Top comments (0)