How to build a blog using Next.js and Markdoc
This post, How to build a blog using Next.js and Markdoc, was originally written on the Docploy blog
We will be using Next.js and Markdoc to create a static blog site from Markdown files. I chose Next.js because the developer experience is amazing for creating static sites. I chose Markdoc as the tool for parsing out our Markdown files.
Install Next.js
Let's start by creating a new Next.js project.
Using yarn
yarn create next-app
Using npm
npx create-next-app
Install Markdoc
Stripe recently released Markdoc as an open source Markdown processor. Stripe uses Markdoc for their documentation, and their documentation is known as one of the best examples of technical documentation. I chose Markdoc because it provides the cleanest writing experience compared to other Markdown processors.
The Markdown is decoupled from any implementation details, so when you are editing your Markdown files, you can fully focus on writing content.
Markdoc also supports validating your Markdown files, which will make it easy to enforce any structural, syntactical, or grammatical rules if your blog has many contributors.
Let's install the Markdoc package.
Using yarn
yarn add @markdoc/markdoc --save
Using npm
npm install @markdoc/markdoc --save
Create your first blog post
Create a new folder in your project called posts/
. This is where your Markdown blog post files will live.
Create a new Markdown file posts/welcome-to-my-new-blog.md
and add the following sample content:
---
title: Welcome to my new blog
date: 2022-07-27
---
Welcome to my new blog created using Next.js and Markdoc.
Set up your first route
Let's begin by setting up a new route for your blog posts.
Create an empty file in your project under the path pages/blog/[slug].js
.
Next.js automatically sets up routes for you when you create files under the pages directory.
The [slug].js
in pages/blog/[slug].js
means a user can go to /blog/welcome-to-my-new-blog
, and the [slug].js
file will be responsible for handling the route and returning the contents of the matching Markdown file.
Using the getStaticPaths function
We will create a getStaticPaths
function in [slug].js
that will return possible blog post URLs based on our Markdown files in the posts/
directory. Next.js looks for the getStaticPaths
function as part of the framework to make the work easier for you.
First, install the glob-promise
package so we can find all of our Markdown files in the posts/
folder in one line of code.
Using yarn
yarn add glob-promise --save
Using npm
npm install glob-promise --save
Add the getStaticPaths function
Create a getStaticPaths
function (annotated below) in [slug].js
that looks like the following:
import glob from 'glob-promise';
import path from 'path';
export const getStaticPaths = async () => {
// Our Markdown files are stored in the posts/ directory
const POSTS_DIR = path.join(process.cwd(), 'posts');
// Find all Markdown files in the posts/ directory
// With The glob-promise library, we can use a one liner to find our Markdown files
const postPaths = await glob(path.join(POSTS_DIR, '**/*.md'));
// For each filename, the slug is the filename without the .md extension
const paths = postPaths.map((postPath) => {
const slug = path.basename(postPath, path.extname(postPath));
return { params: { slug } };
});
// Return the possible paths
return { paths, fallback: false };
};
Next, we need to read the Markdown file when the user visits a blog post route.
Read your Markdown files with Markdoc
Similar to how Next.js automatically supports getStaticPaths
function, Next.js also supports an exported getStaticProps
. We will use the getStaticProps
function to pass the blog post data to the React component, so we can show the user the blog post. We add the getStaticProps
function in the same [slug].js
file that we have been working on.
// ... imports from the last step here ...
import Markdoc from '@markdoc/markdoc';
import fs from 'fs';
// This config tells Markdoc to look for our Paragraph React component (to be created in another step)
const config = {
nodes: {
paragraph: {
render: 'Paragraph',
},
},
};
export const getStaticProps = async (context) => {
// Our Markdown files are stored in the posts/ directory
const POSTS_DIR = path.join(process.cwd(), 'posts');
// Generate the local Markdown path from the URL slug
const {
params: { slug },
} = context;
const fullPath = path.join(POSTS_DIR, slug + '.md');
// Read the Markdown file contents
const source = fs.readFileSync(fullPath, 'utf-8');
// Use Markdoc to create a tree of tokens based on the Markdown file
const ast = Markdoc.parse(source);
// Create a renderable tree
const content = JSON.stringify(Markdoc.transform(ast));
// Return the content as a prop to the React component for now
// We will render the content in the next section
return {
props: {
content,
},
};
};
Render your Markdown in a React component
We will add our last piece of code in the same [slug].js file which will return a React component. As part of Next.js, we can export default a React component, and Next.js will render that React component for us. Our React component uses Markdoc.renderers.react(...) to convert the renderable tree into a React element.
// ... other imports here ...
import React from 'react';
// Return our custom Paragraph component that adds custom Tailwind classes
const components = {
Paragraph: ({ children }) => {
return <p className="leading-relaxed mb-8 text-lg">{children}</p>;
},
};
// Create a React component using Markdoc's React renderer and our list of custom components.
const BlogPostPage = (props) => {
const { content } = props;
const parsedContent = JSON.parse(content);
return (
<div>
{Markdoc.renderers.react(parsedContent, React, {
components,
})}
</div>
);
};
export default BlogPostPage;
Render a list of your posts
Finally, we need to render a list of your posts when your users go to /blog
.
Create a new file, pages/blog.js
. Remember, Next.js will automatically set up the routing for /blog since the new file was created under the pages/
directory. Once again, Next.js will render the default exported React component that gets returned from pages/blog.js
.
Install gray-matter
Install the gray-matter
package so we can extract the title from the Markdown metadata.
Using yarn
yarn add gray-matter --save
Using npm
npm install gray-matter --save
Install dates-fns
Install the date-fns
package to make it easier to format dates.
Using yarn
yarn add date-fns --save
Using npm
npm install date-fns --save
Here is the final pages/blog.js
file with an explanation of the lines.
// ... add imports if they haven't been added yet ...
import Link from 'next/link';
import { format } from 'date-fns';
import fs from 'fs';
import glob from 'glob-promise';
import matter from 'gray-matter';
import path from 'path';
export const getStaticProps = async () => {
// Find all Markdown files in the /posts directory
const POSTS_DIR = path.join(process.cwd(), 'posts');
const postPaths = await glob(path.join(POSTS_DIR, '**/*.md'));
const posts = postPaths.map((postPath) => {
const slug = path.basename(postPath, path.extname(postPath));
const source = fs.readFileSync(postPath, 'utf-8');
// Use gray-matter to fetch the data between the `---` at the top of our Markdown files.
const matterResult = matter(source);
const { title, date } = matterResult.data;
return {
title,
date,
slug,
};
});
// Sort the posts by date
const sortedPosts = posts.sort((a, b) => b.date - a.date);
// We need to format the dates into strings because Next.js expects the props to be serializable as JSON.
const parsedDatePosts = sortedPosts.map((post) => {
return {
...post,
date: format(post.date, 'MM/dd/yyyy'),
};
});
return { props: { posts: parsedDatePosts } };
};
const Blog = (props) => {
const { posts } = props;
return (
<div>
{posts.map((post, i) => {
return (
<div key={i}>
<Link href={'/blog/' + post.slug}>
<a>
<h1>{post.title}</h1>
</a>
</Link>
</div>
);
})}
</div>
);
};
export default Blog;
Conclusion
You can start up your project locally by running yarn dev
.
Visit https://localhost:3000/blog
, and you'll see a list of blog posts. In this case, we only have one blog post. If you click on the name of the blog post, you will go to the full blog post.
You have achieved the ultimate victory. You now have a blog created using Next.js and Markdoc.
I have created a repo that you can clone use as a playground: Next.js and Markdoc example.
If you have any questions, send a message to me at @docploy on Twitter.