7.7 KiB
7.7 KiB
Blog 文章列表 <a> 嵌套导致卡片分裂 — 修复 PRD
版本: v1.0 日期: 2026-05-06 状态: 📝 待评审
一、问题描述
1.1 现象
在 blog.ephron.ren/posts 页面,带有 collection 标签的文章(如 algorithm-efficiency-measure)被浏览器渲染为 2~3 张独立卡片,而非一张完整的卡片。
视觉效果:
┌─────────────────────────────┐
│ 【数据结构】算法效率的度量 │ ← 卡片1: 标题 + 摘要
│ 算法执行时间随问题规模... │
└─────────────────────────────┘
┌─────────────────────────────┐
│ 2026-03-04 │ ← 卡片2: 日期 + 标签
│ [数据结构] [算法] ... │
└─────────────────────────────┘
┌─────────────────────────────┐
│ [数据结构 collection] │ ← 卡片3: collection badge
└─────────────────────────────┘
1.2 影响范围
- 所有拥有 collection 关联的文章在 posts 列表页均受影响
- 不带 collection 的文章显示正常(无嵌套
<a>冲突) - 首页文章列表(home 服务)不受影响(使用不同模板)
1.3 严重程度
🟡 中等 — 功能可用但视觉体验明显异常,影响专业度
二、根因分析
2.1 HTML 源码结构(模板)
blog/templates/index.html 中,文章卡片的结构如下:
<li>
<a href="/posts/{{ post.slug }}" class="post-item">
<span class="post-title">标题</span>
<p class="post-excerpt">摘要...</p>
<div class="post-meta">
<span class="post-date">2026-03-04</span>
<div class="post-tags">...</div>
<!-- ⚠️ 问题在这里 -->
{% if item.collections %}
<div class="post-collections">
{% for col in item.collections %}
<a href="/collections/{{ col.key }}" class="collection-badge">{{ col.title }}</a>
{% endfor %}
</div>
{% endif %}
</div>
</a>
</li>
2.2 浏览器解析行为
HTML 规范禁止 <a> 标签嵌套 <a>(<a> 的内容模型不能包含 interactive content)。
当浏览器遇到嵌套的 <a> 时,会自动"修复"DOM:
- 遇到第一个
<a class="post-item">→ 打开 - 遇到内部
<a class="collection-badge">→ 自动关闭外层<a> - 后续的
</div>和</a>变成游离节点
2.3 实际渲染的 DOM(Playwright 提取)
<li>
<!-- 第1个卡片:标题 + 摘要 -->
<a class="post-item">
<span class="post-title">【数据结构】算法效率的度量</span>
<p class="post-excerpt">...</p>
</a>
<!-- 浏览器自动关闭后的 post-meta -->
<div class="post-meta">
<!-- 第2个卡片:日期 + 标签 -->
<a class="post-item">
<span class="post-date">2026-03-04</span>
<div class="post-tags">...</div>
</a>
<div class="post-collections">
<!-- 第3个卡片:collection badge -->
<a class="post-item">...</a>
<a class="collection-badge">数据结构</a>
</div>
</div>
</li>
一个 <li> 里出现了 3 个 <a class="post-item"> 兄弟节点,浏览器将它们渲染为 3 张独立卡片。
三、修复方案
3.1 方案概述
将 <a> 标签的包裹范围缩小,不包含 post-collections 部分。将整个卡片改为 <div> + JS 点击跳转,或将 collection badge 移到 <a> 外部。
3.2 推荐方案:改为 <div> + 点击事件
将外层 <a class="post-item"> 替换为 <div class="post-item">,通过 CSS cursor: pointer 和 JS click 事件实现整卡点击跳转。
优势:
- 根本性解决嵌套问题
- 不影响现有样式(class 不变)
- collection badge 的
<a>链接可独立工作
修改文件:blog/templates/index.html
3.3 代码 Diff
--- a/blog/templates/index.html
+++ b/blog/templates/index.html
@@ -268,7 +268,7 @@
{% set excerpt = post.content | striptags | truncate(120) %}
<li>
- <a
+ <div
href="/posts/{{ post.slug }}"
class="post-item"
data-post-slug="{{ post.slug }}"
@@ -298,7 +298,7 @@
{% endif %}
</div>
- </a>
+ </div>
</li>
3.4 CSS 补充
确保 .post-item 保留点击样式:
.post-item {
cursor: pointer;
display: block;
/* 保留现有样式 */
}
3.5 JS 补充(如需)
如果不想依赖 <a> 标签的原生跳转,添加点击事件:
document.querySelectorAll('.post-item').forEach(item => {
item.addEventListener('click', (e) => {
// 如果点击的是 collection badge,不触发卡片跳转
if (e.target.closest('.collection-badge')) return;
const slug = item.dataset.postSlug;
if (slug) window.location.href = `/posts/${slug}`;
});
});
四、验证方法
4.1 测试用例
| # | 测试项 | 预期结果 |
|---|---|---|
| 1 | 带 collection 的文章卡片 | 显示为一张完整卡片 |
| 2 | 不带 collection 的文章卡片 | 显示正常(无回归) |
| 3 | 点击卡片标题/摘要区域 | 跳转到文章详情页 |
| 4 | 点击 collection badge | 跳转到 collection 页面(不触发卡片跳转) |
| 5 | 卡片 hover 效果 | 正常显示 |
| 6 | 移动端响应式 | 卡片布局正常 |
4.2 验证命令
# 检查 DOM 中不应出现多个 post-item
python3 -c "
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto('https://blog.ephron.ren/posts')
page.wait_for_selector('.post-item')
# 每个 li 应该只有一个 post-item
lis = page.locator('li:has(.post-item)').all()
for li in lis:
count = li.locator('.post-item').count()
assert count == 1, f'Expected 1 post-item, got {count}'
print('PASS: All cards have single post-item')
browser.close()
"
五、风险评估
| 风险 | 影响 | 缓解措施 |
|---|---|---|
改为 <div> 后 SEO 影响 |
🟡 低 — 搜索引擎已通过 sitemap 索引 | 保留 data-post-slug 属性 |
| 点击事件与 collection badge 冲突 | 🟡 中 | JS 中通过 e.target.closest('.collection-badge') 排除 |
现有 CSS 依赖 <a> 标签 |
🟢 低 | .post-item 使用 class 选择器,不依赖标签名 |
六、附录
A. 相关文件
| 文件 | 说明 |
|---|---|
blog/templates/index.html |
文章列表模板(问题所在) |
blog/static/css/style.css |
卡片样式 |
blog/src/routes/pages.py |
路由:传递 item.collections 到模板 |
B. 复现截图
通过 Playwright 提取的 DOM 片段(algorithm-efficiency-measure 文章):
原始模板 → 浏览器修复后
<a class="post-item"> → <a class="post-item">标题+摘要</a>
标题+摘要 → <div class="post-meta">
<div class="post-meta"> → <a class="post-item">日期+标签</a>
日期+标签 → <div class="post-collections">
<div class="post-col"> → <a class="post-item">...</a>
<a class="collection">→ <a class="collection-badge">数据结构</a>
</div> → </div>
</div> → </div>
</a>