<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Yoshen&apos;s Blog</title>
    <link>https://yoshen.me</link>
    <description>Hello, I&apos;m Yoshen — an indie developer and UX designer. I build things that solve real problems in everyday life, and this is where I document the process.</description>
    <language>zh-CN</language>
    <lastBuildDate>Sun, 30 Aug 2026 00:00:00 GMT</lastBuildDate>
    <atom:link href="https://yoshen.me/feed.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>个人站点自托管实践</title>
      <link>https://yoshen.me/posts/self-hosting-on-aws-lightsail</link>
      <guid isPermaLink="true">https://yoshen.me/posts/self-hosting-on-aws-lightsail</guid>
      <pubDate>Sun, 30 Aug 2026 00:00:00 GMT</pubDate>
      <description>把静态站点从托管平台搬到自建 VPS,并在同机挂几个自用代理节点。本文给出可复制的完整路径:选型框架、架构设计、七阶段执行、真实踩坑,以及直接能用的模板。</description>
      <content:encoded><![CDATA[<blockquote>
<p>托管平台好用,但总有两个时刻你会想自托管:站点需要<strong>特定地区直连友好</strong>;或者你本来就要一台机器<strong>做别的用</strong>(代理、Nas、测试)。本文以一次真实迁移为例,把完整路径和里面的坑一次性讲清楚——<strong>照着做,不踩第二遍。</strong></p>
</blockquote>
<blockquote>
<p><strong>📌 适用范围(先看再读)</strong>:本文案例是<strong>纯静态站点</strong>——构建期生成 HTML,线上零服务端进程、无数据库。</p>
<ul>
<li><strong>通用部分</strong>(对任何 VPS 上的项目都成立):选型三维度、域名单一不变量、双防火墙、DNS/证书链路、安全加固、代理节点、验证分层、运维模型;</li>
<li><strong>静态专属部分</strong>(动态项目请替换):负载画像与"512MB 够用"结论、静态部署流程、缓存策略、纯静态验证标准。
如果你的项目是<strong>动态的</strong>(前后端分离/SSR/需要数据库与常驻进程),请按"运行时常驻内存 ×2 + 数据库所需"重新评估配置,并在文中标注 ⚠️ 的地方自行对应修订。</li>
</ul>
</blockquote>
<hr>
<h2 id="一先定义好三个决策维度">一、先定义"好":三个决策维度</h2>
<p>买任何境外 VPS 前,先回答三个问题——<strong>这三个维度决定 90% 的体验,参数反而是次要的</strong>:</p>
<table>
<thead>
<tr>
<th>维度</th>
<th>要问的问题</th>
<th>对"个人站+代理"的意义</th>
</tr>
</thead>
<tbody>
<tr>
<td>线路</td>
<td>大陆访问走什么路由?晚高峰丢包如何?</td>
<td>访客"秒开"还是"转圈"</td>
</tr>
<tr>
<td>换 IP 机制</td>
<td>IP 被墙后能否低成本换?</td>
<td>大陆直连的最大运维事故是"被墙",机制决定痛苦程度</td>
</tr>
<tr>
<td>条款 &#x26; 门槛</td>
<td>ToS 允许代理吗?支付方便吗?</td>
<td>被删机 = 前功尽弃;支付方式决定能不能买到</td>
</tr>
</tbody>
</table>
<p><strong>负载画像</strong>(量化"够用"):以一个纯静态站为例——约 10MB 文件、日请求几十、月流量不足 1GB,同机再挂两个自用代理节点(月流量数 GB 级)。
结论:<strong>1 核 / 512MB 起步都行——真正贵的是线路和换 IP 的自由,不是配置。</strong></p>
<blockquote>
<p>⚠️ <strong>静态专属</strong>:以上画像与结论只适用于"构建期产物 + 文件服务"的静态站。<strong>动态项目</strong>(服务进程、数据库、定时任务)请以"运行时常驻内存 ×2 + 数据库所需 + 余量"重估配置,不要照搬 512MB——本文后续章节中,除部署方式外,其余方法仍然通用。</p>
</blockquote>
<h2 id="二选型遍历一圈后的决策框架">二、选型:遍历一圈后的决策框架</h2>
<table>
<thead>
<tr>
<th>候选类别</th>
<th>线路</th>
<th>换 IP</th>
<th>年成本</th>
<th>定位</th>
</tr>
</thead>
<tbody>
<tr>
<td>低价云商(普通跨境线路,163/4837 类)</td>
<td>中低,晚高峰抖动</td>
<td>按次收费(数美元/次)</td>
<td>最低(约 $20)</td>
<td>价格友好,但要接受较高的换 IP 频率与成本</td>
</tr>
<tr>
<td>国际大厂主流产品</td>
<td>中等(与运营商适配差异明显,联通/移动通常更佳)</td>
<td>删除重建即换,免费</td>
<td>中等($60-90)</td>
<td>均衡之选,支付无门槛</td>
</tr>
<tr>
<td>精品商(CN2 GIA / 9929 / CMIN2 等)</td>
<td>顶级,晚高峰稳定</td>
<td>自助免换有频次,超次可付费</td>
<td>偏高($50-130,视活动)</td>
<td>线路体验天花板</td>
</tr>
<tr>
<td>主流平台亚太节点</td>
<td>中上(以实测为准)</td>
<td>删除重建即换,免费</td>
<td>常规价位,促销期可显著更低(视当期活动)</td>
<td>三问均无硬伤时的收敛选项;若促销进一步降低首年成本,属于加分而非必要条件</td>
</tr>
</tbody>
</table>
<p><strong>下单前的关键方法</strong>:候选 IP 先用全国连通检测实测(均值 100-200ms、无超时即可用,追求更低延迟再考虑精品线路)——<strong>这一步比看参数重要得多,也是"线路"维度唯一的客观裁判。</strong></p>
<p>决策依据浓缩成一句话:</p>
<blockquote>
<p><strong>没有硬伤的前提下,选"换 IP 免费 + 支付无门槛"里最便宜的那个;访客体验优先于财务时,再升精品线路。</strong></p>
</blockquote>
<h2 id="三架构一台机器多个角色">三、架构:一台机器,多个角色</h2>
<pre><code>访客 ──直连──► DNS(灰云解析,不做反向代理)──► 云主机
                                            │
                      ┌─────────────────────┴──────────────────┐
                      ▼                                        ▼
           Web 服务 :443(HTTPS + 静态站点)        代理节点 ×N:高位端口
           /srv/www(站点文件)                    systemd 守护,随机口令
</code></pre>
<p>三条关键设计(通用):</p>
<ol>
<li><strong>域名是唯一不变量</strong>,IP 是消耗品:访客永远访问域名;换服务器只改一条 DNS 记录,访客无感;</li>
<li><strong>不绑定静态公网 IP</strong>:多数云厂商"删除→重建"即换新 IP(分钟级、免费)——这是被墙场景最便宜的自救;</li>
<li><strong>双重防火墙心智</strong>:系统防火墙(ufw)+ 云平台层防火墙是两道独立的门,<strong>任何端口都要过两道</strong>——新端口只开一道门时,现象是"服务活着,外部永远连不上"。</li>
</ol>
<blockquote>
<p>⚠️ <strong>静态专属</strong>:图中 Web 层 = 文件服务(直接吐静态产物)。<strong>动态项目在此层替换为应用运行时</strong>(如容器或进程管理器,再配数据库与反向代理),架构其余部分(域名、DNS、防火墙、代理节点)保持不变——本文后面"七阶段"里的部署与验证步骤,动态项目按"镜像/代码同步 + 运行时健康检查"对应替换。</p>
</blockquote>
<h2 id="四执行七个阶段每步有验证">四、执行:七个阶段,每步有验证</h2>
<table>
<thead>
<tr>
<th>阶段</th>
<th>做什么</th>
<th>验证什么</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始化</td>
<td>系统升级、Web 服务/防火墙/防爆破、swap、时区、SSH 仅密钥</td>
<td>服务 active、规则到位、swap 生效</td>
</tr>
<tr>
<td>部署</td>
<td>本地产物打包 → 上传 → 解压到站点根</td>
<td>文件数/关键文件存在/首页 200</td>
</tr>
<tr>
<td>域名切换</td>
<td>DNS API(Token 只给"单域名编辑"最小权限)改 A 记录</td>
<td>回读记录、全国解析一致</td>
</tr>
<tr>
<td>HTTPS</td>
<td>Web 服务自动签发证书</td>
<td>证书日志、外部抓取 200</td>
</tr>
<tr>
<td>节点</td>
<td>代理服务程序(systemd 守护)+ 随机生成端口/口令</td>
<td>服务 active、外部端口连通、客户端实测延迟</td>
</tr>
<tr>
<td>安全加固</td>
<td>禁 root 登录、代理进程降权独立用户、审核端口与密钥</td>
<td>服务仍活、端口仍通、权限归位</td>
</tr>
<tr>
<td>验收</td>
<td>外部抓取 + 资源 HTTP 状态 + 全国连通检测 + 真机访问</td>
<td>全绿</td>
</tr>
</tbody>
</table>
<p><strong>一个原则:凡"改",必先"记"(配置进仓库脚本);凡"升级",必先"验"(改前改后各查一次)。</strong></p>
<h2 id="五真实踩坑本篇核心复用价值">五、真实踩坑(本篇核心复用价值)</h2>
<h3 id="1-windows-压缩包解压丢文件换-posix-tar">1. Windows 压缩包解压"丢文件"——换 POSIX tar</h3>
<p>Windows 打的 zip 在 Linux 解压会报"反斜杠路径分隔符"警告,严重时中断后<strong>文件树残缺</strong>——曾出现解压中断导致全部样式丢失,页面白板但 HTML 还在,极具迷惑性。
<strong>结论:跨平台部署一律用 <code>tar</code> 打包(<code>tar -czf out.tgz -C out .</code>,解压 <code>tar -xzf</code>),从此不用 zip。</strong></p>
<h3 id="2-远程命令一行流中文括号必崩远程操作脚本化">2. 远程命令一行流"中文+括号必崩"——远程操作脚本化</h3>
<p><code>ssh host 'echo 中文(备注)...'</code> 经本地 Shell 传参后,远程 bash 遇到中文括号/管道会报 <code>syntax error near unexpected token</code>。
<strong>结论:所有远程命令写成 <code>.sh</code> 上传执行,零踩坑。</strong></p>
<h3 id="3-白板事故只验证-html-的盲区">3. 白板事故:只验证 HTML 的盲区</h3>
<p>"验证通过"时依赖的是抓取 HTML 文本——<strong>CSS 是独立文件,丢没丢文本验证看不到,只有浏览器渲染暴露。</strong>
<strong>结论:站点验证必须分层:HTML 200 → 引用资源(CSS/JS/字体)逐一 HTTP 状态 → 渲染比对。</strong></p>
<h3 id="4-旧-html--404-css-的缓存残留html-no-cache-治本">4. 旧 HTML + 404 CSS 的缓存残留——HTML no-cache 治本</h3>
<p>白板修好后,已访问过的浏览器/应用内置浏览器仍显示旧版,因为<strong>旧 HTML 引用着已消失的样式名</strong>。
<strong>解法:<code>Cache-Control: no-cache, must-revalidate</code> 给 HTML(每次强制校验),静态资源保持 <code>immutable</code>(一年缓存)。</strong></p>
<h3 id="5-云平台隐形门默认防火墙没有自定义端口">5. 云平台"隐形门":默认防火墙没有自定义端口</h3>
<p>实例建好、证书也签了(验证走 80 端口成功),但<strong>测试 443 与高位端口全部超时</strong>——平台层防火墙默认只放 22/80,系统里的放行是<strong>另一道门</strong>。
<strong>结论:任何"端口不通"先问自己:两道门都开了吗?</strong></p>
<h3 id="6-云主机回流限制本机连自己公网-ip-失败">6. 云主机回流限制:本机连自己公网 IP 失败</h3>
<p>服务器上 <code>curl https://域名</code> 返回 000,一度怀疑部署失败——多数云厂商实例访问自身公网 IP 会被网络策略挡住,不代表外部不可达。
<strong>结论:服务器自测用 <code>--resolve 域名:443:127.0.0.1</code> 走回环,同时从外部抓取交叉验证。</strong></p>
<h3 id="7-浏览器记忆与无痕模式">7. 浏览器"记忆"与无痕模式</h3>
<p>普通窗口标签页保留<strong>打开那一刻</strong>的页面;无痕不吃缓存永远最新。"无痕正常、普通旧"不是故障。
<strong>结论:修复/改版后的验收标准动作 = 无痕窗口或"关标签重开"。</strong></p>
<h3 id="8-批处理快捷指令的三大坑">8. 批处理快捷指令的三大坑</h3>
<ul>
<li>批处理按 ANSI 解析,UTF-8 中文路径乱码,文件直接"找不到";</li>
<li>脚本里用到了 Shell 内置只读变量(<code>$Host</code>),赋值即报错;</li>
<li>有的 cmdlet 根本没有你以为的参数(如 <code>Start-Process</code> 无 <code>-LiteralPath</code>,那是 <code>Invoke-Item</code> 的)。
<strong>结论:含中文的路径,用 <code>powershell -EncodedCommand</code>(Base64 编码的 Unicode 命令),让脚本文件本身纯 ASCII,任何语言环境免疫;写脚本前先查参数表。</strong></li>
</ul>
<h3 id="9-教程都推新协议但稳定版才是生产选择">9. 教程都推新协议,但"稳定版"才是生产选择</h3>
<p>代理协议新版本常见"只有 RC/Beta"的过渡期,<strong>生产节点应选官方稳定版</strong>,与客户端全兼容;升级成本也低(换二进制+改配置)。</p>
<h3 id="10-买了为什么没扣款结算周期焦虑">10. "买了为什么没扣款"——结算周期焦虑</h3>
<p>云账单页面都有小字说明更新/抵扣时机。创建当天显示"已用 $0"是正常现象。
<strong>结论:账单类疑问,先读页面小字,再去看账单页——多数"异常"只是周期未到。</strong></p>
<h3 id="11-验证链缺环字体不现身--字体文件缺失">11. 验证链缺环:字体不现身 ≠ 字体文件缺失</h3>
<p>一客户端正常、另一客户端字体缺失,逐层验证后:全部字体子集在位——问题在客户端缓存。
<strong>结论:排查顺序永远是"服务器 → 网络 → 客户端",每层都有可执行的检查,不要跳层下结论。</strong></p>
<h2 id="六验证体系可直接复制">六、验证体系(可直接复制)</h2>
<table>
<thead>
<tr>
<th>层</th>
<th>动作</th>
<th>通过标准</th>
</tr>
</thead>
<tbody>
<tr>
<td>HTML</td>
<td>外部抓取首页/文章/RSS/404</td>
<td>200/200/200/404 与内容正确</td>
</tr>
<tr>
<td>资源</td>
<td>页面引用的 CSS/JS/字体逐个请求</td>
<td>全部 200,Content-Type 正确</td>
</tr>
<tr>
<td>DNS</td>
<td>全国连通检测(填域名)</td>
<td>全网解析一致,平均 &#x3C;200ms,无超时</td>
</tr>
<tr>
<td>端口</td>
<td>443/节点端口外部 TCP 连通</td>
<td>全通</td>
</tr>
<tr>
<td>客户端</td>
<td>无痕/关标签重开 + 代理客户端实测</td>
<td>渲染完整;节点延迟 60-180ms</td>
</tr>
<tr>
<td>服务</td>
<td>一键状态脚本九项</td>
<td>服务 active,站点 200</td>
</tr>
</tbody>
</table>
<h2 id="七安全闭环检查单">七、安全闭环(检查单)</h2>
<p>已完成:SSH 仅密钥、root 登录禁用、防爆破、双防火墙仅业务端口、自动安全更新、各服务以独立低权限用户运行、管理端口仅本机、Web 安全头与托管平台同规格、API Token 最小权限+自动到期、凭据双份(完整值本地 + 脱敏索引入库)。
待做三件:云账号开 MFA / 定期实例快照 / 密钥副本入私密保管。</p>
<h2 id="八运维投入模型每月-5-分钟">八、运维投入模型:每月 5 分钟</h2>
<table>
<thead>
<tr>
<th>频次</th>
<th>动作</th>
</tr>
</thead>
<tbody>
<tr>
<td>每月 1 号(5 分钟)</td>
<td>状态巡检 + 直连一眼 + 节点延迟 + 凭据档案对照</td>
</tr>
<tr>
<td>每季度(10 分钟)</td>
<td>快照 + 全国连通检测 + 账单一眼</td>
</tr>
<tr>
<td>信号驱动</td>
<td>访客打不开/节点断 → 先重启服务(10 秒),再查日志</td>
</tr>
<tr>
<td>年度 3 个日期</td>
<td>证书基本自动;Token 到期前重生成;计费到期前一个月决策续费或换商</td>
</tr>
</tbody>
</table>
<p>**决策五问:**影响谁?影响多久?数据在吗?修复成本?值得做吗?——大多数"问题"问完五问,就自动降级为"不用管"。</p>
<h2 id="九可复用的四张模板">九、可复用的四张模板</h2>
<p><strong>A. 选型三问表</strong>(见第一节)——任何境外 VPS 决策直接套。
<strong>B. 部署检查单</strong>:脚本按 <code>00→NN</code> 编号放版本库随项目走(初始化→修复→部署→验证→诊断→加固),每步幂等可重跑;换机或重建后按序执行即完整还原。
<strong>C. 故障定位顺序</strong>:信号 → 一键状态(服务/端口)→ 全国连通检测(线路)→ 日志 → 客户端重试。
<strong>D. 发布流程</strong>:写内容 → 构建 → 一键发布(打包+上传+解压+校验)→ 无痕验收。</p>
<h2 id="十可带走的十条">十、可带走的十条</h2>
<ol>
<li>先定义"什么算好"——三个维度,不是配置;</li>
<li>配置可以便宜,换 IP 不能麻烦;</li>
<li>域名是唯一不变量,IP 是消耗品;</li>
<li>每道防火墙都要过两道门;</li>
<li>跨平台部署用 tar,不用 zip;</li>
<li>远程命令脚本化,别拼一行流;</li>
<li>验证要分层,HTML 通过 ≠ 渲染正确;</li>
<li>HTML 永不缓存,静态资源一年缓存;</li>
<li>生产选稳定版,新协议等正式版;</li>
<li>运维投入按月算:每月 5 分钟,一年 3 个日期。</li>
</ol>
<blockquote>
<p>文中原则(最小权限、双备份、信号驱动、验证分层)适用于任何"小系统 + 长期跑"的场景。</p>
</blockquote>]]></content:encoded>
      <category>技术向</category>
    </item>
    <item>
      <title>《The Odyssey》</title>
      <link>https://yoshen.me/posts/odyssey-cognitive-reconstruction</link>
      <guid isPermaLink="true">https://yoshen.me/posts/odyssey-cognitive-reconstruction</guid>
      <pubDate>Sun, 23 Aug 2026 03:15:00 GMT</pubDate>
      <description>一部电影没有告诉我多少新知识,但它让我重新整理了自己原本就知道的东西。这篇文章记录这次思考:事件结束不等于因果结束,罪责与后果是两回事,以及一个可以复用的认知结构。</description>
      <content:encoded><![CDATA[<blockquote>
<p>不是影评,也不解析剧情。只记录看完之后一直在想的问题。</p>
</blockquote>
<hr>
<h2 id="一我原本以为自己只是去看一部电影">一、我原本以为自己只是去看一部电影</h2>
<p>看完《奥德赛》之后,我一直在想一个问题:这部电影没有给我任何新知识。英雄、归乡、战争、诱惑、复仇,这些概念在学生时代就学过了。但看完之后,我确实觉得自己对这些概念之间的关系理解得更清楚了。</p>
<p>有些作品不会让你"知道更多",但会让你"想得更清楚"。</p>
<p>我自己的解释是:很多道理我原本就知道,只是它们之间没有连接。这部电影让我第一次系统地看到了它们之间的关系。</p>
<hr>
<h2 id="二特洛伊战争结束了但因果没有结束">二、特洛伊战争结束了,但因果没有结束</h2>
<p>电影里最触动我的问题,不是"奥德修斯什么时候回家",而是:</p>
<blockquote>
<p>经历战争、暴力和失序之后,一个人和一个共同体,怎么重新回到秩序?</p>
</blockquote>
<p>战争作为一个事件可以结束,但战争产生的后果不会同步结束。</p>
<blockquote>
<p>事件结束 ≠ 因果结束。</p>
</blockquote>
<p>一个决定的结果可以延续很久:行为 → 短期收益 → 隐性成本 → 后果累积 → 反馈放大 → 危机显现,最后由或许根本不在场的人承担代价。</p>
<p>我最初想用"因果报应"概括这件事,后来觉得"报应"太神秘。更准确的说法是:<strong>历史债务,或者延迟兑现的后果。</strong></p>
<p>重点不是善恶有报,而是:</p>
<blockquote>
<p>一个系统中的行为,不会因为行为者退出舞台就自动消失后果。</p>
</blockquote>
<p>中国历史里有一个类似的提法:礼崩乐坏。它不是某一条规则失效,而是维持共同体的整套秩序网络——边界、身份、责任、契约——失去了约束力。这里只把礼崩乐坏当作一个解释性类比,不是说两者是同一回事。</p>
<hr>
<h2 id="三从因果报应到系统思维">三、从因果报应到系统思维</h2>
<p>"报应"这个词的问题在于它太整齐了,现实并不保证文学意义上的圆满闭环:</p>
<ul>
<li>破坏规则的人可能获得长期利益;</li>
<li>行为者没有承担主要成本;</li>
<li>代价被转嫁给无辜的人;</li>
<li>旧问题长期悬而未决;</li>
<li>修补旧问题的制度制造出新问题;</li>
<li>"赎罪"可能从未真正完成。</li>
</ul>
<p>所以,一个认知模型不能只收集支持自己的例子,还要主动追问:这个模型解释了什么?它又遗漏了什么?</p>
<p>模型不是世界本身,只是一副观察世界的透镜。一副好透镜应该同时告诉你:它照亮了哪里,又把哪里留在阴影里。</p>
<hr>
<h2 id="四先分清是谁才能讨论责任">四、先分清"是谁",才能讨论"责任"</h2>
<p>讨论责任之前,先把主体分清楚:国家、政府、政权、文明、民族、人民,不是同一个主体;行为的施动者、责任的承担者和后果的承受者,也可能不是同一个对象。</p>
<blockquote>
<p>罪责 ≠ 责任 ≠ 后果</p>
</blockquote>
<p>一个具体的行动者可以消失,一个组织可以终结,后继结构可能继承部分责任,但行为造成的客观后果仍然存在。后来的人不必天然继承前人的罪责,却可能不得不面对前人留下的现实。</p>
<blockquote>
<p>罪责未必能够跨代继承,但后果可以。</p>
</blockquote>
<p>同时要避免另一种简化:把国家、文明这类复杂系统人格化,用"犯错 → 受罚 → 赎罪"去解释历史。更接近现实的图景是:</p>
<p>行为 → 激励 → 后果 → 反馈 → 路径依赖 → 修正 → 新制度 → 新问题</p>
<hr>
<h2 id="五我得到的是一个认知模型">五、我得到的,是一个认知模型</h2>
<p>那么这次观影到底给了我什么?</p>
<p>一个可以脱离《奥德赛》独立使用的认知模型:</p>
<p><strong>初始秩序 → 越界/决策 → 短期结果 → 隐性成本积累 → 反馈与放大 → 危机 → 责任与代价分配 → 修正/清算 → 新秩序 → 新秩序产生新的激励与问题</strong></p>
<p>观点告诉我怎样看待一件事;认知模型告诉我,以后遇到类似问题,可以按什么结构去理解它。</p>
<p>结构越能脱离原作、迁移到组织、人生、制度和其他作品,模型才越成立。当然也要记住边界:</p>
<blockquote>
<p>不要因为有一把锤子,就把所有问题都当成钉子。</p>
</blockquote>
<p>把这一类东西排成阶梯:</p>
<p><strong>知识 → 概念 → 模型 → 框架 → 世界观</strong></p>
<ul>
<li>知识:我知道一些事实;</li>
<li>概念:我能给事实分类和命名;</li>
<li>模型:我理解变量之间可能存在的关系;</li>
<li>框架:面对复杂问题,我知道该从哪些维度观察;</li>
<li>世界观:大量长期稳定的框架,共同塑造我看待世界的方式。</li>
</ul>
<p>这次观影发生的位置在"概念 → 模型"这一层:契约、秩序、战争、制度、责任、因果、共同体这些原本分散的概念,第一次连成了网络。变化不在信息量,而在结构。</p>
<p>知识量决定你有多少节点,认知结构决定这些节点能不能组成网络。知识量差不多的人,认知能力可能差很多——差距不在记住多少,而在知识以什么结构存在,以及能不能在新的问题里重新组合这些结构。</p>
<hr>
<h2 id="六为什么两千多年前的故事仍然与我有关">六、为什么两千多年前的故事,仍然与我有关</h2>
<p>《奥德赛》当然是古希腊的,但离开与归乡、战争与创伤、承诺与背叛、诱惑与克制、家庭、身份、权力、死亡、复仇、宽恕,这些不专属于古希腊人。</p>
<p>这里要区分三个词:</p>
<ul>
<li><strong>普世</strong>:某个问题的成立,不依赖特定的国家、民族、时代或文化;</li>
<li><strong>普世价值</strong>:规范性命题,讨论"无论一个人是谁,什么原则应当适用于他";</li>
<li><strong>普世意义</strong>:一件作品产生于特殊的时代与文化,为什么仍能和后来的人发生关系。</li>
</ul>
<p>《奥德赛》属于第三种。它没有提供放之四海的标准答案,而是触及了所有人都会遇到的人类问题。</p>
<p>"Odyssey"这个词本身就是一次从具体到普遍的抽象:一个具体的人 → 一个人的归乡故事 → 一种旅程结构 → 一种普遍的人生经验。这篇文章走的路也相同。</p>
<hr>
<h2 id="七每个人都会进入自己的-odyssey">七、每个人都会进入自己的 Odyssey</h2>
<p>朋友聊起研究生毕业后的状态:旧身份已经结束,新身份还没有建立。</p>
<p>学校里的一切都有坐标:课程、论文、导师、评价体系、毕业节点。毕业之后进入的是开放系统:没有统一路线,没有统一评分,没有人告诉你什么时候算"抵达"。这种迷茫不一定意味着失败,可能只是:</p>
<blockquote>
<p>一个阶段结束了,下一个身份还在形成。</p>
</blockquote>
<p>所以 Odyssey 最恰当的描述,不是"经历磨难然后成功"这类廉价叙事,而是:</p>
<blockquote>
<p>归途本身也是人生的一部分。</p>
</blockquote>
<p>我们有时候不是在寻找目的地,而是在旅途中慢慢变成那个能够抵达目的地的人。</p>
<p>文明如此,制度如此,一场考试与一次转身,也是如此。</p>
<hr>
<h2 id="最后">最后</h2>
<p>这篇文章想留下的不是结论,而是一个问题:</p>
<blockquote>
<p>我那些已经知道很多年的东西,只是被储存着,还是已经形成了一套能够帮助我理解世界的结构?</p>
</blockquote>
<p>我并没有因为一部电影获得新的世界观。它只是让我重新整理了一遍自己原本就在形成的世界观。</p>
<p>这可能就是《奥德赛》对我真正的意义。</p>
<hr>
<blockquote>
<p>附:文中用"礼崩乐坏"作为跨文化现象的类比——当维持共同体的边界、身份、责任与契约失去约束力时,崩坏的不只是一条规则,而是支撑共同体运转的秩序网络。这是一个解释性类比(explanatory analogy),不代表两地史实等同或构成因果关联。</p>
</blockquote>]]></content:encoded>
      <category>生活</category>
    </item>
    <item>
      <title>把规则仓库做成产品</title>
      <link>https://yoshen.me/posts/blink-engineering-practice</link>
      <guid isPermaLink="true">https://yoshen.me/posts/blink-engineering-practice</guid>
      <pubDate>Sun, 23 Aug 2026 00:00:00 GMT</pubDate>
      <description>以 Blink 为例,拆解一个&quot;个人代理规则仓库&quot;如何被做成一款可分发、可验证、可持续维护的工程产品:两层架构、canonical 规则模型、九道机器门禁、供应链变化门槛、意图驱动的配置层与数据派生的门户。</description>
      <content:encoded><![CDATA[<blockquote>
<p>这篇讲实现:同一思想(规则类型即 DNS 语义、domain-first / IP-last 不变式)如何落地。Blink
(github.com/byyoshen/Blink) 是那套思想的完整落地:29 个 App、7 个客户端、
203 个生成产物、每日自动构建、9 道机器门禁。本文按架构层级拆解,重点看
"可验证性"和"变化可控"这两个词是怎么被写进代码的。</p>
</blockquote>
<hr>
<h2 id="0-一句话概括">0. 一句话概括</h2>
<p>Blink 做了一件事:<strong>把"个人维护的规则集"从手工劳作变成一条自动流水线</strong>,
同时让"正确性"不再是常识,而是机器断言。</p>
<p>它分两层:</p>
<ul>
<li><strong>规则层(自动维护)</strong>:审计上游 → canonical 规则模型 → 渲染七种客户端格式,每日更新;</li>
<li><strong>配置层(人工维护)</strong>:一份意图 + 模板 → 生成七端完整配置文件,人工确认后提交。</li>
</ul>
<p>生成目录只由构建器写入,<strong>绝不手工修改</strong>;仓库不含订阅 URL、token、密码或证书。</p>
<hr>
<h2 id="1-核心抽象canonical-规则模型">1. 核心抽象:canonical 规则模型</h2>
<p>规则层的第一性原理是:客户端之间共享的是<strong>语义</strong>,不是文本。</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="python" data-theme="github-light github-dark"><code data-language="python" data-theme="github-light github-dark" style="display: grid;"><span data-line=""><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># engine/scripts/build.py</span></span>
<span data-line=""><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># Canonical rule kinds. The names happen to match Surge vocabulary, but the</span></span>
<span data-line=""><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D"># model is client-neutral: renderers own every client-specific serialization.</span></span>
<span data-line=""><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">ALLOWED_RULE_TYPES</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> (</span></span>
<span data-line=""><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "DOMAIN"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span data-line=""><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "DOMAIN-SUFFIX"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span data-line=""><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "DOMAIN-KEYWORD"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span data-line=""><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "USER-AGENT"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span data-line=""><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "PROCESS-NAME"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span data-line=""><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "IP-CIDR"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span data-line=""><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">    "IP-CIDR6"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span data-line=""><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span></code></pre></figure>
<p>七个白名单类型是唯一事实源。<strong>渲染层负责所有客户端差异</strong>:</p>
<table>
<thead>
<tr>
<th>渲染器</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>render_classical</code></td>
<td>Surge / Loon / Shadowrocket / Stash 四端,<strong>逐字节相同</strong></td>
</tr>
<tr>
<td><code>render_classical_clash</code></td>
<td>同 classical,去掉 USER-AGENT</td>
</tr>
<tr>
<td><code>render_egern_yaml</code></td>
<td>Egern 自有 YAML schema</td>
</tr>
<tr>
<td><code>render_quantumultx</code></td>
<td>Quantumult X filter 行</td>
</tr>
</tbody>
</table>
<p>这套"数据与序列化分离"的价值在于:任何客户端格式变化,
只改一个 renderer,而不是 29 个 App × 7 个客户端的手工副本。</p>
<hr>
<h2 id="2-语义视图规则类型即-dns-语义">2. 语义视图:规则类型即 DNS 语义</h2>
<p>每个 App 默认输出三类视图,这是上一篇的 domain-first / IP-last 不变式的实现:</p>
<ul>
<li><code>&#x3C;App>-domainset.conf</code> — 纯域名段(<code>DOMAIN</code> / <code>DOMAIN-SUFFIX</code>),<strong>不触发本地 DNS</strong>;</li>
<li><code>&#x3C;App>-nonip.conf</code> — 非 IP 段(含 keyword / UA / process),<strong>不触发 DNS</strong>;</li>
<li><code>&#x3C;App>-ip.conf</code> — IP 段(<code>IP-CIDR</code> / <code>IP-CIDR6</code>),<strong>触发 DNS,必须置后</strong>。</li>
</ul>
<p>视图的一致性有专门门禁守护:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="text" data-theme="github-light github-dark"><code data-language="text" data-theme="github-light github-dark" style="display: grid;"><span data-line=""><span>python engine/scripts/validate_views.py --root .</span></span>
<span data-line=""><span># 断言:IP 不进 nonip、domain 不进 ip、纯域名 App 不产生空 ip 视图、</span></span>
<span data-line=""><span>#       七端齐全、文件头统计(含显式丢弃)正确</span></span></code></pre></figure>
<p>还有一个值得抄的细节:README 用大段 <code>[!WARNING]</code> 说明"文件后缀就是引用方式的答案"
(<code>-domainset.conf</code> 配 <code>DOMAIN-SET</code>、<code>-nonip.conf</code>/<code>-ip.conf</code> 配 <code>RULE-SET</code>、IP 段加
<code>no-resolve</code>)——写反后 Surge 不会报错,流量会<strong>静默落到 FINAL</strong>。
好的文档不只写"怎么做",更写"做错会怎样、如何自查"。</p>
<hr>
<h2 id="3-九道机器门禁把正确性变成断言">3. 九道机器门禁:把正确性变成断言</h2>
<p><code>engine/docs/MACHINE_GATES.md</code> 是整套工程承诺的机器化清单:</p>
<table>
<thead>
<tr>
<th>门禁</th>
<th>命令</th>
<th>断言</th>
</tr>
</thead>
<tbody>
<tr>
<td>单元与回归</td>
<td><code>python -m unittest discover -s engine/tests</code></td>
<td>Parser / renderer / Profile / 变化阈值 / 故障注入</td>
</tr>
<tr>
<td>七端等价性</td>
<td><code>parity_check.py --root . --strict</code></td>
<td>四端逐字节相同;Clash 去 UA;Egern/QX 去 PROCESS-NAME</td>
</tr>
<tr>
<td>产物健康度</td>
<td><code>health_check.py --root .</code></td>
<td>非空、合法、无重复、确定性排序、头统计正确</td>
</tr>
<tr>
<td>语义多视图一致</td>
<td><code>validate_views.py --root .</code></td>
<td>视图类型合法、与 canonical 拆分一致、七端齐全</td>
</tr>
<tr>
<td>产物溯源</td>
<td><code>verify_manifest.py --root .</code></td>
<td>29 App + 七端 + supplement + 构建器 SHA256 完整一致</td>
</tr>
<tr>
<td>Profile 完整性</td>
<td><code>verify_profiles.py --root .</code></td>
<td>七端配置可由 intent/templates <strong>逐字节重建</strong></td>
</tr>
<tr>
<td>跨 App overlap</td>
<td><code>overlap_check.py --root .</code></td>
<td>相对人工基线不得出现<strong>新</strong>重叠</td>
</tr>
<tr>
<td>敏感模式</td>
<td><code>secret_scan.py --root .</code></td>
<td>PAT / 私钥 / 代理 URI / 订阅 URL / 本地绝对路径零容忍</td>
</tr>
<tr>
<td>实时重建 drift</td>
<td><code>build.py --verify-only --strict-diff</code></td>
<td>重抓上游并逐字节比对 203 个产物</td>
</tr>
</tbody>
</table>
<p>设计上有几个聪明的取舍:</p>
<ol>
<li><strong>manifest 不含时间戳、commit、本地路径</strong>——只存内容指纹与统计。
同一输入必须产生逐字节相同的 manifest,"它昨天是对的"才可被验证。</li>
<li><strong>普通 push/PR 只跑离线门禁</strong>;实时重建 drift 留给发布前/排障,
避免第三方瞬时网络变成所有 PR 的随机失败。</li>
<li><strong>不缓存上游文本</strong>:发布构建仍实时读取,防止缓存掩盖上游真实变化。</li>
</ol>
<hr>
<h2 id="4-供应链变化门禁变化必须被看见">4. 供应链变化门禁:变化必须被看见</h2>
<p>规则仓库最大的风险不是"写错",而是<strong>上游悄悄变了</strong>。Blink 的做法:</p>
<ul>
<li>写入前,把实时编译的 canonical 规则与已提交产物做集合比较;</li>
<li>默认阻断条件(任一项成立):
<ul>
<li>新增/删除规则数超过 <strong>20</strong>;</li>
<li>语义变化比例超过 <strong>20%</strong>;</li>
<li>出现此前不存在的<strong>新规则类型</strong>;</li>
</ul>
</li>
<li>失败报告给出 <code>+N/-N</code>、变化比例、新类型和最多五条增删样例;</li>
<li>人工审阅后显式放行:</li>
</ul>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="text" data-theme="github-light github-dark"><code data-language="text" data-theme="github-light github-dark" style="display: grid;"><span data-line=""><span>python engine/scripts/build.py --write --accept-large-change</span></span></code></pre></figure>
<p>注意 <code>--accept-large-change</code> <strong>不关闭</strong>解析、renderer、parity、health、checksum
校验——它只跳过已经人工审阅的变化量阈值。<strong>"确认过的变化"和"没看过的变化"是两种东西。</strong></p>
<p>写入保持原子性:所有 App 成功才更新产物。上游 404 / 超时 = 构建失败,
<strong>保留旧产物并暂停更新</strong>,而不是用缓存静默生成。</p>
<hr>
<h2 id="5-配置层意图驱动--可复现">5. 配置层:意图驱动 + 可复现</h2>
<p>配置层没有手写七份配置,而是:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="text" data-theme="github-light github-dark"><code data-language="text" data-theme="github-light github-dark" style="display: grid;"><span data-line=""><span>engine/sources/profile/intent.yaml   ← 语义意图(策略组/规则引用/能力声明)</span></span>
<span data-line=""><span>        ↓ 模板渲染</span></span>
<span data-line=""><span>Profiles/{Surge,Loon,Stash,Clash,Egern,Shadowrocket,QuantumultX}/...</span></span></code></pre></figure>
<ul>
<li>单一订阅池组织,占位符内置——替换一条订阅 URL 即可复用;</li>
<li>能力映射只允许 <code>FULL</code> / <code>ADAPTED(注释)</code> / <code>UNSUPPORTED(注释)</code> 三态,
<strong>禁止静默删除或伪造</strong>;</li>
<li><code>verify_profiles.py</code> 断言七端配置可以从 intent + templates <strong>逐字节重建</strong>。</li>
</ul>
<p>配置是"意图",所以它不随规则每日更新:规则层自动,配置层人工确认,
自动化边界清晰到不会互相越权。</p>
<hr>
<h2 id="6-分发与门户门户也是数据">6. 分发与门户:门户也是数据</h2>
<p><code>engine/portal/</code> 是一个 React + Vite 的门户,但它<strong>不是手写的内容站</strong>:</p>
<ul>
<li>页面数据从生成产物 + 源清单<strong>派生</strong>(<code>gen_portal_stats.py</code> → <code>stats.json</code>);</li>
<li>无时间戳、随产物变化才变化——门户与规则集共享同一事实源,不存在"文案过期"。</li>
</ul>
<p>接入体验上也下了功夫:每个 App 给一个"完整 .list",想要语义分段再给三视图;
README 顶部就是七客户端的 quickstart 表格和官方引用片段。
<strong>降低接入成本本身也是产品的一部分。</strong></p>
<hr>
<h2 id="7-合规意识个人项目也可以有边界">7. 合规意识:个人项目也可以有边界</h2>
<ul>
<li><code>DISCLAIMER.md</code>:仅供学习研究,使用前阅读;</li>
<li><code>THIRD_PARTY_NOTICES.md</code>:上游来源与许可清单合规;</li>
<li>明确"禁止转载或发布至国内平台";</li>
<li>仓库扫描零敏感信息(这本身就是 secret_scan 门禁的产物)。</li>
</ul>
<p>一个代理规则仓库,上游归属、许可、使用边界写得清清楚楚——这比多数开源项目
都"成熟"。合规不是大厂的专利,是工程素养。</p>
<hr>
<h2 id="8-可以带走的五个工程思维">8. 可以带走的五个工程思维</h2>
<ol>
<li><strong>语义单一事实源,序列化只是下游。</strong> 七种客户端的差异被隔离在 renderer 层,
任何一个都不值得成为"再改一份"的借口。</li>
<li><strong>"正确"必须能被机器复述。</strong> 九道门禁全是可执行命令;文档描述的是承诺,
命令执行的是承诺。</li>
<li><strong>显式大于静默。</strong> 降级要计数、不可能的状态要失败、写错引用方式要大声警告。</li>
<li><strong>变化要审计,而不是感觉。</strong> 阈值阻断 + 人工显式放行;已知重叠固化基线,
新重叠自动阻断——"未知的冗余和已知的冗余是两种风险"。</li>
<li><strong>边界清晰的管线。</strong> 规则层自动、配置层人工、门户派生。每一层只有一种职责,
自动化不会偷偷越权。</li>
</ol>
<blockquote>
<p>最后照例感谢 SukkaW——它的博客与 <a href="https://github.com/SukkaW/Surge">SukkaW/Surge</a>
构建管线是 Blink 设计的思想源头。规则驱动的数据工程,值得每一份个人仓库学习。</p>
</blockquote>]]></content:encoded>
      <category>技术向</category>
      <category>项目</category>
    </item>
    <item>
      <title>这个博客是怎么搭起来的</title>
      <link>https://yoshen.me/posts/blog-engineering-practice</link>
      <guid isPermaLink="true">https://yoshen.me/posts/blog-engineering-practice</guid>
      <pubDate>Sun, 23 Aug 2026 00:00:00 GMT</pubDate>
      <description>记录 Yoshen&apos;s Blog 从内容规划、自有域名落地上线,到双通道部署与全自动收录的决策:内容管道先行、单一事实源、构建期校验、门禁体系、静态导出、主题动效、分享卡与平台预览,以及一批真实踩坑。随项目状态持续修订。</description>
      <content:encoded><![CDATA[<blockquote>
<p>这篇文章写的是 yoshen.me 的技术骨架:一个 Next.js 16 纯静态博客,
没有评论、数据库、管理后台和追踪脚本——写 Markdown,push,一分钟内上线并被搜索引擎收录。
只讲决策与事实,不讲 Next.js 教程。</p>
</blockquote>
<h2 id="现状">现状</h2>
<ul>
<li>内容:4 篇文章、3 个标签,影音、作品集、Now、吉祥物园等板块全部由数据文件驱动;</li>
<li>形态:纯静态导出(<code>output: "export"</code>),线上零服务端进程;</li>
<li>部署:push 后 GitHub Actions 跑门禁;<strong>主站</strong>(AWS Lightsail + Caddy)另跑「一键发布」更新,
<strong>备线</strong> Vercel 自动部署;同一产物同时上传 artifact,可手动托管到
EdgeOne Pages / CloudBase(不向托管平台授予仓库权限);</li>
<li>收录:sitemap(17 个 URL)/ robots / RSS 由内容自动生成,Google 已验证并提交,
Bing 验证文件就位;</li>
<li>仓库私有,源码只在本地与 GitHub 之间流动。</li>
</ul>
<h2 id="一内容管道先行">一、内容管道先行</h2>
<p>第一行业务代码不是页面,是 <code>src/lib/content.ts</code>:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="ts" data-theme="github-light github-dark"><code data-language="ts" data-theme="github-light github-dark" style="display: grid;"><span data-line=""><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 内容目录:生产固定 content/posts;测试可通过 BLOG_CONTENT_DIR 注入 fixture</span></span>
<span data-line=""><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">function</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> postsDir</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span data-line=""><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">  return</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> process.env.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">BLOG_CONTENT_DIR</span></span>
<span data-line=""><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    ?</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">resolve</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(process.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">cwd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(), process.env.</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">BLOG_CONTENT_DIR</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span></span>
<span data-line=""><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">    :</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> path.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(process.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">cwd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(), </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"content"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"posts"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span data-line=""><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre></figure>
<p>它把 <code>content/posts/*.md</code> 变成四件套:<code>getAllPosts</code> / <code>getPostBySlug</code> /
<code>getAllTags</code> / <code>getPostsByTag</code>,页面只是这层 API 的消费者。</p>
<p>先让「写一篇 Markdown → 出现在列表」跑通,再画页面。数据结构由内容决定:
标签体系决定聚合逻辑,种子文章的类型(长文 / 短文 / 代码 / 表格)决定渲染器要
覆盖什么。先写页面再补内容,必然返工。</p>
<h2 id="二单一事实源">二、单一事实源</h2>
<p>站点名、tagline、简介、URL、导航收在 <code>siteConfig</code> 一个对象里,其余全部派生。</p>
<p>收益兑现过两次:域名换过两回(从 Vercel 占位域名到自有域名 <code>wendylala.com</code>,
再于 2026-09 迁到 <code>yoshen.me</code>),每次只改一行 <code>url</code>,
canonical、sitemap、robots、RSS 全部自动修正;简介文案迭代多轮(从「热爱学习」
到英文角色标签),同样只改一处。凡是「同一个值出现在多处」,一律抽成单一来源。</p>
<h2 id="三构建期校验坏数据死在构建时">三、构建期校验:坏数据死在构建时</h2>
<p>frontmatter 缺 <code>title</code> / <code>date</code> / 标签为空,构建直接抛错并指出文件:</p>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="text" data-theme="github-light github-dark"><code data-language="text" data-theme="github-light github-dark" style="display: grid;"><span data-line=""><span>[content] content/posts/tmp-invalid-no-title.md: missing or invalid required field "title"</span></span></code></pre></figure>
<p>作品集、影音数据同一哲学:<strong>实体不合法 = 构建失败</strong>,而不是上线后静默空白。</p>
<h2 id="四门禁体系让机器守住质量">四、门禁体系:让机器守住质量</h2>
<figure data-rehype-pretty-code-figure=""><pre tabindex="0" data-language="json" data-theme="github-light github-dark"><code data-language="json" data-theme="github-light github-dark" style="display: grid;"><span data-line=""><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span data-line=""><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "build"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"tsc --noEmit &#x26;&#x26; next build"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span data-line=""><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "typecheck"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"tsc --noEmit"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span data-line=""><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "lint"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"eslint"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span data-line=""><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">  "test"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"node --test --experimental-strip-types --test-isolation=none </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">src/lib/content.test.ts</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">\"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">"</span></span>
<span data-line=""><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}</span></span></code></pre></figure>
<p>四道命令,任何一道红不许上线;<code>node:test</code> 8 个用例覆盖排序、draft 过滤、
<code>_</code> 前缀忽略、标签聚合、校验报错;<code>CHANGELOG.md</code> 记录构建耗时基线。</p>
<p>门禁两次抓到真实问题:第一次是 React 19 新 lint 规则 <code>set-state-in-effect</code>
(改用 <code>useSyncExternalStore</code> 订阅外部状态);第二次是 About 页 JSX 未转义引号
触发 <code>react/no-unescaped-entities</code>,CI 9 秒红,修成全角引号后转绿。
两次都说明同一件事:<strong>门禁真的在工作</strong>。</p>
<h2 id="五主题与动效">五、主题与动效</h2>
<ul>
<li><strong>主题链</strong>:<code>&#x3C;head></code> 内联脚本在渲染前读 localStorage / 系统偏好打 <code>dark</code> class
(防闪白)→ CSS class 策略 → 资产跟随(吉祥物两套配色交叉淡入、favicon 双版 SVG);</li>
<li><strong>动效语言</strong>:一条缓动 <code>cubic-bezier(0.22, 1, 0.36, 1)</code>,几个基元
(<code>fade-up</code> / <code>fade-in</code> / <code>fade-down</code> / <code>pop-in</code>);位移缩放全走 transform,
颜色走 transition;<code>prefers-reduced-motion</code> 全局降级为瞬时;</li>
<li><strong>时长纪律</strong>:60fps 下流畅区间为 300–600ms(18–36 帧)。整页开场拉链幕取
0.65s ≈ 39 帧——再短像闪一下,再长就拖;</li>
<li><strong>第三方动效取舍</strong>:开场拉链幕、Hero 的「猫看花」小剧场移植自
yui540/css-animations(MIT)。选择标准:纯 CSS、能随主题色、不喧宾夺主、
尊重减动效偏好。现成的效果不值得自绘,但要在代码里署名来源;</li>
<li><strong>首页取舍</strong>:曾放「精选项目」区块,后移除——首页只留核心内容
(问候 → Now → 文章 → 影音),项目由导航独立承载。</li>
</ul>
<h2 id="六seo-与分享卡">六、SEO 与分享卡</h2>
<ul>
<li>sitemap(17 个 URL)全部由内容生成,手工零参与;Google Search Console 用
网域属性 + DNS TXT 验证(零代码),Bing 用 <code>BingSiteAuth.xml</code>;过时的
<code>google*.html</code> 验证文件事后清理,验证状态不受影响;</li>
<li><strong>OG 卡</strong>:最终形态是构建期生成的 1200×630 静态 PNG(灰度统一风格,
<code>scripts/gen-og.mts</code>),绝对 URL + 正确 MIME。演进过三轮:静态 PNG(修 MIME)
→ 时间段轮换变体套装 → 变体占用构建被移除 → 统一单卡;</li>
<li><strong>平台预览差异</strong>:Telegram 按 URL 强缓存、无部分回退(差一项素材整卡不显示)、
会「记住」旧的失败状态;iMessage 每次现抓、更宽松。调试统一用
<code>curl -A "TelegramBot (like TwitterBot)"</code> 看服务器实际返回,发布后用
@WebpageBot 强制重爬。</li>
</ul>
<h2 id="七踩坑精选">七、踩坑(精选)</h2>
<table>
<thead>
<tr>
<th>坑</th>
<th>解法 / 结论</th>
</tr>
</thead>
<tbody>
<tr>
<td>受限环境不能 fork 子进程,Next 构建报 EPERM</td>
<td><code>experimental.workerThreads</code> 走 worker 线程 + 类型检查外置 + dev 入口进程内启动,不依赖 spawn</td>
</tr>
<tr>
<td>Turbopack dev 中文参数不解码</td>
<td>入口安全解码;生产为静态页面,不受影响</td>
</tr>
<tr>
<td>删路由后 typegen 残留</td>
<td>清 <code>.next</code>;少依赖生成物,props 用显式类型</td>
</tr>
<tr>
<td>OG 图无扩展名/平台拒收</td>
<td>静态文件名带 <code>.png</code> 约定 + 确认返回 MIME</td>
</tr>
<tr>
<td>Vercel 默认不跳 www</td>
<td>Domains → Redirect 手动设置跳 apex</td>
</tr>
<tr>
<td>换域名后旧 URL 残留</td>
<td>全仓库搜索旧域名:数据、代码示例、文档一处不漏</td>
</tr>
<tr>
<td>仓库私有化</td>
<td>Vercel 走 GitHub App 授权,与公开/私有无关</td>
</tr>
</tbody>
</table>
<h2 id="八可带走的清单">八、可带走的清单</h2>
<ol>
<li>内容先于代码,数据结构由内容决定;</li>
<li>一个值只写一次,单点修改、全局派生;</li>
<li>坏数据死在构建时,并指出文件;</li>
<li>门禁先于上线——它会抓到真实 bug 来证明自己;</li>
<li>动效克制而一致,时长按帧数算,尊重减动效偏好;</li>
<li>纯静态做到头:构建期做校验、派生、分享卡,线上零服务端;</li>
<li>平台预览都有缓存,上线前先当 bot 自测;</li>
<li>发布成本趋零之后,写作本身就是产品。</li>
</ol>]]></content:encoded>
      <category>技术向</category>
      <category>项目</category>
    </item>
  </channel>
</rss>