Building a Static Blog with Next.js

In the article Building a Static Site with Next.js, we discussed how to create and upload a static site using Next.js.

In this project, we intend to add a static blog to the previous site using Next.js and create and manage its posts using Markdown.

Create a new folder called posts in the main directory of the project. Inside this folder, create a new Markdown file for each blog post you want to create. Then, for each Markdown file, specify the title and post date using the frontmatter.

---
title: My First Blog Post
date: '2023-03-05'
---

This is my first blog post. Stay tuned for more!

In this project, the gray-matter package will be used to process the frontmatter of Markdown files. The remark package will be used to render the Markdown content, and the remark-gfm package will be used to add support for Github Flavored Markdown.

To install the required dependencies, use the following commands.

pnpm add --save-dev gray-matter remark remark-gfm remark-html

Next, create a new folder called utils in the main directory of your project. Inside the utils folder, create a new file called markdown-to-html.ts with the following content.

import { remark } from 'remark'
import gfm from 'remark-gfm'
import html from 'remark-html'

export async function markdownToHtml(markdown: string): Promise<string> {
  const result = await remark().use(gfm).use(html).process(markdown)
  return result.toString()
}

Here, we define a function called markdownToHtml to convert Markdown text to HTML.

This function takes a string of Markdown text as input, converts it to HTML, and returns it as a string. This function uses remark-gfm to add support for Github Flavored Markdown.

Inside the utils folder, create a new file called posts.ts with the following content.

import fs from 'fs'
import matter from 'gray-matter'
import path from 'path'

const postsDirectory = path.join(process.cwd(), 'posts')

export type Post = {
  slug: string
  title: string
  date: string
  content: string
}

export function getSortedPostsData(): Post[] {
  const fileNames = fs.readdirSync(postsDirectory)
  const allPostsData = fileNames.map((fileName) => {
    const slug = fileName.replace(/\.md$/, '')
    const fullPath = path.join(postsDirectory, fileName)
    const fileContents = fs.readFileSync(fullPath, 'utf8')
    const { data, content } = matter(fileContents)
    return {
      slug,
      ...data,
      content,
    }
  })
  // @ts-ignore
  return allPostsData.sort((a, b) => {
    // @ts-ignore
    if (a.date < b.date) {
      return 1
    } else {
      return -1
    }
  })
}

The function getSortedPostsData is responsible for fetching the frontmatter of all blog posts and sorting them in descending order based on date. This function returns an array of objects, where each object represents a blog post with its relevant frontmatter (title and date).

Then, create a new directory named components in the root directory of your project. Inside this directory, create a new file named layout.tsx with the following content.

import Head from 'next/head'
import React, { ReactNode } from 'react'

type Props = {
  title: string
  children?: ReactNode
}

const Layout: React.FC<Props> = ({ title, children }) => {
  return (
    <>
      <Head>
        <title>{title}</title>
        <meta name="description" content="My static blog" />
      </Head>
      <header>
        <h1>My Static Blog</h1>
      </header>
      <main>{children}</main>
      <footer>
        <p>© {new Date().getFullYear()} My Static Blog</p>
      </footer>
    </>
  )
}

export default Layout

This component will be used as the main layout for your blog.

Create a new directory named blog inside the pages directory at the root of your project. Create a new file named index.tsx with the following content.

import Link from 'next/link'
import Layout from '../../components/layout'
import { getSortedPostsData, Post } from '../../utils/posts'

type Props = {
  posts: Post[]
}

export default function Blog({ posts }: Props) {
  return (
    <Layout title="My Static Blog">
      {posts.map((post: Post) => (
        <article key={post.slug}>
          <Link href={`/blog/${post.slug}`}>
            <h2>{post.title}</h2>
            <time dateTime={post.date}>{post.date}</time>
          </Link>
        </article>
      ))}
    </Layout>
  )
}

export async function getStaticProps() {
  const posts = getSortedPostsData()
  return {
    props: {
      posts,
    },
  }
}

The Blog component will be used as the main layout for your blog.

Create a new directory named blog inside the pages directory at the root of your project. Create a new file named index.tsx with the following content.

In this example, we pass the posts array as props to the Blog component. Inside the getStaticProps function, we call getSortedPostsData to fetch the metadata for all blog posts and sort them by date. Then we send the result as props to our component.

In the next step, we need to create the individual blog post pages. To do this, create a new file named [slug].tsx inside the blog directory with the following content.

import Head from 'next/head'
import Layout from '../../components/layout'
import { markdownToHtml } from '../../utils/markdown-to-html'
import { getSortedPostsData, Post } from '../utils/posts'

type Props = {
  post: Post
}

export default function BlogPost({ post }: Props) {
  return (
    <Layout title={post.title}>
      <Head>
        <title>{post.title}</title>
      </Head>
      <article>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
    </Layout>
  )
}

export async function getStaticPaths() {
  const posts = getSortedPostsData()
  const paths = posts.map((post) => ({
    params: {
      slug: post.slug,
    },
  }))

  return {
    paths,
    fallback: false,
  }
}

// @ts-ignore
export async function getStaticProps({ params }) {
  const slug = params?.slug
  const post = getSortedPostsData().find((p) => p.slug === slug)
  // @ts-ignore
  const { title, date, content } = post

  return {
    props: {
      post: {
        slug,
        title,
        date,
        content: await markdownToHtml(content),
      },
    },
  }
}

In the getStaticPaths function, first we use the getAllPostSlugs function which we have imported from the posts.ts file, to retrieve all of the blog post slugs. Then using the map function, we convert each slug into an object with the slug parameter.

Finally, we put the array created from the slugs as the paths parameter in the object that we return from the function. Additionally, we set the fallback value to false to display a 404 error message if the entered slug by the user is not valid.