Convert a Next.js site to a markdown blog
A step-by-step guide to adding a markdown blog to an existing Next.js site.
A Markdown-based blog makes writing and publishing content a breeze.
Markdown is a powerful, lightweight markup language that allows you to write structured content using plain text. With Markdown, you can easily format your text and add rich formatting to your blog posts, without the need for complex HTML tags.
If you have a Next.js site and are looking for a better way to write and manage blog content online, Markdown is a tool that merits strong consideration.
Here are the 3 steps you can take to migrate an existing Next.js website to a site that includes a Markdown blog.
The Migration Steps
1. Markdown posts setup
- Create
blogdirectory at the root of your project.- This is where you will write and store your
mdblog posts.
- This is where you will write and store your
- Create a sample post
blog/sample.md- Here's an example
.mdfile with some frontmatter metadata
--- title: 'Writing Effective Readmes' author: 'Noah Matsell' description: 'How to write an effective Readme for your project repository.' coverImgUrl: 'images/22-09-2021.jpg' date: '2021-09-22' categories: - dev tips --- # Introduction Text content - Here's an example
- In your project
typesfolder, create ablog.tsfile for your post metadata types. These will represent the frontmatter metadata parsed by your app and are useful when handling blog post data within your React components.
// types/blog.ts
export interface Post {
title: string;
description: string;
author: string;
coverImgUrl: string;
date: string;
categories: Category[];
content: string;
slug?: string;
}
2. API Setup
- Install
gray-mattervia npm or yarn. - This package is used to parse yourmdblog post metadata from a string or file. - Create a
lib/api.tsfile to handle getting posts and their contents.- Create a
getPostSlugsfunction. This generates an array of slugs from themdfiles contained within the newblogfolder.
// lib/api.ts const postsDirectory = join(process.cwd(), "blog"); export function getPostSlugs() { return fs.readdirSync(postsDirectory); }- Create a
getPostBySlugfunction. This builds up, parses, and returns post data for a given slug and set of fields.
// lib/api.ts export function getPostBySlug(slug: string, fields: (keyof Post)[] = []) { const realSlug = slug.replace(/\.md$/, ""); const fullPath = join(postsDirectory, `${realSlug}.md`); const fileContents = fs.readFileSync(fullPath, "utf8"); const { data, content } = matter(fileContents); const postData: Partial<Post> = {}; // Ensure only the minimal needed data is exposed fields.forEach((field) => { // set slug to real slug if (field === "slug") { postData[field] = realSlug; } // set content to parsed gray matter content if (field === "content") { postData[field] = content; } // set other properties 1:1 if they exist if (typeof data[field] !== "undefined") { postData[field] = data[field]; } // add [draft] tag to unpublished posts in development if ( process.env.NODE_ENV === "development" && field === "title" && data["title"] && !data["publishDate"] ) { postData[field] = data[field] + " [draft]"; } }); return postData; }- Create a
getAllPostsfunction. This gets all existing posts and their post data for a given set of fields.
[Full api.ts code]// lib/api.ts export function getAllPosts(fields: (keyof Post)[] = []) { const slugs = getPostSlugs(); const posts = slugs .map((slug) => getPostBySlug(slug, fields)) // Sort posts by date in descending order .sort((post1, post2) => { return (post1.publishDate || "0") > (post2.publishDate || "0") ? -1 : 1; }); // Filter out unpublished posts in production if (process.env.NODE_ENV === "production") { return posts.filter((post) => post.publishDate); } return posts; } - Create a
- Create a
lib/markdownToHtml.tsfile. This is needed to translate themdcontent of a post to html content.- Install
remarkandremark-htmlvia npm or yarn.
// lib/markdownToHtml.ts import { remark } from "remark"; import html from "remark-html"; export default async function markdownToHtml(markdown: string) { const result = await remark() .use(html, { sanitize: false }) .process(markdown); return result.toString(); } - Install
3. Component Setup
-
Under your
pagesdirectory, create ablogdirectory. -
Create
pages/blog/[slug].tsx, a dynamic route that's responsible for taking yourmdblog posts and turning them into pages!- Make a
Postcomponent, which is a React component that renders the parsed content data of a given blog post.
// pages/blog/[slug].tsx ... interface Props { post: PostType; morePosts: PostType[]; } export default function Post({ post }: Props) { // The component that renders your a blog post. } ...- The post component can be composed of
Layout,PostHeader,PostBodyand/or any other presentational components you like.Postcomponent. - Render your parsed post comment anywhere inside of the
Postcomponent. Or, create a childPostBodycomponent:
// components/PostBody.tsx ... import markdownStyles from "../styles/markdown-styles.module.css"; const PostBody = ({ content }: Props) => { return ( <div className="max-w-2xl mx-auto"> <div className={markdownStyles["markdown"]} dangerouslySetInnerHTML={{ __html: content }} /> </div> ); }; ... - Make a
-
This is where your custom styling and CSS can be applied to the rendered post content. In the example above, I've created
markdown-styles.module.cssand applied it to the renderingdivtag. -
Make a
getStaticPropsfn- Exporting a function called getStaticProps will pre-render a page at build time using the props returned from the function
// pages/blog/[slug].tsx ... export async function getStaticProps({ params }: Params) { const post = getPostBySlug(params.slug, [ "title", "date", "slug", "author", "content", ]); const content = await markdownToHtml(post.content || ""); return { props: { post: { ...post, content, }, }, }; } ...- The props object is a key-value pair, where each value is received by the page component. It should be a serializable object so that any props passed, could be serialized with JSON.stringify.
-
Make a
getStaticPathsfn- When exporting a function called getStaticPaths from a page that uses Dynamic Routes, Next.js will statically pre-render all the paths specified by getStaticPaths.
// pages/blog/[slug].tsx ... export async function getStaticPaths() { const posts = getAllPosts(["slug"]); return { paths: posts.map((post) => { return { params: { slug: post.slug, }, }; }), fallback: false, }; } ...- The paths key determines which paths will be pre-rendered.
- This is ultimately how we dynamically create all of the blog post pages at build time for the posts in our
blog/directory.
Wrapping up
With these steps in place, your Next.js website will now be able to render Markdown blog content, and you can easily manage and update your blog content using the Markdown syntax.
Your final project structure should look something like this:
.
├── blog
│ ├── sample.md
│ └── writing-effective-readmes.md
├── components
│ ├── HeroPost.tsx
│ ├── Layout.tsx
│ ├── MorePosts.tsx
│ ├── PostBody.tsx
│ ├── PostHeader.tsx
│ └── PostTitle.tsx
├── lib
│ ├── api.ts
│ └── markdownToHtml.ts
├── pages
│ ├── _app.tsx
│ ├── api
│ ├── blog
│ │ ├── [slug].tsx
│ │ └── index.tsx
│ └── index.tsx
├── styles
│ ├── blog.css
│ ├── globals.css
│ └── markdown-styles.module.css
├── next.config.js
├── package.json
├── postcss.config.js
├── public
├── tailwind.config.js
├── tsconfig.json
└── types
└── blog.ts
Next steps
Here are some ideas for extending the functionality of your markdown blog in cool ways:
- Add code highlighting via Prism
- Make a blog 'homepage'
- Add a dynamic table of contents
- Add a share button using the Web Share API



