Introducing My New Next.js Blog (with a free template you can use for your own blog!)
Ever since I started working full time, I haven't had much time to work on my blog. I've been wanting to update it for a while now, and I finally got around to it. The last version of my blog was made with Gatsby, and while I loved it, I wanted to try something new. I decided to remake my blog with Next.js and Tailwind CSS. Here's how I did it.
(If you'd like to create your own blog and skip the setup, you can use this template I created: https://github.com/bandrewfisher/next-js-markdown-blog-template)
Creating the project
I followed the instructions on the Next.js website to create a new project. All I had to do was run this command, and then follow the prompts:
npx create-next-app@latest
The project it creates comes with Tailwind CSS and TypeScript already set up, which is a big time saver.
Setting up Contentlayer
I wanted to be able to write my blog posts in Markdown, so I decided to use Contentlayer. Contentlayer is a tool that lets you load content from Markdown files into your Next.js project. It's super easy to set up, and it works great with Tailwind CSS. Plus, you can take advantage of Next.js's static site generation to make your blog super fast and SEO-friendly.
I followed the instructions on the Contentlayer website to set it up.
First, I installed the Contentlayer dependencies:
npm install contentlayer next-contentlayer date-fns rehype-prism-plus
We'll use the rehype-prism-plus
package later to add syntax highlighting to our blog posts.
Then, I updated my next.config.mjs
file to use Contentlayer:
// next.config.mjs
import { withContentlayer } from "next-contentlayer";
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
...
};
export default withContentlayer(nextConfig);
I updated my tsconfig.json
to add an import alias and include the generated Contentlayer files:
// tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
...
"contentlayer/generated": ["./.contentlayer/generated"]
},
...
},
"include": [
...
".contentlayer/generated"
]
}
I added .contentlayer
to my .gitignore
file:
# .gitignore
.contentlayer
Then, I added a contentlayer.config.js
file to define the schema for our blog posts:
// contentlayer.config.js
import { defineDocumentType, makeSource } from "contentlayer/source-files";
import rehypePrism from "rehype-prism-plus";
export const Post = defineDocumentType(() => ({
name: "Post",
filePathPattern: `**/*.md`,
fields: {
title: {
type: "string",
required: true,
},
date: {
type: "date",
required: true,
},
description: {
type: "string",
required: true,
},
},
computedFields: {
url: {
type: "string",
resolve: (post) => `/posts/${post._raw.flattenedPath}`,
},
},
}));
export default makeSource({
contentDirPath: "posts",
documentTypes: [Post],
markdown: { rehypePlugins: [rehypePrism] },
});
rehypePrims is a plugin for syntax highlighting in Markdown files with Prism. We'll come back to that later.
This config file tells Contentlayer to look for Markdown files in the posts
directory and create a Post
document type with title
, date
, and description
fields. It also tells Contentlayer to generate a url
field for each post that we can use to link to the post.
I created a few Markdown files (.md extension) in the posts
directory to test it out.
They would look something like this:
---
title: My new blog
date: 2024-09-23
description: Something about the post
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit...
Rendering the blog posts
Right now, we have Contentlayer set up to generate HTML content for our Markdown blog posts, but we need a way to render that content in our Next.js app. We can do this by creating a new page in our pages
directory that uses the generateStaticParams
function to generate static paths for each one of our posts.
I created a new file called [slug].tsx
in the pages/posts
directory:
import { format, parseISO } from "date-fns";
import { allPosts } from "contentlayer/generated";
export const generateStaticParams = async () =>
allPosts.map((post) => ({ slug: post._raw.flattenedPath }));
export const generateMetadata = ({ params }: { params: { slug: string } }) => {
const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
if (!post) throw new Error(`Post not found for slug: ${params.slug}`);
return { title: post.title };
};
const PostLayout = ({ params }: { params: { slug: string } }) => {
const post = allPosts.find((post) => post._raw.flattenedPath === params.slug);
if (!post) throw new Error(`Post not found for slug: ${params.slug}`);
return (
<article className="prose mx-auto max-w-xl py-8">
<div className="mb-8 text-center">
<time dateTime={post.date} className="mb-1 text-xs text-gray-600">
{format(parseISO(post.date), "LLLL d, yyyy")}
</time>
<h1 className="text-3xl font-bold">{post.title}</h1>
</div>
<div
className="[&>*]:mb-3 [&>*:last-child]:mb-0"
dangerouslySetInnerHTML={{ __html: post.body.html }}
/>
</article>
);
};
export default PostLayout;
Now, each post will have its own page with a URL like /posts/my-new-blog
. The generateStaticParams
function generates the static paths for each post, and the generateMetadata
function generates the metadata for each post, which we can use to set the page title.
If you wrote a post called my-new-blog.md
, you would be able to access it at http://localhost:3000/posts/my-new-blog
.
However, since Tailwind removes all default styling for HTML elements, you'll probably notice that the blog post doesn't look very good. If you added headers, for example, they'll look the same as regular text.
That's what the prose
class is for, even though it won't do anything now. This comes from the Tailwind CSS Typography plugin, which provides a set of typographic defaults that will make our blog content look beautiful. Let's set it up.
Setting up Tailwind CSS Typography
Install the plugin from the instructions given in the GitHub repository:
npm install -D @tailwindcss/typography
Now add the plugin to your tailwind.config.js
file:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
// ...
},
plugins: [
require('@tailwindcss/typography'),
// ...
],
}
And that's it! Now your blog posts will look much better.
Syntax highlighting
Remember the rehypePrism
plugin we added to our Contentlayer config? Now we can use it to add syntax highlighting to our blog posts.
First, install the Prism CSS theme you want to use. You can find a list of themes on the Prism website. I'm just going to use the default theme and select a few languages I want to highlight, like Python, JavaScript, and bash.
Download the CSS file and add it to app/prism.css
.
Now in my app/layout.tsx
file, I added this line:
// app/layout.tsx
import './prism.css'
Add a code block to your Markdown post:
def hello_world():
print("Hello, world!")
And that's it! Now your code blocks will have syntax highlighting. It works because of the rehypePrism
plugin we added to our Contentlayer config. It will split up the code blocks in your Markdown files and add the necessary classes for Prism to highlight them.
Displaying all of our blog posts
Right now we can navigate directly to our posts, but we have no way to see all of them at once. Let's create a new page that lists all of our blog posts.
I created a new file called page.tsx
in the pages/posts
directory:
import Link from "next/link";
import { compareDesc, format, parseISO } from "date-fns";
import { allPosts, Post } from "contentlayer/generated";
function PostCard(post: Post) {
return (
<div className="mb-8">
<h2 className="mb-1 text-xl">
<Link
href={post.url}
className="text-blue-800 hover:underline hover:underline-offset-2"
>
{post.title}
</Link>
</h2>
<time dateTime={post.date} className="mb-2 block text-xs text-gray-600">
{format(parseISO(post.date), "LLLL d, yyyy")}
</time>
<div className="text-sm [&>*]:mb-3 [&>*:last-child]:mb-0">
{post.description}
</div>
</div>
);
}
export default function Blog() {
const posts = allPosts.sort((a, b) =>
compareDesc(new Date(a.date), new Date(b.date))
);
return (
<div className="mx-auto max-w-xl">
<h1 className="mb-8 text-center text-3xl font-black">Blog</h1>
{posts.map((post, idx) => (
<PostCard key={idx} {...post} />
))}
</div>
);
}
This will display all of the blog posts in reverse chronological order. Each post will have a title, date, and description, and you can click on the title to navigate to the post.
You can access this page at http://localhost:3000/posts
.
Conclusion
That's it! We've set up a new blog with Next.js, Tailwind CSS, and Contentlayer. We've created a way to write blog posts in Markdown, render them in our Next.js app, and display them on a blog page. We've also added syntax highlighting to our code blocks and made our blog posts look beautiful with Tailwind CSS Typography.
Use this template for your own blog!
That was a lot of work, but you don't have to do it all yourself. I created a GitHub repository with all of this setup work already done for you. Just clone it and run yarn install
and then yarn dev
to get started. You can use it as a starting point for your own blog and customize it however you like.