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} + + +
+
+ +
+
+
+ +
+
+ + + + 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;