Secure_Doc // Encryption_Active

博客系统设计

阅读时间 3 分钟2025-12-31 12:12

博客系统设计方案

本文档描述了为 Next.js 项目接入博客系统的详细设计方案,支持 MDX/MD 文档解析为静态页面,并支持多语言(en, zh-CN, ja)。

技术选型

推荐方案:react-markdown + next-mdx-remote

使用 react-markdown 配合 next-mdx-remote 实现 MDX 渲染,这是一个轻量级且灵活的方案。

核心依赖

包名用途
react-markdownMarkdown 渲染
next-mdx-remoteMDX 远程/动态渲染
gray-matter解析 frontmatter
rehype-highlight代码语法高亮
rehype-slug为标题添加 id
remark-gfm支持 GitHub Flavored Markdown

方案优势

  1. 轻量级 - 无需额外构建步骤
  2. 灵活性高 - 可以动态加载内容
  3. 成熟稳定 - 广泛使用的库
  4. 易于集成 - 与 Next.js App Router 完美配合
  5. 支持 MDX - 可在 Markdown 中使用 React 组件

目录结构设计

project-root/
├── content/
│   └── blog/
│       ├── getting-started/
│       │   ├── en.mdx              # 英文版本
│       │   ├── zh-CN.mdx           # 中文版本
│       │   ├── ja.mdx              # 日文版本
│       │   └── thumbnail.jpg       # 文章缩略图
│       ├── nextjs-best-practices/
│       │   ├── en.mdx
│       │   ├── zh-CN.mdx
│       │   ├── ja.mdx
│       │   └── thumbnail.png
│       └── [...更多文章]/
│
├── src/
│   ├── app/
│   │   └── [lang]/
│   │       └── (main)/
│   │           └── blog/
│   │               ├── page.tsx           # 博客列表页
│   │               └── [slug]/
│   │                   └── page.tsx       # 博客文章详情页
│   │
│   ├── components/
│   │   └── blog/
│   │       ├── BlogCard.tsx              # 文章卡片组件
│   │       ├── BlogList.tsx              # 文章列表组件
│   │       ├── BlogContent.tsx           # 文章内容渲染组件
│   │       ├── CodeBlock.tsx             # 代码高亮组件
│   │       ├── ReadingTime.tsx           # 阅读时间组件
│   │       └── MDXComponents.tsx         # MDX 自定义组件映射
│   │
│   └── lib/
│       └── blog/
│           ├── index.ts                  # 博客工具函数导出
│           ├── api.ts                    # 博客数据获取 API
│           └── types.ts                  # 类型定义

MDX 文章格式

Frontmatter 结构

---
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...

支持的 Frontmatter 字段

字段类型必填描述
titlestring文章标题
descriptionstring文章摘要/描述
datestring发布日期 (YYYY-MM-DD)
authorstring作者名称
thumbnailstring缩略图路径
publishedboolean是否发布(默认 true)

核心实现

博客数据获取 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')

// 获取所有博客文章的元数据
export async function getAllPosts&#40;locale: string&#41;: Promise<BlogPostMeta[]> {
  const slugs = fs.readdirSync&#40;BLOG_DIR&#41;.filter&#40;name => {
    const stat = fs.statSync&#40;path.join&#40;BLOG_DIR, name&#41;&#41;
    return stat.isDirectory&#40;&#41;
  }&#41;

  const posts: BlogPostMeta[] = []

  for &#40;const slug of slugs&#41; {
    const filePath = path.join&#40;BLOG_DIR, slug, `${locale}.mdx`&#41;
    
    if &#40;!fs.existsSync&#40;filePath&#41;&#41; continue

    const fileContent = fs.readFileSync&#40;filePath, 'utf-8'&#41;
    const { data } = matter&#40;fileContent&#41;

    if &#40;data.published === false&#41; continue

    posts.push&#40;{
      slug,
      title: data.title,
      description: data.description,
      date: data.date,
      author: data.author,
      thumbnail: data.thumbnail,
      readingTime: calculateReadingTime&#40;fileContent, locale&#41;,
    }&#41;
  }

  return posts.sort&#40;&#40;a, b&#41; => 
    new Date&#40;b.date&#41;.getTime&#40;&#41; - new Date&#40;a.date&#41;.getTime&#40;&#41;
  &#41;
}

// 获取单篇文章
export async function getPostBySlug&#40;
  slug: string, 
  locale: string
&#41;: Promise<BlogPost | null> {
  const filePath = path.join&#40;BLOG_DIR, slug, `${locale}.mdx`&#41;
  
  if &#40;!fs.existsSync&#40;filePath&#41;&#41; return null

  const fileContent = fs.readFileSync&#40;filePath, 'utf-8'&#41;
  const { data, content } = matter&#40;fileContent&#41;

  return {
    slug,
    title: data.title,
    description: data.description,
    date: data.date,
    author: data.author,
    thumbnail: data.thumbnail,
    content,
    readingTime: calculateReadingTime&#40;content, locale&#41;,
  }
}

// 计算阅读时间
function calculateReadingTime&#40;content: string, locale: string&#41;: number {
  const wordsPerMinute = locale === 'zh-CN' || locale === 'ja' ? 400 : 200
  const text = content.replace&#40;/---[\s\S]*?---/, ''&#41; // 移除 frontmatter
  const wordCount = locale === 'zh-CN' || locale === 'ja' 
    ? text.length 
    : text.split&#40;/\s+/&#41;.length
  return Math.max&#40;1, Math.ceil&#40;wordCount / wordsPerMinute&#41;&#41;
}

类型定义

// 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 BlogPost