📖 文章摘要
从 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 库),因为:
- 文章详情页需要动态提取 heading 生成 TOC
- 自定义 Renderer 需要在前端执行
- 后端缓存的 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
评论
暂无评论,来写第一条吧
