# Blog 文章列表 `` 嵌套导致卡片分裂 — 修复 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 的文章显示正常(无嵌套 `` 冲突) - 首页文章列表(home 服务)不受影响(使用不同模板) ### 1.3 严重程度 🟡 中等 — 功能可用但视觉体验明显异常,影响专业度 --- ## 二、根因分析 ### 2.1 HTML 源码结构(模板) `blog/templates/index.html` 中,文章卡片的结构如下: ```html
  • 标题

    摘要...

    {% if item.collections %}
    {% for col in item.collections %} {{ col.title }} {% endfor %}
    {% endif %}
  • ``` ### 2.2 浏览器解析行为 **HTML 规范禁止 `` 标签嵌套 ``**(`` 的内容模型不能包含 interactive content)。 当浏览器遇到嵌套的 `` 时,会自动"修复"DOM: 1. 遇到第一个 `` → 打开 2. 遇到内部 `` → **自动关闭**外层 `` 3. 后续的 `` 和 `` 变成游离节点 ### 2.3 实际渲染的 DOM(Playwright 提取) ```html
  • 【数据结构】算法效率的度量

    ...

    ... 数据结构
  • ``` 一个 `
  • ` 里出现了 **3 个 `` 兄弟节点**,浏览器将它们渲染为 3 张独立卡片。 --- ## 三、修复方案 ### 3.1 方案概述 将 `` 标签的包裹范围缩小,**不包含 `post-collections` 部分**。将整个卡片改为 `
    ` + JS 点击跳转,或将 collection badge 移到 `` 外部。 ### 3.2 推荐方案:改为 `
    ` + 点击事件 将外层 `` 替换为 `
    `,通过 CSS `cursor: pointer` 和 JS `click` 事件实现整卡点击跳转。 **优势**: - 根本性解决嵌套问题 - 不影响现有样式(class 不变) - collection badge 的 `` 链接可独立工作 **修改文件**:`blog/templates/index.html` ### 3.3 代码 Diff ```diff --- a/blog/templates/index.html +++ b/blog/templates/index.html @@ -268,7 +268,7 @@ {% set excerpt = post.content | striptags | truncate(120) %}
  • - - +
  • ``` ### 3.4 CSS 补充 确保 `.post-item` 保留点击样式: ```css .post-item { cursor: pointer; display: block; /* 保留现有样式 */ } ``` ### 3.5 JS 补充(如需) 如果不想依赖 `` 标签的原生跳转,添加点击事件: ```javascript 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 验证命令 ```bash # 检查 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() " ``` --- ## 五、风险评估 | 风险 | 影响 | 缓解措施 | |------|------|----------| | 改为 `
    ` 后 SEO 影响 | 🟡 低 — 搜索引擎已通过 sitemap 索引 | 保留 `data-post-slug` 属性 | | 点击事件与 collection badge 冲突 | 🟡 中 | JS 中通过 `e.target.closest('.collection-badge')` 排除 | | 现有 CSS 依赖 `` 标签 | 🟢 低 | `.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 文章): ``` 原始模板 → 浏览器修复后 标题+摘要 标题+摘要 →
    ```