← 返回首页
终于解决了博客网站一个特别隐蔽的Bug

终于解决了博客网站一个特别隐蔽的Bug

发布于 2026年11月9日 12:01⏱ 5 分钟阅读👁 7

项目:jianglai520.com(Next.js 16 + Tailwind CSS 4 + TipTap)

行内代码反引号 Bug 分析报告

项目:jianglai520.com(Next.js 16 + Tailwind CSS 4 + TipTap) 日期:2026-11-09 结论:HTML 完全正常,反引号由 @tailwindcss/typography 的 CSS 伪元素生成


一、问题现象

文章正文里的行内代码,例如:

请读取 `read_file` 的内容。

预期显示:

请读取 read_file 的内容。

(read_file 是等宽字体 + 淡青底的行内代码块,不显示反引号)

实际显示:

请读取 `read_file` 的内容。

(反引号原样出现在页面上)

影响范围:所有含行内代码的文章,不是某一篇。


二、为什么这个 Bug 很隐蔽

这个问题的迷惑性在于:从三个层面看,代码都是"正确"的。

检查层 结果 容易得出的错误结论
数据库原始内容 `read_file` 是正常 Markdown 反引号 "数据没问题"
服务端渲染 HTML <code class="language-text">read_file</code> "渲染器没问题"
全量抓取页面扫描 可见正文里反引号字符数量为 0 "用户看到的是缓存"
浏览器实际显示 反引号可见 ← 真实问题

关键点:反引号不在 HTML 里,也不在数据库里,而是浏览器根据 CSS 在渲染时"画"上去的。

抓 curl、看页面源码、用爬虫、扫 HTML,都看不到它。


三、排查过程(真实时间线)

1. 初始误判:以为是 Markdown 渲染器不支持

检查 app/components/Markdown.tsx:

  • 使用 remark-parse → remark-gfm → remark-rehype → rehype-stringify
  • 支持行内代码、表格、代码块
  • 结论:渲染管线正常

2. 检查数据库

查询 posts.content,发现:

  • 行内代码是正常反引号(`read_file`),不是转义字符
  • 但发现另一个独立问题:部分表格被压平成段落(TipTap 编辑器不支持表格)

3. 抓线上 HTML

curl -sL https://jianglai520.com/posts/50 | grep -o '<code[^>]*>[^<]*</code>'

输出:

<code class="language-text">read_file</code>

HTML 完全正常,没有任何反引号。

4. 全量扫描 14 篇文章

去掉代码块后统计可见正文里的 `、**、##、|---:

fences: 0, ticks: 0, stars: 0, hashes: 0, tableSep: 0

结论:所有文章正文 HTML 都是干净的。

5. 转折点:区分「内容层」和「表现层」

用户坚持"页面上就是有反引号"。重新思考:

HTML 正常,但显示不正常 → 一定是浏览器渲染阶段加了东西。

浏览器渲染阶段可能加东西的来源:

  1. CSS ::before / ::after 伪元素
  2. 浏览器扩展
  3. 字体连字(ligature)
  4. 用户代理默认样式

6. 定位到 Tailwind Typography

检查项目依赖:Markdown 容器用了:

<div className="markdown-body prose prose-invert max-w-none" ... />

prose 来自 @tailwindcss/typography。查看插件源码:

// node_modules/@tailwindcss/typography/src/styles.js
code: {
  color: 'var(--tw-prose-code)',
  fontWeight: '600',
},
'code::before': {
  content: '"`"',   // ← 元凶
},
'code::after': {
  content: '"`"',   // ← 元凶
},

Typography 默认会给所有行内代码加反引号装饰。


四、根因

@tailwindcss/typography 的 prose 类默认样式:

.prose code::before { content: "`"; }
.prose code::after  { content: "`"; }

浏览器渲染 <code>read_file</code> 时:

实际 DOM:       <code>read_file</code>
浏览器显示:      `read_file`
                 ↑           ↑
              伪元素       伪元素

所以:

  • 数据库里没有多余反引号
  • HTML 里没有多余反引号
  • 反引号是 CSS 伪元素 content 生成的

为什么代码块不受影响?

Typography 对 pre code 单独做了重置:

'pre code::before': { content: 'none' },
'pre code::after':  { content: 'none' },

所以只有行内代码会出现反引号。


