diff --git a/.spec/specs/archive/2026-11-02_blog-5h-content-social/completion-summary.md b/.spec/specs/archive/2026-11-02_blog-5h-content-social/completion-summary.md new file mode 100644 index 0000000..fb4becd --- /dev/null +++ b/.spec/specs/archive/2026-11-02_blog-5h-content-social/completion-summary.md @@ -0,0 +1,95 @@ +# Phase 5h:内容表达能力与社交反馈 - 完成总结 + +## 交付结论 +- 结果:完成 +- 完成时间:2026-11-02 21:16 + +## 假设回顾 +### 已验证假设 +- 瞬间富文本走与全站文案同一条净化管线即可满足「字体/颜色可换」且不引入 XSS 面(危险标签整棵丢弃、协议白名单 http/https/mailto) +- 每条瞬间一个 Artalk page_key(`/moments//`)不会与页面级 pv 冲突;Artalk 实例按需挂载(展开才 init)可避免 "one page one Artalk" 的全局实例冲突 +- 开源页可用本地数据(已有 repo 元数据 + GitHub GraphQL 拉取)自绘 followers/stars/语言占比,无需任何第三方徽章服务 + +### 仍未完全验证的假设 +- 无 + +## 交付范围 +### 已交付 +- T1 文章置顶(契约 + 排序 + 标记 + 后台) +- T2 瞬间富文本(字体可换 / 颜色可换 / 多形式) +- T3 瞬间逐条评论(Artalk 独立 page_key + 折叠懒挂载) +- T4 计数徽标(眼睛图标 = 浏览数,气泡图标 = 评论数) +- T5 开源页重构(原生 GitHub 档案卡,去外链图依赖) +- T6 关于页承接 README 的个人叙述 +- T7 写作模板示例文章 + 文档与 hint 收尾 + +### 未交付 +- 无 + +### 偏差说明 +- 无 + +## 简化决策 +### 保持简单的关键选择 +- 最简单可行方案:置顶先用 boolean + 排序键;瞬间富文本直接把 `text` 交给既有 `renderRich`;评论用现有 Artalk 组件克隆一份带折叠的;计数徽标只做 pv + count 两个数 +- 暂不引入:置顶权重/多级置顶、瞬间的图片轮播、评论楼中楼定制、开源页的实时 Star 曲线 +- 不做的抽象:通用「内容社交层」框架(只服务现有页面) + +### 本轮明确不做的内容 +- 评论回复通知/邮件推送、点赞/投票、阅读进度、站点级总访问量统计(Artalk 无匿名聚合端点) + +## 变更边界 +### 本轮主要改动模块 +- 见 tasks.md 的 boundary 记录 + +### 明确未触碰的区域 +- 评论回复通知/邮件推送、点赞/投票、阅读进度、站点级总访问量统计(Artalk 无匿名聚合端点) + +## 验证证据 +### 备注 +- 机器证据:runs.jsonl(见验证范围的机器验证率) + +## 验证范围 +- 级别:package +- 机器验证率:7/7 +- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查 + +## 哲学生效证据 +### 行为成效回填 +- Development Record 行为门禁通过 + +### 一致性门禁回顾 +- 验收面(checklist 或成功标准)已通过 + +## Git 记录 +- 日期时间:2026-11-02 21:16 +- 范围:Phase 5h:内容表达能力与社交反馈 +- 功能:T1 文章置顶(契约 + 排序 + 标记 + 后台);T2 瞬间富文本(字体可换 / 颜色可换 / 多形式);T3 瞬间逐条评论(Artalk 独立 page_key + 折叠懒挂载);T4 计数徽标(眼睛图标 = 浏览数,气泡图标 = 评论数);T5 开源页重构(原生 GitHub 档案卡,去外链图依赖);T6 关于页承接 README 的个人叙述;T7 写作模板示例文章 + 文档与 hint 收尾 +- 操作:提交 / 推送 +- 成效:Development Record 行为门禁通过 +- 提交:pending local commit +- 提交区间:ed473e996a3707f98015a40e4388aa4fa58b79ac..bdaf02f9cae774e2a094eb716a95be3f0eddab56 +- 推送:not-run (push stage follows commit) + +## AGENTS.md 增量提议 +```diff +Phase 5h 资产:①置顶契约(sortPosts 统一四处排序,只改顺序不动 pubDate/URL/RSS)②瞬间富文本(复用 renderRich 净化链,危险标签整棵丢弃)③瞬间逐条评论(展开才挂 Artalk;评论键用虚拟路径 /moments//——实测 # 在查询串会被截断成页面级键,导致所有瞬间共用一个评论流)④计数徽标 ViewMeta(眼睛/气泡、零第三方图片;修同载双计与同页数字差 1 两个自伤 bug,口径=「请求次数对」不等于「展示自洽」)⑤开源页去 README 外链依赖(7 类第三方图与 align 居中失效是「没居中/图片不显示」的根因)⑥灰阶纪律清剿:项目墙 15 个彩色语言点、并列 36% 单着色、全局 .tap 泄漏致卡片居中 ⑦视觉验收新增两条可复用断言法:Range.getClientRects 判折行、scrollWidth==innerWidth 判横向溢出。 +``` + +## 被拒绝的扩展提议 +- 无 + +## 门禁证据 +- check_spec_package.py exit 0 + +## 遗留事项 +- 无;问题处置见下方结构化区块 + +## 问题处置 + +```json +{ + "issues": [], + "version": 1 +} +``` diff --git a/.spec/specs/archive/2026-11-02_blog-5h-content-social/runs.jsonl b/.spec/specs/archive/2026-11-02_blog-5h-content-social/runs.jsonl new file mode 100644 index 0000000..fc8ec5f --- /dev/null +++ b/.spec/specs/archive/2026-11-02_blog-5h-content-social/runs.jsonl @@ -0,0 +1,28 @@ +{"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": 21.239, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:58:45+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": 21.893, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:07+00:00"} +{"task": "task_0003", "cmd": "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\"", "exit": 0, "duration_s": 21.073, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:28+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": 23.015, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:51+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.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"} +{"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": 21.074, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:12:48+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.656, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:09+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.386, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:29+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.357, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:49+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": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:49+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.589, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:14:11+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": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:14:11+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.396, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:14:57+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": 21.377, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:15:19+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.908, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:15:40+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": 21.893, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:02+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": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:02+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": 20.399, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:22+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": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:22+00:00"} diff --git a/.spec/specs/archive/2026-11-02_blog-5h-content-social/spec.md b/.spec/specs/archive/2026-11-02_blog-5h-content-social/spec.md new file mode 100644 index 0000000..67df994 --- /dev/null +++ b/.spec/specs/archive/2026-11-02_blog-5h-content-social/spec.md @@ -0,0 +1,91 @@ +# Phase 5h:内容表达能力与社交反馈 - 项目范围 + + + +## 1. 问题定义 +- **项目目标**:①文章支持置顶(作者指定优先)②瞬间的文字与文章同级表达力(字体可换、颜色可换、多段落/列表/强调)③瞬间每条可独立评论 ④浏览量与评论数用统一的「眼睛/气泡」计数徽标呈现(对齐开源页 GitHub 档案的眼图标语汇)⑤开源页开篇从 GitHub README 直接移植导致:`
` 内联对齐失效、shields/vercel/snake/komarev 等外链图在慢网下空白破版、README 的「About Me」与开源页主题无关——重构为原生组件并从 README 解耦 ⑥关于页承接读完 README 后仍想保留的个人叙述 ⑦新增一篇「写作模板」示例文章,把可用排版能力一次演示完 +- **目标用户**:站长(写得更自由,少打字面 HTML)、读者(读到更丰富且不破版的页) +- **核心价值**:内容表达力与后台编辑力对上,同时把外链依赖与版式事故面砍掉;社交反馈(浏览/评论)在灰阶体系内可量化可见 + +## 2. 假设与待确认 + +### 2.1 已确认事实 +- Artalk v2 匿名可读:`GET /artalk/api/v2/comments?site_name=…&page_key=…&limit=1` 返回 `{count, roots_count, page:{pv}}`(生产实测,2026-11-02);`POST /artalk/api/v2/pages/pv` 返回 `{pv}`(现有组件在用) +- 富文本管线已存在且已净化:`renderRich`(markdown + rehype-content-sanitize 标签/属性/协议三层白名单)与 `rehype-class-whitelist`(35 项排版类,单一事实源 `public/admin/typography-classes.js`) +- 现有 6 自托管字族(含 font-huiwen 子集化 420KB 单页)与 16 彩色语义类 + 4 灰阶语义类 +- 开源页 README 现由 `intro_from: "off"` 关闭,缓存文件 `content/.github-readme.md`(3138B)含 6 类外链图(readme-typing-svg / shields.io / github-readme-stats / streak-stats / activity-graph / snake / komarev) +- 首页/归档/书架三处列表排序均为 `pubDate` 降序,无置顶概念(`grep pinned` 只命中开源页仓库白名单) + +### 2.2 关键假设 +- 瞬间富文本走与全站文案同一条净化管线即可满足「字体/颜色可换」且不引入 XSS 面(危险标签整棵丢弃、协议白名单 http/https/mailto) +- 每条瞬间一个 Artalk page_key(`/moments//`)不会与页面级 pv 冲突;Artalk 实例按需挂载(展开才 init)可避免 "one page one Artalk" 的全局实例冲突 +- 开源页可用本地数据(已有 repo 元数据 + GitHub GraphQL 拉取)自绘 followers/stars/语言占比,无需任何第三方徽章服务 + +### 2.3 待确认问题 +- [x] 瞬间文字是否允许 HTML 类名(字体/颜色)→ 用户原话「字体可换、颜色可换等」,按富文本管线放行 35 项白名单类 +- [x] 开源页要不要与 README 完全一致 → 用户原话「可以自行进行美化,无需完全一致」,按原生组件重构 +- [x] About Me 类内容放哪 → 用户原话「有些可以直接移植到关于页面中」,迁到 about.yaml blocks + +### 2.4 可选解释与取舍 +- 置顶只作用于列表排序与视觉标记,不改 URL / 不改 pubDate 语义(订阅源按时间仍正确) +- 计数徽标一次只拉当前页的 pv(POST 计一次)与评论数(GET 读);瞬间列表按可视区懒拉,避免 N 次请求把首屏拖慢 +- 开源页保留 `intro_from` 开关作为 compat,但默认 off 且文档标注「不推荐」;README 渲染通道不删除(后台仍可切回) + +### 2.5 已证伪路径 +- 直接渲染 README HTML 并试图用 CSS 修 `align="center"`:内联属性与 sanitize 的交互不可控,且外链图仍会空白破版 → 弃,改原生组件 +- 每条瞬间都 init 一个 Artalk 实例常驻:内存与请求成本高、且 Artalk 对多实例的支持未验证 → 改为展开时挂载 +- 用第三方徽章服务显示访客数(komarev 等):外链依赖 + 灰阶体系外颜色 → 弃,用 Artalk pv 自绘 + +## 3. 功能范围 + +### 3.1 核心功能(MVP) +- [x] T1 文章置顶:posts/postsEn schema `pinned`、四处列表排序(首页双轨/归档/书架)、视觉标记(灰阶 pin)、后台开关 +- [x] T2 瞬间富文本:moments `text` 支持 markdown + 白名单排版类(字体/颜色/强调/多段/列表)、净化管线复用、后台改 markdown 编辑器、旧纯文本零回归 +- [x] T3 瞬间逐条评论:每条 moment 独立 page_key 的 Artalk 评论区(折叠、展开时挂载)、评论数联动、双语页共用同一 key +- [x] T4 计数徽标:新增 `ViewMeta` 组件(眼睛图标 = 浏览数、气泡图标 = 评论数),接入文章页/瞬间页/关于页/开源页 +- [x] T5 开源页重构:原生 GitHub 档案卡(头像/名字/关注者/星标/语言占比,本地数据自绘)替换 README 外链图;`align` 失效与破图问题消失;README 通道降级为可选 +- [x] T6 关于页承接:把 README 的 About Me / Find Me 迁为 about.yaml blocks(prose + linklist),开源页只留开源相关 +- [x] T7 写作模板示例:中英双语示例文章,演示标题层级/彩色强调/字族/引用/代码/图/表/链接卡片/折叠,并作为置顶功能的活样本;docs 与后台 hint 同步 + +### 3.2 扩展功能 +- 无 + +### 3.3 不在范围内 +- 评论回复通知/邮件推送、点赞/投票、阅读进度、站点级总访问量统计(Artalk 无匿名聚合端点) + +## 4. 最小实现路径 +- 最简单可行方案:置顶先用 boolean + 排序键;瞬间富文本直接把 `text` 交给既有 `renderRich`;评论用现有 Artalk 组件克隆一份带折叠的;计数徽标只做 pv + count 两个数 +- 暂不引入:置顶权重/多级置顶、瞬间的图片轮播、评论楼中楼定制、开源页的实时 Star 曲线 +- 不做的抽象:通用「内容社交层」框架(只服务现有页面) + +## 5. 技术决策 +- 技术栈:Astro 7.4.4 + zod schema + Artalk v2 HTTP API + GitHub GraphQL(已有 token) +- 证据协议:evidence: runs +- 本轮允许改动:src/content.config.ts、src/components/{PostList,ArtalkComments}.astro、src/components/pages/{HomePage,MomentsPage,BooksPage}.astro、src/components/geek/{ViewMeta,GithubProfile}.astro(新)、src/pages/archives/[...slug].astro、src/pages/en/archives/[...slug].astro、src/pages/{about,opensource,en/about,en/opensource}.astro、src/lib/{projects,site-content,heatmap}.ts、content/site/{about,opensource}.yaml、content/posts/*(新增示例文章)、content/posts-en/*、public/admin/{config.yml,typography-classes.js}、docs/*、tests/e2e.mjs +- 本轮不应触碰:nginx/ESA、灯箱与图库(GalleryPage/PhotoZoom)、Header/Footer 结构、构建链顺序、tokens.css/global.css 基础尺度 +- Git integration branch:`spec/2026-11-02_blog-5h-content-social` + +### 5.1 验证策略 +- 范围级别:package +- 变更对象:内容模型(置顶/瞬间富文本)、渲染组件(计数/评论/档案卡)、后台配置、示例内容 +- 快速检查:`pnpm astro build`(node26)+ `python3 scripts/build-perf/config-check.py .` +- 集成检查:`E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs`(37 项,本包新增置顶顺序/徽标存在/瞬间富文本断言)+ content-lint 0/0/0 +- 全项目检查:生产实测四条(开源页零第三方外链图、瞬间评论可发可见、徽标数值与 Artalk 端点对账一致、关于页承接内容渲染);**升级条件**——仅当改动触及 BaseLayout/全局样式(tokens/字体栈)或 Artalk 主题接管层时才执行,本包因往 tokens 补 emoji 回退链已触发一次并记录了理由 +- 升级触发:若动到 BaseLayout/全局样式或 Artalk 主题接管层,升级为全站视觉复核(glm-5.3-flash:max,≤6 图/轮) + +## 6. 成功标准与验证方式 +- [x] 置顶文章排在列表首位且带可见标记,取消置顶后恢复时间序 -> verify: task_0001 +- [x] 瞬间文字可用字体类与彩色强调,危险标签被净化丢弃,旧纯文本渲染不变 -> verify: task_0002 +- [x] 每条瞬间可展开评论区并成功发帖,双语页共用同一评论流 -> verify: task_0003 +- [x] 文章/瞬间/关于/开源四页显示「浏览数 + 评论数」徽标且数值与 Artalk 一致 -> verify: task_0004 +- [x] 开源页零第三方图片请求,档案卡数据来自本地/GraphQL,居中与破图问题不再出现 -> verify: task_0005 +- [x] README 的个人叙述出现在关于页,开源页只保留开源相关内容 -> verify: task_0006 +- [x] 示例文章渲染全部演示能力且构建通过,docs/hint 同步 -> verify: task_0007 + +## 7. 风险与约束 +- 灰阶外壳红线不变:徽标图标与数字用 tokens(`--ink-*`/`--line`),不引入第三方彩色徽章 +- 彩色仅正文强调(≤3 处/页)——示例文章本身要守纪律,成为正面样本而非反例 +- 瞬间富文本必须复用 `renderRich` 净化链,禁止新增 set:html 直通路径 +- Artalk 多实例风险:评论区折叠 + 展开挂载,若实测冲突则退回「逐条跳转到锚点页」 +- 双语链路:所有新文案走 `t()`/`pickBi`,en 轨不得出现中文硬编码 +- 多车道并行:content.config.ts / public/admin/config.yml 由 orchestrator 先落契约再放行,车道只改各自 boundary 内文件 diff --git a/.spec/specs/archive/2026-11-02_blog-5h-content-social/tasks.md b/.spec/specs/archive/2026-11-02_blog-5h-content-social/tasks.md new file mode 100644 index 0000000..0e6f989 --- /dev/null +++ b/.spec/specs/archive/2026-11-02_blog-5h-content-social/tasks.md @@ -0,0 +1,44 @@ +# Phase 5h:内容表达能力与社交反馈 - 任务拆解 + +## 使用规则 +- boundary/verify 齐备;verify 只读断言;车道工作树开发禁 git,收编由 orchestrator 统一提交 +- 构建一律 `export PATH="$HOME/.local/node/bin:$PATH"`(Node 26)+ `pnpm astro build` +- **契约先行**:`src/content.config.ts` 与 `public/admin/config.yml` 由 orchestrator 先落契约(T1 时一并提交),车道不得再改这两个文件;需要新字段一律回报 orchestrator +- E2E 口径:`pnpm astro build → cp -a dist /tmp/dist-e2e → 用 18925 端口起快照服务 → E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs` + +## 阶段一:实现 + +- [x] T1 文章置顶(契约 + 排序 + 标记 + 后台) + - claim: orchestrator + - 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 -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` + +- [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 -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"` + +- [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/${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 + - boundary: src/components/geek/ViewMeta.astro(**契约组件,由 orchestrator 在 T1 提交时一并落盘**,车道只消费不改语义)、src/components/ArtalkComments.astro(pv 小字行改为 ViewMeta,评论数取 `comments?limit=1` 的 count)、src/pages/archives/[...slug].astro、src/pages/en/archives/[...slug].astro(挂 ViewMeta) + - verify: `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"` + +- [x] T5 开源页重构(原生 GitHub 档案卡,去外链图依赖) + - claim: op-gitea + - boundary: 新增 src/components/geek/GithubProfile.astro(头像/名字/简介/关注者/总星标/语言占比,数据来自 projects.ts 与 GraphQL;零第三方图片与徽章)、src/lib/projects.ts(补统计聚合:followers/stars/language 汇总,失败降级不炸构建)、src/pages/opensource.astro、src/pages/en/opensource.astro(用 GithubProfile 替换 readme 渲染段;README 通道保留开关但默认 off 且标注不推荐)、content/site/opensource.yaml(注释说明 intro_from 现状) + - verify: `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"` + +- [x] T6 关于页承接 README 的个人叙述 + - claim: op-gitea + - boundary: content/site/about.yaml(新增 blocks:关于/兴趣 prose、站点与项目 linklist、Find Me 社交块;双语 zh/en;测试值不得留在文件里)、src/pages/about.astro 与 en(仅当块渲染需要时改,优先零改动) + - verify: `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"` + +- [x] T7 写作模板示例文章 + 文档与 hint 收尾 + - claim: orchestrator + - boundary: content/posts/writing-template.md、content/posts-en/writing-template.md(en 手写 locked:true 直译,不交给 TMT 重译)、docs/写作指南.md(模板用法 + 排版类清单去重指引)、docs/管理台.md(瞬间富文本/逐条评论/置顶/徽标说明)、public/admin/config.yml(moments text hint、posts pinned 已含) + - verify: `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` diff --git a/content/.link-cache.json b/content/.link-cache.json index e7eec4a..628deb9 100644 --- a/content/.link-cache.json +++ b/content/.link-cache.json @@ -27,5 +27,12 @@ "image": "https://astro.build/og/astro.jpg", "favicon": "https://astro.build/favicon.svg", "ts": 1793593759816 + }, + "https://astro.build": { + "title": "Astro", + "desc": "Astro builds fast content sites, powerful web applications, dynamic server APIs, and everything in-between.", + "image": "https://astro.build/og/astro.jpg", + "favicon": "https://astro.build/favicon.svg", + "ts": 1793621147252 } } diff --git a/content/moments/2026-11-02.md b/content/moments/2026-11-02.md new file mode 100644 index 0000000..a99811c --- /dev/null +++ b/content/moments/2026-11-02.md @@ -0,0 +1,10 @@ +--- +pubDate: 2026-11-02 +tags: [站点更新] +text: | + 瞬间现在和文章同级表达力:`text` 支持 markdown 与排版类,这一句是青色 + 楷体,也可以**加粗**。 + + 另外每条瞬间自带评论区——展开才加载,不刷时间线就不为它付成本。 +--- + +站点更新记录:瞬间富文本与逐条评论上线。 diff --git a/content/posts-en/writing-template.md b/content/posts-en/writing-template.md new file mode 100644 index 0000000..ff4fab2 --- /dev/null +++ b/content/posts-en/writing-template.md @@ -0,0 +1,125 @@ +--- +title: "Writing template: this post is the template" +pubDate: 2026-11-02 +category: tech +tags: [writing, template] +pinned: true +locked: true +description: One post that exercises every typographic capability on the site — heading levels, colour emphasis, typefaces, quotes, code windows, tables, images, link cards, folding. Copy it to start a new post. +--- + +This is not a tutorial — it is a **template you can copy**. Every section below actually runs: what you see is what Markdown produces. To start a new post, copy this file, change the slug, and delete what you do not need. + +It is also the live sample for **pinning**: `pinned: true` in the frontmatter lifts a post to the front of the home page, the archive and the bookshelf, while its date, URL and RSS timestamps stay untouched. + +## 1. Heading levels and paragraphs + +Use `##` for a section and `###` for a subsection. **Never use `#`** — the post title comes from the frontmatter `title`, and a second level-one heading confuses both screen readers and search snippets. + +Leave a blank line between paragraphs. Inline you can use **bold**, *italic*, ~~strikethrough~~, `inline code`, and [ordinary links](https://astro.build). + +## 2. Colour emphasis: three per page at most + +The rule comes from the design baseline: the shell is greyscale, so colour exists to weight one judgement in the body — three per page, maximum. More than that and it turns loud. + +First use: state one key conclusion in blue. Then carry on normally. + +Second use: warn about a trap in red. For example, “after changing a Markdown plugin you must clear `node_modules/.astro`, or the change silently does nothing”. + +Third use: confirm a working path in green. After three, this section stays uncoloured. + +There are eight hues (blue / teal / green / yellow / orange / red / pink / purple), each with a standard and a `-soft` variant, plus four greyscale classes (`ink-muted` / `ink-warm` / `ink-cool` / `ink-faint`) for lowering tone — those do not count against the colour budget. + +## 3. Typefaces: change only what you wrap + +Body text is sans-serif by default. To give one sentence a different temperament, wrap it: + +This sentence is in Kai. Good for epigraphs, quoted lines in a review, or paragraphs that should be read slowly. + +This sentence is in Huiwen Ming. Over a long paragraph it noticeably slows reading, so use it for short lines only. + +Six families are available (`font-kai`, `font-song`, `font-hei-sys`, `font-fangsong`, `font-huiwen`, `font-kuaile`), plus an artistic heading class `font-art-kuaile`. **Mind the weight**: switching a whole page to a family makes every visitor download a CJK font, so avoid changing the body default casually. + +### A level-three heading looks like this + +One step smaller than level two, bold, with tighter spacing. Use it to split a section into topics. + +## 4. Quotes and lists + +> Block quotes are for quoting someone. An attribution can follow as italics. +> +> — an invented attribution + +Unordered list: + +- First item +- Second item, with `inline code` +- Third item, nested: + - Child one + - Child two + +Ordered lists suit steps: + +1. Copy the template +2. Change the slug and title in the frontmatter +3. Delete the sections you do not need + +## 5. Code blocks + +Blocks of four lines or more get a greyscale window bar (three dots); shorter fragments do not, so small snippets do not look like screenshots. + +```ts +// Sort contract: pinned first → newest first → id as a tie-breaker +export function sortPosts(posts: T[]): T[] { + return [...posts].sort((a, b) => { + const pin = Number(b.data.pinned === true) - Number(a.data.pinned === true); + return pin !== 0 ? pin : b.data.pubDate.getTime() - a.data.pubDate.getTime(); + }); +} +``` + +Refer to variables inline like `posts`. + +## 6. Tables + +| Capability | Syntax | Note | +| --- | --- | --- | +| Colour emphasis | `` | three per page at most | +| Typeface | `` | a whole page adds font weight | +| Image | `![alt](./assets//x.jpg)` | see the writing guide for size rules | +| Video | frontmatter `video:` | lives in public/media, ≤50MB | +| Link card | a bare URL on its own line | OG fetched at build time, falls back to a plain link | + +## 7. Images + +Reference images by relative path, stored under `content/posts/assets//`; they expand on click and vectors stay sharp: + +![Sample image: a greyscale coast](../posts/assets/media-sample/sample-photo.jpg) + +## 8. Link cards + +A bare URL on its own line becomes an OG preview card at build time; if the fetch fails it degrades to a plain link, never breaking the layout: + +https://astro.build + +## 9. Folding long content + +Long appendices, full logs and big configs can fold away with zero JavaScript, using native `details`: + +

