From c7d1e7b975281aac2d6ed98545f5b1ecb40d3059 Mon Sep 17 00:00:00 2001 From: wait4xx Date: Mon, 2 Nov 2026 21:05:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(moments):=20=E8=AF=84=E8=AE=BA=E6=B5=81?= =?UTF-8?q?=E9=94=AE=E4=BB=8E=20#=20=E6=94=B9=E4=B8=BA=E8=99=9A?= =?UTF-8?q?=E6=8B=9F=E8=B7=AF=E5=BE=84=EF=BC=8C=E9=81=BF=E5=85=8D=E9=94=AE?= =?UTF-8?q?=E8=A2=AB=E6=88=AA=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实测发现 `#` 在查询串里是 fragment 分隔符:裸传 `/moments/#2026-11-02` 会被服务端 截断成页面级键 `/moments/`(生产 Artalk 实测,响应里 key 变成 "/moments/"), 后果是所有瞬间共用同一个评论流、pv 也串在一起。 - page_key 改为虚拟路径 `/moments//`(生产实测键原样保留,零转义风险; 将来真做单条瞬间页时键天然对齐) - spec 与 runs.jsonl 口径同步(七项 verify 在新指纹下重跑全绿) 验证: 构建 51 页 exit 0;产物含 /moments/2026-11-02/ 键;content-lint 0/0/0; spec_verify 七项 exit=0;check 收敛 Spec: 2026-11-02_blog-5h-content-social (T3 修正) Co-authored-by: GLM-5.3 --- .../2026-11-02_blog-5h-content-social/runs.jsonl | 7 +++++++ .spec/specs/2026-11-02_blog-5h-content-social/spec.md | 2 +- .../specs/2026-11-02_blog-5h-content-social/tasks.md | 2 +- src/components/MomentComments.astro | 11 +++++++---- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/.spec/specs/2026-11-02_blog-5h-content-social/runs.jsonl b/.spec/specs/2026-11-02_blog-5h-content-social/runs.jsonl index 464661d..1148e05 100644 --- a/.spec/specs/2026-11-02_blog-5h-content-social/runs.jsonl +++ b/.spec/specs/2026-11-02_blog-5h-content-social/runs.jsonl @@ -5,3 +5,10 @@ {"task": "task_0005", "cmd": "grep -q \"GithubProfile\" src/pages/opensource.astro && python3 -c \"import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.03, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:52+00:00"} {"task": "task_0006", "cmd": "python3 -c \"import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types\" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.067, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T13:00:13+00:00"} {"task": "task_0007", "cmd": "test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q \"pinned: true\" content/posts/writing-template.md && grep -qE \"font-kai|c-blue\" content/posts/writing-template.md && grep -q \"写作模板\" docs/写作指南.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T13:00:13+00:00"} +{"task": "task_0001", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -q \"pinned\" src/content.config.ts && grep -q \"pinned\" src/components/PostList.astro && grep -q \"pinned\" public/admin/config.yml", "exit": 0, "duration_s": 20.732, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:04:09+00:00"} +{"task": "task_0002", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ 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\"", "exit": 0, "duration_s": 20.695, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:04:30+00:00"} +{"task": "task_0003", "cmd": "test -f src/components/MomentComments.astro && grep -q \"MomentComments\" src/components/pages/MomentsPage.astro && grep -q 'moments/${slug}/' src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.501, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:04:50+00:00"} +{"task": "task_0004", "cmd": "test -f src/components/geek/ViewMeta.astro && grep -q \"ViewMeta\" src/components/ArtalkComments.astro && grep -q \"ViewMeta\" src/pages/archives/[...slug].astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.83, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:11+00:00"} +{"task": "task_0005", "cmd": "grep -q \"GithubProfile\" src/pages/opensource.astro && python3 -c \"import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.029, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:11+00:00"} +{"task": "task_0006", "cmd": "python3 -c \"import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types\" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.719, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:33+00:00"} +{"task": "task_0007", "cmd": "test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q \"pinned: true\" content/posts/writing-template.md && grep -qE \"font-kai|c-blue\" content/posts/writing-template.md && grep -q \"写作模板\" docs/写作指南.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:33+00:00"} diff --git a/.spec/specs/2026-11-02_blog-5h-content-social/spec.md b/.spec/specs/2026-11-02_blog-5h-content-social/spec.md index 6fcf42b..67df994 100644 --- a/.spec/specs/2026-11-02_blog-5h-content-social/spec.md +++ b/.spec/specs/2026-11-02_blog-5h-content-social/spec.md @@ -18,7 +18,7 @@ ### 2.2 关键假设 - 瞬间富文本走与全站文案同一条净化管线即可满足「字体/颜色可换」且不引入 XSS 面(危险标签整棵丢弃、协议白名单 http/https/mailto) -- 每条瞬间一个 Artalk page_key(`/moments/#`)不会与页面级 pv 冲突;Artalk 实例按需挂载(展开才 init)可避免 "one page one Artalk" 的全局实例冲突 +- 每条瞬间一个 Artalk page_key(`/moments//`)不会与页面级 pv 冲突;Artalk 实例按需挂载(展开才 init)可避免 "one page one Artalk" 的全局实例冲突 - 开源页可用本地数据(已有 repo 元数据 + GitHub GraphQL 拉取)自绘 followers/stars/语言占比,无需任何第三方徽章服务 ### 2.3 待确认问题 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 da53e32..0e6f989 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 @@ -21,7 +21,7 @@ - [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 -qE "汇总: [0-9]+/[0-9]+ PASS"` + - verify: `test -f src/components/MomentComments.astro && grep -q "MomentComments" src/components/pages/MomentsPage.astro && grep -q 'moments/${slug}/' src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"` - [x] T4 计数徽标(眼睛图标 = 浏览数,气泡图标 = 评论数) - claim: op-artalk diff --git a/src/components/MomentComments.astro b/src/components/MomentComments.astro index 448a248..727b027 100644 --- a/src/components/MomentComments.astro +++ b/src/components/MomentComments.astro @@ -6,8 +6,11 @@ * 都白付;Artalk 官方也未背书"同页多实例长期并存"。于是「展开才挂」是折中: * 读者点了才付成本,未点零开销(连 Artalk.js 都不下载)。 * - * 评论流约定:`page_key = /moments/#`,**中英双语共用同一键**(评论不分裂, - * 与文章页 `/archives//` 双语共用同理);site 恒为 wait4x.online。 + * 评论流约定:`page_key = /moments//`(虚拟路径),**中英双语共用同一键** + * (评论不分裂,与文章页 `/archives//` 同理);site 恒为 wait4x.online。 + * 为什么不用 `#` 形态:`#` 在查询串里是 fragment 分隔符,裸传会被服务端截断成 + * 页面级键 `/moments/`(实测),届时所有瞬间会共用同一个评论流、pv 也串在一起。 + * 虚拟路径既无转义风险,将来真做单条瞬间页时键也天然对齐。 * * 计数不在这里自己 fetch:复用 geek/ViewMeta(mode="readonly" 只读,不写 pv), * 放在卡片底部让读者不开评论也能看见条数。 @@ -24,8 +27,8 @@ interface Props { const { slug, locale = 'zh' } = Astro.props; -/** 评论流键:双语共用;`#` 与文章页 page_key 形状保持一致的可读性 */ -const pageKey = `/moments/#${slug}`; +/** 评论流键:双语共用的虚拟路径(形如 /moments/2026-11-02/),无转义风险 */ +const pageKey = `/moments/${slug}/`; /** 容器 id 只允许 CSS 安全字符(slug 理论上是文件名,仍兜一层) */ const panelId = `mom-atk-${slug.replace(/[^a-zA-Z0-9_-]/g, '-')}`; const label = t('moment_comments', locale);