2 min readLast updated: 2025-12-31 12:12
Blog System Design
This document describes the detailed design for integrating a blog system into Next.js projects, supporting MDX/MD document parsing into static pages with multilingual support (en, zh-CN, ja).
Technology Stack
Recommended: react-markdown + next-mdx-remote
Using react-markdown with next-mdx-remote for MDX rendering - a lightweight and flexible solution.
Core Dependencies
| Package | Purpose |
|---|---|
react-markdown | Markdown rendering |
next-mdx-remote | MDX remote/dynamic rendering |
gray-matter | Parse frontmatter |
rehype-highlight | Code syntax highlighting |
rehype-slug | Add IDs to headings |
remark-gfm | GitHub Flavored Markdown support |
Advantages
- Lightweight - No extra build steps required
- Flexible - Can dynamically load content
- Mature & Stable - Widely used libraries
- Easy Integration - Works perfectly with Next.js App Router
- MDX Support - Use React components in Markdown
Directory Structure
project-root/
├── content/
│ └── blog/
│ ├── getting-started/
│ │ ├── en.mdx # English version
│ │ ├── zh-CN.mdx # Chinese version
│ │ ├── ja.mdx # Japanese version
│ │ └── thumbnail.jpg # Article thumbnail
│ └── [...more articles]/
│
├── src/
│ ├── app/
│ │ └── [lang]/
│ │ └── (main)/
│ │ └── blog/
│ │ ├── page.tsx # Blog list page
│ │ └── [slug]/
│ │ └── page.tsx # Blog article detail page
│ │
│ ├── components/
│ │ └── blog/
│ │ ├── BlogCard.tsx # Article card component
│ │ ├── BlogList.tsx # Article list component
│ │ ├── BlogContent.tsx # Article content renderer
│ │ └── MDXComponents.tsx # MDX custom component mapping
│ │
│ └── lib/
│ └── blog/
│ ├── index.ts # Blog utility exports
│ ├── api.ts # Blog data fetching API
│ └── types.ts # Type definitions
MDX Article Format
Frontmatter Structure
---
title: "Getting Started with Next.js"
description: "A comprehensive guide to building modern web applications"
date: "2024-12-17"
author: "John Doe"
thumbnail: "/blog/getting-started/thumbnail.jpg"
published: true
---
# Getting Started with Next.js
Your article content here...
Supported Frontmatter Fields
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | ✅ | Article title |
| description | string | ✅ | Article summary/description |
| date | string | ✅ | Publish date (YYYY-MM-DD) |
| author | string | ❌ | Author name |
| thumbnail | string | ❌ | Thumbnail path |
| published | boolean | ❌ | Whether published (default true) |
Core Implementation
Blog Data Fetching API
// src/lib/blog/api.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { BlogPost, BlogPostMeta } from './types'
const BLOG_DIR = path.join(process.cwd(), 'content/blog')
// Get all blog post metadata
export async function getAllPosts(locale: string): Promise<BlogPostMeta[]> {
const slugs = fs.readdirSync(BLOG_DIR).filter(name => {
const stat = fs.statSync(path.join(BLOG_DIR, name))
return stat.isDirectory()
})
const posts: BlogPostMeta[] = []
for (const slug of slugs) {
const filePath = path.join(BLOG_DIR, slug, `${locale}.mdx`)
if (!fs.existsSync(filePath)) continue
const fileContent = fs.readFileSync(filePath, 'utf-8')
const { data } = matter(fileContent)
if (data.published === false) continue
posts.push({
slug,
title: data.title,
description: data.description,
date: data.date,
author: data.author,
thumbnail: data.thumbnail,
readingTime: calculateReadingTime(fileContent, locale),
})
}
return posts.sort((a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime()
)
}
// Get single post
export async function getPostBySlug(
slug: string,
locale: string
): Promise<BlogPost | null> {
const filePath = path.join(BLOG_DIR, slug, `${locale}.mdx`)
if (!fs.existsSync(filePath)) return null
const fileContent = fs.readFileSync(filePath, 'utf-8')
const { data, content } = matter(fileContent)
return {
slug,
title: data.title,
description: data.description,
date: data.date,
author: data.author,
thumbnail: data.thumbnail,
content,
readingTime: calculateReadingTime(content, locale),
}
}
// Calculate reading time
function calculateReadingTime(content: string, locale: string): number {
const wordsPerMinute = locale === 'zh-CN' || locale === 'ja' ? 400 : 200
const text = content.replace(/---[\s\S]*?---/, '')
const wordCount = locale === 'zh-CN' || locale === 'ja'
? text.length
: text.split(/\s+/).length
return Math.max(1, Math.ceil(wordCount / wordsPerMinute))
}
Type Definitions
// src/lib/blog/types.ts
export interface BlogPostMeta {
slug: string
title: string
description: string
date: string
author?: string
thumbnail?: string
readingTime: number
}
export interface BlogPost extends BlogPostMeta {
content: string
}
Blog List Page
// src/app/[lang]/(main)/blog/page.tsx
import { getAllPosts } from '@/lib/blog/api'
import { BlogList } from '@/components/blog/BlogList'
export default async function BlogPage({
params,
}: {
params: Promise<{ lang: string }>
}) {
const { lang } = await params
const posts = await getAllPosts(lang)
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-8">Blog</h1>
<BlogList posts={posts} lang={lang} />
</div>
)
}
MDX Content Renderer
// src/components/blog/BlogContent.tsx
'use client'
import { MDXRemote } from 'next-mdx-remote/rsc'
import remarkGfm from 'remark-gfm'
import rehypeHighlight from 'rehype-highlight'
import rehypeSlug from 'rehype-slug'
import { BlogPost } from '@/lib/blog/types'
import { MDXComponents } from './MDXComponents'
interface BlogContentProps {
post: BlogPost
lang: string
}
export function BlogContent({ post, lang }: BlogContentProps) {
return (
<article className="container mx-auto px-4 py-12 max-w-4xl">
<header className="mb-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="flex items-center gap-4 text-gray-600">
<time dateTime={post.date}>
{new Date(post.date).toLocaleDateString(lang, {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
{post.author && <span>by {post.author}</span>}
<span>{post.readingTime} min read</span>
</div>
</header>
<div className="prose prose-lg max-w-none">
<MDXRemote
source={post.content}
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [rehypeHighlight, rehypeSlug],
},
}}
components={MDXComponents}
/>
</div>
</article>
)
}
Installation
pnpm add react-markdown next-mdx-remote gray-matter remark-gfm rehype-highlight rehype-slug
Code Highlighting Styles
Import highlight.js CSS styles:
// src/app/[lang]/(main)/blog/layout.tsx
import 'highlight.js/styles/github-dark.css'
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return children
}
Multilingual Configuration
Blog Translation Files
// public/locales/en/blog.json
{
"title": "Blog",
"subtitle": "Latest articles and updates",
"readMore": "Read more",
"readingTime": "{{minutes}} min read",
"publishedOn": "Published on {{date}}",
"noArticles": "No articles found",
"backToBlog": "Back to Blog"
}
// public/locales/zh-CN/blog.json
{
"title": "博客",
"subtitle": "最新文章和更新",
"readMore": "阅读更多",
"readingTime": "{{minutes}} 分钟阅读",
"publishedOn": "发布于 {{date}}",
"noArticles": "暂无文章",
"backToBlog": "返回博客"
}
Summary
This solution uses react-markdown + next-mdx-remote to implement a blog system, providing:
- ✅ Lightweight implementation, no extra build steps
- ✅ Full MDX support, can use React components
- ✅ Complete multilingual support
- ✅ Code syntax highlighting
- ✅ Reading time estimation
- ✅ SEO optimization (static generation + dynamic metadata)
- ✅ Responsive design