Create Blog with MDX and Contentlayer

Build a Static Blog with Contentlayer, a content SDK that validates and transforms your content into type-safe JSON data you can easily import into your application.

📆 August 2022, 3

⏳ 6 min read

  • # mdx
  • # contentlayer
  • # next.js
  • # typescript

A blog is a medium where we can share information, thoughts, or other content. Just like this blog, here I share my thoughts about web development and programming in general. When building a personal blog, there are many ways to store articles and serve them to visitors. We can use a CMS (Content Management System) platform to manage content. Alternatively, we can write content in MDX format, store it in a GitHub repo, and manage it with Contentlayer. Some of you might not be familiar with MDX and Contentlayer. So, in this article, I’ll show you what MDX and Contentlayer are and how we can build a personal blog with these two tools.

MDX

First, let’s get to know MDX. Have you ever seen a readme.md file when you initialize a repository on GitHub? Readme.md allows us to write documentation for a GitHub repository. It has a .md file extension. An .md file is a text file created using one of several possible dialects of the Markdown language. It is saved in plain text format but includes inline symbols that define how to format the text (e.g., bold, indentations, headers, table formatting). MD files are designed for authoring plain text documentation that can be easily converted to HTML.

For example, if I write a sample.md file like this:

# This is heading 1
This is how we write paragraphs in md files.
- This is a list
- List one
- List two
Or you can make a **bold** text, _italic_, or **_both_**

Then, converted to HTML, it will look like this:

<h1>This is heading 1</h1>
<p>This is how we write paragraphs in md files.</p>
<ul>
<li>This is a list</li>
<li>List one</li>
<li>List two</li>
</ul>
<p>Or you can make a <strong>bold</strong> text, <em>italic</em>, or <strong><em>both</strong></em></p>

And that’s MD all about. Basically, MDX is the same as MD but it has more features. MDX allows you to use JSX in your markdown content. You can import components, such as interactive charts or alerts, and embed them within your content. This makes writing long-form content with components a blast.

import { Chart } from "./snowfall.js";
export const year = 2018;
# Last year's snowfall
In {year}, the snowfall was above average.
It was followed by a warm spring which caused
flood conditions in many of the nearby rivers.
<Chart year={year} color="#fcb32c" />

Then, converted to HTML, it will looks like this

<h1>This is heading 1</h1>
<p>This is how we write paragraph in md files.</p>
<ul>
<li>This is a list</li>
<li>List one</li>
<li>List two</li>
</ul>
<p>Or you can make a <strong>bold</strong> text, <em>italic</em>, or <strong><em>both</strong></em></p>

And that’s MD all about. Basically, MDX is same as MD but it has more features. MDX allows you to use JSX in your markdown content. You can import components, such as interactive charts or alerts, and embed them within your content. This makes writing long-form content with components a blast.

import { Chart } from "./snowfall.js";
export const year = 2018;
# Last year’s snowfall
In {year}, the snowfall was above average.
It was followed by a warm spring which caused
flood conditions in many of the nearby rivers.
<Chart year={year} color="#fcb32c" />

Contentlayer

Contentlayer is a content preprocessor that validates and transforms your content into type-safe JSON you can easily import into your application. Contentlayer significantly reduces the boilerplate and external tools required to effectively integrate MDX content with the rest of your app. I’ve been using Contentlayer for this blog and really enjoy it. Without further ado, let’s add Contentlayer to generate our MDX content.

Install and Configure Contentlayer

Install Contentlayer packages in your Next.js project.

Terminal window
npm install contentlayer next-contentlayer

Next, we need to configure our next.config.js file to hook Contentlayer into the next dev and next build processes.

next.config.js
const { withContentlayer } = require("next-contentlayer");
module.exports = withContentlayer({
// your next config goes here
});

Then add the following code inside tsconfig.json (if using TypeScript) or jsconfig.json file.

tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"contentlayer/generated": ["./.contentlayer/generated"]
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
}
},
"include": ["next-env.d.ts", "**/*.tsx", "**/*.ts", ".contentlayer/generated"]
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^
}

Define Document Type

Create a schema for your MDX content. To do this, you need to create a new file called contentlayer.config.ts in the root folder of your Next.js project. Here I create a schema for my blog post content.

contentlayer.config.ts
import { defineDocumentType, makeSource } from "contentlayer/source-files";
const Post = defineDocumentType(() => ({
name: "Post",
filePathPattern: `blog/*.mdx`,
contentType: "mdx",
fields: {
title: {
type: "string",
description: "The title of the post",
required: true,
},
description: {
type: "string",
required: true,
},
date: {
type: "date",
description: "The date of the post",
required: true,
},
categories: {
type: "list",
of: Category,
},
},
computedFields: {
url: {
type: "string",
resolve: (post) => `/${post._raw.flattenedPath}`,
},
},
}));
export default makeSource({
documentTypes: [Post],
});

This configuration will generate a single document type named Post. These documents are expected to be .mdx files that live within a blog directory in my project. The data objects generated from these files will have the following properties:

  • title: Title for the blog post (string). Pulled from the file’s frontmatter.
  • description: Description for the blog post.
  • date: JavaScript Date object, pulled from the file’s frontmatter.
  • categories: Post categories. This property is a list of Category type.

In the categories property, I define it as a list of Category. Category itself is a nested type that I define with defineNestedType().

const Category = defineNestedType(() => ({
name: "Category",
fields: {
name: { type: "string", required: true },
},
}));

You can also add more than one document type. To do that, you just need to create a new schema like what you did when creating a post schema. Then, in makeSource, you export these two schemas inside the documentTypes property.

export default makeSource({
contentDirPath: "contents",
documentTypes: [Post, Project],
});

Lastly, create a “hello world” post inside the configured directory for your post document, e.g., contentDirPath + filePathPattern = /contents/blog

---
title: Lorem Ipsum
description: Excepteur consequat nostrud esse esse fugiat dolore.
Reprehenderit occaecat.
date: 2021-12-24
categories: ["contentlayer", "next.js"]
---
Ullamco et nostrud magna commodo nostrud occaecat quis pariatur id ipsum. Ipsum
consequat enim id excepteur consequat nostrud esse esse fugiat dolore.
Reprehenderit occaecat exercitation non cupidatat in eiusmod laborum ex eu
fugiat aute culpa pariatur. Irure elit proident consequat veniam minim ipsum ex
pariatur.

Get All Posts

Now we can tie it all together by bringing the Post data into our pages. But we need a library to help us format the date and get all posts sorted by newest date.

Terminal window
npm install date-fns

Let’s get our Post inside the getStaticProps() function. Then display the post title, description, and date in our page component.

import { compareDesc, format, parseISO } from "date-fns";
import { allPosts, Post } from "contentlayer/generated";
import { GetStaticProps } from "next";
const Home = ({ posts }: { posts: Post[] }) => {
return (
<div>
{posts.map((post, key) => (
<div key={key}>
<h1>{post.title}</h1>
<p>{post.description}</p>
<p>{format(parseISO(post.date), "LLLL d, yyyy")}</p>
</div>
))}
</div>
);
};
export default Home;
export const getStaticProps: GetStaticProps = async () => {
const posts = allPosts.sort((a, b) => {
return compareDesc(new Date(a.date), new Date(b.date));
});
return {
props: { posts },
};
};
Edit this page Tweet this article