diff --git a/.spec/specs/2026-11-02_blog-5h-content-social/tasks.md b/.spec/specs/2026-11-02_blog-5h-content-social/tasks.md
index 5653736..ef36f6d 100644
--- a/.spec/specs/2026-11-02_blog-5h-content-social/tasks.md
+++ b/.spec/specs/2026-11-02_blog-5h-content-social/tasks.md
@@ -13,12 +13,12 @@
- boundary: src/content.config.ts(postsSchema 加 `pinned: z.boolean().optional()`,posts/postsEn 共用)、src/components/PostList.astro(置顶优先排序 + pin 标记)、src/pages/archives/index.astro(排序)、src/components/pages/{HomePage,BooksPage}.astro(排序)、public/admin/config.yml(posts 集合 pinned 开关 + 中文 hint)、src/i18n/ui.ts(置顶标签)
- verify: `E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && grep -q "pinned" src/content.config.ts && grep -q "pinned" src/components/PostList.astro && grep -q "pinned" public/admin/config.yml`
-- [ ] T2 瞬间富文本(字体可换 / 颜色可换 / 多形式)
+- [x] T2 瞬间富文本(字体可换 / 颜色可换 / 多形式)
- claim: worker-one
- boundary: src/components/pages/MomentsPage.astro(`text` 由纯文本 `
` 改为走 `renderRich` 净化+排版类管线,保留 inline 语义;空文本老数据零回归)、public/admin/config.yml 中 moments 集合 `text` 字段改 markdown widget + 提示可用排版类(**仅在 orchestrator 提交的字段块内改 widget 类型,不改结构**)、docs 提示留给 T7
- verify: `E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && grep -qE "renderRich|renderMarkdown" src/components/pages/MomentsPage.astro && python3 -c "import yaml;d=yaml.safe_load(open('public/admin/config.yml'));m=[c for c in d['collections'] if c['name']=='moments'][0];t=[f for f in m['fields'] if f['name']=='text'][0];assert t['widget']=='markdown',t"`
-- [ ] T3 瞬间逐条评论(Artalk 独立 page_key + 折叠懒挂载)
+- [x] T3 瞬间逐条评论(Artalk 独立 page_key + 折叠懒挂载)
- claim: worker-one
- boundary: 新增 src/components/MomentComments.astro(折叠 disclosure,展开时首次挂载 Artalk;page_key 约定 `/moments/#`,en 页同一 key 不分裂;评论数徽标复用 T4 的 ViewMeta 计数接口)、src/components/pages/MomentsPage.astro(每条卡片挂评论区)
- verify: `test -f src/components/MomentComments.astro && grep -q "MomentComments" src/components/pages/MomentsPage.astro && grep -q "moments/#" src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS"`
diff --git a/src/components/MomentComments.astro b/src/components/MomentComments.astro
new file mode 100644
index 0000000..448a248
--- /dev/null
+++ b/src/components/MomentComments.astro
@@ -0,0 +1,234 @@
+---
+/**
+ * 瞬间逐条评论(Phase 5h T3):每条瞬间一个 `` 折叠区,**展开才首次挂载 Artalk**。
+ *
+ * 为什么惰性挂载:时间线一屏能有十几条,若每张卡常驻一个 Artalk 实例,内存与请求
+ * 都白付;Artalk 官方也未背书"同页多实例长期并存"。于是「展开才挂」是折中:
+ * 读者点了才付成本,未点零开销(连 Artalk.js 都不下载)。
+ *
+ * 评论流约定:`page_key = /moments/#`,**中英双语共用同一键**(评论不分裂,
+ * 与文章页 `/archives//` 双语共用同理);site 恒为 wait4x.online。
+ *
+ * 计数不在这里自己 fetch:复用 geek/ViewMeta(mode="readonly" 只读,不写 pv),
+ * 放在卡片底部让读者不开评论也能看见条数。
+ */
+import ViewMeta from './geek/ViewMeta.astro';
+import { t } from '../i18n/ui';
+import type { Locale } from '../lib/locale';
+
+interface Props {
+ /** 瞬间条目 id(即文件名 slug),用于生成评论流键与容器 id */
+ slug: string;
+ locale?: Locale;
+}
+
+const { slug, locale = 'zh' } = Astro.props;
+
+/** 评论流键:双语共用;`#` 与文章页 page_key 形状保持一致的可读性 */
+const pageKey = `/moments/#${slug}`;
+/** 容器 id 只允许 CSS 安全字符(slug 理论上是文件名,仍兜一层) */
+const panelId = `mom-atk-${slug.replace(/[^a-zA-Z0-9_-]/g, '-')}`;
+const label = t('moment_comments', locale);
+const failText = locale === 'zh' ? '评论加载失败' : 'Comments failed to load';
+const retryText = locale === 'zh' ? '重试' : 'Retry';
+---
+
+
+
+
+ {label}
+
+
+
+
+
+ {failText}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/pages/MomentsPage.astro b/src/components/pages/MomentsPage.astro
index 44fadd8..6247952 100644
--- a/src/components/pages/MomentsPage.astro
+++ b/src/components/pages/MomentsPage.astro
@@ -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 白名单 → 链接卡片),因此瞬间支持 **加粗**、、列表与换行。
+// 预渲染成 map 再进模板:renderRich 是 async,模板里不能 await。
+// 纯文本老数据零回归:无 markdown 的文本会得到单个 ,与旧的 {text} 直出视觉等价。
+const richText = new Map();
+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();
/>
)}
- {m.data.text && {m.data.text}
}
+ {m.data.text && }
{m.data.tags.length > 0 && (
{m.data.tags.map((tag) => {`#${tag}`})}
)}
+
))}
@@ -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 归零,避免卡片被 的默认外边距撑开;
+ 段间只留半档间距,列表缩进对齐正文基线。基调保持 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;