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 (
<div className="max-w-4xl mx-auto px-4">
<h1>Blog</h1>
<div className="grid gap-6">
{posts.map((post) => (
<Link href={`/blog/${post.slug}`} key={post.slug}>
<article className="p-6 border rounded-lg hover:shadow-lg">
<h2>{post.title}</h2>
<p className="text-gray-600">{post.description}</p>
<time>{post.date.toLocaleDateString()}</time>
</article>
</Link>
))}
</div>
</div>
);
}
Individual Post Page
export default async function PostPage({
params
}: {
params: { slug: string }
}) {
const post = await getBlogPostBySlug(params.slug);
return (
<article className="max-w-2xl mx-auto">
<h1>{post.title}</h1>
<time>{post.date.toLocaleDateString()}</time>
<div
className="prose"
dangerouslySetInnerHTML={{ __html: post.htmlContent }}
/>
</article>
);
}
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
.mdfile inapp/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.