+Open the folded example + +A folded region can hold anything — paragraphs, lists, code blocks. + +- Best for “read on demand” detail +- Not for conclusions (readers may never open it) + +
+ +## 10. Closing after the rule + +--- + +Three sentences are enough to close: what this covered, what the reader should take away, and where to go next. The template is not a demand to write ten sections — it is a promise that **the capabilities are at hand when you need to express something**. + +Next time, copy this file and delete until only your point remains. diff --git a/content/posts/writing-template.md b/content/posts/writing-template.md new file mode 100644 index 0000000..3c2167f --- /dev/null +++ b/content/posts/writing-template.md @@ -0,0 +1,124 @@ +--- +title: "写作模板:这篇文章本身就是模板" +pubDate: 2026-11-02 +category: tech +tags: [写作, 模板] +pinned: true +description: 一篇文章演示全站可用的排版能力:标题层级、彩色强调、字族、引用、代码窗、表格、图、链接卡、折叠。复制它开始写新文。 +--- + +这篇不是教程,是**可复制的模板**。下面每一节都在真跑——你看到的效果,就是 Markdown 源码能产出的效果。写新文章时,把本文件复制一份、改个 slug、删掉不需要的段落即可。 + +它同时是**置顶功能**的活样本:`frontmatter` 里的 `pinned: true` 会让它排在首页、归档与书架的最前面,而日期、URL、订阅源里的时间都不变。 + +## 一、标题层级与段落 + +正文用 `##` 作一级节标题,`###` 作二级。**不要用 `#`**——文章标题由 frontmatter 的 `title` 渲染,正文再写 `#` 会出现两个一级标题,屏幕阅读器与搜索摘要都会困惑。 + +段落之间空一行即可。行内可以**加粗**、*斜体*、~~删除线~~、`行内代码`,也可以放[普通链接](https://astro.build)。 + +## 二、彩色强调:一页不超过三处 + +纪律来自设计基线:站点外壳是灰阶的,颜色只用来给正文某一个判断加重,一页最多三处。超过就变成花哨。 + +第一处:一个关键结论用蓝色提出。 后面照常叙述。 + +第二处:需要警示的坑用红色。 比如「改完 Markdown 插件必须清 `node_modules/.astro`,否则改动静默不生效」。 + +第三处:确认可行的路径用绿色。 三处之后,本节不再上色。 + +可用色相共 8 组(blue / teal / green / yellow / orange / red / pink / purple),每组有标准与 `-soft` 两档;另有 4 个灰阶层级类(`ink-muted` / `ink-warm` / `ink-cool` / `ink-faint`)用于降调,不算彩色额度。 + +## 三、字族:换字体只作用于被包裹的文字 + +正文默认是无衬线。想给某句话换气质,用类包起来就行: + +这一句是楷体。适合题记、书评里引的原句,或者需要慢一点读的段落。 + +这一句是汇文明朝体。长段落用它会明显降低阅读速度,所以只用在短句上。 + +可用字族共 6 族(`font-kai` 楷、`font-song` 宋、`font-hei-sys` 黑、`font-fangsong` 仿宋、`font-huiwen` 汇文明朝、`font-kuaile` 快乐体),另有 `font-art-kuaile` 艺术字标题类。**注意体积**:整页换字族会让访问者下载一份中文字体,正文默认字体不要轻易改。 + +### 三级标题长这样 + +比二级小一档、加粗、上下留白更紧。用于把一节拆成几个小话题。 + +## 四、引用与列表 + +> 引用块用于摘录别人的话。署名可以换成斜体放在最后。 +> +> —— 一句虚构的署名 + +无序列表: + +- 第一项 +- 第二项,可以带 `行内代码` +- 第三项,可以嵌套: + - 子项一 + - 子项二 + +有序列表适合步骤: + +1. 复制模板 +2. 改 frontmatter 的 slug 与标题 +3. 删掉不需要的段落 + +## 五、代码块 + +四行及以上的代码块会带上灰阶窗条(三个点),短代码块不加——避免小片段看着像截图。 + +```ts +// 排序契约:置顶优先 → 时间倒序 → id 兜底 +export function sortPosts(posts: T[]): T[] { + return [...posts].sort((a, b) => { + const pin = Number(b.data.pinned === true) - Number(a.data.pinned === true); + return pin !== 0 ? pin : b.data.pubDate.getTime() - a.data.pubDate.getTime(); + }); +} +``` + +行内提到变量时用 `posts` 这种形式即可。 + +## 六、表格 + +| 能力 | 写法 | 注意 | +| --- | --- | --- | +| 彩色强调 | `` | 一页 ≤3 处 | +| 换字族 | `` | 整页换会拉字体体积 | +| 图片 | `![说明](./assets//x.jpg)` | 体积纪律见写作指南 | +| 视频 | frontmatter `video:` | 放 public/media,≤50MB | +| 链接卡片 | 裸链接单独成段 | 构建期抓 OG,失败回落普通链接 | + +## 七、图片 + +图片用相对路径引用,挂在 `content/posts/assets//` 下,点击可放大(矢量图不糊): + +![示例配图:一片灰阶海岸](./assets/media-sample/sample-photo.jpg) + +## 八、链接卡片 + +一个裸链接单独成段,构建期会抓取 OG 预览生成卡片;抓不到就退回普通链接,不会坏版: + +https://astro.build + +## 九、折叠长内容 + +长附录、完整日志、大段配置可以折叠,零 JS,原生 `details`: + +
+展开看一个被折叠的长例子 + +折叠区里可以放任何东西——段落、列表、代码块。 + +- 适合放「按需查看」的细节 +- 不适合放结论(读者可能永远不展开) + +
+ +## 十、分隔线之后收尾 + +--- + +收尾三句就够:这篇讲了什么、读者该带走什么、下一步去哪。模板不是要你写满十节,而是**在你需要表达时,能力都在手边**。 + +下一篇开始,复制本文件,删到只剩你要说的。 diff --git a/content/site/about.yaml b/content/site/about.yaml index 0e07f45..83c5740 100644 --- a/content/site/about.yaml +++ b/content/site/about.yaml @@ -62,6 +62,69 @@ blocks: zh: 街拍与山野,随身一台无反,在等光的间隙按快门。 en: Street and landscapes with a mirrorless camera, pressing the shutter while waiting for the light. + - type: prose + title: + zh: 关于我 + en: About me + body: + zh: | + 气象是我起点的专业——一个想做预报的人,依旧把云图当日常读物。 + + 写代码是第二条主线:Linux、容器、自托管,把日常工具一件件磨顺手。 + + 作品大多在 [wait4x.online](https://wait4x.online/)。 + + 信条:**拥抱开源,热爱开源**。 + en: | + Meteorology is where I started — an aspiring forecaster who still reads weather charts for fun. + + Coding is the second thread: Linux, containers, self-hosting — polishing everyday tools one by one. + + Most of my work lives at [wait4x.online](https://wait4x.online/). + + Motto: **embrace open source, love open source**. + + - type: linklist + title: + zh: 找到我 + en: Find me + items: + - label: + zh: 本站 + en: Website + href: 'https://wait4x.online/' + note: + zh: 文章、图库与瞬间 + en: Posts, gallery and moments + - label: + zh: 邮件 + en: Email + href: 'mailto:admin@wait4x.online' + note: + zh: 有话直说 + en: Say hello directly + - label: + zh: GitHub + en: GitHub + href: 'https://github.com/wait4xx' + note: + zh: 开源项目与提交 + en: Open-source projects and commits + - label: + zh: 自托管 gitea + en: Self-hosted gitea + href: 'https://git.wait4x.online/wait4x/blog' + note: + zh: 本站源码 + en: Source of this site + - label: + zh: RSS + en: RSS + href: /rss.xml + note: + zh: 订阅更新 + en: Subscribe to updates + - type: prose title: zh: 本站如何构建 diff --git a/content/site/opensource.yaml b/content/site/opensource.yaml index b0dff4e..fe7a110 100644 --- a/content/site/opensource.yaml +++ b/content/site/opensource.yaml @@ -4,8 +4,8 @@ blurb: repo_url: https://git.wait4x.online/wait4x/blog repo_label: git.wait4x.online/wait4x/blog blurb_after: - zh: —— Astro SSG、双写作流、构建发布全链皆在仓中。以下是我在自托管 gitea 与 GitHub 上的全部公开仓库,以及最近一年的贡献活动。 - en: — the Astro SSG, dual-writing workflow, and the full build & deploy pipeline live in the repo. Below are all my public repositories on self-hosted gitea and GitHub, plus contributions from the past year. + zh: ' —— Astro SSG、双写作流、构建发布全链皆在仓中。以下是我在自托管 gitea 与 GitHub 上的全部公开仓库,以及最近一年的贡献活动。' + en: ' — the Astro SSG, dual-writing workflow, and the full build & deploy pipeline live in the repo. Below are all my public repositories on self-hosted gitea and GitHub, plus contributions from the past year.' # ---- 项目墙可见性与排序(后台「开源页」可改;留空/缺省 = 现状) ---- # mode: all=全部(默认) | only=仅 include 列出 | exclude=排除 exclude 列出 @@ -18,5 +18,9 @@ exclude: [] sort: pinned-first # 来源过滤:both=双源合并(默认) | github | gitea perSource: both -# 开篇文案来源:off=用本文件 blurb(默认) | github-readme=构建期取 wait4xx/wait4xx 的 README 渲染 +# 开篇来源(compat,**不推荐开启**):off=用本文件 blurb + 原生档案卡(默认) +# github-readme = 构建期取 wait4xx/wait4xx 的 README 原样渲染在档案卡下方 +# ⚠ 开启会引入 7 类第三方外链图(readme-typing-svg / shields.io 徽章 / github-readme-stats / +# streak-stats / activity-graph / 贡献蛇 / komarev 访客徽章),慢网或被墙时整片空白; +# 且 README 用
居中,本站渲染不认该属性 → 居中失效。内容已由档案卡承接。 intro_from: "off" diff --git a/docs/写作指南.md b/docs/写作指南.md index 7b98257..6429cce 100644 --- a/docs/写作指南.md +++ b/docs/写作指南.md @@ -2,6 +2,36 @@ > 适用:`content/posts/*.md`(书评与技术轨通用)。资产目录约定 + 三种媒体的写法 + 体积纪律。 +## 写作模板(从这里开始,2026-11-02 起) + +别从空白文件起步——**复制现成的模板**: + +```bash +cp content/posts/writing-template.md content/posts/my-new-post.md +# 改 frontmatter 的 title / pubDate / tags / description,删掉不需要的段落 +``` + +模板本身是一篇真文章(线上读:`/archives/writing-template/`),每节都在跑一种能力,你看到的效果就是源码的效果:标题层级、三处彩色强调、两族字体、引用、列表、代码窗条、表格、图片放大、链接卡片、`
` 折叠、分隔线。 + +写之前只需记住三条纪律: +- **彩色 ≤3 处/页**:外壳是灰阶,颜色只用来给某个判断加重;超过就花哨 +- **整页换字族要谨慎**:会给访问者拉一份中文字体(汇文明朝整页约 420KB,已子集化) +- **折叠放细节不放结论**:`
` 里的内容可能永远没人展开 + +## 置顶文章 + +首页、归档、书架三处列表默认按日期倒序。想让某篇长期排在最前,frontmatter 加一行: + +```yaml +pinned: true +``` + +语义边界(刻意设计成这样): +- **只影响列表顺序**,不改 `pubDate`、不改 URL、不影响 RSS 的时间序列(订阅者不会看到时间错乱) +- 多篇同时置顶时,它们内部仍按日期倒序 +- 列表里会显示一个灰阶「置顶」小徽标,取消置顶后立刻回到时间序 +- 后台「文章」编辑页有「置顶」开关,不必手改 frontmatter + ## 目录约定 ``` @@ -223,6 +253,23 @@ typography: **层 3 · 艺术字体**:`font-art-kuaile`(站酷快乐体,艺术字专用类)。⚠️ **仅用于标题/短句**:艺术字字形大,CJK 切片体积高(实测用到页 +163KB),长段使用会显著抬高单页字体成本。 +## 瞬间(/moments)怎么写 + +瞬间与文章一样支持富文本,但入口在后台「瞬间」集合(`content/moments/*.md`): + +```yaml +--- +pubDate: 2026-11-02 +tags: [随手拍] +text: 今天把**置顶**做完了,顺手试了 彩色 +--- +``` + +- `text` 支持 markdown(`**加粗**`、列表、`> 引用`)与**同一套排版类**(字体/颜色),因为走的是与全站文案相同的净化管线:危险标签整棵丢弃,只放行 35 项白名单类 +- 一条瞬间 = 一行时间线卡片,**每条都带独立评论区**(展开才挂载,双语页共用同一评论流,评论不分裂) +- 每条的「浏览 / 评论」数字是只读统计(不会因为别人点开评论而虚增浏览) +- `text` 与 `image` 至少填一项;图片建议先压缩 + ## 页面块(关于页 blocks,字段速查) 关于页正文可由九个块拼成(后台拖拽排序,每块有「显示」开关)。完整操作见 [管理台](./管理台.md);这里是写内容时对照的字段表——文本字段一律 `{zh, en}`。 diff --git a/docs/管理台.md b/docs/管理台.md index 14593a6..c3846ab 100644 --- a/docs/管理台.md +++ b/docs/管理台.md @@ -24,6 +24,27 @@ - **文章英文版**:不在后台写。跑 `python3 scripts/translate/translate_posts.py` 生成 `content/posts-en/` 同名文件;手写润色过的译文在 frontmatter 写 `locked: true`,翻译脚本会无条件跳过它。 - **保存即提交**:后台保存等于向仓库提交,push 后约 10 秒自动构建上线。 +## 文章置顶(5h 新增) + +文章编辑页有一个「置顶」开关。打开后这篇文章排在首页分轨、归档、书架三处列表的最前面,列表里显示一个灰阶「置顶」小徽标。 + +语义边界(刻意如此,别当 bug 报): +- **只影响列表顺序**:`pubDate` 不变、URL 不变、RSS 里的时间序列不变,订阅者不会看到时间错乱 +- 多篇同时置顶时,置顶组内部仍按日期倒序 +- 关掉开关立即恢复时间序,没有"冷却期" + +## 瞬间(5h 起与文章同级) + +「瞬间」集合的「文字」字段是 **markdown 编辑器**,与文章同一套表达力: + +- markdown:`**加粗**`、`*斜体*`、列表、`> 引用`、`` `行内代码` `` +- 排版类 HTML:`彩色 + 楷体`(可用类清单见写作指南「文章排版类」一节,与文章完全同一份白名单) +- 仍守外壳灰阶纪律:彩色是**强调**不是装饰,一页 ≤3 处 + +**每条瞬间自带评论区**:时间线卡片下方有「评论」折叠条,读者点开才加载评论(不点开不为它付任何加载成本)。中文页与英文页共用同一条评论流,语言切换不会丢评论。 + +**每条的浏览 / 评论数字是只读的**:读者点开评论区不会虚增浏览数(只有整页访问才计一次)。 + ## 照片上传流 1. 图库照片 → New Entry:填标题(**必填**,空标题会让构建炸——实测坑)、选分组(street/landscape)、可写 alt @@ -118,6 +139,12 @@ blocks: ### 开源页(opensource.yaml) +**开篇是原生「GitHub 档案卡」(5h 起)**:头像 / 名字@handle / 定位叙述 / 社交图标 / 统计四格(关注者、公开仓、总星标)/ 语言占比灰阶条(只有第一名用强调色,其余灰阶)。**零第三方图片、零 iframe**。数据失败只隐藏对应数字,不会破版。 + +想换那段「定位叙述」,改本文件的 `blurb` + `blurb_after`(双语,后台可改)——它们现在同时供给档案卡与项目墙下方的说明。 + +下面的 `intro_from: "github-readme"` **仍可用但不推荐**:那会把 GitHub 专用 README 原样渲染,带 7 类外链图(shields.io、readme-typing-svg、github-readme-stats、streak-stats、activity-graph、snake、komarev 访客徽章)与 `
` 居中——本站按普通块渲染,故居中失效;外链图在慢网或被墙时全空白。这也是当初"部分内容没居中、有些图片没显示"的根因。 + | 字段 | 取值 | 说明 | |---|---|---| | `pinned` | 仓短名数组 | 置顶顺序即展示顺序 | @@ -143,6 +170,17 @@ token 位在 `~/docker_apps/blog/github-token.txt`(本机,0600)。 - **README 里的相对链接不重写**(如 `./img/x.png`、`docs/foo.md`):它们相对 GitHub 仓根解析,在本站没有对应产物,点了多半 404。保原样比猜一个基址可信,所以不做改写。 - **外部徽章图片加固但不参与构建**:shields.io、readme-typing-svg 这类第三方徽章在构建期**不抓取**,渲染时统一补 `loading="lazy"`(缺 alt 补空),它们慢、被墙或 404 都不影响页面渲染;尺寸已由样式限制在内容宽度内。 +## 浏览与评论计数(5h 新增) + +文章页、瞬间页、开源页、关于页都用同一枚灰阶徽标:**眼睛图标 = 浏览数,气泡图标 = 条评论**,数据来自自建 Artalk,不依赖任何第三方徽章服务。 + +几条行为约定(省得当成 bug): + +- **一页只计一次浏览**:「计一次」发生在这页唯一那个 `page` 模式徽标(文章页在标题下方),同一页其余徽标都是只读镜像,显示的是同一个数字——不会出现"页头 17、评论区 16" +- **开评论不算浏览**:瞬间逐条徽标是只读的,读者展开评论区不会给那条动态虚增浏览量 +- **接口挂掉显示「–」而不是 0**:宁可留白,也不给读者看一个错误的零 +- **换页重计、同页刷新重计**:这是浏览量的定义,不是重复计数的 bug(软导航回同一页也算新一次访问) + ## 纪律(与写作指南同源) - **图片体积**:上传前自行压缩(长边 ≤2400px)——Sveltia 内建优化**刻意关闭**(会抹 EXIF 断构建期提取管线) diff --git a/public/admin/config.yml b/public/admin/config.yml index ccaabfa..ad0c1cb 100644 --- a/public/admin/config.yml +++ b/public/admin/config.yml @@ -38,6 +38,7 @@ collections: - { label: 日期, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' } - { label: 分轨, name: category, widget: select, options: [{label: 阅读, value: reading}, {label: 技术, value: tech}, {label: 照片, value: photo}] } - { label: 标签, name: tags, widget: list, required: false } + - { label: 置顶, name: pinned, widget: boolean, required: false, default: false, hint: '置顶文章排在首页/归档/书架最前;只影响顺序,不改日期、URL 与 RSS 时间语义' } - { label: 情绪色(书评色脊), name: mood, widget: color, required: false, hint: '#RRGGBB,仅阅读轨使用' } - { label: 原书作者, name: bookAuthor, widget: string, required: false } - { label: 一句话描述(书架策展语), name: description, widget: text, required: false } @@ -97,7 +98,11 @@ collections: sortable_fields: [pubDate, text] fields: - { label: 时间, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' } - - { label: 文字, name: text, widget: text, required: false, hint: '一句话(与图片至少填一项)' } + - label: 文字 + name: text + widget: markdown + required: false + hint: '与图片至少填一项。支持 markdown(**加粗**、列表、> 引用)与排版类 HTML,例如 彩色+楷体;装饰色仍守“外壳灰阶、彩色只作强调”的纪律' - { label: 图片, name: image, widget: image, required: false, hint: '与文字至少填一项;建议先压缩' } - label: 标签 name: tags diff --git a/src/components/ArtalkComments.astro b/src/components/ArtalkComments.astro index af5362f..463b8ba 100644 --- a/src/components/ArtalkComments.astro +++ b/src/components/ArtalkComments.astro @@ -7,15 +7,14 @@ interface Props { } const { pageKey, pageTitle } = Astro.props; import { t } from '../i18n/ui'; +import ViewMeta from './geek/ViewMeta.astro'; import { localeOf } from '../lib/locale'; const locale = localeOf(Astro.url.pathname); --- - -

- – - {' '}{t('pv_viewed', locale)} -

+ +

- built from + built from{' '} {short === 'dev' ? dev : {short}} · {new Date(date).toISOString().slice(0, 16).replace('T', ' ')}Z · Astro

diff --git a/src/components/geek/GithubProfile.astro b/src/components/geek/GithubProfile.astro new file mode 100644 index 0000000..892c4f0 --- /dev/null +++ b/src/components/geek/GithubProfile.astro @@ -0,0 +1,260 @@ +--- +// 开源页原生档案卡(5h T5):零第三方图片、零 iframe +// 数据来源:①content/site/home.yaml 的头像/社交(单一事实源)②content/site/opensource.yaml 的 +// 定位叙述(blurb/blurb_after,双语可后台改)③GitHub REST 的 followers/公开仓数(token 在宿主 +// 0600 文件,缺位或失败即隐藏该数字)④src/lib/projects.ts 的仓库列表(语言占比与星标汇总) +// 降级纪律:任何一项取数失败只隐藏该项,绝不炸构建、不出现空图片框 +import { Image } from 'astro:assets'; +import { homeAvatar, homeSocials, openSourceCopy } from '../../lib/site-content'; +import { githubUserStats, loadProjects } from '../../lib/projects'; +import { iconPath } from '../../lib/icons'; +import type { Locale } from '../../lib/locale'; + +interface Props { + locale?: Locale; +} +const { locale = 'zh' } = Astro.props; + +const avatar = await homeAvatar(); +const socials = await homeSocials(); +const copy = await openSourceCopy(locale); + +// 仓库数据:档案卡只关心"语言占比 + 星标汇总",取全量(不受项目墙可见性影响) +let langs: { name: string; count: number; pct: number }[] = []; +let totalStars: number | null = null; +try { + const repos = await loadProjects({ mode: 'all', sort: 'stars' }); + if (repos.length) { + totalStars = repos.reduce((sum, r) => sum + (r.stars ?? 0), 0); + const tally = new Map(); + for (const r of repos) { + if (!r.language) continue; + tally.set(r.language, (tally.get(r.language) ?? 0) + 1); + } + const total = [...tally.values()].reduce((a, b) => a + b, 0); + langs = [...tally.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, 5) + .map(([name, count]) => ({ + name, + count, + pct: total ? Math.round((count / total) * 100) : 0, + })); + } +} catch { + console.warn('[profile] 仓库数据不可用,语言占比与星标汇总已隐藏'); +} + +// 并列判定用百分比最大值(四舍五入后并列很常见:Python/TS 各 36%): +// 只给第一根上强调色会让并列者显得"落后",故并列者一并着色 +const maxPct = langs.reduce((m, l) => Math.max(m, l.pct), 0); +const stats = await githubUserStats(); +const L = { + zh: { followers: '关注者', repos: '公开仓库', stars: '总星标', langs: '语言占比' }, + en: { followers: 'Followers', repos: 'Public repos', stars: 'Total stars', langs: 'Languages' }, +}[locale]; +--- + +
+
+ {avatar ? ( + wait4x + ) : ( + + )} +
+

+ wait4x@wait4xx +

+

+ {copy.blurb}{copy.repoLabel}{' '}{copy.blurbAfter.trim()} +

+ +
+
+ + {(stats || totalStars !== null) && ( +
+ {stats && ( + <> +
{L.followers}
{stats.followers}
+
{L.repos}
{stats.publicRepos}
+ + )} + {totalStars !== null && ( +
{L.stars}
{totalStars}
+ )} +
+ )} + + {langs.length > 0 && ( +
+

{L.langs}

+
    + {langs.map((l) => ( +
  • + {l.name} +
  • + ))} +
+
+ )} +
+ + diff --git a/src/components/geek/ProjectWall.astro b/src/components/geek/ProjectWall.astro index 68ff66f..a2ee66c 100644 --- a/src/components/geek/ProjectWall.astro +++ b/src/components/geek/ProjectWall.astro @@ -23,11 +23,9 @@ interface Props { } const { limit = 6, locale = 'zh', pinned, options } = Astro.props; const title = t('open_source_projects', locale); -const LANG_COLORS: Record = { - Astro: '#ff5a03', TypeScript: '#3178c6', JavaScript: '#f1e05a', - Python: '#3572a5', Go: '#00add8', Rust: '#dea584', Shell: '#89e051', - HTML: '#e34c26', CSS: '#563d7c', Dockerfile: '#384d54', -}; +// 2026-11-02 视觉终审:原先按 GitHub linguist 给每个仓染语言色点,开源页一次铺 +// 15+ 个仓 = 15 种彩色,破「外壳灰阶、彩色只作正文强调(≤3 处/页)」红线。 +// 改为统一灰阶点:语言信息靠文字承载,颜色不再做无意义的编码。 let projects: RepoCard[] = []; try { @@ -50,7 +48,7 @@ try { {p.language && ( - + {p.language} )} @@ -75,6 +73,10 @@ try { .repo { display: flex; flex-direction: column; + /* 显式接管交叉轴:全局 .tap 是 inline-flex + align-items:center, + 列向 flex 下会把标题/元信息行水平居中(视觉终审 P1,卡片左缘参差) */ + align-items: stretch; + text-align: left; gap: 2px; padding: var(--space-3); border: 1px solid var(--line); @@ -87,5 +89,12 @@ try { .repo-desc { font-size: 13px; color: var(--ink-3); } .repo-meta { display: flex; gap: var(--space-3); font-size: 12px; color: var(--ink-3); margin-top: 4px; } .lang { display: inline-flex; align-items: center; gap: 4px; } - .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } + .dot { + width: 7px; + height: 7px; + border-radius: 50%; + display: inline-block; + background: var(--ink-4); + flex: none; + } diff --git a/src/components/geek/ViewMeta.astro b/src/components/geek/ViewMeta.astro new file mode 100644 index 0000000..b44ed3d --- /dev/null +++ b/src/components/geek/ViewMeta.astro @@ -0,0 +1,208 @@ +--- +/** + * 浏览 / 评论计数徽标(Phase 5h T4):眼睛图标 = 浏览数,气泡图标 = 评论数。 + * + * 为什么这么设计: + * - 语汇对齐开源页从 GitHub README 移植过来的「👀 Visitors」徽章,但改成本地 + * SVG + tokens 着色,不引入任何第三方图片(komarev/shields 在慢网下空白破版)。 + * - 数据只用 Artalk 匿名可读端点(2026-11-02 生产实测): + * GET /artalk/api/v2/comments?site_name=…&page_key=…&limit=1 + * → { count, roots_count, page: { pv } } (一次拿到评论数与浏览量) + * POST /artalk/api/v2/pages/pv { page_key, site_name } → { pv } + * - `mode="readonly"` 只读(瞬间列表每条都要显示,逐条计数会污染数据); + * `mode="page"` 计入一次浏览(文章/关于/开源等整页场景)。 + * + * SSR 渲染占位「–」:无 JS / 接口失败时退化为一个短横,不显示 0 误导读者。 + */ +interface Props { + /** 评论流键(与 ArtalkComments 的 pageKey 同语义,双语页共用) */ + pageKey: string; + locale?: 'zh' | 'en'; + /** page = 计入一次浏览;readonly = 只读,不改动计数 */ + mode?: 'page' | 'readonly'; + /** 是否显示评论数(瞬间逐条场景也显示) */ + showComments?: boolean; + /** 是否显示浏览数 */ + showPv?: boolean; + /** 视觉变体:inline = 行内小字(默认);chip = 带底框的徽标条 */ + variant?: 'inline' | 'chip'; +} + +const { + pageKey, + locale = 'zh', + mode = 'page', + showComments = true, + showPv = true, + variant = 'inline', +} = Astro.props; + +const L = { + pv: locale === 'zh' ? '次浏览' : 'views', + cm: locale === 'zh' ? '条评论' : 'comments', +}; +--- + +

+ {showPv && ( + + + – + + )} + {showComments && ( + + + – + + )} +

+ + + + diff --git a/src/components/pages/BooksPage.astro b/src/components/pages/BooksPage.astro index f8348b4..eb77ced 100644 --- a/src/components/pages/BooksPage.astro +++ b/src/components/pages/BooksPage.astro @@ -5,13 +5,15 @@ import { getCollection } from 'astro:content'; import BaseLayout from '../../layouts/BaseLayout.astro'; import { t } from '../../i18n/ui'; import type { Locale } from '../../lib/locale'; +import { sortPosts } from '../../lib/post-order'; import { Image } from 'astro:assets'; interface Props { locale?: Locale } const { locale = 'zh' } = Astro.props; -const books = (await getCollection('posts', ({ data }) => !data.draft && data.category === 'reading' && !data.tags.includes('hidden'))) - .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()); +const books = sortPosts( + await getCollection('posts', ({ data }) => !data.draft && data.category === 'reading' && !data.tags.includes('hidden')), +); // 回退契约(schema 注释声称而此前未实现——视觉复核抓到的缺口): // description 缺省取正文第一个非空段落(去 markdown 记号)前 60 字 diff --git a/src/components/pages/HomePage.astro b/src/components/pages/HomePage.astro index aad9192..b801f94 100644 --- a/src/components/pages/HomePage.astro +++ b/src/components/pages/HomePage.astro @@ -10,6 +10,7 @@ import ProjectWall from '../geek/ProjectWall.astro'; import Heatmap from '../geek/Heatmap.astro'; import SocialLinks from '../SocialLinks.astro'; import { t } from '../../i18n/ui'; +import { sortPosts } from '../../lib/post-order'; import type { Locale } from '../../lib/locale'; import { homeConfig, @@ -30,9 +31,10 @@ const cfg = await homeConfig(locale); const subtitleHtml = cfg.subtitle ? await renderMarkdown(cfg.subtitle) : ''; const announcementHtml = cfg.announcement ? await renderRich(cfg.announcement) : ''; -const posts = ( - await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden')) -).sort((a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime()); +// 置顶优先 → 时间倒序(sortPosts 契约见 src/lib/post-order.ts) +const posts = sortPosts( + await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden')), +); // en 轨列表标题吃 postsEn 译题(缺译回退 zh,与 /en/archives 同策略) const enTitle = locale === 'en' diff --git a/src/components/pages/MomentsPage.astro b/src/components/pages/MomentsPage.astro index 44fadd8..d5130c6 100644 --- a/src/components/pages/MomentsPage.astro +++ b/src/components/pages/MomentsPage.astro @@ -2,7 +2,10 @@ import { Image } from 'astro:assets'; import { getCollection } from 'astro:content'; import BaseLayout from '../../layouts/BaseLayout.astro'; +import MomentComments from '../MomentComments.astro'; +import ViewMeta from '../geek/ViewMeta.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,16 +17,25 @@ 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(); ---

{t('moments_title', locale)}

{t('moments_note', locale)}

-

- – - {' '}{t('pv_viewed', locale)} -

+ +
    {moments.map((m) => (
  1. @@ -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}`})}

    )} +
  2. ))} @@ -67,25 +80,6 @@ const iso = (d: Date) => d.toISOString();