How to create a blog with Next.js and Markdown

Domagoj Karimović

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:

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.

utils/posts.js
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.

;