init: consolidate all ephron.ren PRDs and docs
This commit is contained in:
250
prd-blog-post-collections-a-nesting-fix.md
Normal file
250
prd-blog-post-collections-a-nesting-fix.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# 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` 中,文章卡片的结构如下:
|
||||
|
||||
```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:
|
||||
|
||||
1. 遇到第一个 `<a class="post-item">` → 打开
|
||||
2. 遇到内部 `<a class="collection-badge">` → **自动关闭**外层 `<a>`
|
||||
3. 后续的 `</div>` 和 `</a>` 变成游离节点
|
||||
|
||||
### 2.3 实际渲染的 DOM(Playwright 提取)
|
||||
|
||||
```html
|
||||
<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
|
||||
|
||||
```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` 保留点击样式:
|
||||
|
||||
```css
|
||||
.post-item {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
/* 保留现有样式 */
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 JS 补充(如需)
|
||||
|
||||
如果不想依赖 `<a>` 标签的原生跳转,添加点击事件:
|
||||
|
||||
```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()
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、风险评估
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| 改为 `<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>
|
||||
```
|
||||
Reference in New Issue
Block a user