In this tutorial, we will walk through the steps to create a blog with NextJS.
Set up a NextJS project
First, make sure you have Node.js installed on your machine. Then, open your terminal and navigate to the directory where you want to create your NextJS project. Run the following command to create a new NextJS project:
npx create-next-app my-blog
This will create a new directory called my-blog
with a basic NextJS project structure.
The pages that we're going to create for this blog are:
- Home page
- Post page
- Tag Page
The content we're going to write in the blog will be in markdown format. We will add tags to each post to make similar posts easy to find on their own page.
Create A Content Folder
Create a folder in the root of the project called _posts
. This folder will contain all the markdown files for the blog posts. Inside the _posts
folder, create a new markdown file called first-blog.md
with the following content:
---
title: "My First Blog Post"
slug: my-first-blog-post
tags:
- NextJS
- ReactJS
createdAt: 2024-04-08 12:00:00
---
-- Add your blog content here --
Fetch Content API
Next, we need to create the api that will be used to fetch all the content from this _posts
folder, we'll need to be able to sort them by date, search by tags
and search by slug.
Once we have the posts we want we'll need to convert this markdown into HTML to be displayed when you visit the post page.
Create a file in the root of the project inside /lib/blog/api.ts
this is the file that we will use to fetch the content from the _posts
folder.
The first page we're to create is the home page, this page will display all the posts in the _posts
folder, we'll do this by creating a getPosts()
function.
import { glob } from "glob";
import { join } from "path";
import fs from "fs";
import matter from "gray-matter";
// Create a variable for the location of the blog posts
const postsDirectory = join(process.cwd(), "_posts/blog");
/**
* Using the glob package to get all the files in the _posts folder that have a markdown extension
*/
export function getPostSlugs() {
return glob.sync("**/!(*.*).md", { cwd: postsDirectory });
}
/**
* Get a single post
* @param filepath - The path to the post
*
* Uses the filepatch to get the contents of the markdown file.
* It will then use the gray-matter package to parse the front matter of the markdown file.
*/
export function getPost(filepath: string) {
const fullPath = join(postsDirectory, filepath);
const fileContents = fs.readFileSync(fullPath, "utf8");
const { data } = matter(fileContents);
return {...data, filepath};
}
/**
* Get all posts
* @param limit - The number of posts to return
*
* Get all the posts in the _posts folder and sort them by date
* Using the limit parameter we can decide how many blog posts to return
*/
export function getPosts(limit: number = -1) {
const slugs = getPostSlugs();
const posts = slugs
.map((slug) => getPost(slug))
.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
return limit === -1 ? posts : posts.slice(0, limit);
}
Using the the above code on the Home page we can call the getPosts()
function, this will then fetch all the blog post slugs,
fetch the content for each posts and sort them by date.
Home Page
In NextJS we can create the home page by creating a file in the app directory called page.tsx
from here we can simply call the getPosts()
function, loop through the results and display the posts.
import { getPosts } from "../lib/blog/api";
import Link from "next/link";
export default function Home() {
const posts = getPosts();
return (
<div>
{posts.map((post) => (
<div key={post.slug}>
<h2><Link href={post.slug}>{post.title}</Link></h2>
<p>{post.createdAt}</p>
<p>{post.tags.join(", ")}</p>
</div>
))}
</div>
);
}
Post Page
The post page will only display a single post based on the slug, we can create this page by creating a foles in the app directory called [slug]
inside this folder we'll create the page.tsx
file.
import { notFound } from "next/navigation";
import { getPostBySlug } from "@/lib/blog/api";
type Params = {
params: {
slug: string;
};
};
export default function Post({ params }: Params) {
const post = getPostBySlug(params.slug);
if (!post) {
return notFound();
}
const content = await markdownToHtml(post.content || "");
return (
<div>
<h1>{post.title}</h1>
<p>{post.createdAt}</p>
<p>{post.tags.join(", ")}</p>
<div dangerouslySetInnerHTML={{ __html: content }} />
</div>
);
}
Here we take the params from the URL of the slug and send this to a function called getPostBySlug()
this function will then fetch the post
by the slug parameter.
This function will go into the lib/blog/api.ts
file we created earlier.
export function getPostBySlug(slug: string): Post | undefined {
return getPosts().find((post) => post.slug === slug);
}
If the post is not found then we'll need to return the 404 page.
If the post is found then we need to convert the markdown content to HTML, we can do this by creating a function called markdownToHtml()
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypeShiki from "@shikijs/rehype";
export default async function markdownToHtml(markdown: string) {
const file = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeShiki, {
// or `theme` for a single theme
theme: "github-dark",
})
.use(rehypeStringify)
.process(markdown);
return file.toString();
}
This is using a few packages in order to achieve this.
-
unified
- A tool for processing text using plugins. -
remark-parse
- A markdown parser. -
remark-rehype
- A markdown to HTML compiler. -
rehype-stringify
- An HTML compiler. -
rehype-shiki
- A syntax highlighter. Allowing us to highlight code blocks in the markdown. -
@shikijs/rehype
- A syntax highlighter for rehype.
We will now be able to see the content of the post in HTML format.
Tag Page
The tag page will be used to search through the posts by tags, we can create this page by creating a file in the app directory called tags/[slug]
inside this folder we'll create the page.tsx
file.
This will create url of /tags/reactjs
for example.
import { getPostsByTag } from "@/lib/blog/api";
import { notFound } from "next/navigation";
type Params = {
params: {
slug: string;
};
};
export default function TagPage({ params }: Params) {
const tagSlug = params.slug;
const capitalSlug = tagSlug.charAt(0).toUpperCase() + tagSlug.slice(1);
const posts = getPostsByTag(tagSlug);
if (posts.length === 0) {
return notFound();
}
return (
<div>
<div className="bg-gray-200 p-8 mb-8">
<h1 className="text-4xl">#{capitalSlug}</h1>
</div>
<div className="my-4">
{posts.map((post) => (
<div key={post.slug}>
<h2><Link href={post.slug}>{post.title}</Link></h2>
<p>{post.createdAt}</p>
<p>{post.tags.join(", ")}</p>
</div>
))}
</div>
</div>
);
}
We need to create the getPostsByTag()
function in the lib/blog/api.ts
file.
export function getPostsByTag(tag: string) {
const posts = getPosts()
// .map((slug) => getPost(slug));
.filter((post) => post.tags?.includes(tag));
return posts;
}
If there are no posts for the tag then we'll need to return the 404 page.
If there are posts for the tag then we'll display the posts.
Conclusion
In this tutorial, we have walked through the steps to create a blog with NextJS. We have created the home page, post page, and tag page. We have also created a content folder to store all the markdown files for the blog posts. We have fetched the content from the _posts
folder and converted the markdown content to HTML to be displayed on the post page. We have also added tags to each post to make similar posts easy to find on their own page.
This is just the starting point of creating a blog in NextJS, there are few extras that we will add to this blog at a later date such as:
- Pagination
- Search
- Comments
- RSS Feed
- Sitemap
- SEO
- Analytics
Top comments (3)
Another great option is using MDX. This allows you to map the markdown components to your design system. Really useful when you are using a lot of images or internal links and want to make use of the NextJS
<Image/>
or<Link/>
components etc.For example
I like that idea, thanks Matt
very good