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.