<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Atelier · 造物间</title><description>以作品为证据、以创作过程建立信任的独立开发者主页</description><link>https://website.85138175.xyz/</link><language>zh-cn</language><item><title>把真实域名写进代码</title><link>https://website.85138175.xyz/log/2026-11-13-%E6%8A%8A%E7%9C%9F%E5%AE%9E%E5%9F%9F%E5%90%8D%E5%86%99%E8%BF%9B%E4%BB%A3%E7%A0%81/</link><guid isPermaLink="true">https://website.85138175.xyz/log/2026-11-13-%E6%8A%8A%E7%9C%9F%E5%AE%9E%E5%9F%9F%E5%90%8D%E5%86%99%E8%BF%9B%E4%BB%A3%E7%A0%81/</guid><description>M7 拿到域名、联系方式与预约链接后，把清单上的待提供项变成代码默认值并跑完首推前全链</description><pubDate>Fri, 13 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;从清单到默认值&quot;&gt;从「清单」到「默认值」&lt;/h2&gt;
&lt;p&gt;上一轮我把 M7 需要的东西列成了一张待提供清单，每一项都写清「怎么获取」。
这一轮用户一次性补齐：子域名前缀和 Pages 项目名都用 &lt;code&gt;website&lt;/code&gt;、Cal.com 预约链接、
Cloudflare Pages 已按推荐 A 方案连通 GitHub 仓库、本机代理端口 3067。
清单上的 ⬜ 该变成代码里的默认值了。&lt;/p&gt;
&lt;h2 id=&quot;一处改动五个地方生效&quot;&gt;一处改动，五个地方生效&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;astro.config.mjs&lt;/code&gt; 的 &lt;code&gt;site&lt;/code&gt; 改成读 &lt;code&gt;process.env.PUBLIC_SITE_URL&lt;/code&gt;，默认
&lt;code&gt;https://website.85138175.xyz&lt;/code&gt;。这一处同时决定 canonical、sitemap、OG、RSS、JSON Feed
的绝对地址——所以改完不用到处找硬编码。顺手把 &lt;code&gt;Seo.astro&lt;/code&gt;、&lt;code&gt;privacy.astro&lt;/code&gt;、
&lt;code&gt;robots.txt&lt;/code&gt;、作品 &lt;code&gt;url&lt;/code&gt;、&lt;code&gt;.env.example&lt;/code&gt; 里的占位域名 &lt;code&gt;atelier.example.com&lt;/code&gt; 全清掉。&lt;/p&gt;
&lt;p&gt;这里有个不显眼的坑：&lt;strong&gt;&lt;code&gt;vite&lt;/code&gt; 在项目根不可解析&lt;/strong&gt;（pnpm 严格隔离），所以不能用
&lt;code&gt;loadEnv&lt;/code&gt;；而且 config 在 Vite 加载 &lt;code&gt;.env&lt;/code&gt; 之前执行，本来也读不到 &lt;code&gt;import.meta.env&lt;/code&gt;。
用 &lt;code&gt;process.env&lt;/code&gt; 是唯一正确的选择。&lt;/p&gt;
&lt;h2 id=&quot;三个看起来没问题的地方&quot;&gt;三个「看起来没问题」的地方&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;微信号不是 URL。&lt;/strong&gt; &lt;code&gt;contact_channels&lt;/code&gt; 原本强制 &lt;code&gt;{label, url}&lt;/code&gt;，组件渲染成
&lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt;。把 &lt;code&gt;dianfeng1998&lt;/code&gt; 塞进去会造出一条死链。改成支持 &lt;code&gt;value&lt;/code&gt; 纯文本字段、
用 &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt; 展示；也刻意不加「一键复制」按钮——那需要客户端 JS，会破坏首屏零脚本
(E7) 这个从 M1 守到现在的约束。取舍要明说，不是偷懒。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CSP 的「首方」必须和 site 同源。&lt;/strong&gt; &lt;code&gt;csp-check.mjs&lt;/code&gt; 的 &lt;code&gt;originOf()&lt;/code&gt; 原来拿占位域名
当 base 解析绝对 URL。产物里一旦出现指向真实域名的链接，就会被误判成外域、写进
&lt;code&gt;form-action&lt;/code&gt; / &lt;code&gt;script-src&lt;/code&gt; 白名单——一个静默放宽的策略。给两个 CSP 脚本加
&lt;code&gt;SELF_ORIGIN&lt;/code&gt;，和 &lt;code&gt;site&lt;/code&gt; 用同一个环境变量，首方相对/绝对 URL 才都归类正确。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;冒烟测试要能发现「域名没换」。&lt;/strong&gt; 给 &lt;code&gt;deploy-smoke.mjs&lt;/code&gt; 加一条 &lt;code&gt;robots.txt&lt;/code&gt;
断言，检查它指向 &lt;code&gt;website.85138175.xyz&lt;/code&gt; 的 sitemap。否则这类回归只能靠人眼。&lt;/p&gt;
&lt;h2 id=&quot;首推前的全链复验&quot;&gt;首推前的全链复验&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pnpm run quality&lt;/code&gt;（build + CSP + check 61 files 0/0/0 + E4 三条 + 24 单测 +
CI 自检 23 项）、&lt;code&gt;pnpm run quality:browser&lt;/code&gt;（LCP 1004ms / Lighthouse 100×4 /
axe 0 / Playwright 7 / 拔动态层 5 页可读）、&lt;code&gt;pnpm deploy:smoke&lt;/code&gt;（11 页 + feed +
sitemap + robots + 真 CSP 头）全绿。产物审计确认 canonical/OG/RSS/Feed/sitemap
都是真实域名、无占位残留，&lt;code&gt;/contact&lt;/code&gt; 有真实邮箱、微信、预约链接且微信无死链。&lt;/p&gt;
&lt;p&gt;清单文档与近况页也同步成 ✅。接下来就是把它推到远端、触发 GitHub Actions 与
Cloudflare Pages 的首次真实构建——那是另一个「纸上通过」与「真跑过」的差距，
等它真的绿了再说。&lt;/p&gt;
</content:encoded><category>m7</category><category>deployment</category><category>config</category><category>csp</category></item><item><title>第一次上线与回滚演练</title><link>https://website.85138175.xyz/log/2026-11-13-%E7%AC%AC%E4%B8%80%E6%AC%A1%E4%B8%8A%E7%BA%BF%E4%B8%8E%E5%9B%9E%E6%BB%9A%E6%BC%94%E7%BB%83/</link><guid isPermaLink="true">https://website.85138175.xyz/log/2026-11-13-%E7%AC%AC%E4%B8%80%E6%AC%A1%E4%B8%8A%E7%BA%BF%E4%B8%8E%E5%9B%9E%E6%BB%9A%E6%BC%94%E7%BB%83/</guid><description>M7 上线后第一次发布→回滚→再发布演练，这条日志就是哨兵——它在线上出现或消失，用来证明平台真的切了产物</description><pubDate>Fri, 13 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;今天做到了哪一步&quot;&gt;今天做到了哪一步&lt;/h2&gt;
&lt;p&gt;站点终于真的上线了，不是「CI 全绿」意义上的绿，而是自定义域名
&lt;code&gt;https://website.85138175.xyz&lt;/code&gt; 打开就是完整的 11 个页面、RSS、JSON Feed、
sitemap 和站内搜索。这一步之前有一道坎：Cloudflare Pages 没配构建命令时，
会把整个仓库源码当静态站发布却照样报 “Deploy successful”，是远端冒烟
&lt;code&gt;deploy:smoke:remote&lt;/code&gt; 把它揪出来的。配好 &lt;code&gt;pnpm build&lt;/code&gt; + &lt;code&gt;dist&lt;/code&gt; +
&lt;code&gt;PNPM_VERSION=11.28.5&lt;/code&gt;、重新触发构建后，线上冒烟才转成 &lt;code&gt;PASS / failed:0&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id=&quot;为什么专门发一条日志&quot;&gt;为什么专门发一条日志&lt;/h2&gt;
&lt;p&gt;「发布→回滚→再发布」演练需要一个&lt;strong&gt;人眼能分辨的信号&lt;/strong&gt;来证明平台确实切了产物，
而不是又一轮「按钮变绿」。所以这条日志本身就是那枚哨兵：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它出现在日志页 &lt;code&gt;/log/&lt;/code&gt;、RSS &lt;code&gt;&amp;lt;item&amp;gt;&lt;/code&gt;、JSON Feed &lt;code&gt;items&lt;/code&gt;、sitemap 和 Pagefind 索引里；&lt;/li&gt;
&lt;li&gt;回滚到上一份部署后，它应当&lt;strong&gt;全部消失&lt;/strong&gt;（回到发布前的产物）；&lt;/li&gt;
&lt;li&gt;再发布后，它应当&lt;strong&gt;全部回来&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每一步都用同一条 &lt;code&gt;deploy:smoke:remote&lt;/code&gt; 加一次「这条日志在不在」的定点检查做裁判。
如果回滚后它还在，说明回滚没生效，演练不算通过。&lt;/p&gt;
&lt;h2 id=&quot;演练里让我踏实的一点&quot;&gt;演练里让我踏实的一点&lt;/h2&gt;
&lt;p&gt;回滚之所以可以是「指一下上一份静态产物」这么轻，是因为整个 R1 的读路径是全静态的：
没有数据库、没有会话、没有服务端渲染。表单、预约、分析这些动态层就算短暂抖动，
首页、作品、日志、能力页仍然完整可读——M5 的 &lt;code&gt;test:nodynamic&lt;/code&gt; 早就把第三方域名
全指向不可达地址验证过一次了。架构上少一个耦合，运维上就少一个要一起回滚的东西。&lt;/p&gt;
&lt;h2 id=&quot;还没闭环的&quot;&gt;还没闭环的&lt;/h2&gt;
&lt;p&gt;回滚按钮在 Cloudflare 后台，只能由账号本人在 &lt;strong&gt;Deployments → All deployments →
目标部署的三点菜单 → Rollback to this deployment&lt;/strong&gt; 里点：本项目走 Git 集成、
刻意不放 API Token 给自动化。演练完成后，这条日志会留在站上，算 M8 首批真实内容的
第一条。邮件送达（需要真实表单端点）、5 人 3 秒测试都还等真人来做。&lt;/p&gt;
</content:encoded><category>m7</category><category>deployment</category><category>rollback</category><category>dogfooding</category></item><item><title>绿灯不等于上线</title><link>https://website.85138175.xyz/log/2026-11-13-%E7%BB%BF%E7%81%AF%E4%B8%8D%E7%AD%89%E4%BA%8E%E4%B8%8A%E7%BA%BF/</link><guid isPermaLink="true">https://website.85138175.xyz/log/2026-11-13-%E7%BB%BF%E7%81%AF%E4%B8%8D%E7%AD%89%E4%BA%8E%E4%B8%8A%E7%BA%BF/</guid><description>首推后 CI 4/4 全绿、CF 也报 Deploy successful，但远端冒烟发现线上发布的是仓库源码而非 dist/</description><pubDate>Fri, 13 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;今天做到了哪一步&quot;&gt;今天做到了哪一步&lt;/h2&gt;
&lt;p&gt;把代码首推到 &lt;code&gt;zjc99999/website&lt;/code&gt; 之后，第一次在 GitHub 托管 runner 上把整套门禁真跑了一遍。
第一次 4 个 job 里红了 1 个：audit 的「确认没有密钥被提交」。查下来不是站点漏了密钥，
而是 &lt;code&gt;sk_live_&lt;/code&gt; 这条规则太宽，把一篇创作日志里「提到这条正则」的正文也扫成了密钥。
更麻烦的是本地 &lt;code&gt;pnpm verify:ci&lt;/code&gt; 当时却是全绿——不是因为干净，而是 Windows 下 git 默认
&lt;code&gt;core.quotepath&lt;/code&gt;，中文文件名被八进制转义，&lt;code&gt;existsSync&lt;/code&gt; 找不到就静默跳过，等于什么都没测。&lt;/p&gt;
&lt;p&gt;一处假红、一处假绿，方向相反，病根相同：规则和执行各写了两份，谁也不保证对方在测什么。
所以没有给两边各打补丁，而是收敛成 &lt;code&gt;scripts/secret-scan.mjs&lt;/code&gt; 一个引擎：规则收紧到
&lt;code&gt;sk_live_&lt;/code&gt; 后面必须跟 20 个以上字母数字（这样规则文本自己不会匹配自己，也就不需要白名单），
&lt;code&gt;git ls-files -z&lt;/code&gt; 配 &lt;code&gt;core.quotepath=false&lt;/code&gt; 拿真实文件名，再断言「确实扫到了文件」「没有静默跳过」
「ci.yml 引用了同一个脚本」。故意把一条真密钥探针塞进 tracked 的 README，它红了；
拿出来，它绿了。第二次 push run &lt;code&gt;43395389553&lt;/code&gt; 4/4 全绿。&lt;/p&gt;
&lt;h2 id=&quot;但绿灯没把站点送上线&quot;&gt;但绿灯没把站点送上线&lt;/h2&gt;
&lt;p&gt;Cloudflare Pages 的 check-run 也是绿的：「✅ Deploy successful!」。
直到跑 &lt;code&gt;deploy:smoke:remote https://website-1tq.pages.dev&lt;/code&gt; 才看见另一幅景象：
11 个核心页全部 404，&lt;code&gt;/package.json&lt;/code&gt; 却返回 200，&lt;code&gt;/index.html&lt;/code&gt; 404，RSS、JSON Feed、
sitemap、Pagefind 全 404，连 &lt;code&gt;/robots.txt&lt;/code&gt; 都是 Cloudflare 自己的默认文案。&lt;/p&gt;
&lt;p&gt;也就是说：平台把&lt;strong&gt;整个 Git 仓库根目录&lt;/strong&gt;当静态站发布了。根因在项目没配构建命令/输出目录——
CF 的规则是「没有 build command 就把仓库根当静态资源上传，退出码 0 即成功」，
所以一个没跑过 &lt;code&gt;pnpm build&lt;/code&gt; 的部署照样能报绿。顺带还纠正了一个推断：平台分配域是
&lt;code&gt;website-1tq.pages.dev&lt;/code&gt;（带随机后缀），不是清单里写的 &lt;code&gt;website.pages.dev&lt;/code&gt;，后者实测 522 不存在。&lt;/p&gt;
&lt;h2 id=&quot;记下来的三条&quot;&gt;记下来的三条&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CI 全绿 ≠ 线上产物正确。&lt;/strong&gt; CI 只能保证 &lt;code&gt;dist/&lt;/code&gt; 造得对；平台按什么目录发布、用哪个
pnpm 版本，是另一层配置。消费端冒烟才是唯一裁判，平台按钮的绿不能当验收签字。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;假测试往往成对出现。&lt;/strong&gt; 一边把说明文字当密钥（假红），一边因文件名转义啥也没扫（假绿）。
与其修两个现象，不如问「这个断言到底测到了什么」——然后让它自己证明扫过文件、规则不自匹配。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认值要按平台文档来，不按本机来。&lt;/strong&gt; CF Build image v3 默认 pnpm 10.11.1，不认
&lt;code&gt;packageManager&lt;/code&gt; 字段，要在后台显式设 &lt;code&gt;PNPM_VERSION=11.28.5&lt;/code&gt;；Node 22.23.4 恰好够 astro 用，
不用另设。这些都不是能从本机 &lt;code&gt;pnpm run quality&lt;/code&gt; 里推出来的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;还没闭环的&quot;&gt;还没闭环的&lt;/h2&gt;
&lt;p&gt;CF 后台的 Build command / 输出目录 / &lt;code&gt;PNPM_VERSION&lt;/code&gt; 要用户本人去配，配完重新部署，
远端冒烟才会从 FAIL 转 PASS；之后才是自定义域名 CNAME、HTTPS、发布→回滚→再发布演练、
国内与第三方实测、作者真实收到通知、5 人 3 秒测试。今天先把「绿灯不等于上线」这件事
写进演练文档和近况里，等远端冒烟真的全绿了，再回头把这句话划掉。&lt;/p&gt;
</content:encoded><category>m7</category><category>deployment</category><category>ci</category><category>testing</category><category>cloudflare</category></item><item><title>给首屏定了一条硬性预算</title><link>https://website.85138175.xyz/log/2026-11-12-first-screen-budget/</link><guid isPermaLink="true">https://website.85138175.xyz/log/2026-11-12-first-screen-budget/</guid><description>身份 1 句、价值 1 句、作品 ≤3、主 CTA 1、次 CTA 1、导航 ≤5</description><pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;「简洁、一目了然」不能只当形容词，得变成能验收的数字。&lt;/p&gt;
&lt;p&gt;首屏（不滚动）只允许：身份一句话、价值主张一句话、最多 3 张作品卡、1 个主 CTA、1 个次 CTA、导航不超过 5 项。禁止长段落、技能标签云、全量日志列表、动效轮播、第三方聊天挂件、cookie 横幅。&lt;/p&gt;
&lt;p&gt;配套性能预算：首屏 JS ≤50KB、LCP &amp;lt;1.5s、CLS &amp;lt;0.1、Lighthouse ≥95。&lt;/p&gt;
&lt;p&gt;验收方式是找 5 个人做「3 秒测试」——看 3 秒后能不能说清我是谁、做什么、能帮他什么、下一步点哪，至少 4 个人说得出才算过。&lt;/p&gt;
</content:encoded><category>设计</category><category>首屏</category></item><item><title>前台 15 个页面落地了</title><link>https://website.85138175.xyz/log/2026-11-12-front-end-lands/</link><guid isPermaLink="true">https://website.85138175.xyz/log/2026-11-12-front-end-lands/</guid><description>从占位首页到完整前台，顺手修正了一个关于证据链的错误假设</description><pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今天把 M3 做完了：15 个静态页面、8 个组件、3 个 lib 模块，&lt;code&gt;pnpm build&lt;/code&gt; / &lt;code&gt;check&lt;/code&gt; / &lt;code&gt;test:rules&lt;/code&gt; 全绿，全站 15 个页面都是零 &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;过程中有个值得记下的修正：规划书里原本假定 Astro 的 &lt;code&gt;reference()&lt;/code&gt; 会校验「证据指向的条目是否真实存在」。写了个探针把 &lt;code&gt;product_id&lt;/code&gt; 指向一个不存在的 id，&lt;code&gt;astro build&lt;/code&gt; 居然照样通过——说明它只管类型，不管存在性。于是加了一层 &lt;code&gt;src/lib/evidence.ts&lt;/code&gt;，在构建期逐条解析每条能力的 &lt;code&gt;evidence_ids&lt;/code&gt;，目标不存在就直接抛错中断构建，并补了第三条自动化回归用例。&lt;/p&gt;
&lt;p&gt;这件事的教训是：&lt;strong&gt;「看起来被框架保证了」和「真的被保证了」之间，差一次故意写错的探针&lt;/strong&gt;。证据链是这个产品的立身之本，不能只靠约定。&lt;/p&gt;
</content:encoded><category>工程</category><category>Astro</category><category>决策</category></item><item><title>脚手架跑通了</title><link>https://website.85138175.xyz/log/2026-11-12-scaffold-works/</link><guid isPermaLink="true">https://website.85138175.xyz/log/2026-11-12-scaffold-works/</guid><description>Astro 7.4.11 空构建、类型检查、冻结锁文件全部通过</description><pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今天把最小工程立起来了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pnpm install&lt;/code&gt; → 295 个包，零 peer 冲突；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pnpm build&lt;/code&gt; → &lt;code&gt;output: &quot;static&quot;&lt;/code&gt;，&lt;code&gt;dist/index.html&lt;/code&gt; 1554 B，零客户端 JS；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pnpm check&lt;/code&gt; → 0 errors / 0 warnings / 0 hints；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--frozen-lockfile&lt;/code&gt; → 可复现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个小坑：Astro 7 把 &lt;code&gt;compressHTML&lt;/code&gt; 默认值从 &lt;code&gt;true&lt;/code&gt; 改成了 &lt;code&gt;&apos;jsx&apos;&lt;/code&gt;，会吞掉行内元素之间的空格。中文文案里行内链接多，显式设回 &lt;code&gt;true&lt;/code&gt; 才稳。&lt;/p&gt;
&lt;p&gt;另一个坑：TypeScript latest 已经到 7.0.2，但 &lt;code&gt;@astrojs/check&lt;/code&gt; 的 peer 只接受 &lt;code&gt;^5 || ^6&lt;/code&gt;，所以锁死 6.0.3，不跟 latest。&lt;/p&gt;
</content:encoded><category>工程</category><category>Astro</category></item><item><title>为什么先做纯静态</title><link>https://website.85138175.xyz/log/2026-11-12-why-static-first/</link><guid isPermaLink="true">https://website.85138175.xyz/log/2026-11-12-why-static-first/</guid><description>把「读路径」与「写路径」拆开，是这次最关键的一个决定</description><pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;独立开发者最怕的不是功能少，是运维把自己拖垮。&lt;/p&gt;
&lt;p&gt;所以这次把系统切成两半：&lt;strong&gt;读路径全静态&lt;/strong&gt;（Git 里的 MDX → 构建成 HTML → CDN），&lt;strong&gt;写路径才动态&lt;/strong&gt;（订阅、联系表单）。这样一来：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;没有数据库、没有会话、没有运行时依赖，部署就是传一堆 HTML；&lt;/li&gt;
&lt;li&gt;第三方表单服务、邮件服务、分析服务即使全挂，访客照样能看完整个网站；&lt;/li&gt;
&lt;li&gt;内容与代码同仓，&lt;code&gt;git push&lt;/code&gt; 就是发布，版本史就是备份。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代价是「在线后台编辑」得等到真有第二个人要写内容时再做。这个代价现在完全付得起。&lt;/p&gt;
</content:encoded><category>架构</category><category>决策</category></item><item><title>把手工验证变成一条命令</title><link>https://website.85138175.xyz/log/2026-11-12-%E6%8A%8A%E6%89%8B%E5%B7%A5%E9%AA%8C%E8%AF%81%E5%8F%98%E6%88%90%E4%B8%80%E6%9D%A1%E5%91%BD%E4%BB%A4/</link><guid isPermaLink="true">https://website.85138175.xyz/log/2026-11-12-%E6%8A%8A%E6%89%8B%E5%B7%A5%E9%AA%8C%E8%AF%81%E5%8F%98%E6%88%90%E4%B8%80%E6%9D%A1%E5%91%BD%E4%BB%A4/</guid><description>M6 把前面所有「跑过一次」的验证固化成可复现的门禁，并补上欠着的 Lighthouse 分数</description><pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;为什么做这一步&quot;&gt;为什么做这一步&lt;/h2&gt;
&lt;p&gt;前几步的验证有一个共同问题：它们都是「我跑过一次，当时是绿的」。
E4 三条硬校验、Slow 4G 下的 LCP、Pagefind 能不能搜到、拔掉动态层后 5 个页面可读、
表单字段有没有带来源——这些散落在十几个脚本里，靠人记得按顺序跑。
一旦换了机器、或者有人改了 &lt;code&gt;src/pages/search.astro&lt;/code&gt; 里那段内联脚本，
之前的「绿」就悄悄失效了，而没人会知道。&lt;/p&gt;
&lt;p&gt;M6 要解决的不是「再测一遍」，而是&lt;strong&gt;让「测过」这件事本身可复现、且断了会报警&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&quot;落地了什么&quot;&gt;落地了什么&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;两条链：&lt;code&gt;pnpm run quality&lt;/code&gt;（构建 + CSP 生成 + check + E4 三条 + 24 个单测 + 链自检），
&lt;code&gt;pnpm run quality:browser&lt;/code&gt;（性能/搜索/线索/不变量 + Playwright + Lighthouse + axe）。
本地和 &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; 跑的是同一组脚本，CI 不另搞一套只在 CI 里绿的断言。&lt;/li&gt;
&lt;li&gt;补上 M4 欠的账：之前只有 CDP 自测 LCP，不是 Lighthouse 分数。这一步用本机真实 Chrome
跑 Lighthouse 13，四类分数都是 100（门槛 95），G2 才算真的闭环。&lt;/li&gt;
&lt;li&gt;严格 CSP 从&lt;strong&gt;产物&lt;/strong&gt;反推：审计 18 个 HTML 后确认只有 &lt;code&gt;/search&lt;/code&gt; 一个内联脚本，
于是 &lt;code&gt;script-src&lt;/code&gt; 只放行 &lt;code&gt;&apos;self&apos;&lt;/code&gt; + 一个 sha256 + &lt;code&gt;&apos;wasm-unsafe-eval&apos;&lt;/code&gt;，不放 &lt;code&gt;unsafe-inline&lt;/code&gt;。
本地服务器和 Playwright 把 CSP 作为真实 HTTP 响应头发出，测的是与托管平台一致的行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;又一次假测试的教训&quot;&gt;又一次「假测试」的教训&lt;/h2&gt;
&lt;p&gt;这一步修掉的 bug 里，绝大多数不在站点代码，而在测试自己：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;verify-ci.mjs&lt;/code&gt; 初版用字符串包含判断「回归脚本有没有进链」，把经 &lt;code&gt;test&lt;/code&gt; 间接进入的
&lt;code&gt;test:rules&lt;/code&gt;/&lt;code&gt;test:unit&lt;/code&gt; 误报成孤立 → 改成 BFS 可达性。&lt;/li&gt;
&lt;li&gt;Playwright 首跑 4 条红：主 CTA 在首屏和次级区各出现一次（设计如此）、
CSP 断言误伤了 &lt;code&gt;style-src&lt;/code&gt; 里明确放开的 &lt;code&gt;unsafe-inline&lt;/code&gt;、中文 slug 被浏览器百分号编码。&lt;/li&gt;
&lt;li&gt;axe 首跑倒是&lt;strong&gt;真的&lt;/strong&gt;抓到 2 处站点缺陷：作品/日志列表页 h1 下面直接是 h3，
跳过了 h2。这是 M6 唯一改到生产代码的修复（卡片标题支持 &lt;code&gt;headingLevel&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有一个不大但会咬人的发现：&lt;code&gt;pnpm ci&lt;/code&gt; 是 pnpm 内建的 &lt;code&gt;clean-install&lt;/code&gt; 命令，
会静默遮蔽 package.json 里的同名脚本。所以门禁改叫 &lt;code&gt;quality&lt;/code&gt;，不叫 &lt;code&gt;ci&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id=&quot;故意写坏一次&quot;&gt;故意写坏一次&lt;/h2&gt;
&lt;p&gt;CI 的 DoD 要求「故意破坏一条 schema 时 CI 必须红」。我没有只写一个断言，
而是真的往 &lt;code&gt;src/content/capabilities/&lt;/code&gt; 塞了一条 &lt;code&gt;evidence_ids: []&lt;/code&gt; 的内容：
&lt;code&gt;pnpm build&lt;/code&gt; 退出码 1、错误可读；删掉后退出码 0。
因为 &lt;code&gt;quality&lt;/code&gt; 链里包含真实 &lt;code&gt;build&lt;/code&gt;，把违规内容提交进仓库时 CI 也必然红。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「测试通过了」和「测试真的测了」之间，差的往往是一次故意写坏的探针。
这句话 M3 写过一次，M5 又写过一次，M6 再写一次——它值得重复。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><category>m6</category><category>ci</category><category>testing</category><category>lighthouse</category><category>csp</category></item><item><title>把动态层拔掉，站还站得住吗</title><link>https://website.85138175.xyz/log/2026-11-12-%E6%8B%94%E6%8E%89%E5%8A%A8%E6%80%81%E5%B1%82%E4%B9%8B%E5%90%8E/</link><guid isPermaLink="true">https://website.85138175.xyz/log/2026-11-12-%E6%8B%94%E6%8E%89%E5%8A%A8%E6%80%81%E5%B1%82%E4%B9%8B%E5%90%8E/</guid><description>用一次故意写坏的构建，验证「读路径全静态」不是一句口号</description><pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;为什么要做这一步&quot;&gt;为什么要做这一步&lt;/h2&gt;
&lt;p&gt;概要设计里写了一条不变量：&lt;strong&gt;即使动态层全部宕机，网站（读）仍完整可用&lt;/strong&gt;。
这句话很容易写成口号——因为默认情况下我们根本没配表单端点、没配统计脚本，
页面当然能打开，但那只是「碰巧没依赖」，不是「被验证过没有依赖」。&lt;/p&gt;
&lt;p&gt;M5 的做法是反过来的：构建时故意把在线表单、预约外链、Umami 脚本&lt;strong&gt;全部指向一个不可达域名&lt;/strong&gt;
（&lt;code&gt;offline-dynamic.invalid&lt;/code&gt;），再用 Chrome 的 &lt;code&gt;Network.setBlockedURLs&lt;/code&gt; 真正拦掉它们，
然后依次打开首页、作品列表、日志列表、作品详情、一条中文 slug 的日志详情。&lt;/p&gt;
&lt;h2 id=&quot;实测结果&quot;&gt;实测结果&lt;/h2&gt;
&lt;p&gt;5 个页面全部有标题、有正文、有导航、图片没坏；首方资源零失败；
第三方脚本被拦了 7 次（&lt;code&gt;blockedReason: &quot;inspector&quot;&lt;/code&gt;）。
也就是说：表单/预约/统计全断，读者依然能完整认识我、读到作品和创作记录。&lt;/p&gt;
&lt;h2 id=&quot;一个比站点-bug-更值得记的教训&quot;&gt;一个比站点 bug 更值得记的教训&lt;/h2&gt;
&lt;p&gt;这个回归脚本第一次跑是 FAIL，但站点一行代码都不用改——&lt;strong&gt;坏的是测试自己&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;探针环境变量漏了 &lt;code&gt;PUBLIC_&lt;/code&gt; 前缀，Vite 根本没把它们内联进产物，动态层压根没启用，
测试却准备宣称「拔掉动态层后一切正常」。这是最危险的一种假通过。&lt;/li&gt;
&lt;li&gt;本地静态服务器没有模拟托管平台的 clean URL，详情页 404。&lt;/li&gt;
&lt;li&gt;中文 slug 在 &lt;code&gt;location.href&lt;/code&gt; 里被百分号编码，而传入的 URL 是 Unicode，等待条件永远不成立。&lt;/li&gt;
&lt;li&gt;把「正文 &amp;gt; 200 字」当可读性下限，结果把本来就很短的作品列表页（162 字）判成坏页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;修完这 6 处，脚本才真正测到了它想测的东西。
&lt;strong&gt;「测试通过了」和「测试真的测了」之间，差的往往就是一次故意写坏的探针。&lt;/strong&gt;
这与 M3 那次 &lt;code&gt;reference()&lt;/code&gt; 探针是同一个道理，只是这次翻车的是测试代码本身。&lt;/p&gt;
&lt;h2 id=&quot;还有一件没闭环的事&quot;&gt;还有一件没闭环的事&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;pnpm test:lead&lt;/code&gt; 证明了：订阅和联系表单会带着邮箱、同意、来源留痕被完整 POST 到端点，
浏览器能拿到回执，而且即使端点打不开，页面上仍留着邮件兜底链接。
但「作者真的收到邮件/通知」属于外部送达，得等部署时配上真实端点、人工提交一次才算数。
脚本不冒充这一步，规划书里也把它单列成了待办。&lt;/p&gt;
</content:encoded><category>m5</category><category>invariant</category><category>testing</category><category>cdp</category></item><item><title>让搜索引擎和阅读器读懂这个站</title><link>https://website.85138175.xyz/log/2026-11-12-%E8%AE%A9%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E5%92%8C%E9%98%85%E8%AF%BB%E5%99%A8%E8%AF%BB%E6%87%82%E8%BF%99%E4%B8%AA%E7%AB%99/</link><guid isPermaLink="true">https://website.85138175.xyz/log/2026-11-12-%E8%AE%A9%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E5%92%8C%E9%98%85%E8%AF%BB%E5%99%A8%E8%AF%BB%E6%87%82%E8%BF%99%E4%B8%AA%E7%AB%99/</guid><description>接上 RSS/JSON Feed、SEO、站内搜索与性能实测</description><pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前台 16 个页面已经能看，但一个只给人看的站还不够。这一步把它接到机器能
消费的通道上：RSS 2.0（带全文 &lt;code&gt;content:encoded&lt;/code&gt;）、JSON Feed 1.1、canonical /
OG / JSON-LD、sitemap，以及一个完全静态的站内搜索。&lt;/p&gt;
&lt;p&gt;几个没有想当然的地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;先读源码再写端点&lt;/strong&gt;。&lt;code&gt;@astrojs/rss&lt;/code&gt; 的 &lt;code&gt;site&lt;/code&gt; 会经 &lt;code&gt;createCanonicalURL&lt;/code&gt; 补尾斜杠，
&lt;code&gt;pubDate&lt;/code&gt; 必须传 &lt;code&gt;Date&lt;/code&gt;；Astro 的内容条目自带 &lt;code&gt;rendered.html&lt;/code&gt;，所以 RSS 能给全文，
不必自己重渲染。JSON Feed 官网取不到，就按 1.1 结构手写，不赌记忆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;搜索不引入服务端&lt;/strong&gt;。Pagefind 在 &lt;code&gt;astro:build:done&lt;/code&gt; 钩子里扫 &lt;code&gt;dist/&lt;/code&gt; 生成静态索引，
搜索页按需懒加载 UI 脚本——首页依然是 0 KB 可执行 JS。UI 的构造参数是从压缩
产物里反推出来的，所以又写了一个真实浏览器回归：输入「静态」返回 8 条结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能不靠感觉&lt;/strong&gt;。用 Chrome DevTools Protocol 模拟 Slow 4G + CPU×4 实测：
LCP 972ms、CLS 0、TBT 0、首屏 JS 0KB。脚本明确写了「这不是 Lighthouse」，
分数留到 M6 的 CI 固化。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个诚实的坑：&lt;code&gt;pnpm test&lt;/code&gt; 之前指向还没装的 vitest，一直是个哑弹；嵌套脚本里
&lt;code&gt;npm_execpath&lt;/code&gt; 又不透传，硬校验回归会找不到 &lt;code&gt;pnpm&lt;/code&gt;。最后改成直接调用
&lt;code&gt;node_modules/astro/bin/astro.mjs&lt;/code&gt;，不依赖包管理器，回归才真正跑通。&lt;/p&gt;
&lt;p&gt;教训和上次 &lt;code&gt;reference()&lt;/code&gt; 那次是同一条：&lt;strong&gt;「配置写上了」不等于「真的生效了」&lt;/strong&gt;，
能写探针就写探针，能在浏览器里点一下就点一下。&lt;/p&gt;
</content:encoded><category>seo</category><category>rss</category><category>search</category><category>performance</category></item><item><title>让门禁在任何机器上都能跑</title><link>https://website.85138175.xyz/log/2026-11-12-%E8%AE%A9%E9%97%A8%E7%A6%81%E5%9C%A8%E4%BB%BB%E4%BD%95%E6%9C%BA%E5%99%A8%E4%B8%8A%E9%83%BD%E8%83%BD%E8%B7%91/</link><guid isPermaLink="true">https://website.85138175.xyz/log/2026-11-12-%E8%AE%A9%E9%97%A8%E7%A6%81%E5%9C%A8%E4%BB%BB%E4%BD%95%E6%9C%BA%E5%99%A8%E4%B8%8A%E9%83%BD%E8%83%BD%E8%B7%91/</guid><description>M7 准备把写死本机路径的 CI 改成标准 GitHub Actions，并把部署回滚变成可签字清单</description><pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;一条自己骗自己的-ci&quot;&gt;一条自己骗自己的 CI&lt;/h2&gt;
&lt;p&gt;M6 收尾时我列了四项诚实留白，其中最刺眼的是这条：&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;
里写着 &lt;code&gt;NODE: D:\Ai\dsh-data\...\node.exe&lt;/code&gt;、&lt;code&gt;PNPM: ...\pnpm.mjs&lt;/code&gt;、
&lt;code&gt;CHROME_PATH: C:\Program Files\Google\Chrome\Application\chrome.exe&lt;/code&gt;，注释还一本正经地说
「Windows runner 因为本机 Chrome 和 DSH bundled Node 都在这里」。&lt;/p&gt;
&lt;p&gt;这些路径只在这台开发机上存在。GitHub 托管 runner 上既没有 &lt;code&gt;D:\Ai\dsh-data&lt;/code&gt;，
也没有 &lt;code&gt;C:\Program Files\Google\Chrome&lt;/code&gt;；&lt;code&gt;&amp;amp; $env:NODE $env:PNPM ...&lt;/code&gt; 的 PowerShell 写法
更是在 Linux 上直接不存在。本地两条链全绿、23 项 CI 自检全绿，但那检查的是
「脚本链没断」，不是「这份 workflow 真的能在远端跑起来」。&lt;strong&gt;全绿 ≠ 真绿&lt;/strong&gt;，
和 M3 那次 &lt;code&gt;reference()&lt;/code&gt; 探针是同一类问题，只是这次骗的是部署而不是证据链。&lt;/p&gt;
&lt;h2 id=&quot;这轮做了什么&quot;&gt;这轮做了什么&lt;/h2&gt;
&lt;p&gt;用户选了「先做不依赖账号的准备」，所以我没有碰任何域名或平台账号，只修工程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;把 16 处 Windows 硬编码 Chrome 收敛成 &lt;code&gt;scripts/find-chrome.mjs&lt;/code&gt; 一个探测器：
显式 &lt;code&gt;CHROME_PATH&lt;/code&gt;/&lt;code&gt;PW_CHROME&lt;/code&gt; 覆盖 → 三平台常见绝对路径 → PATH 扫描。
关键是&lt;strong&gt;惰性&lt;/strong&gt;：&lt;code&gt;serve.mjs&lt;/code&gt; 会 import &lt;code&gt;cdp-harness.mjs&lt;/code&gt; 却从不启动浏览器，
顶层抛错会连本地预览都起不来。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.npmrc&lt;/code&gt; 里 &lt;code&gt;storeDir=D:\.pnpm-store&lt;/code&gt; 删掉。Linux 上这会被当成一个字面目录名；
pnpm v11 自己会选同卷 store，真要固定就用环境变量或 &lt;code&gt;--store-dir&lt;/code&gt;，不写进仓库。&lt;/li&gt;
&lt;li&gt;ci.yml 整篇重写：&lt;code&gt;ubuntu-latest&lt;/code&gt; + &lt;code&gt;pnpm/action-setup@v4&lt;/code&gt; + &lt;code&gt;actions/setup-node@v4&lt;/code&gt;，
browser job 用 &lt;code&gt;node scripts/find-chrome.mjs&lt;/code&gt; 把探测结果写进 &lt;code&gt;$GITHUB_ENV&lt;/code&gt;。
部署 job 刻意&lt;strong&gt;不加&lt;/strong&gt;——没有账号时把它塞进 CI 只会制造第二种假绿。&lt;/li&gt;
&lt;li&gt;新增 &lt;code&gt;docs/部署与回滚演练.md&lt;/code&gt; 和 &lt;code&gt;scripts/deploy-smoke.mjs&lt;/code&gt;：
本地托管 &lt;code&gt;dist/&lt;/code&gt; 验 11 页 / feed / sitemap / Pagefind / 真 CSP 头，
远端模式对线上 URL 跑同一套消费端检查，发布→回滚→再发布有签字表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;又踩到两次假测试&quot;&gt;又踩到两次「假测试」&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;verify-ci.mjs&lt;/code&gt; 的密钥 hygiene 检查突然红了：M6 把 ci.yml 和脚本自身提交进仓库后，
它们&lt;strong&gt;自己&lt;/strong&gt;就含有 &lt;code&gt;sk_live_|AKIA...&lt;/code&gt; 那条检测正则，被自己扫中。这不是站点有密钥，
是检测器照到了镜子。修法是把两个检测器文件列入白名单、改检查项名，
ci.yml 的 audit job 同步排除。&lt;/p&gt;
&lt;p&gt;另一次是 YAML 自检：想用 &lt;code&gt;pwsh -c&lt;/code&gt;/&lt;code&gt;node -e&lt;/code&gt; 内联解析，分别被引号吞掉、
&lt;code&gt;require(&apos;yaml&apos;)&lt;/code&gt; 在 pnpm 严格隔离下不可解析、Python 没有 &lt;code&gt;yaml&lt;/code&gt; 模块。最后还是写独立
&lt;code&gt;.mjs&lt;/code&gt; 引 &lt;code&gt;node_modules/.pnpm/yaml@2.8.3&lt;/code&gt; 才通过。「复杂 JS 一律独立 .mjs」
这条纪律，这轮又替我省了一次自我怀疑。&lt;/p&gt;
&lt;h2 id=&quot;仍然没有做的事&quot;&gt;仍然没有做的事&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;没有真部署，没有 GitHub Actions 托管 runner 首跑——这两件都等 ⑤域名 / ⑥联系方式 /
⑧远端仓库 / ⑨平台账号。&lt;/li&gt;
&lt;li&gt;没有验证表单外部送达、DNS 邮件记录、国内可访问性。&lt;code&gt;deploy-smoke&lt;/code&gt; 在远端模式里
连 CSP 响应头都只记录不判失败（不同平台对 &lt;code&gt;_headers&lt;/code&gt; 的映射不一样），
因为这些必须真人按演练文档核对，脚本不冒充。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;M6 把手工验证变成一条命令；M7 准备则把那条命令从「只在我这台机器上成立」
搬到了「任何装了 Node 24 和 Chrome 的机器上成立」。剩下的，就是等一个真实的域名。&lt;/p&gt;
</content:encoded><category>m7</category><category>ci</category><category>deployment</category><category>portability</category></item><item><title>Atelier / 造物间</title><link>https://website.85138175.xyz/products/atelier/</link><guid isPermaLink="true">https://website.85138175.xyz/products/atelier/</guid><description>一个先自用、以作品为证据、以创作过程建立信任的独立开发者主页</description><pubDate>Thu, 12 Nov 2026 00:00:00 GMT</pubDate><category>独立开发</category><category>Astro</category><category>内容站点</category></item></channel></rss>