Building a Markdown-Based Blog System with Next.js

When we decided to add a blog to qz-l, we wanted something simple, fast, and easy to maintain. No complex databases, no heavy CMS—just markdown files and static generation.

Why Markdown?

Markdown is perfect for developer-focused projects because:

Simple format: Easy to write and version control

No database needed: Store posts as files in your repository

Fast: Static generation means instant page loads

Flexible: Can be extended with custom plugins

Searchable: All content is in plain text

Our Architecture

File Structure

app/blog/
├── page.tsx # Blog listing page
├── [slug]/
│ └── page.tsx # Individual blog post page
└── posts/
├── welcome.md
├── markdown-blog-setup.md
└── your-post.md

Frontmatter Format

Each markdown file starts with YAML frontmatter containing metadata:

---
title: Your Post Title
description: Brief description for listings
date: 2025-11-16
author: Your Name
---

# Your Post Content Here

Key Technologies

1. gray-matter

Parses frontmatter from markdown files:

import matter from 'gray-matter';

const { data, content } = matter(fileContent);
// data = { title, date, author, description }
// content = markdown content

2. marked

Converts markdown to HTML:

import { marked } from 'marked';

const html = await marked(markdownContent);

3. Next.js Static Generation

Blog posts are pre-rendered at build time for maximum performance:

export async function generateStaticParams() {
const posts = getAllBlogPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}

Implementation Highlights

Getting All Posts

export async function getAllBlogPosts(): Promise<BlogPost[]> {
const postsDirectory = join(process.cwd(), 'app/blog/posts');
const files = readdirSync(postsDirectory);

const posts = files
.filter((file) => file.endsWith('.md'))
.map((file) => {
const content = readFileSync(join(postsDirectory, file), 'utf-8');
const { data, content: body } = matter(content);

return {
slug: file.replace('.md', ''),
title: data.title,
date: new Date(data.date),
author: data.author,
description: data.description,
content: body,
};
})
.sort((a, b) => b.date.getTime() - a.date.getTime());

return posts;
}

Blog Listing Page

Display all posts with metadata:

export default async function BlogPage() {
const posts = await getAllBlogPosts();

return (

Blog

{posts.map((post) => (


{post.title}
{post.description}
{post.date.toLocaleDateString()}


))}


);
}

Individual Post Page

export default async function PostPage({
params
}: {
params: { slug: string }
}) {
const post = await getBlogPostBySlug(params.slug);

return (

{post.title}
{post.date.toLocaleDateString()}
<div
className="prose"
dangerouslySetInnerHTML={{ __html: post.htmlContent }}
/>

);
}

Benefits of This Approach

✅ Zero Database: Simpler deployment, no migrations
✅ Version Control: Posts are tracked in git
✅ Fast Performance: Static generation means instant loads
✅ Developer Friendly: Write posts in your IDE
✅ Easy Backup: Posts are just files in your repository
✅ Scalable: Works perfectly for 10 or 1000 posts
✅ SEO Friendly: Static HTML is great for search engines

Future Enhancements

As your blog grows, you can add:

RSS Feed: Auto-generate from markdown posts

Search: Index posts and implement full-text search

Tags/Categories: Organize posts by topic

Comments: Integration with services like Giscus

Email Notifications: Notify subscribers of new posts

Analytics: Track popular posts

Getting Started

To add a new post to qz-l:

Create a new .md file in app/blog/posts/

Add frontmatter with metadata

Write your content in markdown

Deploy—the post appears automatically!

Conclusion

This markdown-based approach gives us the best of both worlds: simplicity of static files with the power of a modern framework. It's perfect for projects that want a blog without the overhead of a full CMS.

If you're building a developer-focused product, we highly recommend this approach. It's simple, maintainable, and scales beautifully.

Happy blogging! 📝

Want to learn more? Check out the gray-matter and marked documentation for more customization options.