How to create a blog with Next.js and Markdown
January 26, 2023
8 min read
In this tutorial, we will learn how to create a blog with Next.js and Markdown. We will use Javascript and Tailwind CSS to make our blog look great. We will be using the popular Next.js framework to create our blog. Next.js is a React framework that makes it easy to create static and server-side rendered applications.
Here is a list of the packages we will be using:
- next-mdx-remote - Allows us to use Markdown files in our Next.js application.
- gray-matter - For parsing the frontmatter of our Markdown files.
- dayjs - Very lightweight library for parsing dates.
- rehype-autolink-headings - Allows us to add anchor links to our headings.
- rehype-slug - Allows us to add slugs to our headings.
- rehype-highlight - Allows us to add syntax highlighting to our code blocks.
- tailwindcss - CSS framework for styling our application.
Getting Started
Let's start by creating a new Next.js application. We will be using the create-next-app package to create our application.
npx create-next-app blog
Next, we will install the packages we will be using.
cd blog
npm install next-mdx-remote gray-matter dayjs rehype-autolink-headings rehype-slug rehype-highlight
Setting up our folder structure
Next, we will create a blog
folder in the pages
folder. This will be where we will store our blog posts.
We will also create a components
folder in the root folder. This will be where we will store our components.
Lastly, we will create a posts
folder in the root folder and a utils
folder. This will be where we will store our Markdown and utility files.
mkdir pages/blog
mkdir components
mkdir posts
mkdir utils
Setting up Tailwind CSS
Next, we will set up Tailwind CSS. We will be using the official Next.js guide to set up Tailwind CSS.
First, we will install Tailwind CSS and its peer dependencies and initialize it
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Optional: if you want to use @tailwindcss/typography
, you will need to install it.
npm install @tailwindcss/typography
Next, we will add the following to our tailwind.config.js
file.
module.exports = {
content: ['./components/**/*.jsx', './pages/**/*.jsx'],
plugins: [
require('@tailwindcss/typography'), // Optional
],
}
Creating our utility functions
Next, we will create our utility functions. We will create a getSlugs
function, the job of that function is to read all the files in our posts
folder and return an array of slugs.
We will also create a getAllPosts
function, the job of that function is to get all the posts and sort them by date.
Lastly, we will create a getPostBySlug
function, the job of that function is to get a post by its slug.
Create a new file under utils
called posts.js
and add the following code. Here we will be creating most of our logic regarding our blog posts.
import path from 'path';
import fs from 'fs';
import matter from 'gray-matter';
const PATH = path.join(process.cwd(), 'posts');
export const getSlugs = () => {
const files = fs.readdirSync(path.join('posts'));
return files.map(fileName => {
return fileName.replace('.mdx', '');
});
};
export const getAllPosts = () => {
const posts = getSlugs()
.map((slug) => getPostBySlug(slug))
.sort((a, b) => {
if (a.meta.date > b.meta.date) return 1;
if (a.meta.date < b.meta.date) return -1;
return 0;
})
.reverse();
return posts;
};
export const getPostBySlug = (slug) => {
const postPath = path.join(PATH, `${slug}.mdx`);
const src = fs.readFileSync(postPath);
const { content, data } = matter(src);
return {
content,
meta: {
slug,
description: data.description,
readTime: data.readTime,
title: data.title,
tags: data.tags.sort(),
date: data.date.toString(),
},
};
};
Creating our components
We need to create a few components. The most important one is the PostCard
component.
This component will be used to display a post in a list of posts.
Create a new file under components
called PostCard.jsx
and add the following code.
import Link from 'next/link';
import dayjs from 'dayjs';
export default function PostCard({ post }) {
return (
<Link href={`/blog/${post.meta.slug}`}>
<a className="flex flex-col justify-between p-4 bg-white rounded-lg shadow-lg hover:shadow-2xl transition duration-300">
<div className="flex flex-col justify-between">
<h2 className="text-2xl font-bold text-gray-800">{post.meta.title}</h2>
<p className="mt-2 text-gray-600">{post.meta.description}</p>
</div>
<div className="flex flex-col justify-between mt-4">
<div className="flex flex-row justify-between">
<p className="text-sm text-gray-500">
{dayjs(post.meta.date).format('MMMM D, YYYY')}
</p>
<p className="text-sm text-gray-500">{post.meta.readTime}</p>
</div>
<div className="flex flex-row justify-between mt-2">
{post.meta.tags.map((tag) => (
<p
key={tag}
className="px-2 py-1 text-sm text-gray-800 bg-gray-200 rounded-md"
>
{tag}
</p>
))}
</div>
</div>
</a>
</Link>
);
}
Creating our pages
Finally we get on to creating our pages. We will create a blog
page, a blog/[slug]
page, and a 404
page.
Create a new file under pages
called blog.jsx
and add the following code.
import { getAllPosts } from '../utils/posts';
import PostCard from '../components/PostCard';
export default function Blog({ posts }) {
return (
<div className="flex flex-col justify-center items-center w-full flex-1 px-20 text-center">
<h1 className="text-6xl font-bold">Blog</h1>
<p className="mt-3 text-2xl">
A collection of my thoughts, ideas, and ramblings.
</p>
<div className="flex flex-col justify-center items-center max-w-2xl w-full flex-1 px-20 text-center">
{posts.map((post) => (
<PostCard key={post.meta.slug} post={post} />
))}
</div>
</div>
);
}
export async function getStaticProps() {
const posts = getAllPosts();
return {
props: {
posts,
},
};
}
Create a new file under pages
called blog/[slug].jsx
and add the following code.
import Image from 'next/image';
import { MDXRemote } from 'next-mdx-remote';
import { serialize } from 'next-mdx-remote/serialize';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeHighlight from 'rehype-highlight';
import { getPostBySlug, getSlugs } from '../../utils/posts';
import 'highlight.js/styles/atom-one-dark.css';
import dayjs from 'dayjs';
const components = {
Image,
};
export default function BlogPost({ post }) {
return (
<div className="flex flex-col justify-center items-center w-full flex-1 px-20 text-center">
<h1 className="text-6xl font-bold">{post.meta.title}</h1>
<p className="mt-3 text-2xl">{post.meta.description}</p>
<div className="flex flex-row justify-between mt-4">
<p className="text-sm text-gray-500">
{dayjs(post.meta.date).format('MMMM D, YYYY')}
</p>
<p className="text-sm text-gray-500">{post.meta.readTime}</p>
</div>
<div className="flex flex-row justify-between mt-2">
{post.meta.tags.map((tag) => (
<p
key={tag}
className="px-2 py-1 text-sm text-gray-800 bg-gray-200 rounded-md"
>
{tag}
</p>
))}
</div>
<div className="prose prose-lg max-w-none w-full">
<MDXRemote {...post.content} components={components} />
</div>
</div>
);
}
export const getStaticProps = async ({ params }) => {
const { slug } = params;
const { content, meta } = getPostBySlug(slug);
const mdxSource = await serialize(content, {
mdxOptions: {
rehypePlugins: [
rehypeSlug,
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
rehypeHighlight,
],
},
});
return { props: { post: { source: mdxSource, meta } } };
};
export const getStaticPaths = async () => {
const paths = getSlugs().map((slug) => ({ params: { slug } }));
return {
paths,
fallback: false,
};
};
NOTE: The prose
class in the given example will not work unless you have @tailwindcss/typography
installed. If you do not have it installed, you can remove the prose
class, but your results may vary.
You can read about the prose
class here.
Create a new file under pages
called 404.jsx
and add the following code.
export default function Custom404() {
return (
<div className="flex flex-col justify-center items-center w-full flex-1 px-20 text-center">
<h1 className="text-6xl font-bold">404 - Page Not Found</h1>
</div>
);
}
Conclusion
In this tutorial, we created a blog using Next.js and MDX. You can view the source code for this tutorial on GitHub.