feat(moments): 瞬间富文本与逐条评论(5h T2/T3)

瞬间文字从纯文本直出升级到与文章同级表达力,每条动态获得独立评论流。

T2 富文本:
- text 在 frontmatter 预渲染成 momentId → html 的 Map(renderRich 是 async,
  模板内不能 await),模板只此一处 set:html 且喂的是净化产物
- 走与全站文案同一条管线:markdown → 内容净化(标签/属性/协议三层白名单)
  → class 白名单 35 项,故 <span class="c-blue font-kai"> 生效、
  <script> 整棵子树被丢弃(实测)
- 纯文本老数据零回归:无 markdown 的 text 得到单个 <p>,与旧直出视觉等价
- 卡片内富文本首尾 margin 归零、段间半档、列表缩进 1.35em,15px/ink-2 基调不变

T3 逐条评论:
- 新增 MomentComments.astro:<details> 折叠,**展开才拉 Artalk 并 init**
  (未展开连 Artalk.js 都不下载,时间线一屏十几条不白付成本)
- page_key = /moments/#<slug>,中英双语共用同一评论流(与文章页同策略)
- pvAdd: false 且复用 ViewMeta readonly 计数:读者开评论不会虚增浏览数
- 5s 兜底失败提示 + 重试;软导航回页对已展开的框补挂
- Playwright 实测同页两个容器各自 init 成功(各渲染 31 个 atk-* 节点,无异常),
  故无需降级;若日后内存吃紧可对上一实例调 destroy() 一行切换

验证: 构建 exit 0;E2E 37/37(快照口径);临时瞬间实测 markdown 粗体、
      彩色+楷体类保留、行内码、<script> 净化丢弃、details 与 /moments/# 键、
      ViewMeta 只读挂载全部命中(验收数据已还原)

Spec: 2026-11-02_blog-5h-content-social (T2/T3)

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-02 20:20:31 +08:00
1 parent 5bf0c327bf
commit 8e74bead62
3 files changed
+285 -3

No files matched your search

+49 -1
View File
@@ -2,7 +2,9 @@
import { Image } from 'astro:assets';
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import MomentComments from '../MomentComments.astro';
import { t } from '../../i18n/ui';
import { renderRich } from '../../lib/site-content';
import type { Locale } from '../../lib/locale';
// /en/ 侧由 T4 薄页传 locale='en';zh 路由无人传参,默认 zh 零回归
@@ -14,6 +16,16 @@ const { locale = 'zh' } = Astro.props;
const moments = (await getCollection('moments')).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
);
// 富文本预渲染(Phase 5h T2):text 走与页面文案同一条管线(markdown → 内容净化 →
// class 白名单 → 链接卡片),因此瞬间支持 **加粗**、<span class="c-blue font-kai">、列表与换行。
// 预渲染成 map 再进模板:renderRich 是 async,模板里不能 await。
// 纯文本老数据零回归:无 markdown 的文本会得到单个 <p>,与旧的 {text} 直出视觉等价。
const richText = new Map<string, string>();
for (const m of moments) {
if (!m.data.text) continue;
richText.set(m.id, await renderRich(m.data.text));
}
const iso = (d: Date) => d.toISOString();
---
@@ -46,12 +58,13 @@ const iso = (d: Date) => d.toISOString();
/>
</figure>
)}
{m.data.text && <p>{m.data.text}</p>}
{m.data.text && <div class="mom-text" set:html={richText.get(m.id) ?? ''} />}
{m.data.tags.length > 0 && (
<p class="mom-tags">
{m.data.tags.map((tag) => <code class="tag">{`#${tag}`}</code>)}
</p>
)}
<MomentComments slug={m.id} locale={locale} />
</div>
</li>
))}
@@ -208,6 +221,41 @@ const iso = (d: Date) => d.toISOString();
background: var(--bg-soft);
}
.mom-card > p { font-size: 15px; color: var(--ink-2); }
/* 富文本正文(T2):首尾 margin 归零,避免卡片被 <p> 的默认外边距撑开;
段间只留半档间距,列表缩进对齐正文基线。基调保持 15px / ink-2。 */
.mom-text {
font-size: 15px;
color: var(--ink-2);
line-height: 1.7;
}
.mom-text :global(> :first-child) { margin-top: 0; }
.mom-text :global(> :last-child) { margin-bottom: 0; }
.mom-text :global(p) { margin: 0 0 var(--space-1); }
.mom-text :global(ul),
.mom-text :global(ol) {
margin: 0 0 var(--space-1);
padding-left: 1.35em;
}
.mom-text :global(li) { margin: 0; }
.mom-text :global(li + li) { margin-top: 2px; }
.mom-text :global(blockquote) {
margin: var(--space-1) 0;
padding-left: var(--space-2);
border-left: 2px solid var(--line);
color: var(--ink-3);
}
.mom-text :global(code) {
font-family: var(--font-mono);
font-size: 0.92em;
background: var(--bg);
border-radius: 4px;
padding: 1px 5px;
}
.mom-text :global(a) {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 0.18em;
}
.mom-fig {
border-radius: 8px;
overflow: hidden;