init: consolidate all ephron.ren PRDs and docs

This commit is contained in:
Ubuntu
2026-05-15 10:39:54 +08:00
parent 9568533314
commit ee8cddf8b8
21 changed files with 6991 additions and 2 deletions

View 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 实际渲染的 DOMPlaywright 提取)
```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>
```