用 python-markdown 渲染博客正文,最小实现只要三行:
import markdown
html = markdown.markdown(text, extensions=["extra", "toc"])
但要把体验做到能看,还得处理三件事:目录、代码高亮、中文锚点。
坑:中文标题的锚点是空的¶
toc 扩展默认使用 markdown.extensions.toc.slugify,它内部做了 NFKD 归一化后强制转 ASCII:
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
也就是说 ## 开始之前 会被处理成空字符串,页面上所有中文标题的锚点都变成 #section 或者直接失效,目录点击全部跳到顶部。
解决办法是传一个自己的 slugify:
import re
def slugify(value, separator="-", unicode=False):
value = re.sub(r"[^\w\u4e00-\u9fff\s-]", "", value, flags=re.UNICODE)
value = re.sub(r"[\s]+", separator, value.strip().lower())
return value.strip(separator) or "section"
markdown.Markdown(
extensions=["extra", "toc"],
extension_configs={"toc": {"slugify": slugify, "permalink": True}},
)
permalink 的样式
开启 permalink: True 后,每个标题后面会插入一个 ¶ 锚点链接。记得给 .anchor 设置 opacity: 0,hover 时再显示,否则正文会显得很吵。
目录数据从哪来¶
很多人不知道 toc 扩展除了生成 md.toc(一段 HTML)之外,还会在实例上挂一个 toc_tokens,是结构化的 Python 对象:
md = markdown.Markdown(extensions=["toc"])
html = md.convert(text)
tokens = md.toc_tokens # [{'level':2,'id':'xx','name':'xx','children':[...]}]
它是嵌套的,渲染成侧边栏目录之前需要摊平一层:
def flatten(tokens):
out = []
for t in tokens:
out.append({"level": t["level"], "text": t["name"], "id": t["id"]})
out.extend(flatten(t.get("children") or []))
return out
存成 JSON 跟着文章一起入库,详情页就不必每次重新解析一遍 Markdown。
代码高亮:Pygments 还是前端高亮¶
我一开始用的是 highlight.js,后来换成了服务端 Pygments,理由有三个:
- 没有 FOUC。页面加载完就是高亮好的,不会先闪一下白代码块。
- JS 体积归零。博客的 JS 只有几百行交互逻辑,首屏更快。
- 配色可控。用
HtmlFormatter().get_style_defs()直接导出 CSS。
配置方式:
EXTENSION_CONFIGS = {
"pymdownx.superfences": {},
"pymdownx.highlight": {
"use_pygments": True,
"css_class": "highlight",
"pygments_style": "github-dark",
},
}
不过有个细节:Pygments 的样式是写死在 CSS 里的,一旦生成就是固定配色,没法跟着明暗主题切换。我的做法是生成两份,分别包在 :root 和 [data-theme="dark"] 里——这点 CSS 体积完全可以接受。
最终的扩展组合¶
EXTENSIONS = [
"extra", # 表格 / 脚注 / 定义列表
"admonition", # !!! note 提示块
"sane_lists",
"pymdownx.details", # 可折叠块
"pymdownx.tasklist", # - [x]
"pymdownx.superfences",
"pymdownx.highlight",
"toc",
]
这套组合覆盖了写技术文章 99% 的需求,剩下的 1%(比如数学公式、流程图)我倾向于用图片代替——博客的复杂度应该控制住,否则最后维护的是博客本身,而不是内容。