五、修复方案

在 app/globals.css 增加覆盖:

/* 去掉 @tailwindcss/typography 自动给行内代码加的反引号(::before/::after) */
.markdown-body code::before,
.markdown-body code::after {
  content: none;
}

为什么这样能覆盖?

规则 特异性
.prose :where(code):not(:where([class~="not-prose"] *))::before (0, 1, 0)(:where 不增加特异性)
.markdown-body code::before (0, 1, 1)

后者特异性更高,且不依赖 CSS 加载顺序,稳定生效。

修复后效果

请读取 `read_file` 的内容。

显示为:

请读取 read_file 的内容。

(read_file 显示为行内代码块,不再有可见反引号)


六、验证方式

1. 构建产物验证

npm run build
grep -o "markdown-body code[^}]*}" .next/static/css/*.css

输出包含:

.markdown-body code:before,.markdown-body code:after{content:none}

2. 浏览器 DevTools 验证(推荐)

打开任意文章,F12 → Elements:

  1. 选中行内代码 <code>
  2. 在 Styles 面板查看 ::before / ::after
  3. 修复前:content: "\`"
  4. 修复后:content: none

或在 Console 执行:

getComputedStyle(
  document.querySelector(".markdown-body code"),
  "::before"
).content
  • 修复前:"`"
  • 修复后:none / normal

七、为什么之前会误判(复盘)

这次排查走了弯路,核心原因有三个:

1. 把「HTML 正确」等同于「显示正确」

HTML 只是内容层。最终显示还受:

  • CSS 伪元素(::before / ::after)
  • content 属性
  • 字体连字
  • 浏览器扩展

影响。

2. 两个问题混在一起

同期还存在另一个独立问题:TipTap 编辑器把 Markdown 表格压平。两个问题都表现为"页面上出现原始 Markdown 符号",容易混为一谈:

问题 根因 修复
表格压平 编辑器不支持表格,保存时结构丢失 源码模式 + 批量修复数据
行内代码反引号 Typography 的 CSS 伪元素 一条 CSS 覆盖

3. 全量扫描的盲区

扫描的是HTML 文本,天然看不到 CSS 伪元素生成的内容。扫描结果"0 个反引号"反而强化了"页面没问题"的错误判断。


八、通用排查清单(以后遇到类似问题)

当"页面显示和 HTML 不一致"时,按这个顺序查:

  1. 看 HTML:目标元素是否正确?
    curl -sL <url> | grep -o '<code[^>]*>[^<]*</code>'
  2. 看 DevTools Elements:元素上有没有 ::before / ::after?
  3. 看 computed style:
    getComputedStyle(el, "::before").content
  4. 搜索依赖的默认样式:
    grep -rn "code::before\|content:" node_modules/@tailwindcss/typography/src/styles.js
  5. 检查特异性:覆盖规则是否足够强?
  6. 最后才怀疑缓存:缓存通常影响整页,不会只影响行内代码。

九、经验总结

核心教训

HTML 对 ≠ 显示对。 内容正确只代表"内容层"正确,最终呈现还隔着一层 CSS。

具体经验

  1. Tailwind Typography 的 prose 自带很多装饰:行内代码反引号、引用引号、表格边框等。接入自定义设计系统时,要有意识地逐项覆盖。
  2. 伪元素是排查盲区:curl、爬虫、页面源码、RSC payload 都看不到 ::before / ::after 的 content。
  3. 不要用"扫描 HTML 没发现"来否定用户反馈:用户看到的是浏览器渲染结果,不是 HTML。
  4. 先分离问题再修复:同期多个渲染问题要分别定位根因,避免"修了一个以为解决了全部"。

一句话版本

行内代码的反引号不是 Markdown 语法残留,而是 Tailwind Typography 的 code::before/after 伪元素;覆盖 content: none 即可。


附:修复前后对照

修复前

<!-- HTML:正常 -->
<code class="language-text">read_file</code>
 
<!-- 浏览器实际显示: -->
`read_file`

修复后

.markdown-body code::before,
.markdown-body code::after {
  content: none;
}
<!-- HTML:不变 -->
<code class="language-text">read_file</code>
 
<!-- 浏览器实际显示: -->
read_file

评论 (0)

还没有评论,来写第一条吧~

写评论