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

Using react-markdown with next-mdx-remote for MDX rendering - a lightweight and flexible solution.

Core Dependencies

PackagePurpose
react-markdownMarkdown rendering
next-mdx-remoteMDX remote/dynamic rendering
gray-matterParse frontmatter
rehype-highlightCode syntax highlighting
rehype-slugAdd IDs to headings
remark-gfmGitHub Flavored Markdown support

Advantages

  1. Lightweight - No extra build steps required
  2. Flexible - Can dynamically load content
  3. Mature & Stable - Widely used libraries
  4. Easy Integration - Works perfectly with Next.js App Router
  5. 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

FieldTypeRequiredDescription
titlestringArticle title
descriptionstringArticle summary/description
datestringPublish date (YYYY-MM-DD)
authorstringAuthor name
thumbnailstringThumbnail path
publishedbooleanWhether 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