In the ever-evolving landscape of web development, creating a personal blog or a content-rich website has become increasingly accessible. Next.js, a powerful React framework, provides an excellent platform for building such applications. Its features, like server-side rendering (SSR), static site generation (SSG), and a robust ecosystem, make it an ideal choice for developers of all skill levels. This tutorial will guide you through building a simple blog using Next.js, focusing on Markdown support for easy content creation. We’ll explore how to fetch, parse, and display Markdown files, enabling you to manage your blog content efficiently.
Why Build a Blog with Next.js?
Why choose Next.js for a blog? Here’s why:
- Performance: Next.js optimizes your website for speed. SSR and SSG mean faster initial load times and improved SEO.
- SEO-Friendly: SSR and SSG make your content easily crawlable by search engines, boosting your site’s visibility.
- Developer Experience: Next.js offers a great developer experience with features like hot module replacement and built-in routing.
- Markdown Support: Easily write content using Markdown, a simple and widely-used markup language.
- Scalability: Next.js is designed to handle websites of all sizes, from small blogs to large-scale applications.
This tutorial is designed for beginners to intermediate developers. Basic knowledge of JavaScript, HTML, and CSS is helpful, but we’ll break down each step so you can follow along easily.
Setting Up Your Next.js Project
Let’s get started by creating a new Next.js project. Open your terminal and run the following command:
npx create-next-app my-nextjs-blog --typescript
This command does the following:
- Creates a new Next.js project named “my-nextjs-blog”. You can replace this with your desired project name.
- Uses TypeScript for type safety and improved code maintainability.
Navigate into your project directory:
cd my-nextjs-blog
Now, install the necessary dependencies for Markdown parsing. We’ll use two main packages:
gray-matter: To parse the frontmatter (metadata at the beginning of your Markdown file).remark,remark-html, andremark-prism: To parse the Markdown content into HTML and add syntax highlighting.
npm install gray-matter remark remark-html remark-prism
Project Structure
Before we dive into the code, let’s establish a basic project structure:
my-nextjs-blog/
├── pages/
│ ├── _app.tsx
│ ├── index.tsx // Home page
│ └── posts/
│ └── [slug].tsx // Dynamic post page
├── public/
│ └── ...
├── styles/
│ └── globals.css
├── posts/
│ └── example-post.md // Your first Markdown post
└── next.config.js
Create a `posts` directory at the root of your project to store your Markdown files. Inside, create a sample Markdown file called `example-post.md` (or any name you prefer). This is where your blog posts will live.
Creating a Sample Markdown Post
Open `posts/example-post.md` and add some content. Markdown files typically start with frontmatter, which is metadata about the post. Here’s an example:
---
title: "My First Blog Post"
date: "2024-01-01"
author: "Your Name"
---
# Hello, World!
This is my first blog post written in Markdown.
Here are some things I learned:
- Next.js is awesome.
- Markdown is easy to use.
- Building a blog is fun!
```javascript
console.log("Hello from the code block!");
```
This example includes frontmatter (the lines between the `—`) with a title, date, and author. The rest is the Markdown content. Notice the code block; we’ll handle syntax highlighting later.
Fetching and Parsing Markdown Files
Now, let’s create functions to fetch and parse these Markdown files. Create a file named `lib/posts.ts` in your project root, and add the following code:
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { remark } from 'remark'
import html from 'remark-html'
import prism from 'remark-prism'
export interface PostData {
slug: string;
title: string;
date: string;
author: string;
content: string;
}
const postsDirectory = path.join(process.cwd(), 'posts')
export async function getSortedPostsData() {
// Get file names under /posts
const fileNames = fs.readdirSync(postsDirectory)
const allPostsData = await Promise.all(
fileNames.map(async (fileName) => {
// Remove ".md" from file name to get slug
const slug = fileName.replace(/.md$/, '')
// Read markdown file as string
const fullPath = path.join(postsDirectory, fileName)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents)
// Use remark to convert markdown into HTML
const processedContent = await remark()
.use(html)
.use(prism) // Add syntax highlighting
.process(matterResult.content)
const content = processedContent.toString()
// Combine the data with the slug
return {
slug,
...(matterResult.data as {
title: string;
date: string;
author: string;
}),
content,
}
})
)
// Sort posts by date
return allPostsData.sort(({ date: a }, { date: b }) => {
if (a < b) {
return 1
}
if (a > b) {
return -1
}
return 0
})
}
export async function getAllPostSlugs() {
const fileNames = fs.readdirSync(postsDirectory)
return fileNames.map((fileName) => {
return {
params: {
slug: fileName.replace(/.md$/, ''),
},
}
})
}
export async function getPostData(slug: string) {
const fullPath = path.join(postsDirectory, `${slug}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
const matterResult = matter(fileContents)
const processedContent = await remark()
.use(html)
.use(prism) // Add syntax highlighting
.process(matterResult.content)
const content = processedContent.toString()
// Combine the data with the slug
return {
slug,
...(matterResult.data as {
title: string;
date: string;
author: string;
}),
content,
}
}
Let’s break down this code:
- Import Statements: Import necessary modules for file system operations, path manipulation, Markdown parsing, and syntax highlighting.
- `postsDirectory` Variable: Defines the directory where your Markdown files are stored.
- `getSortedPostsData()` Function:
- Reads all filenames from the `postsDirectory`.
- Iterates through each filename, extracts the slug (filename without the .md extension).
- Reads the file content using `fs.readFileSync`.
- Parses the frontmatter using `gray-matter`.
- Converts Markdown to HTML using `remark`, `remark-html`, and `remark-prism` (for syntax highlighting).
- Combines the extracted data (slug, frontmatter, and HTML content) into an object.
- Sorts the posts by date in descending order.
- `getAllPostSlugs()` Function: Retrieves all post slugs. This is used for dynamic routes (more on this later).
- `getPostData(slug)` Function:
- Takes a slug as input.
- Reads the corresponding Markdown file.
- Parses the frontmatter and Markdown content into HTML, just like `getSortedPostsData()`.
- Returns an object containing the post data (slug, frontmatter, and HTML content).
Displaying Posts on the Home Page
Now, let’s modify the `pages/index.tsx` file to display a list of your blog posts. Replace the existing content with the following:
import type { NextPage } from 'next'
import Link from 'next/link'
import { getSortedPostsData, PostData } from '../lib/posts'
interface Props {
allPostsData: PostData[];
}
const Home: NextPage<Props> = ({ allPostsData }) => {
return (
<div className="container">
<h1>My Blog</h1>
<ul>
{allPostsData.map(({ slug, title, date }) => (
<li key={slug}>
<Link href={`/posts/${slug}`}>
<a>{title}</a>
</Link>
<br />
<small>{date}</small>
</li>
))}
</ul>
</div>
)
}
export async function getStaticProps() {
const allPostsData = await getSortedPostsData()
return {
props: {
allPostsData,
},
}
}
export default Home
Here’s what this code does:
- Imports: Imports necessary modules like `Link` (for navigation), `getSortedPostsData` (from `lib/posts.ts`), and `PostData` interface.
- `Props` Interface: Defines the type of props the component will receive (an array of `PostData`).
- `Home` Component:
- Receives `allPostsData` as a prop.
- Maps through the `allPostsData` array and renders a list item for each post.
- Uses the `Link` component to create a link to the individual post page (we’ll create this page next).
- `getStaticProps()` Function:
- This Next.js function is used for Static Site Generation (SSG).
- Calls `getSortedPostsData()` to fetch the data.
- Returns the data as props to the `Home` component.
Now, start your development server by running `npm run dev` in your terminal. You should see a list of your posts (currently just one, “My First Blog Post”) on the home page. The links won’t work yet, as we haven’t created the individual post pages.
Creating Dynamic Post Pages
Next.js makes it easy to create dynamic routes using the file system. To create a page for each individual post, we’ll use a dynamic route. Create a file named `[slug].tsx` inside the `pages/posts` directory. This file will handle the rendering of individual blog posts.
import type { NextPage } from 'next'
import { useRouter } from 'next/router'
import { getAllPostSlugs, getPostData, PostData } from '../../lib/posts'
import React from 'react';
interface Props {
postData: PostData;
}
const Post: NextPage<Props> = ({ postData }) => {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
return (
<div>
<h1>{postData.title}</h1>
<p>{postData.date} by {postData.author}</p>
<div dangerouslySetInnerHTML={{ __html: postData.content }} />
</div>
)
}
export async function getStaticPaths() {
const paths = await getAllPostSlugs()
return {
paths,
fallback: false,
}
}
export async function getStaticProps({ params }: { params: { slug: string } }) {
const postData = await getPostData(params.slug)
return {
props: {
postData,
},
}
}
export default Post
Let’s break down this code:
- Imports: Imports necessary modules like `useRouter`, `getAllPostSlugs`, `getPostData`, and `PostData`.
- `Props` Interface: Defines the type of props the component will receive (a `PostData` object).
- `Post` Component:
- Uses `useRouter` to access the router object.
- Checks if `router.isFallback` is true. If it is, it returns a loading state.
- Renders the post title, date, author, and content.
- Uses `dangerouslySetInnerHTML` to render the HTML content generated from Markdown. Be cautious when using this. Ensure your content is sanitized to prevent XSS vulnerabilities.
- `getStaticPaths()` Function:
- This Next.js function is used for SSG and defines which paths (URLs) should be pre-rendered.
- Calls `getAllPostSlugs()` to get all the slugs of your posts.
- Returns an array of paths, each containing a `params` object with a `slug` property.
- `fallback: false` means that if a path isn’t generated at build time, it will result in a 404 error.
- `getStaticProps()` Function:
- This Next.js function is also used for SSG.
- Receives the `params` object from `getStaticPaths()`.
- Calls `getPostData()` to fetch the data for the specific post based on the slug.
- Returns the post data as props to the `Post` component.
Now, when you click on the link to your blog post on the home page, you should see the rendered content from your Markdown file.
Styling Your Blog
Let’s add some basic styling to make your blog look better. Open `styles/globals.css` and add the following CSS:
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
.container {
max-width: 800px;
margin: 2rem auto;
padding: 1rem;
}
h1, h2, h3, h4, h5, h6 {
margin-bottom: 0.5rem;
}
code {
background-color: #f0f0f0;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: monospace;
}
pre {
background-color: #f0f0f0;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
}
This CSS provides basic styling for the container, headings, links, and code blocks. You can customize it further to match your desired design.
Common Mistakes and How to Fix Them
Here are some common mistakes and how to avoid them:
- Incorrect File Paths: Double-check your file paths, especially in the `lib/posts.ts` file and when linking between pages.
- Typos in Frontmatter: Frontmatter is sensitive to typos. Make sure your keys (e.g., `title`, `date`, `author`) are spelled correctly.
- Missing Dependencies: Ensure you’ve installed all the necessary dependencies (gray-matter, remark, remark-html, remark-prism).
- Incorrect Markdown Syntax: Markdown has specific syntax rules. Use a Markdown editor or previewer to ensure your Markdown is formatted correctly.
- `dangerouslySetInnerHTML` Security: If you allow user-generated content, sanitize the HTML to prevent cross-site scripting (XSS) attacks.
- Caching Issues: When developing locally, Next.js may cache your files. If you make changes and don’t see them, try restarting the development server or clearing the cache.
Key Takeaways
Here’s a summary of what we’ve covered:
- We set up a Next.js project with TypeScript.
- We installed and configured the necessary packages for Markdown parsing and syntax highlighting.
- We created a project structure to organize your Markdown files.
- We wrote functions to fetch and parse Markdown files, including frontmatter and HTML content.
- We displayed a list of blog posts on the home page.
- We created dynamic routes to render individual blog posts.
- We added basic styling to improve the appearance of your blog.
FAQ
Here are some frequently asked questions:
- How do I add more blog posts? Simply create new `.md` files in the `posts` directory and add the necessary frontmatter and content. The home page and individual post pages will automatically update.
- How can I customize the styling? You can modify the CSS in `styles/globals.css` or create CSS modules or styled components for more granular styling control.
- How do I deploy this blog? You can deploy your Next.js blog to platforms like Vercel, Netlify, or AWS. These platforms offer easy deployment and hosting options.
- How do I add images to my blog posts? You can add images by placing them in the `public` directory and referencing them in your Markdown using the appropriate syntax (e.g., ``).
Building a blog with Next.js and Markdown is a fantastic way to showcase your skills and share your knowledge. This tutorial provides a solid foundation for creating a dynamic and engaging blog. Now that you’ve learned the basics, explore more advanced features like pagination, categories, tags, and comments. Consider using a headless CMS for content management to further streamline your workflow. The possibilities are endless, and with each new feature you implement, you’ll become more proficient in Next.js and web development in general. Keep experimenting, keep learning, and most importantly, keep creating!
