Markdown 渲染管线:从 marked 到带锚点的文章目录

2026年6月14日 blogTech 5 分钟阅读 16 次阅读
📖 文章摘要

从 Markdown 原文到文章目录的完整管线。本文详解 marked 自定义 Renderer、TOC 提取算法、写入时渲染缓存和代码复制按钮的实现。

Markdown 渲染管线

从 Markdown 原文到最终带目录的文章页面,中间经过多层处理。

渲染流程

用户输入(Markdown 原文)
    ↓
FastAPI 后端写入(content_md + 同步渲染 content_html)
    ↓
Nuxt SSR 读取(content_md 通过 marked 渲染为 HTML)
    ↓
页面展示(自定义 heading id + 代码复制按钮)
    ↓
目录提取(从 MD 原文抽取 h2-h4 结构)

后端渲染缓存

写入文章时,后端使用 markdown 库同步渲染 HTML 并存到 content_html 字段:

def _render_markdown(text: str) -> str:
    return markdown.markdown(text, extensions=["fenced_code", "codehilite", "tables"])

这种写入时渲染策略避免了每次请求都重复渲染 Markdown。

但前端实际使用的是客户端渲染(marked 库),因为:

  1. 文章详情页需要动态提取 heading 生成 TOC
  2. 自定义 Renderer 需要在前端执行
  3. 后端缓存的 HTML 可作为降级方案

前端渲染(marked)

前端使用 marked 库渲染 Markdown,并自定义了 heading renderer:

const renderer = new marked.Renderer()
renderer.heading = ({ text, depth }) => {
  const id = text
    .toLowerCase()
    .replace(/[^\w一-龥]+/g, '-')
    .replace(/^-|-$/g, '')
  return `<h${depth} id="${id}">${text}</h${depth}>`
}
marked.setOptions({ renderer, breaks: true })

每个 heading 会自动生成中文友好的锚点 ID(保留中文字符、非字母数字替换为连字符),配合 CSS scroll-margin-top: 80px 使锚点跳转避开固定导航栏。

TOC 提取

通过正则从 Markdown 原文提取 h2-h4 结构,生成可点击的目录:

const headings = computed(() => {
  const regex = /^(#{2,4})\s+(.+)$/gm
  const items = []
  let match
  while ((match = regex.exec(article.value.content_md)) !== null) {
    const text = match[2].trim()
    const id = text.toLowerCase().replace(/[^\w一-龥]+/g, '-').replace(/^-|-$/g, '')
    items.push({ level: match[1].length, text, id })
  }
  return items
})

目录层级通过 level 字段区分(2 = h2、3 = h3、4 = h4),渲染时用缩进或字号区别。

代码块与复制按钮

每个代码块右上角悬浮复制按钮:

.code-copy-btn {
  position: absolute;
  top: 8px; right: 8px;
  backdrop-filter: blur(4px);
  background: rgba(255,255,255,0.08);
  color: rgba(255,255,255,0.7);
  border: 1px solid rgba(255,255,255,0.15);
}

通过 position: absolute 定位在代码块的 position: relative 容器内。

样式处理

文章正文使用自定义样式类 .prose(而非 Tailwind 的 typography 插件),主要规则:

.prose p, .prose li, .prose blockquote {
  font-size: 17px;
  line-height: 1.75rem;
  letter-spacing: 0.025em;
}

深色模式适配:

.dark .prose { color: #ffffff; }
.dark .prose a { color: #93c5fd; }
.dark .prose code { color: #f472b6; }

阅读时间计算

根据 Markdown 长度估算阅读时间:

const readingTime = computed(() => {
  return Math.max(1, Math.ceil(article.value.content_md.length / 500))
})

按中文阅读速度约 500 字/分钟估算,不少于 1 分钟。

最后更新:2026年6月29日CC BY-NC-SA 4.0

评论

暂无评论,来写第一条吧

© 2026 My Blog. Built with Nuxt.js + FastAPI.