Building a Personal Blog with Next.js and Markdown
I've always wanted to have a personal blog where I could share my thoughts and experiences as a developer. After trying several platforms, I decided to build my own using Next.js and Markdown. It gives me complete control over the design and functionality, and I can write posts in a format I'm comfortable with.
Why Next.js and Markdown?
When I started thinking about building a blog, I wanted something that was:
- Easy to write in
- Fast and performant
- SEO-friendly
- Customizable
Markdown ticked all the boxes for writing—it's simple, readable, and I can focus on content rather than formatting. Next.js provided the framework I needed for performance and SEO.
Setting up the project structure
I started with a basic Next.js project and organized it like this:
my-blog/
├── app/
│ ├── blog/
│ │ ├── [slug]/
│ │ │ └── page.tsx
│ │ └── page.tsx
│ └── layout.tsx
├── content/
│ └── blog/
│ ├── post-1.md
│ ├── post-2.md
│ └── ...
├── lib/
│ └── markdown.ts
└── components/
└── BlogPost.tsxProcessing Markdown files
The key to making this work is processing Markdown files into HTML. I used a few libraries:
gray-matterfor parsing frontmatterremarkandrehypefor processing Markdownremark-htmlfor converting to HTMLremark-prismfor syntax highlighting
// lib/markdown.ts
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 async function getPostData(slug: string) {
const fullPath = path.join(process.cwd(), 'content/blog', `${slug}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const matterResult = matter(fileContents);
const processedContent = await remark()
.use(prism)
.use(html)
.process(matterResult.content);
return {
slug,
content: processedContent.toString(),
...matterResult.data,
};
}Frontmatter for metadata
Each Markdown file starts with frontmatter that contains metadata:
--- title: "My First Blog Post" date: "2025-03-05" excerpt: "This is my first blog post about..." tags: ["nextjs", "markdown", "blog"] --- # My First Blog Post This is the content of my blog post...
Dynamic routing for blog posts
Next.js App Router makes dynamic routing straightforward:
// app/blog/[slug]/page.tsx
import { getPostData, getAllPostSlugs } from '@/lib/markdown';
export async function generateStaticParams() {
const slugs = getAllPostSlugs();
return slugs.map((slug) => ({ slug }));
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const postData = await getPostData(params.slug);
return (
<article>
<h1>{postData.title}</h1>
<p className="text-gray-600">{postData.date}</p>
<div dangerouslySetInnerHTML={{ __html: postData.content }} />
</article>
);
}SEO optimization
SEO is crucial for a blog. I made sure each post has proper metadata:
// app/blog/[slug]/page.tsx (continued)
export async function generateMetadata({ params }: { params: { slug: string } }) {
const postData = await getPostData(params.slug);
return {
title: postData.title,
description: postData.excerpt,
openGraph: {
title: postData.title,
description: postData.excerpt,
type: 'article',
publishedTime: postData.date,
},
};
}Adding syntax highlighting
Code blocks look much better with syntax highlighting. I used Prism.js:
// lib/markdown.ts (updated)
import prism from 'remark-prism';
const processedContent = await remark()
.use(prism, { plugins: ['line-numbers'] })
.use(html)
.process(matterResult.content);Styling the blog posts
I wanted a clean, readable design. I used Tailwind CSS with some custom components:
// components/BlogPost.tsx
export function BlogPost({ postData }) {
return (
<article className="max-w-3xl mx-auto">
<header className="mb-8">
<h1 className="text-4xl font-bold mb-4">{postData.title}</h1>
<p className="text-gray-600 text-lg">{postData.excerpt}</p>
<div className="flex items-center gap-4 mt-4 text-sm text-gray-500">
<time>{formatDate(postData.date)}</time>
<div className="flex gap-2">
{postData.tags.map(tag => (
<span key={tag} className="bg-gray-100 px-2 py-1 rounded">
{tag}
</span>
))}
</div>
</div>
</header>
<div
className="prose prose-lg max-w-none"
dangerouslySetInnerHTML={{ __html: postData.content }}
/>
</article>
);
}Adding a blog index page
The blog index shows all posts:
// app/blog/page.tsx
import { getAllPosts } from '@/lib/markdown';
import Link from 'next/link';
export default async function BlogIndex() {
const posts = await getAllPosts();
return (
<div className="max-w-4xl mx-auto">
<h1 className="text-4xl font-bold mb-8">Blog</h1>
<div className="space-y-8">
{posts.map((post) => (
<article key={post.slug} className="border-b pb-8">
<h2 className="text-2xl font-bold mb-2">
<Link href={`/blog/${post.slug}`} className="hover:text-blue-600">
{post.title}
</Link>
</h2>
<p className="text-gray-600 mb-2">{post.excerpt}</p>
<div className="flex items-center gap-4 text-sm text-gray-500">
<time>{formatDate(post.date)}</time>
<div className="flex gap-2">
{post.tags.map(tag => (
<span key={tag} className="bg-gray-100 px-2 py-1 rounded text-xs">
{tag}
</span>
))}
</div>
</div>
</article>
))}
</div>
</div>
);
}Performance considerations
To keep the blog fast, I implemented a few optimizations:
- Static generation for all posts
- Image optimization with Next.js Image component
- Lazy loading for long blog posts
- Minimal JavaScript bundle
Deployment and hosting
I deployed the blog on Vercel, which works perfectly with Next.js. The build process automatically generates all the static pages, and the blog loads quickly.
My experience
Building my own blog has been incredibly rewarding. I have complete control over the design and functionality, and writing in Markdown feels natural. The performance is excellent, and SEO works great. If you're thinking about starting a blog, I highly recommend building it yourself with Next.js and Markdown.
Related articles