<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://li8.me/</id>
  <title type="text">二次元宅男录</title>
  <subtitle type="text">记录技术与生活</subtitle>
  <updated>2026-10-31T00:00:00.000Z</updated>
  <author><name>八奈奈</name></author>
  <link rel="alternate" href="https://li8.me/"/>
  <link rel="self" href="https://li8.me/atom.xml"/>
  <generator uri="https://github.com/CuteLeaf/Firefly">Firefly v1.0.0</generator>
    <entry>
      <id>https://li8.me/posts/10/</id>
      <title type="text">斗破127集夯爆了</title>
      <published>2026-10-31T14:25:00.000Z</published>
      <updated>2026-10-31T00:00:00.000Z</updated>
      <author><name>八奈奈</name></author>
      <link rel="alternate" href="https://li8.me/posts/10/"/>
      <summary type="text"></summary>
      <content type="html"><![CDATA[<p>无论是感情戏还是打斗群像都是夯爆了。经费炸裂，斗破官方你的钱花不完了是吧？给我爽到了。NB这样就让我很期待双帝之战了。这集已经没有什么可挑的，爽爆了。</p>
<p></p><figure><img src="/images/u/20261031-224501-e8dae7.jpg.jpg" alt="1275" /><figcaption>1275</figcaption></figure><p></p>
<p></p><figure><img src="/images/u/20261031-224505-2ebc97.png" alt="1268" /><figcaption>1268</figcaption></figure><p></p>
<p></p><figure><img src="/images/u/20261031-224511-8d59e0.png" alt="1267" /><figcaption>1267</figcaption></figure><p></p>]]></content>
    </entry>
    <entry>
      <id>https://li8.me/posts/9/</id>
      <title type="text">文章目录滑不动？锅在 flex，账在我头上</title>
      <published>2026-10-11T12:30:00.000Z</published>
      <updated>2026-10-11T12:30:00.000Z</updated>
      <author><name>八奈奈</name></author>
      <link rel="alternate" href="https://li8.me/posts/9/"/>
      <summary type="text">从被 flex 默认值压扁的侧栏卡片，到抢方向盘的自动跟随，再到藏在每个读接口里的网络往返——一次目录修复加后台提速的联合记录</summary>
      <content type="html"><![CDATA[<p>这周又攒了两个问题一起修：一个是 PC 端文章页的目录滑不动，一个是后台的内容列表慢半拍。两件事八竿子打不着，但排查完发现它们的剧本出奇地像——表面症状和真正的病灶，隔了好几层。老规矩，把过程记下来，给下次的自己抄作业。</p>
<section><h2>一、目录滑不动：先给滚动条洗清冤屈<a href="#一目录滑不动先给滚动条洗清冤屈"><span>#</span></a></h2><p>现象很朴素：文章长了以后，PC 端右侧的目录显示不全，底部直接被裁掉，滚轮放上去毫无反应。移动端没事，就 PC 有。</p><p>我的第一反应是滚动条样式的问题——毕竟这个站的滚动条定制过，透明度调得很低，第一眼确实看不见。改了半天透明度，滚动条是看得见了，但目录还是滚不动。看得见摸不着，那就不是样式的问题。</p><p>回头把侧栏的布局翻出来看，才找到真凶。侧栏是个纵向 flex 容器，外面套着 <code>max-height</code> 和 <code>overflow-y: auto</code>，意图很直白：最多这么高，装不下就内部滚动。但 flex 有个不太直觉的默认行为——<strong>子元素默认 <code>flex-shrink: 1</code>，容器装不下的时候，所有子元素会按比例被压扁</strong>。</p><p>也就是说：目录卡片、作者卡片这些加起来比视口高的时候，flex 先做的不是触发外层滚动，而是把每个卡片均匀压扁。目录内容被裁掉一截之后，卡片内部的高度刚好没超出——没超出，自然就没得滚，滚动条委屈地认为「我没有活干」。</p><p>修复反而简单得可笑：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/* 子卡片禁止收缩，装不下就让侧栏整体滚 */</span></div></div><div><div><div>2</div></div><div><span>#left-sidebar-sticky</span><span> </span><span>&gt;</span><span> </span><span>*</span><span>,</span></div></div><div><div><div>3</div></div><div><span>#right-sidebar-sticky</span><span> </span><span>&gt;</span><span> </span><span>*</span><span> {</span></div></div><div><div><div>4</div></div><div><span><span>  </span></span><span>flex-shrink: </span><span>0</span><span>;</span></div></div><div><div><div>5</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>一行 CSS，目录恢复滑动。写这段布局的时候，我大概默认了「装不下就会滚动」，但 flex 的收缩优先级排在溢出前面。这个默认值，坑过的大概不止我一个。</p></section>
<section><h2>二、修完压缩，又撞上「弹回去」<a href="#二修完压缩又撞上弹回去"><span>#</span></a></h2><p>以为完事了，实测时冒出了更迷惑的现象：我把目录往下滚到想看的小节，松手不到一秒，它自己弹回去了。</p><p>这个就不是 CSS 了，是交互逻辑打架。目录有个「自动跟随」功能——页面滚到哪个小节，目录就把对应条目滚到可视区中间，之前写的，一直工作得挺好。问题在于它太尽职：我手动滚动目录这个动作，在它眼里和「用户翻了页面」没区别，下一帧就坚持把活动项拉回中间。我的手和它的逻辑，只能活一个。</p><p>解法是给「用户的手」加发言权：在目录上监听 <code>wheel</code> 和 <code>touchmove</code>，一旦检测到手动滚动就打个时间戳，四秒之内自动跟随闭嘴：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span><span>private</span><span> </span><span>userScrollingUntil</span><span> </span></span><span>=</span><span> </span><span>0</span><span>;</span></div></div><div><div><div>2</div></div><div><span><span>private</span><span> </span><span>readonly</span><span> </span></span><span>markUserScrolling</span><span> </span><span>=</span><span> () </span><span>=&gt;</span><span> {</span></div></div><div><div><div>3</div></div><div><span>  </span><span>this</span><span>.</span><span>userScrollingUntil</span><span> </span><span>=</span><span><span> </span><span>Date</span><span>.</span></span><span>now</span><span>() </span><span>+</span><span> </span><span>4000</span><span>;</span></div></div><div><div><div>4</div></div><div><span>};</span></div></div><div><div><div>5</div></div><div>
</div></div><div><div><div>6</div></div><div><span>// scrollToActiveItem 开头：</span></div></div><div><div><div>7</div></div><div><span>if</span><span><span> (</span><span>Date</span><span>.</span></span><span>now</span><span>() </span><span>&lt;</span><span> </span><span>this</span><span>.</span><span>userScrollingUntil</span><span>) </span><span>return</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>四秒是试出来的。太短的话页面稍微一滚它又开始抢方向盘，太长的话点到新小节时目录跟不上趟。</p><p>顺手还对了另外两笔账：侧栏的 <code>top</code> 和 <code>max-height</code> 之前是各写各的，底部会超出视口几十像素，这次对齐了；滚动条透明度也从「隐身模式」调回到肉眼可见——一个用户看不见的滚动条等于没有滚动条，这是我第一节白忙活半小时换来的觉悟。</p></section>
<section><h2>三、后台慢半拍：接口在替 git 排队<a href="#三后台慢半拍接口在替-git-排队"><span>#</span></a></h2><p>第二个问题是后台的内容列表越用越慢，刷新一次要等好几秒，网络不好的时候直接超时。</p><p>排查时先看接口本身——查询逻辑没什么毛病，本地数据读取快得很。但每个读接口在干活之前都先干了一件事：<strong>同步一次内容仓库</strong>。当初的思路是「读之前先拉最新，保证内容新鲜」，听起来很负责，代价是每次请求都背一次网络往返。列表页一打开，好几个接口排着队跑网络。同步快的时候几百毫秒，慢的时候——就是你看到的转圈圈。</p><p>修复思路一句话：<strong>把慢的东西从关键路径上挪走</strong>。仓库同步不再跟着请求走，改成后台线程自己定期做，同步加硬超时，防止哪次网络抽风把线程挂死在一个请求上：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>def</span><span> </span><span>_repo_sync_loop</span><span>(</span><span>interval</span><span>=</span><span>45</span><span>):</span></div></div><div><div><div>2</div></div><div><span>    </span><span>while</span><span> </span><span>True</span><span>:</span></div></div><div><div><div>3</div></div><div><span>        </span><span>try</span><span>:</span></div></div><div><div><div>4</div></div><div><span><span>            </span></span><span>gitops.</span><span>pull</span><span>()</span></div></div><div><div><div>5</div></div><div><span>        </span><span>except</span><span> </span><span>Exception</span><span>:</span></div></div><div><div><div>6</div></div><div><span>            </span><span>pass</span><span>  </span><span># 这轮失败了下轮再来，别影响主流程</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>time.</span><span>sleep</span><span>(interval)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>同步周期 45 秒。对个人后台来说，「内容最多延迟一分钟」和「每次打开都等几秒」之间，根本不用犹豫。改完之后列表页的响应从秒级等待回到毫秒级，终于像个本地工具该有的样子。</p></section>
<section><h2>写在最后<a href="#写在最后"><span>#</span></a></h2><p>两件事修完，发现教训居然是同一句话：</p><blockquote><p>症状出现在哪里，病灶不一定在哪里。看得见的问题（滚不动）可能藏在看不见的布局默认值里；等得起的耗时（内容同步）不该出现在等不起的地方（接口路径上）。</p></blockquote><p>再补一条给写 CSS 的自己：flex 的默认值不会「大概按我想的来」。<code>flex-shrink</code> 这个属性我看了很多年，第一次真正理解它，是在自己博客的目录滚不动的那天。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://li8.me/posts/8/</id>
      <title type="text">我给博客换了链接，然后它挂了一早上</title>
      <published>2026-10-01T08:30:00.000Z</published>
      <updated>2026-10-01T08:30:00.000Z</updated>
      <author><name>八奈奈</name></author>
      <link rel="alternate" href="https://li8.me/posts/8/"/>
      <summary type="text">从乱码查询参数到数字编号和拼音 slug——一次链接结构翻新的完整记录，以及凌晨自动更新把 nginx 弄宕机九小时的复盘与三层加固</summary>
      <content type="html"><![CDATA[<p>这周折腾了两件事，本来不想写成文章，但第二件事实在太有戏剧性——我把博客的链接全部翻新了一遍，第二天凌晨它就挂了九个小时。两件事还挺有因果关系的味道，一起记下来。</p>
<section><h2>一、链接翻新：从「能访问」到「有规律」<a href="#一链接翻新从能访问到有规律"><span>#</span></a></h2><section><h3>原来的链接有多随意<a href="#原来的链接有多随意"><span>#</span></a></h3><p>先自曝家丑。这个站原来的链接长这样：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>文章：/posts/how-i-built-this-blog/</span></div></div><div><div><div>2</div></div><div><span>标签：/archive/?tag=%E9%9A%8F%E7%AC%94</span></div></div><div><div><div>3</div></div><div><span>分类：/archive/?category=%E6%8A%80%E6%9C%AF</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>文章 slug 是标题转的英文，长短随缘；标签和分类点开更离谱——<code>%E9%9A%8F%E7%AC%94</code> 这是「随笔」的 URL 编码，一串百分号乱码，别说访客看不懂，搜索引擎对这种查询参数式链接也不太待见：它本质上是归档页的过滤参数，不是独立页面，没有自己的权重。</p></section><section><h3>文章改数字编号<a href="#文章改数字编号"><span>#</span></a></h3><p>第一个决定：文章 slug 全部改成纯数字，<code>/posts/1/</code>、<code>/posts/2/</code>，按发布顺序排。理由很简单——短、整齐、永远不重名。缺点是链接没了语义，但对个人博客来说，标题在列表和搜索结果里都有展示，URL 里那点语义没那么值钱。</p><p>改的时候给自己挖了个坑：旧链接全部 301 重定向到新地址，这个做了；但<strong>文章正文里的互相引用</strong>忘了改——三篇技术文互相链接用的还是旧 slug，全站扫描死链的时候才暴露出来。顺手把关于页里一个指向不存在的 <code>/posts/</code> 的入口也修成了归档页。</p><p>教训：改 URL 结构，扫描器跑一遍全站比肉眼靠谱得多。全站 1928 个站内链接，人工根本看不过来。</p></section><section><h3>分类标签建独立页面<a href="#分类标签建独立页面"><span>#</span></a></h3><p>这是更大的一块。原主题根本没有分类详情页，所谓「点标签」就是跳归档页带个过滤参数。这次直接新建了两类静态页面：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>分类：/categories/ji-shu/    （技术）</span></div></div><div><div><div>2</div></div><div><span>标签：/tags/sui-bi/          （随笔）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>slug 的规则：<strong>中文转全拼小写，英文原样，短横线分隔</strong>。看名字就能拼出 URL，以后打新标签也自动按这个规则生成页面，规律是死的，不会因为手滑打出新格式。</p><p>实现上用的是 pinyin-pro（主题本来就带了）在构建期转换，页面静态生成、自动进 sitemap，每个分类和标签从此都有了自己可收录的落地页。</p></section><section><h3>顺手把图压了<a href="#顺手把图压了"><span>#</span></a></h3><p>审计图片的时候发现相册里有四张原图是原样发布的：一张 1.5MB 的 PNG、两张快 1MB 的 JPG。全部转成 WebP（质量 85，肉眼无感），整个相册从 4.2MB 降到 1.76MB，减了 58%。</p></section></section>
<section><h2>二、宕机九小时：一次教科书级的连锁反应<a href="#二宕机九小时一次教科书级的连锁反应"><span>#</span></a></h2><p>链接翻新的第二天凌晨 06&lt;44&gt;，博客挂了。而且不是慢，是完全打不开，一直挂到下午。</p><section><h3>时间线还原<a href="#时间线还原"><span>#</span></a></h3><p>翻了系统日志，链条其实很短：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>06:44:05  系统自动安全更新（unattended-upgrade）启动，升级 openssl</span></div></div><div><div><div>2</div></div><div><span>06:44:18  openssl 是 nginx 的依赖库 → 触发 nginx 自动重启</span></div></div><div><div><div>3</div></div><div><span>06:44:18  重启瞬间 DNS 抖动：配置里的封面代理域名 t.alcy.cc 解析失败</span></div></div><div><div><div>4</div></div><div><span>06:44:18  nginx -t 报错：host not found in upstream → 启动失败</span></div></div><div><div><div>5</div></div><div><span>（然后呢？然后就没有然后了）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>两个问题叠加成了九小时的宕机：</p><p><strong>第一，nginx 启动时要解析配置里所有的 upstream 域名。</strong> 平时这不是问题，但「启动的那一刻 DNS 抖了一下」就够了——DNS 恢复后配置完全正常，可 nginx 已经躺在那里不会自己爬起来。</p><p><strong>第二，nginx 的 systemd 重启策略是 <code>Restart=no</code>。</strong> Ubuntu 的默认值。失败不重试，一次失败就是永久失败。</p><p>最讽刺的是：我明明有每分钟跑的监控脚本、有每 5 分钟的部署兜底任务，但没有任何一个东西负责「发现 nginx 挂了就把它拉起来」。监控和数据都在，就差最后一步。</p></section><section><h3>三层修复<a href="#三层修复"><span>#</span></a></h3><p>修的时候就没打算只修「这一次」：</p><p><strong>第一层，根治启动依赖。</strong> 封面代理的 upstream 改成「变量 + resolver」：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>resolver </span><span>223.5.5.5</span><span> </span><span>119.29.29.29</span><span> valid=300s ipv6=off;</span></div></div><div><div><div>2</div></div><div><span>set </span><span>$</span><span>alcy_upstream</span><span> </span><span>"t.alcy.cc"</span><span>;</span></div></div><div><div><div>3</div></div><div><span>rewrite</span><span> </span><span>^/cover-proxy/alcy/(.*)$</span><span> /$</span><span>1</span><span> </span><span>break</span><span>;</span></div></div><div><div><div>4</div></div><div><span>proxy_pass </span><span>https://$</span><span>alcy_upstream</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用变量做 upstream 时，域名解析推迟到运行时——启动时不再碰 DNS。以后 DNS 再抖，最坏也就是那一次封面图请求 502，nginx 本体稳如泰山。有个小坑：<code>proxy_pass</code> 带变量时不会自动剥离 location 前缀，必须手动 <code>rewrite</code> 补上，不然转发路径就是错的。</p><p><strong>第二层，自动拉起。</strong> 给 nginx 的 systemd 加了 <code>Restart=on-failure</code>，挂掉 5 秒后自动重试。就算再来一次同样的故障，DNS 恢复的几秒内它就自己站起来了。</p><p><strong>第三层，看门狗。</strong> 监控脚本旁边加了一个每分钟跑的看门狗：nginx 或后台服务不是 active 就直接拉起，写日志。它不聪明，但足够愣——专治各种「以为它会自己好」。</p></section><section><h3>零损失的安慰<a href="#零损失的安慰"><span>#</span></a></h3><p>复盘时最庆幸的一点：宕机的九个小时里没有发布任何内容，最后一次部署停在宕机前一晚，所有数据完好。而且部署兜底任务走的是 cron 直连，根本不经过 nginx——就算那九小时里我发了文章，内容也会照常上线，只是没人看得到而已。</p></section></section>
<section><h2>写在最后<a href="#写在最后"><span>#</span></a></h2><p>这次折腾完，两件原本不相干的事最后落在了同一个认知上：</p><blockquote><p>「它能跑」和「它挂了我能立刻知道、立刻站起来」是两码事。前者叫功能，后者才叫运维。</p></blockquote><p>链接是给人和搜索引擎看的，规律比个性重要；服务是给凌晨四点的自动更新折腾的，兜底比侥幸重要。文章里如果有你也用得上的东西，欢迎翻翻这个系列的其他几篇——现在它们都在 <code>/posts/1/</code> 到 <code>/posts/6/</code> 里，链接干净得很。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://li8.me/posts/7/</id>
      <title type="text">博客正式上线了</title>
      <published>2026-09-28T04:30:00.000Z</published>
      <updated>2026-09-28T04:30:00.000Z</updated>
      <author><name>八奈奈</name></author>
      <link rel="alternate" href="https://li8.me/posts/7/"/>
      <summary type="text"></summary>
      <content type="html"><![CDATA[<p>🎉域名ICP备案总算是顺利通过啦!我的小站终于可以正式和大家见面咯～ 已经把备案等待期间写的所有文章都迁移过来啦，其实从提交备案申请那天开始，我就已经动笔攒内容了，熬到今天备案落地、完成全站迁移，说起来今天是正式开站的日子，但其实站子早在备案第一天就悄悄搭好骨架咯😂😂😂</p>]]></content>
    </entry>
    <entry>
      <id>https://li8.me/posts/6/</id>
      <title type="text">这或许是最有性价比，最便捷的4k掌上副屏方案—索尼xzp</title>
      <published>2026-09-24T16:14:00.000Z</published>
      <updated>2026-09-25T00:00:00.000Z</updated>
      <author><name>八奈奈</name></author>
      <link rel="alternate" href="https://li8.me/posts/6/"/>
      <summary type="text"></summary>
      <content type="html"><![CDATA[<section><h1>效果图<a href="#效果图"><span>#</span></a></h1><p>国际惯例先来效果图</p><p></p><figure><img src="/images/u/20260925-002358-e93af0.jpg.jpg" alt="xzp" /><figcaption>xzp</figcaption></figure><p></p></section>
<section><h1>把我的4K老手机折腾成电脑副屏<a href="#把我的4k老手机折腾成电脑副屏"><span>#</span></a></h1><section><h2>折腾起因：吃灰多年的日版神机<a href="#折腾起因吃灰多年的日版神机"><span>#</span></a></h2><p>我的日版手机自从<strong>百来块</strong>淘来之后，就一直在吃灰。4+64的存储配上骁龙835，<strong>根本带不动日常应用</strong>，更带不动这块4K屏幕。而且因为日版<strong>不能解锁</strong>，还无法全局开启4K。</p></section><section><h2>转折：论坛发现可解锁<a href="#转折论坛发现可解锁"><span>#</span></a></h2><p>但是逛了逛论坛，发现<strong>居然可以解锁了</strong>！解锁之后就刷EVO系统开全局4K。不过日版刷了会<strong>直接掉基带</strong>，只能刷回原版，没办法的事。</p><p>这配置应该也没人当主力机用了，我干脆把包里的<strong>谷歌全家桶、打电话拨号</strong>这些占用的东西全部删除，系统分区只剩<strong>1个多G</strong>，但还是很卡。</p><p>我也尝试过第三方开源开发的应用，虽然有全局4K，但<strong>手机性能还是带不动</strong>——看一会儿B站视频就卡、发热非常严重。</p></section><section><h2>灵机一动：让电脑性能来带它<a href="#灵机一动让电脑性能来带它"><span>#</span></a></h2><p>既然手机性能带不动，那就<strong>拿电脑性能带它</strong>，当电脑副屏！毕竟这货可是<strong>实打实的4K</strong>呀。</p><p>我自己做了个模块配合 Spacedesk 使用，模块的作用大致是：</p><ul>
<li>走 USB 连接</li>
<li>开机插线自动启动、自动连接显示</li>
<li>保证连接使用过程中屏幕不息屏</li>
</ul><blockquote><p>其实做模块的话，理论上可以实现不依靠第三方软件，直接当正常的显示屏连接。但是有点麻烦，所以<strong>怎么方便怎么来</strong>。</p></blockquote></section><section><h2>使用感受：很爽，但有两个缺陷<a href="#使用感受很爽但有两个缺陷"><span>#</span></a></h2><ol>
<li><strong>电池容量太小</strong>，只能插线（不过电池容量大的话也只能插线，详情看第2点）</li>
<li><strong>屏幕最高只有60Hz</strong>，还不能跑满，平均只能跑到50多</li>
</ol></section><section><h2>那这有什么用？<a href="#那这有什么用"><span>#</span></a></h2><p>肯定会有人问：<strong>花这么大劲折腾，到底有什么用？</strong></p><p>我电脑是<strong>七彩虹橘宝 R16 Pro 2.5K屏</strong>，正好缺个4K屏看动漫追剧。但是当初选笔记本就是图<strong>便捷携带</strong>，不可能花大价钱去买一个4K显示屏——那样太不性价比，带起来也吃力。</p><p>不过<strong>索尼XZP</strong>这个屏幕尺寸真的<strong>非常适合看日漫</strong>，还是可以接受的。主要还是<strong>方便、便宜</strong>。</p></section><section><h2>那发不发热呢？<a href="#那发不发热呢"><span>#</span></a></h2><p>多多少少都会有点小热，可以接受。</p></section><section><h2>使用模式<a href="#使用模式"><span>#</span></a></h2>

<table><thead><tr><th>使用场景</th><th>设备</th></tr></thead><tbody><tr><td>游戏模式</td><td>自带屏</td></tr><tr><td>看番追剧</td><td>XZP</td></tr></tbody></table><hr /><blockquote><p>温馨提示：本文为设备折腾经验的分享，刷机、解锁、ROOT 等操作可能带来数据丢失、设备变砖、保修失效等风险，请务必在充分了解并备份后再操作。</p></blockquote></section></section>]]></content>
    </entry>
    <entry>
      <id>https://li8.me/posts/5/</id>
      <title type="text">论现代的奇葩脑回路</title>
      <published>2026-09-24T10:34:00.000Z</published>
      <updated>2026-09-24T10:34:00.000Z</updated>
      <author><name>八奈奈</name></author>
      <link rel="alternate" href="https://li8.me/posts/5/"/>
      <summary type="text"></summary>
      <content type="html"><![CDATA[<p>前几天和一位打游戏认识的高中年龄段女网友聊天，得知她和男朋友分手了——谈了七个月，散了。</p>
<p>这本不算什么稀奇事，现在这个年代，分分合合跟换头像一样频繁。真正让我愣住的是后面那段对话。</p>
<p>我问她怎么分的。她倒也不避讳，说早就在和自己的男闺蜜走得很近，聊天聊到半夜、周末单独出去吃饭看电影，男友介意，提了出来，吵了几次，最后就分了。我说，那你男闺蜜知道你们这个关系吗？她理直气壮：“我们只是好朋友啊，他是我男闺蜜，清清白白的。”</p>
<p>我不知道现在的社会到底怎么了。我不知道我接下来的话算不算苛责，但我真的想问：这个世界上，真的有男人能坦然接受自己的另一半，和另一个男生保持这种”比朋友近一点、比恋人远一点”的关系吗？</p>
<p>她整个人浑身上下都写着”我没做错什么”。我不明白我这一句话是不是不该说，但她反过来给我讲起了道理：时代变了，男女之间有纯友谊，是我思想老古董，是把人想得太龌龊。</p>
<p>那一刻我忽然意识到，我们俩讲的可能根本不是一回事。她讲的是”我又没出轨”，我讲的是”边界感”。</p>
<section><h2>触网太早，心智没跟上<a href="#触网太早心智没跟上"><span>#</span></a></h2><p>放在几年前，我大概只会把这事归结为”人各有志”。但她是高中年龄段的人，这让我多想了一层：现在的未成年人，触网实在太早了。</p><p>我们这一代人，小时候顶多玩玩单机游戏，情感观是摔了跟头才慢慢长出来的。而现在的小孩，小学就有了自己的手机，短视频、社交平台无缝切换，成年世界的情感模式——暧昧、养鱼、快餐式恋爱——隔着屏幕直接灌进还没长好的心智里。</p><p>社会发展太快，环境一天一个样，但人的心智是要靠时间和经历一寸一寸长出来的。接受的信息量是成年的，处理信息的心智是未成年的，中间这道落差，就是各种”奇葩脑回路”的生长土壤。</p><p>她们不是不聪明，是太早学会了太多概念，却没机会真正理解那些概念背后的分量。“边界感""情绪价值""独立自我”，张口就来，可这些东西要踩过坑才懂。</p></section>
<section><h2>不是奔着爱去的，是为新鲜感买单<a href="#不是奔着爱去的是为新鲜感买单"><span>#</span></a></h2><p>再回到她这件事本身。琢磨了几天，我的评价其实就一句话：她这段感情，从头到尾都不是奔着爱去的，而是在为自己的新鲜感买单。</p><p>恋爱七个月，男闺蜜全程在线；男友介意了，先沟通的明明是男友，她却把自己摘得干干净净。在她的逻辑里，恋爱是体验项目，男朋友是体验的一部分，男闺蜜也是体验的一部分，谁让她觉得新鲜、谁让她情绪舒服，她就往谁那边靠。</p><p>至于爱？爱是要负责任的，是要为对方收敛自由的，是要在”新鲜感消退”之后依然选择留下来的。而这些恰恰是网络短视频从来不教的部分——短视频只教”心动”，不教”守约”；只教”做自己”，不教”顾别人”。</p><p>新鲜感驱动的感情有个共同结局：热度一过，就要找下一个刺激源。这次是男闺蜜，下次还可以是别人。男朋友换了一个又一个，其实换的从来不是人，是那口新鲜气。</p></section>
<section><h2>边界感不是清白，是分寸<a href="#边界感不是清白是分寸"><span>#</span></a></h2><p>她一直在强调”清清白白”——没牵手、没接吻、没越界，所以没问题。但感情里的边界感，从来不是用法律标准来衡量的。</p><p>深夜的专属聊天、只对彼此说的悄悄话、电视剧里男女主角才有的相处模式，这些东西一样都没”越界”，但每一样都在把本该属于伴侣的情绪价值，外包给另一个人。恋爱里的安全感是有限的，你往外面漏一点，留给自己伴侣的就少一点。</p><p>说白了：出轨是结果，越界是过程。等到有”结果”的时候，早就不知道越了多少次界了。</p></section>
<section><h2>讲道理讲不通的根源<a href="#讲道理讲不通的根源"><span>#</span></a></h2><p>最让我无语的其实不是她的行为，而是那个逻辑闭环：</p><p>你提出疑问 → 她说你不信任她 → 你解释你介意的是相处模式 → 她说那是你的问题，她问心无愧 → 最后你反而成了那个”思想有问题”的人。</p><p>绕了一圈，话题从”这事合不合适”变成了”你心态好不好”。只要她一口咬定自己清白，任何讨论都无法进行——因为她根本不觉得这件事有讨论的必要。</p><p>后来我想通了：讲道理的前提，是双方承认同一套标准。她那套标准里，“没实质越界”就是满分答案；而我这套标准里，“让伴侣感到不安还毫不在意”本身就是问题。标准都不同，道理是讲不通的。</p></section>
<section><h2>写在最后<a href="#写在最后"><span>#</span></a></h2><p>我不太担心她一个人，我担心的是这种状态不是个例。触网太早的一代正在批量长大，网络教给他们一整套成熟的恋爱话术，却没人在意他们有没有成熟的心智去撑起这些话术。</p><p>如果你也遇到了这样的人，别急着讲道理，先问自己一个问题：你要的是这个人，还是这段关系里随时会漏风的安全感？</p><p>想清楚这个，答案其实就不难选了。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://li8.me/posts/4/</id>
      <title type="text">《败犬女主》第 8、8.5、9 卷品鉴：全员神人，只有温八正常</title>
      <published>2026-09-22T12:10:00.000Z</published>
      <updated>2026-09-24T00:00:00.000Z</updated>
      <author><name>八奈奈</name></author>
      <link rel="alternate" href="https://li8.me/posts/4/"/>
      <summary type="text">台版实体书到手，又连夜补完网络汉化，把 8、8.5、9 三卷连起来品了一遍。老马的告白炸场、8.5 的全员悬置、第九卷的神人狂欢——三卷连读才是完整的过山车。</summary>
      <content type="html"><![CDATA[<p>这三卷我是连着读完的：台版实体书到手，先捧着把第八卷翻完，又连夜补完 8.5 和第九卷的汉化。</p>
<p>合上书的感受一句话就能说完：<strong>这三卷里，除了温八，没一个正常的。</strong></p>
<p>第八卷，老马把话挑明；8.5 卷，全员假装无事发生；第九卷，樱井三人组把”神人”两个字焊死在书页上。而温水和八奈见，从头到尾都像误闯片场的一对正常少男少女——正常吃饭，正常拌嘴，正常地谁也不敢把那层纸捅破。</p>
<p>本篇全程剧透，三卷一起聊。没读到的朋友先去补课，回来我们接着品。</p>
<section><h2>第八卷：老马的告白，全书最正面的一击<a href="#第八卷老马的告白全书最正面的一击"><span>#</span></a></h2><p>第八卷主线是生徒会选举，马剃天爱星的政治题材。说实话选举本身我看得中规中矩，直到最后一击。</p><p>卷末，天爱星把话挑明了。我是捧着台版实体书读到这一段的——第 290 页，插画里老马双手交握在胸前，脸红得不行：</p><p></p><figure><img src="/assets/images/laoma.webp" alt="台版《敗北女角太多了！》第八卷第 290 页：老马告白的名场面" /><figcaption>台版《敗北女角太多了！》第八卷第 290 页：老马告白的名场面</figcaption></figure><p></p><p>然后是这两句：</p><blockquote><p>“温水同学，我喜欢你。”</p><p>“当然我指的是恋爱的喜欢。你愿意和我交往吗？”</p></blockquote><p>不绕弯子，不打哑谜，是全系列到目前为止最正面、最不留退路的一次进攻。温水的内心是：</p><blockquote><p>“我——被告白了。”</p><p>“只会在小说中出现过的事件，我现在成为当事人，在我眼前发生。”</p></blockquote><p>注意，他没有拒绝。到第八卷收尾为止，这个答案就那么空着——这一空，就空出了整个 8.5 卷。</p><p>老马这一击的含金量，要放在”败犬”的坐标系里看：这个系列里其他女生表达喜欢的方式，是自动翻译、是天台让路、是”我不碍事”；只有天爱星，是把话放到台面上等你接。僵尸系学姐，全书唯一的直球选手。就冲这一段，第八卷封神不亏。</p></section>
<section><h2>8.5 卷：全员悬置，温水的不体面现场<a href="#85-卷全员悬置温水的不体面现场"><span>#</span></a></h2><p>8.5 卷是短篇集，讲告白之后的日常——或者说，讲所有人怎么假装那场告白没发生过。</p><p>最损的一场戏：老八主动开口试探温水——“温水君——怎么看马剃同学？“温水打个哈哈不接。然后八奈见顺着台阶自己翻译：”……如果那么想的话，我们也不能妨碍你啊。”</p><p>翻译翻译：你敢喜欢她，我就退出。</p><p>看，这就是败犬。试探完了，先替你把退路铺好，顺便把自己的心再埋深一寸。而温水两头都不敢接：不敢对老马的告白给答案，也不敢接老八递过来的那个”我不碍事”。他心里的自动翻译倒是转得飞快，把每一句危险发言都翻译成安全的日常对话，翻完糊弄过去，装作无事发生。</p><p>短篇集的好处在这里体现得淋漓尽致：告白之后的每一段日常，读者都知道底下压着什么，但角色们个个若无其事。这种全桌人都在装傻的悬置感，比任何误会冲突都折磨人——折磨的同时，糖也是真没停过。温水和老马的关系在微妙地升温，温水和老八的相处也在微妙地变味，所有人都在”少しずつ”（一点点地）动。</p><p>而全卷糖分最高的一场戏，我必须单拎出来讲：8.5 卷加笔短篇《把秘密塞进口袋》，老八和烧盐在社办边啃鱿鱼干边复习的日常回。八奈见一边嘎吱嘎吱嚼着鱿鱼干，一边状似无意地开口试探：”……那个呀，那天晚上你和温水君一起出去了对吧？“——名古屋那一晚，她记到现在。结果试探没进行三句，烧盐头都没抬，反手就是一枪：“测验结束后不是有花火大会吗……我要不要约阿温呢？”</p><p>老八当场僵住，连”咦”都是破音的。烧盐还嫌不够，收尾补刀：“事后才慌张的话，我可不管哦。“我读到这段直接拍腿：平时看烧盐是文艺部吉祥物，真坐上感情牌桌，她才是全场牌最顺的人——句句装傻，句句带刺，把老八那点”若无其事式试探”的小心思一次性击穿。老八最后只能借口买奶茶落荒而逃。</p><p>但这篇真正的杀招在结尾。温水把老八叫住，支支吾吾、眼神乱飘，手还下意识往她手边碰——八奈见内心都开始 “某种事件正在发生”了，紧张到整理头发。结果递过来的，是一块虎鲸形状的名古屋特产最中饼：“八奈见同学从名古屋回来时想买，但是没钱买不了吧？我买来当给家里人的土产，所以想分你一个。”</p><p>她以为要被告白，结果是点心。气得当场炸毛：“你的问题就出在这里！温水君！”</p><p>可下一秒，她把最中饼塞进口袋，丢下一句”不是说要对大家保密吗”，脚步轻盈地走了。标题的”秘密”就藏在这——品品这个细节：温水的温柔从来不走嘴，他记得的是老八在名古屋盯着最中饼却没钱买的那一眼。对一个从小让球让到大、连贪吃都要顾着面子的姑娘来说，有人默默接住她这点小心思，比一百句情话都致命。而且你把 8.5 的这块最中饼和 9 卷旅馆那句”可是我的啊”连起来看，根本是同一套连招：先让她尝到糖，再让她喊出心声。雨森老贼，你小子是懂饲养败犬的。</p><p>然后第九卷告诉你：装傻是要还的。</p></section>
<section><h2>第九卷·温八篇：全员神人中唯一的正常糖<a href="#第九卷温八篇全员神人中唯一的正常糖"><span>#</span></a></h2><p>第九卷主线看起来很扯：樱井的女友小春想跟樱井”制造既成事实”，拉五个人搞飞驒高山旅行，计划让小两口错过末班车共处一室。结果樱井看穿一切淡定上车，真正错过末班车的是温水和八奈见——房订好了，就剩他俩。</p><p>同宿这一夜，雨森写得非常干净。没有越界，没有俗套的意外碰撞，就是两个高中生在同一家旅馆的同一间房里，尴尬、拌嘴、谁都不肯先睡、又谁都不舍得先睡。第二天早上那副鸡飞狗跳的样子——老八的表情管理当场碎掉，温水假装镇定其实耳朵通红——是我这半年来在轻小说里看到过的最好笑也最好嗑的一章。</p><p>然后是名场面。小春家出跑到温水家蹭住，当面质问八奈见：“我确认一下，八奈见同学没有和温水君交往吧？那么，我和温水君进旅馆也没问题吧？”</p><p>八大王当场破防：</p><blockquote><p>“温水君可是我的啊？！”</p></blockquote><p>喊完她自己僵在原地，双手乱摇：“不对不对不对，不是那个意思，你明白我的意思吧？”</p><p>分析一下这四秒钟。九卷以来，老八的人设是什么？是每一次心动都能自动翻译成”朋友”的嘴硬王者，是把”我们只是青梅竹马”当护身符的标准败犬。而这一句失态，是九年伪装的第一次系统报错——报错之后秒怂，怂完之后躲着温水好几天不敢见面。</p><p>这一连串反应比任何直球告白都真实。真心的流露从来不是设计好的告白，是防不住的那一句。而且你注意，她躲的不是丢脸，是”被他知道”——躲了好几天之后两个人又若无其事地聊回去了，谁都没提那一晚。这种默契的不提，比提了更甜。</p><p>还有一场隐藏高光：有人问温水”如果有人叫你不要见我，你会怎么做”，他的回答绕开了所有人——连亲妹妹都可以摆到局外人位置去。这个嘴巴很严的男人，宁可把妹妹划出去，也没把八奈见划出去。答案早就在选择题里了，只是他不承认。</p><p>对照着看更有意思：老马在第八卷选择了最直球的告白，老八在第九卷只有靠破防才漏出半句真心。两个路线，两种败法。所以我说温八是这三卷里唯一的正常：正常的吃醋，正常的别扭，正常的装傻，正常的谁也不捅破——放在这群神人中间，这份正常甜得发光。</p></section>
<section><h2>第九卷·神人组：樱井、小春、放虎原<a href="#第九卷神人组樱井小春放虎原"><span>#</span></a></h2><p>现在讲让我血压拉满的主线——这三个人，一个比一个神人。</p><p>先看他们的操作有多离谱。小春，樱井的女友，想跟男朋友更进一步，手段是委托男朋友的哥们儿帮忙设局制造既成事实；设局失败后直接家出，搬进这个哥们儿家里住了五天，用意是刺激男朋友。这五天里她还顺手把”我和温水进旅馆也没问题吧”甩在老八脸上。见了两面加一天旅行就敢上门住，“朋友妻”这个身份也太好用了——我第一次读到这儿，合上手机缓了半天。</p><p>但小春的离谱，是有账本的。回忆杀揭开的那一刻，我血压直接转向心酸——</p><p>初一那年的秋天，放虎原家里出事，父亲把哥哥逐出家门，撂下”别再跨进这个家门”。她一个人躲进田边小屋，在雨里哭。冒雨追来的是小学六年级的樱井，把自己的衣服脱下来借给淋透的她。然后——放虎原从背后环住他的手臂，樱井把手覆了上去。那一晚，两个人待到了天亮。</p><p>对，你们没看错，一个初一，一个小学六年级。这配置放在任何一部作品里都是重量级。</p><p>要我复述那一晚具体干了什么？我都不想说。主要是雨森也没让我有说的余地——写到”把手覆上去”就断章了！就这四个字，信息量直接拉满，剩下的全靠读者自己脑补。我盯着这段看了得有一分钟，脑子里只有一个声音：写清楚啊！……不对，你别写了，你还是留白吧，写清楚了我以后没法直视樱井了。</p><p>更绝的还在后面。两家父亲的反应速度，比这两人的感情进展快多了——第二天就为遗产撕破脸，一个喊”云雀诱拐了小学生弘人”，一个喊”我女儿被玷污了”。好家伙，恋爱还没个说法呢，大人已经把它换算成官司的筹码了。没有一个人问那两个孩子昨晚经历了什么，一个都没有。</p><p>在孩子们抱团取暖的地方，大人们只看得见把柄。吐槽归吐槽，这一段是真的狠——我见过很多写”大人丑恶”的，写这么直白的，这是头一份。</p><p>替孩子们收场的，又是另一个孩子。小春冲进骂战，一句”当时我也在场""我和弘人在交往”，把”诱拐”和”玷污”两个帽子同时摘了。她用一句编造的恋爱，给放虎原和樱井挡了刀——然后把自己钉死在这个谎上八年。</p><p>八年。她跟樱井的交往始于救场，不是始于喜欢。所以她才执着于”既成事实”，执着于”更进一步”——她要的不是那种事，是一个八年都没等到的确认：弘人心里到底有没有我。</p><p>樱井的回答？“等云雀姐毕业。”</p><p>让女朋友等另一个女人毕业。看到这儿我人都麻了——你逃避就逃避吧，你别两头抓啊。烟花大会上小春一头槌撞散了”再给我一次机会”，喊出分手，那一下我居然松了口气：八年了，这个女孩终于替自己喊了一次。</p><p>顺带一提，支线也神人浓度超标：姬宫听说温八同宿，专程跑来给温水”传授经验”，确认啥也没发生才闭嘴。这本书就没有正常人在谈恋爱。</p></section>
<section><h2>三卷连评：一场安排好的过山车<a href="#三卷连评一场安排好的过山车"><span>#</span></a></h2><p>单独看，第八卷是政治+告白，8.5 是发糖短篇，第九卷是神人修罗场。但连起来看，你才发现这三卷是一个完整的结构：</p><p><strong>第八卷把雷埋下（老马告白），8.5 卷把雷按住（全员悬置），第九卷在雷上跳舞（神人狂欢+温八突破）。</strong></p><p>雨森的算计在于：老马的直球把温水的”迟钝”逼到失效，8.5 的悬置把温水”装糊涂”的成本越垒越高，到第九卷，樱井三人组直接给他看了一场”装糊涂二十年”的现场版——放虎原就是一个把喜欢烂在心里、计划毕业就离开丰桥的青梅竹马，这个配置跟老八像得我后背发凉。温水看着樱井线的结局，等于看着自己和小八的”如果放任不管”版本。</p><p>所以樱井线根本不是水剧情：那是杀鸡给猴看，鸡是樱井，猴是温水和屏幕前的我们。</p><p>打分的话：第八卷五星（告白一击封神），8.5 卷四星（糖管够但体量小），第九卷四星半（樱井线收得偏急，扣半星）。三卷连读的体验，五星。</p><p>最终评价还是那句话：</p><p><strong>这三卷，除了温八能让我感到正常，其他人真的都太神人了。</strong></p><p>老马的神人在于敢把所有败犬不敢说的话一口气说完；小春的神人、樱井的神人、放虎原的神人，在于把八年的错位谈成了一场谁也退不出困局；连姬宫都在支线里神人。全员疯批狂欢里，只有温泉旅馆那一夜的两个人，在认真地尴尬、认真地别扭、认真地不敢说。</p><p>第十卷，老马的答案不能再拖了，佳树要登场了，温八只差一个命名。台版实体书我已经开始收了——这三卷，值得摆在书架上。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://li8.me/posts/3/</id>
      <title type="text">零依赖服务器监控面板：不用现成工具，自己写一个</title>
      <published>2026-09-21T09:20:24.000Z</published>
      <updated>2026-09-21T09:20:24.000Z</updated>
      <author><name>八奈奈</name></author>
      <link rel="alternate" href="https://li8.me/posts/3/"/>
      <summary type="text">不装任何监控软件，用一段 bash 脚本 + Nginx + 博客页面，给小内存服务器做一个零依赖、可配置、防扫描的服务器状态面板</summary>
      <content type="html"><![CDATA[<p><a href="/posts/1/">《从零搭建个人博客》</a>里提过，我的服务器是 2 核 2G 的小机型。想在博客里放一个服务器状态面板，现成方案不少——beszel、netdata、dashdot 都很漂亮——但动辄吃掉几百 MB 内存，还得单独开端口、配登录。2G 内存的机器经不起这么造。</p>
<p>最后的方案：<strong>一段 bash 脚本 + 一段 Nginx 配置 + 一个博客页面</strong>，零依赖、零端口、零额外内存占用，效果见本站的「状态」页。这篇把完整实现过程和踩过的坑记录下来。</p>
<section><h2>一、整体架构<a href="#一整体架构"><span>#</span></a></h2><p>先想清楚数据从哪来、到哪去：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>┌─ 服务器 ────────────────────────────┐</span></div></div><div><div><div>2</div></div><div><span>│  monitor.sh（cron 每 30 秒）          │</span></div></div><div><div><div>3</div></div><div><span>│    ↓ 读 /proc 内核数据，算出指标       │</span></div></div><div><div><div>4</div></div><div><span>│    ↓ 写成 JSON 文件（原子替换）        │</span></div></div><div><div><div>5</div></div><div><span>│  Nginx 把这个 JSON 以固定路径暴露      │</span></div></div><div><div><div>6</div></div><div><span>└──────────────┬──────────────────────┘</span></div></div><div><div><div>7</div></div><div><span><span>               </span></span><span>↓ HTTP（浏览器 fetch）</span></div></div><div><div><div>8</div></div><div><span>┌─ 博客页面 ──────────────────────────┐</span></div></div><div><div><div>9</div></div><div><span>│  JS 每 30 秒拉取 JSON → 渲染卡片       │</span></div></div><div><div><div>10</div></div><div><span>└────────────────────────────────────┘</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>关键认识：<strong>数据采集根本不需要 SSH，也不需要在浏览器端连服务器</strong>。脚本跑在服务器本地，读 <code>/proc</code> 虚拟文件——那是内核自己维护的运行时账本，数据最权威。浏览器端只是一个普通 HTTP 请求拉 JSON。</p></section>
<section><h2>二、第一步：采样脚本<a href="#二第一步采样脚本"><span>#</span></a></h2><p>完整脚本如下，可以直接抄（保存为 <code>/opt/server-mon/monitor.sh</code>，记得 <code>chmod +x</code>）：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#!/usr/bin/env bash</span></div></div><div><div><div>2</div></div><div><span># 读取 /proc 生成服务器状态 JSON，由 cron 定时执行</span></div></div><div><div><div>3</div></div><div><span>set</span><span> </span><span>-u</span></div></div><div><div><div>4</div></div><div><span>OUT</span><span>=</span><span>/var/www/mon/status.json</span></div></div><div><div><div>5</div></div><div><span>STATE</span><span>=</span><span>/var/lib/server-mon/prev.txt</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 自动识别默认网卡</span></div></div><div><div><div>8</div></div><div><span>IFACE</span><span><span>=</span><span>$(</span></span><span>ip</span><span> </span><span>route</span><span> </span><span>show</span><span> </span><span>default</span><span> 2&gt;</span><span>/dev/null</span><span> | </span><span>awk</span><span> </span><span>'{print $5; exit}'</span><span>)</span></div></div><div><div><div>9</div></div><div><span><span>[ </span><span>-z</span><span> </span></span><span>"</span><span><span>${</span><span>IFACE</span></span><span>:-</span><span>}</span><span>"</span><span> ] &amp;&amp; </span><span>IFACE</span><span>=</span><span>eth0</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span># --- CPU 计数器 ---</span></div></div><div><div><div>12</div></div><div><span>read</span><span> </span><span>-r</span><span> </span><span>_</span><span> </span><span>user</span><span> </span><span>nice</span><span> </span><span>system</span><span> </span><span>idle</span><span> </span><span>iowait</span><span> </span><span>irq</span><span> </span><span>softirq</span><span> </span><span>steal</span><span> </span><span>_</span><span> </span><span>&lt;</span><span> </span><span>/proc/stat</span></div></div><div><div><div>13</div></div><div><span>CPU_TOTAL</span><span><span>=</span><span>$((</span></span><span>user</span><span> </span><span>+</span><span> </span><span>nice</span><span> </span><span>+</span><span> </span><span>system</span><span> </span><span>+</span><span> </span><span>iowait</span><span> </span><span>+</span><span> </span><span>irq</span><span> </span><span>+</span><span> </span><span>softirq</span><span> </span><span>+</span><span> </span><span>steal</span><span> </span><span>+</span><span> </span><span>idle</span><span>))</span></div></div><div><div><div>14</div></div><div><span>CPU_BUSY</span><span><span>=</span><span>$((</span></span><span>user</span><span> </span><span>+</span><span> </span><span>nice</span><span> </span><span>+</span><span> </span><span>system</span><span> </span><span>+</span><span> </span><span>iowait</span><span> </span><span>+</span><span> </span><span>irq</span><span> </span><span>+</span><span> </span><span>softirq</span><span> </span><span>+</span><span> </span><span>steal</span><span>))</span></div></div><div><div><div>15</div></div><div>
</div></div><div><div><div>16</div></div><div><span># --- 内存 ---</span></div></div><div><div><div>17</div></div><div><span>memtotal</span><span><span>=</span><span>$(</span></span><span>awk</span><span> </span><span>'/^MemTotal:/{print $2}'</span><span> </span><span>/proc/meminfo</span><span>)</span></div></div><div><div><div>18</div></div><div><span>memavail</span><span><span>=</span><span>$(</span></span><span>awk</span><span> </span><span>'/^MemAvailable:/{print $2}'</span><span> </span><span>/proc/meminfo</span><span>)</span></div></div><div><div><div>19</div></div><div>
</div></div><div><div><div>20</div></div><div><span># --- 磁盘（根分区）---</span></div></div><div><div><div>21</div></div><div><span>dtotal</span><span><span>=</span><span>$(</span></span><span>df</span><span> </span><span>-kP</span><span> </span><span>/</span><span> | </span><span>tail</span><span> </span><span>-1</span><span> | </span><span>awk</span><span> </span><span>'{print $2}'</span><span>)</span></div></div><div><div><div>22</div></div><div><span>dused</span><span><span>=</span><span>$(</span></span><span>df</span><span> </span><span>-kP</span><span> </span><span>/</span><span> | </span><span>tail</span><span> </span><span>-1</span><span> | </span><span>awk</span><span> </span><span>'{print $3}'</span><span>)</span></div></div><div><div><div>23</div></div><div><span>davail</span><span><span>=</span><span>$(</span></span><span>df</span><span> </span><span>-kP</span><span> </span><span>/</span><span> | </span><span>tail</span><span> </span><span>-1</span><span> | </span><span>awk</span><span> </span><span>'{print $4}'</span><span>)</span></div></div><div><div><div>24</div></div><div>
</div></div><div><div><div>25</div></div><div><span># --- 网络计数器 ---</span></div></div><div><div><div>26</div></div><div><span>RX</span><span><span>=</span><span>$(</span></span><span>grep</span><span> </span><span>"^[[:space:]]*</span><span>$IFACE</span><span>:"</span><span> </span><span>/proc/net/dev</span><span> | </span><span>awk</span><span> </span><span>'{print $2}'</span><span>)</span></div></div><div><div><div>27</div></div><div><span>TX</span><span><span>=</span><span>$(</span></span><span>grep</span><span> </span><span>"^[[:space:]]*</span><span>$IFACE</span><span>:"</span><span> </span><span>/proc/net/dev</span><span> | </span><span>awk</span><span> </span><span>'{print $10}'</span><span>)</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span># --- 负载 / 运行时长 ---</span></div></div><div><div><div>30</div></div><div><span>load1</span><span><span>=</span><span>$(</span></span><span>cut</span><span> </span><span>-d</span><span>' '</span><span> </span><span>-f1</span><span> </span><span>/proc/loadavg</span><span>)</span></div></div><div><div><div>31</div></div><div><span>UPTIME</span><span><span>=</span><span>$(</span></span><span>cut</span><span> </span><span>-d.</span><span> </span><span>-f1</span><span> </span><span>/proc/uptime</span><span>)</span></div></div><div><div><div>32</div></div><div><span>TS</span><span><span>=</span><span>$(</span></span><span>date</span><span> </span><span>+%s</span><span>)</span></div></div><div><div><div>33</div></div><div>
</div></div><div><div><div>34</div></div><div><span># --- 读上次采样，算差值 ---</span></div></div><div><div><div>35</div></div><div><span>prev_ct</span><span>=</span><span>0</span><span>; </span><span>prev_cb</span><span>=</span><span>0</span><span>; </span><span>prev_rx</span><span>=</span><span>0</span><span>; </span><span>prev_tx</span><span>=</span><span>0</span><span>; </span><span>prev_ts</span><span>=</span><span>0</span></div></div><div><div><div>36</div></div><div><span><span>[ </span><span>-f</span><span> </span></span><span>"</span><span>$STATE</span><span>"</span><span> ] &amp;&amp; </span><span>read</span><span> </span><span>-r</span><span> </span><span>prev_ct</span><span> </span><span>prev_cb</span><span> </span><span>prev_rx</span><span> </span><span>prev_tx</span><span> </span><span>prev_ts</span><span> &lt; </span><span>"</span><span>$STATE</span><span>"</span></div></div><div><div><div>37</div></div><div>
</div></div><div><div><div>38</div></div><div><span>dct</span><span><span>=</span><span>$((</span></span><span>CPU_TOTAL</span><span> </span><span>-</span><span> </span><span>prev_ct</span><span>)); </span><span>dcb</span><span><span>=</span><span>$((</span></span><span>CPU_BUSY</span><span> </span><span>-</span><span> </span><span>prev_cb</span><span>))</span></div></div><div><div><div>39</div></div><div><span>drx</span><span><span>=</span><span>$((</span></span><span>RX</span><span> </span><span>-</span><span> </span><span>prev_rx</span><span>));        </span><span>dtx</span><span><span>=</span><span>$((</span></span><span>TX</span><span> </span><span>-</span><span> </span><span>prev_tx</span><span>))</span></div></div><div><div><div>40</div></div><div><span>dt</span><span><span>=</span><span>$((</span></span><span>TS</span><span> </span><span>-</span><span> </span><span>prev_ts</span><span>))</span></div></div><div><div><div>41</div></div><div><span>[ </span><span>"</span><span>$dt</span><span>"</span><span><span>  </span><span>-lt</span><span> </span></span><span>1</span><span> ] &amp;&amp; </span><span>dt</span><span>=</span><span>1</span></div></div><div><div><div>42</div></div><div><span>[ </span><span>"</span><span>$dtx</span><span>"</span><span><span> </span><span>-lt</span><span> </span></span><span>0</span><span> ] &amp;&amp; </span><span>dtx</span><span>=</span><span>0</span></div></div><div><div><div>43</div></div><div><span>[ </span><span>"</span><span>$drx</span><span>"</span><span><span> </span><span>-lt</span><span> </span></span><span>0</span><span> ] &amp;&amp; </span><span>drx</span><span>=</span><span>0</span></div></div><div><div><div>44</div></div><div>
</div></div><div><div><div>45</div></div><div><span>if</span><span> [ </span><span>"</span><span>$prev_ts</span><span>"</span><span><span> </span><span>-eq</span><span> </span></span><span>0</span><span> ] || [ </span><span>"</span><span>$dct</span><span>"</span><span><span> </span><span>-le</span><span> </span></span><span>0</span><span> ]; </span><span>then</span></div></div><div><div><div>46</div></div><div><span>  </span><span>cpu_pct</span><span>=</span><span>-1</span><span>; </span><span>rx_s</span><span>=</span><span>0</span><span>; </span><span>tx_s</span><span>=</span><span>0</span></div></div><div><div><div>47</div></div><div><span>else</span></div></div><div><div><div>48</div></div><div><span>  </span><span>cpu_pct</span><span><span>=</span><span>$(</span></span><span>awk</span><span> </span><span>-v</span><span> </span><span>b="</span><span>$dcb</span><span>"</span><span> </span><span>-v</span><span> </span><span>t="</span><span>$dct</span><span>"</span><span> </span><span>'BEGIN{p=b/t*100; if(p&lt;0)p=0; if(p&gt;100)p=100; printf "%.1f", p}'</span><span>)</span></div></div><div><div><div>49</div></div><div><span>  </span><span>rx_s</span><span><span>=</span><span>$(</span></span><span>awk</span><span> </span><span>-v</span><span> </span><span>r="</span><span>$drx</span><span>"</span><span> </span><span>-v</span><span> </span><span>t="</span><span>$dt</span><span>"</span><span> </span><span>'BEGIN{printf "%.1f", r/t/1024}'</span><span>)</span></div></div><div><div><div>50</div></div><div><span>  </span><span>tx_s</span><span><span>=</span><span>$(</span></span><span>awk</span><span> </span><span>-v</span><span> </span><span>r="</span><span>$dtx</span><span>"</span><span> </span><span>-v</span><span> </span><span>t="</span><span>$dt</span><span>"</span><span> </span><span>'BEGIN{printf "%.1f", r/t/1024}'</span><span>)</span></div></div><div><div><div>51</div></div><div><span>fi</span></div></div><div><div><div>52</div></div><div>
</div></div><div><div><div>53</div></div><div><span>printf</span><span> </span><span>'%s %s %s %s %s\n'</span><span> </span><span>"</span><span>$CPU_TOTAL</span><span>"</span><span> </span><span>"</span><span>$CPU_BUSY</span><span>"</span><span> </span><span>"</span><span>$RX</span><span>"</span><span> </span><span>"</span><span>$TX</span><span>"</span><span> </span><span>"</span><span>$TS</span><span>"</span><span> &gt; </span><span>"</span><span>$STATE</span><span>"</span></div></div><div><div><div>54</div></div><div>
</div></div><div><div><div>55</div></div><div><span>MEM_PCT</span><span><span>=</span><span>$(</span></span><span>awk</span><span> </span><span>-v</span><span> </span><span>t="</span><span>$memtotal</span><span>"</span><span> </span><span>-v</span><span> </span><span>a="</span><span>$memavail</span><span>"</span><span> </span><span>'BEGIN{printf "%.1f", (t&gt;0)? (t-a)/t*100 : 0}'</span><span>)</span></div></div><div><div><div>56</div></div><div><span>DISK_PCT</span><span><span>=</span><span>$(</span></span><span>awk</span><span> </span><span>-v</span><span> </span><span>u="</span><span>$dused</span><span>"</span><span> </span><span>-v</span><span> </span><span>a="</span><span>$davail</span><span>"</span><span> </span><span>'BEGIN{t=u+a; printf "%.1f", (t&gt;0)? u/t*100 : 0}'</span><span>)</span></div></div><div><div><div>57</div></div><div><span>TOTRX</span><span><span>=</span><span>$(</span></span><span>awk</span><span> </span><span>-v</span><span> </span><span>r="</span><span>$RX</span><span>"</span><span> </span><span>'BEGIN{printf "%.2f", r/1073741824}'</span><span>)</span></div></div><div><div><div>58</div></div><div><span>TOTTX</span><span><span>=</span><span>$(</span></span><span>awk</span><span> </span><span>-v</span><span> </span><span>t2="</span><span>$TX</span><span>"</span><span> </span><span>'BEGIN{printf "%.2f", t2/1073741824}'</span><span>)</span></div></div><div><div><div>59</div></div><div>
</div></div><div><div><div>60</div></div><div><span>mkdir</span><span> </span><span>-p</span><span> </span><span>/var/www/mon</span></div></div><div><div><div>61</div></div><div><span># 注意：JSON 里刻意不写主机名、IP、网卡名，避免泄露服务器信息</span></div></div><div><div><div>62</div></div><div><span>printf</span><span> </span><span>'{"ts":%s,"uptime":%s,"load1":"%s","cpu":{"usage":%s},"mem":{"total_kb":%s,"avail_kb":%s,"used_pct":%s},"disk":{"total_kb":%s,"used_kb":%s,"used_pct":%s},"net":{"rx_kbps":%s,"tx_kbps":%s,"total_rx_gb":%s,"total_tx_gb":%s}}\n'</span><span> </span><span>\</span></div></div><div><div><div>63</div></div><div><span>  </span><span>"</span><span>$TS</span><span>"</span><span> </span><span>"</span><span>$UPTIME</span><span>"</span><span> </span><span>"</span><span>$load1</span><span>"</span><span> </span><span>"</span><span>$cpu_pct</span><span>"</span><span> </span><span>"</span><span>$memtotal</span><span>"</span><span> </span><span>"</span><span>$memavail</span><span>"</span><span> </span><span>"</span><span>$MEM_PCT</span><span>"</span><span> </span><span>\</span></div></div><div><div><div>64</div></div><div><span>  </span><span>"</span><span>$dtotal</span><span>"</span><span> </span><span>"</span><span>$dused</span><span>"</span><span> </span><span>"</span><span>$DISK_PCT</span><span>"</span><span> </span><span>"</span><span>$rx_s</span><span>"</span><span> </span><span>"</span><span>$tx_s</span><span>"</span><span> </span><span>"</span><span>$TOTRX</span><span>"</span><span> </span><span>"</span><span>$TOTTX</span><span>"</span><span> &gt; </span><span>"</span><span>$OUT</span><span>.tmp"</span></div></div><div><div><div>65</div></div><div><span>mv</span><span> </span><span>"</span><span>$OUT</span><span>.tmp"</span><span> </span><span>"</span><span>$OUT</span><span>"</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>几个准确性设计，都是查过资料才定的：</p><p><strong>CPU 用两次计数差值，不是瞬时值。</strong> <code>/proc/stat</code> 的 CPU 时间是开机以来的累计值，单次读没有任何意义。脚本把上次计数存在 <code>prev.txt</code>，这次减上次，差值才是这段时间的真实占用率。这也是为什么页面刚上线时 CPU 会显示几秒的 ”…”——第一次运行还没有差值，用 <code>-1</code> 标记，前端识别后显示省略号而不是错误数据。</p><p><strong>内存用 <code>MemAvailable</code> 口径。</strong> Linux 会把空闲内存拿去做文件缓存（buff/cache），<code>free</code> 命令里的”可用”就是 <code>MemAvailable</code>：等于空闲 + 可回收缓存。用它算占用率才贴近真实体验，直接拿 <code>MemTotal - MemFree</code> 会把缓存算成”已用”，2G 机器看起来永远 90%+。</p><p><strong>磁盘百分比和 <code>df</code> 命令同口径。</strong> 有两种算法：<code>used/total</code> 和 <code>used/(used+avail)</code>。后者才是 <code>df</code> 命令显示的那个百分比——因为 <code>total</code> 里含一部分 root 保留块（普通用户写不进去），分母用 <code>used+avail</code> 才不会把面板显示得比实际乐观。</p><p><strong>JSON 用「写临时文件 + mv」原子替换。</strong> 如果前端 fetch 的瞬间脚本正写到一半，会拿到半截 JSON 直接解析报错。先写 <code>.tmp</code> 再 <code>mv</code>，<code>mv</code> 在同一分区是原子操作，任何时刻文件都是完整的。</p><p><strong>计数器回绕保护。</strong> 服务器重启后 <code>/proc/net/dev</code> 的流量计数会归零，差值算出负数，<code>[ "$dtx" -lt 0 ] &amp;&amp; dtx=0</code> 把它钳到 0，面板不会出现负速率。</p></section>
<section><h2>三、第二步：定时采样<a href="#三第二步定时采样"><span>#</span></a></h2><p>cron 最小粒度是一分钟，想要 30 秒采样有个经典技巧——一条规则里执行两次，第二次延后半分钟：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>* * * * * /opt/server-mon/monitor.sh &gt;/dev/null 2&gt;&amp;1; </span><span>sleep</span><span> </span><span>30</span><span>; </span><span>/opt/server-mon/monitor.sh</span><span> &gt;</span><span>/dev/null</span><span> 2&gt;&amp;1</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>用 <code>crontab -e</code> 添加后，<code>crontab -l</code> 确认。采样间隔缩短一半有两个好处：网速/CPU 的统计窗口更短，短时突发流量更能捕捉到；页面每 30 秒刷新时每次都能拿到新数据，而不是同样的数字看一分钟。</p><p>脚本的差值逻辑天然支持任意间隔（用实际时间差 <code>dt</code> 做除数），所以就算 cron 延迟几分钟跑，速率计算依然准确。</p></section>
<section><h2>四、第三步：Nginx 暴露数据<a href="#四第三步nginx-暴露数据"><span>#</span></a></h2><p>JSON 文件在 <code>/var/www/mon/status.json</code>，在站点 Nginx 配置里加一个 location：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span># 路径里那串随机字符是「路径令牌」，防止端点被扫描，需与博客配置一致</span></div></div><div><div><div>2</div></div><div><span>location</span><span> ^~ </span><span>/server-status/&lt;随机令牌&gt;/ </span><span>{</span></div></div><div><div><div>3</div></div><div><span>   <span> </span></span><span>alias </span><span>/var/www/mon/;</span></div></div><div><div><div>4</div></div><div><span>   <span> </span></span><span>add_header </span><span>Cache-Control </span><span>"no-store"</span><span>;</span></div></div><div><div><div>5</div></div><div><span>   <span> </span></span><span>add_header </span><span>X-Robots-Tag </span><span>"noindex, nofollow"</span><span> always;</span></div></div><div><div><div>6</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>三个细节：</p><ol>
<li><strong><code>no-store</code></strong>：状态数据永远不能被缓存，否则访客看到的是几分钟前的旧数据；</li>
<li><strong><code>noindex, nofollow</code></strong>：告诉搜索引擎不要收录这个数据端点；</li>
<li><strong>路径令牌</strong>：JSON 的真实地址里拼一段随机字符串。改完 <code>nginx -t</code> 测试语法，再 <code>systemctl reload nginx</code> 平滑生效，站点不中断。</li>
</ol></section>
<section><h2>五、第四步：博客展示页<a href="#五第四步博客展示页"><span>#</span></a></h2><p>页面是 Astro 写的（其他框架同理，核心就是 fetch + 渲染）。骨架：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>DATA_URL</span><span> </span><span>=</span><span> </span><span>"/server-status/&lt;随机令牌&gt;/status.json"</span><span>;</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>REFRESH_SEC</span><span> </span><span>=</span><span> </span><span>30</span><span>;</span></div></div><div><div><div>3</div></div><div><span>const</span><span> </span><span>STALE_SEC</span><span> </span><span>=</span><span> </span><span>180</span><span>;      </span><span>// 数据超过 3 分钟没更新视为异常</span></div></div><div><div><div>4</div></div><div><span>const</span><span> </span><span>WARN_THRESHOLD</span><span> </span><span>=</span><span> </span><span>60</span><span>;  </span><span>// 占用 60% 进度条转黄</span></div></div><div><div><div>5</div></div><div><span>const</span><span> </span><span>HIGH_THRESHOLD</span><span> </span><span>=</span><span> </span><span>85</span><span>;  </span><span>// 占用 85% 转红</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>async</span><span> </span><span>function</span><span> </span><span>refresh</span><span>() {</span></div></div><div><div><div>8</div></div><div><span>  </span><span>try</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>const</span><span> </span><span>res</span><span> </span><span>=</span><span> </span><span>await</span><span> </span><span>fetch</span><span>(</span><span>DATA_URL</span><span> </span><span>+</span><span> </span><span>"?t="</span><span> </span><span>+</span><span><span> </span><span>Date</span><span>.</span></span><span>now</span><span>(), { </span><span>cache</span><span>:</span><span> </span><span>"no-store"</span><span> });</span></div></div><div><div><div>10</div></div><div><span>    </span><span>if</span><span> (</span><span>!</span><span><span>res</span><span>.</span></span><span>ok</span><span>) </span><span>throw</span><span> </span><span>new</span><span> </span><span>Error</span><span><span>(</span><span>res</span><span>.</span></span><span>status</span><span>);</span></div></div><div><div><div>11</div></div><div><span>    </span><span>const</span><span> </span><span>d</span><span> </span><span>=</span><span> </span><span>await</span><span><span> </span><span>res</span><span>.</span></span><span>json</span><span>();</span></div></div><div><div><div>12</div></div><div><span>    </span><span>render</span><span><span>(</span><span>d</span><span>);  </span></span><span>// 按 CPU/内存/磁盘/网络分块渲染</span></div></div><div><div><div>13</div></div><div><span>    </span><span>const</span><span> </span><span>stale</span><span> </span><span>=</span><span><span> </span><span>Date</span><span>.</span></span><span>now</span><span>() </span><span>/</span><span> </span><span>1000</span><span> </span><span>-</span><span><span> </span><span>d</span><span>.</span></span><span>ts</span><span> </span><span>&gt;</span><span> </span><span>STALE_SEC</span><span>;</span></div></div><div><div><div>14</div></div><div><span>    </span><span>setStatus</span><span>(</span><span>!</span><span><span>stale</span><span>, </span><span>stale</span><span> </span></span><span>?</span><span> </span><span>"数据延迟（采样服务可能异常）"</span><span> </span><span>:</span><span> </span><span>"运行正常"</span><span>);</span></div></div><div><div><div>15</div></div><div><span><span>  </span></span><span>} </span><span>catch</span><span> {</span></div></div><div><div><div>16</div></div><div><span>    </span><span>setStatus</span><span>(</span><span>false</span><span>, </span><span>"无法获取状态数据"</span><span>);</span></div></div><div><div><div>17</div></div><div><span><span>  </span></span><span>}</span></div></div><div><div><div>18</div></div><div><span>}</span></div></div><div><div><div>19</div></div><div><span>refresh</span><span>();</span></div></div><div><div><div>20</div></div><div><span>setInterval</span><span><span>(</span><span>refresh</span><span>, </span></span><span>REFRESH_SEC</span><span> </span><span>*</span><span> </span><span>1000</span><span>);</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><code>?t=时间戳</code> 是防止浏览器或中间层自作聪明缓存请求。渲染上有两个容错设计：每个字段赋值前都判空，某个字段缺失只跳过那一处，不会让整个面板白屏；进度条按占用分三档变色（正常主题色 → 黄 → 红），一眼就能看出哪项吃紧。</p></section>
<section><h2>六、第五步：配置化<a href="#六第五步配置化"><span>#</span></a></h2><p>把所有参数收进博客的统一配置文件（<code>src/config/index.ts</code> 的「服务器状态」模块），页面和导航都从这里读：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>serverStatusConfig</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>enable</span><span>:</span><span> </span><span>true</span><span>,        </span><span>// 总开关：关闭后导航隐藏入口，页面跳转 404</span></div></div><div><div><div>3</div></div><div><span>  </span><span>title</span><span>:</span><span> </span><span>"服务器状态"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>dataUrl</span><span>:</span><span> </span><span>"..."</span><span>,      </span><span>// 由路径令牌拼出</span></div></div><div><div><div>5</div></div><div><span>  </span><span>refreshSec</span><span>:</span><span> </span><span>30</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>staleSec</span><span>:</span><span> </span><span>180</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>warnThreshold</span><span>:</span><span> </span><span>60</span><span>,</span></div></div><div><div><div>8</div></div><div><span>  </span><span>highThreshold</span><span>:</span><span> </span><span>85</span><span>,</span></div></div><div><div><div>9</div></div><div><span>  </span><span>cards</span><span>:</span><span> { </span><span>cpu</span><span>:</span><span> </span><span>true</span><span>, </span><span>mem</span><span>:</span><span> </span><span>true</span><span>, </span><span>disk</span><span>:</span><span> </span><span>true</span><span>, </span><span>net</span><span>:</span><span> </span><span>true</span><span> },  </span><span>// 卡片开关</span></div></div><div><div><div>10</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>导航栏入口也写成条件渲染——<code>enable</code> 一关，导航上「状态」直接消失，不需要再改第二个文件。</p></section>
<section><h2>七、关于防泄露<a href="#七关于防泄露"><span>#</span></a></h2><p>面板数据是公开的（所有访客都能看），所以做了四层处理：</p>

<table><thead><tr><th>措施</th><th>防什么</th></tr></thead><tbody><tr><td>JSON 路径拼随机令牌</td><td>扫描器批量猜 <code>/server-status/status.json</code> 这类常见路径，直接 404</td></tr><tr><td><code>noindex</code> 响应头</td><td>搜索引擎不收录数据端点</td></tr><tr><td>JSON 字段脱敏</td><td>不输出主机名、IP、网卡名、系统版本，只有资源占用指标</td></tr><tr><td>总开关联动</td><td>关闭后整个站不再出现数据地址</td></tr></tbody></table><p>要诚实说明边界：令牌会出现在网页源码里，所以它防的是扫描器和顺手试探，<strong>不防决心翻你源码的人</strong>。这是「隐藏入口」不是「加密认证」，对个人博客是成本收益比最高的一档。真要更严格，可以上 Nginx 的 <code>secure_link</code> 模块做签名校验，或者限制访问 IP——对个人博客来说通常没必要。</p><p>另外，想换令牌随时可以：改博客配置里的一处 + Nginx 配置里的一处，reload 即可，旧地址立刻作废，相当于换门牌号。</p></section>
<section><h2>八、踩过的坑<a href="#八踩过的坑"><span>#</span></a></h2><p>记录几个调试时真实撞上的问题：</p><ol>
<li><strong>Windows 编辑的脚本在 Linux 跑不了</strong>：报 <code>integer expected</code>。Windows 换行是 CRLF，bash 把 <code>\r</code> 当成了内容的一部分。修复：<code>sed -i 's/\r$//' monitor.sh</code>。写服务器脚本最好直接在服务器上用 nano/vim 写，或传完必跑一次这条命令。</li>
<li><strong>网速永远是 0</strong>：<code>/proc/net/dev</code> 每行行首有空格，直接 <code>awk -F'[: ]+' '{print $2}'</code> 会把空字段算进去取错列。修复：先用 <code>grep "^[[:space:]]*eth0:"</code> 锁定网卡那一行，再用默认分隔符取列。</li>
<li><strong>磁盘百分比和 <code>df</code> 对不上</strong>：算法口径问题，见前文，改用 <code>used/(used+avail)</code>。</li>
<li><strong>内存显示 1.69 TB</strong>：前端单位换算错位——<code>1048576 KB</code> 是 1 GB 不是 1 TB，把 2G 内存显示成了 1.69 TB。修完的换算表：KB → 1024 → MB → 1024 → GB → 1024 → TB，写的时候直接把 1073741824 这类常数标注在代码注释里。</li>
<li><strong>文章时间偏移 8 小时</strong>：CI 在 UTC 时区构建，frontmatter 里的时间不写时区就会被按 UTC 解析。修复：所有 <code>published</code> 明确写 <code>+08:00</code>。</li>
</ol></section>
<section><h2>九、小结<a href="#九小结"><span>#</span></a></h2><p>这套面板的全部成本：约 70 行 bash + 一段 Nginx 配置 + 一个页面组件，运行时内存占用约等于零（脚本每 30 秒跑一次，跑完就退）。准确度上因为直接读内核数据、用差值法和标准口径，数字和 <code>free</code>、<code>df</code> 完全对得上。</p><p>适合的场景很明确：小内存服务器、想要和博客风格统一的轻量面板、不想多开端口和服务。如果你需要历史曲线、告警通知，那还是上 beszel 这类专业工具——但我 2G 的小机器，这个刚好。</p><blockquote><p>系列文章：<a href="/posts/1/">《从零搭建个人博客》</a> · <a href="/posts/2/">《blog 日常使用指南》</a></p></blockquote></section>]]></content>
    </entry>
    <entry>
      <id>https://li8.me/posts/2/</id>
      <title type="text">博客日常使用指南：写文章、发动态、加书签（自用记录）</title>
      <published>2026-09-21T04:07:42.000Z</published>
      <updated>2026-09-22T00:00:00.000Z</updated>
      <author><name>八奈奈</name></author>
      <link rel="alternate" href="https://li8.me/posts/2/"/>
      <summary type="text">手把手教程：从把网站跑起来，到写第一篇文章、用正文语法、发布上线，再到发动态、加书签、改配置，每一步都给出可直接复制的命令和模板</summary>
      <content type="html"><![CDATA[<p>上一篇<a href="/posts/1/">《从零搭建个人博客》</a>记录了怎么把站搭起来。这篇是<strong>手把手使用手册</strong>：每一步都给出可以直接复制的命令和模板，照着做就行，不用动脑子（划掉）不用查资料。</p>
<blockquote><p>记住一个总原则：<strong>所有站点配置都集中在 <code>src/config/index.ts</code> 一个文件里</strong>（按模块分区、带编号注释），<strong>所有内容都在 <code>src/content/</code> 目录下</strong>。改完保存，本地开发页面即时热更新。</p></blockquote>
<section><h2>第 0 步：把网站跑起来<a href="#第-0-步把网站跑起来"><span>#</span></a></h2><p>写文章之前，先让网站在本地转起来。只需要做一次。</p><section><h3>1. 确认环境<a href="#1-确认环境"><span>#</span></a></h3><p>打开终端（Windows 用 PowerShell），分别输入这两条命令检查版本：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>node</span><span> </span><span>-v</span><span>   </span><span># 需要 v22.23 以上</span></div></div><div><div><div>2</div></div><div><span>pnpm</span><span> </span><span>-v</span><span>   </span><span># 需要 11.22 以上</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>如果提示「不是内部或外部命令」，说明还没安装：去 <a href="https://nodejs.org/" target="_blank">nodejs.org</a> 装 Node.js（会自带核心），再输入 <code>npm install -g pnpm</code> 装 pnpm。装完重开一个终端再检查一次。</p></section><section><h3>2. 启动网站<a href="#2-启动网站"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>cd</span><span> </span><span>D:</span><span>\b</span><span>log</span><span>\F</span><span>irefly</span><span>    </span><span># 换成你的项目文件夹路径</span></div></div><div><div><div>2</div></div><div><span>pnpm</span><span> </span><span>install</span><span>          </span><span># 第一次运行才需要，安装依赖</span></div></div><div><div><div>3</div></div><div><span>pnpm</span><span> </span><span>dev</span><span>              </span><span># 启动本地开发服务器</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>看到终端里出现 <code>Local http://localhost:4321/</code> 就说明成功了。用浏览器打开这个地址，你看到的就是自己的网站。</p><p><strong>热更新</strong>：只要 <code>pnpm dev</code> 开着，你改任何文件保存，浏览器页面会自动刷新。写文章时建议左边开编辑器、右边开浏览器，改一段看一眼。</p><p>按 <code>Ctrl + C</code> 停止开发服务器。</p></section></section>
<section><h2>第 1 步：写第一篇文章<a href="#第-1-步写第一篇文章"><span>#</span></a></h2><section><h3>文章放在哪里<a href="#文章放在哪里"><span>#</span></a></h3><p>所有文章都是 <code>src/content/posts/</code> 目录下的 Markdown 文件，<strong>一个 <code>.md</code> 文件就是一篇文章</strong>。不需要注册或登记，文件放进去就自动出现在首页、归档和 RSS 里。</p><p>目录按分类建了文件夹：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>src/content/posts/</span></div></div><div><div><div>2</div></div><div><span>├── tech/     # 技术类：建站、教程、运维</span></div></div><div><div><div>3</div></div><div><span>├── daily/    # 日常随笔</span></div></div><div><div><div>4</div></div><div><span>└── anime/    # 追番相关</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>以后想加新分类，在 <code>posts/</code> 下新建一个文件夹就行，不需要改任何配置。</p></section><section><h3>方法一：用脚手架创建（推荐）<a href="#方法一用脚手架创建推荐"><span>#</span></a></h3><p>在项目根目录的终端里执行（把 <code>tech/my-first-post</code> 换成你的文件名）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>pnpm</span><span> </span><span>new-post</span><span> </span><span>--</span><span> </span><span>tech/my-first-post</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这条命令会自动做三件事：</p><ol>
<li>在 <code>src/content/posts/tech/</code> 下创建 <code>my-first-post.md</code>；</li>
<li>写好 frontmatter 模板（标题、日期、slug 都是现成的）；</li>
<li>如果文件名里写了中文，会自动转成拼音作为 URL，比如 <code>我的笔记</code> → <code>wo-de-bi-ji</code>。</li>
</ol><p>文件名可以带子文件夹前缀（如 <code>tech/xxx</code>），也可以不带（直接生成在 <code>posts/</code> 根目录，再手动挪进分类文件夹）。</p></section><section><h3>方法二：手动创建<a href="#方法二手动创建"><span>#</span></a></h3><p>在 <code>src/content/posts/tech/</code> 里直接新建一个 <code>我的文章.md</code>，把下面的模板整个粘进去，然后按注释逐行改：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>title</span><span>: </span><span>我的第一篇文章</span><span>            </span><span># 必填，显示在列表和浏览器标签页</span></div></div><div><div><div>3</div></div><div><span>published</span><span>: </span><span>2026-09-22</span><span>           </span><span># 必填，发布日期</span></div></div><div><div><div>4</div></div><div><span>description</span><span>: </span><span>一两句话的摘要</span><span>       </span><span># 建议填，列表页和分享卡会显示</span></div></div><div><div><div>5</div></div><div><span>tags</span><span>: [</span><span>建站</span><span>, </span><span>随笔</span><span>]               </span><span># 标签，方括号里逗号分隔，可以多个</span></div></div><div><div><div>6</div></div><div><span>category</span><span>: </span><span>技术</span><span>                  </span><span># 分类，和所在文件夹保持一致</span></div></div><div><div><div>7</div></div><div><span>draft</span><span>: </span><span>false</span><span>                    </span><span># false = 正式发布；true = 草稿</span></div></div><div><div><div>8</div></div><div><span>slug</span><span>: </span><span>my-first-post</span><span>             </span><span># 可选，自定义 URL；不填就用文件名</span></div></div><div><div><div>9</div></div><div><span>---</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>正文从这里开始，标准 Markdown 语法……</span></div></div></code></pre><div><div></div><div></div></div></figure></div><blockquote><p><strong>脚手架用户注意</strong>：<code>pnpm new-post</code> 生成的模板里有一行 <code>image: ''</code>（空值 = 不显示封面）。<strong>想要随机二次元封面，就把这一行删掉</strong>；想指定封面再填图片直链。</p></blockquote></section><section><h3>frontmatter 字段速查<a href="#frontmatter-字段速查"><span>#</span></a></h3><p>frontmatter 就是文件开头两行 <code>---</code> 之间的那段 YAML，是文章的”身份证”。常用字段：</p>

<table><thead><tr><th>字段</th><th>必填</th><th>作用</th><th>说明</th></tr></thead><tbody><tr><td><code>title</code></td><td>✅</td><td>文章标题</td><td>列表、页面标题都用它</td></tr><tr><td><code>published</code></td><td>✅</td><td>发布日期</td><td>决定排序和归档年份</td></tr><tr><td><code>description</code></td><td>建议</td><td>摘要</td><td>列表页显示；不填会截取正文开头</td></tr><tr><td><code>tags</code></td><td>可选</td><td>标签</td><td><code>[标签A, 标签B]</code> 格式</td></tr><tr><td><code>category</code></td><td>建议</td><td>分类</td><td>一篇只归一类，建议与文件夹同名</td></tr><tr><td><code>draft</code></td><td>可选</td><td>草稿开关</td><td><code>true</code> 只在本地预览可见，构建时排除</td></tr><tr><td><code>slug</code></td><td>建议</td><td>URL 地址</td><td>见下方「URL 规则」</td></tr><tr><td><code>image</code></td><td>可选</td><td>封面图</td><td>删掉这行 = 随机封面；填直链 = 指定封面；<code>''</code> = 无封面</td></tr><tr><td><code>pinned</code></td><td>可选</td><td>置顶</td><td><code>true</code> 固定在文章列表最前面</td></tr><tr><td><code>updated</code></td><td>可选</td><td>最后编辑时间</td><td>改过文章后填新日期</td></tr><tr><td><code>series</code></td><td>可选</td><td>系列名</td><td>同名系列自动组成系列页，配合 <code>seriesOrder: 1</code> 排序</td></tr><tr><td><code>password</code></td><td>可选</td><td>访问密码</td><td>填了则文章加密，读者需输密码；<code>passwordHint</code> 可给提示</td></tr></tbody></table><p>几个要点：</p><ul>
<li><strong>URL 规则</strong>：<code>tech/docker-basics.md</code> 不写 <code>slug</code> 时地址是 <code>/posts/tech/docker-basics/</code>；写了 <code>slug: docker-basics</code> 则是干净的 <code>/posts/docker-basics/</code>。<strong>建议每篇都写 <code>slug</code></strong>，以后调整文件夹不影响链接；</li>
<li><strong>中文文件名可以用</strong>（脚手架会自动转拼音），但手动建文件时建议文件名用英文短横线风格，URL 更好看；</li>
<li><strong>时区坑点</strong>：<code>published</code> 只写日期没问题；如果精确到几点几分，必须写成 <code>2026-09-21T09:00:00+08:00</code> 带时区的格式。因为构建机在 GitHub 的 UTC 时区，不带时区会被当成 UTC，时间全部偏移 8 小时。</li>
</ul></section><section><h3>保存，看效果<a href="#保存看效果"><span>#</span></a></h3><p>保存文件，浏览器（<code>http://localhost:4321</code>）会自动刷新——首页列表里出现你的新文章了。点进去检查排版，改到满意为止。</p></section></section>
<section><h2>第 2 步：正文常用写法<a href="#第-2-步正文常用写法"><span>#</span></a></h2><p>正文就是标准 Markdown，下面每种给一个最小可复制示例。</p><section><h3>基础排版<a href="#基础排版"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>## 二级标题（一级标题留给文章大标题，正文从二级开始）</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>普通段落，</span><span>**加粗**</span><span>、</span><span>*斜体*</span><span>、~~删除线~~、</span><span><span>`</span><span>行内代码</span><span>`</span></span><span>。</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>-</span><span> 无序列表项</span></span></div></div><div><div><div>6</div></div><div><span><span>-</span><span> 另一项</span></span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span><span>1.</span><span> 有序列表项</span></span></div></div><div><div><div>9</div></div><div><span><span>2.</span><span> 另一项</span></span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>&gt; 引用块，适合放注意事项或台词</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>[</span><span>链接文字</span><span>]</span><span><span>(</span><span>https://example.com</span><span>)</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>图片<a href="#图片"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>![</span><span>图片说明</span><span>]</span><span><span>(</span><span>https://example.com/photo.jpg</span><span>)</span></span><span>   # 网络图片，直接贴直链</span></div></div><div><div><div>2</div></div><div><span>![</span><span>本地图片</span><span>]</span><span><span>(</span><span>/assets/images/photo.jpg</span><span>)</span></span><span>        # 本地图片放到 public/assets/images/ 下</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>代码块<a href="#代码块"><span>#</span></a></h3><p>三个反引号开头，后面跟语言名：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>```js</span></div></div><div><div><div>2</div></div><div><span><span>console</span><span>.</span></span><span>log</span><span>(</span><span>"Hello, blog!"</span><span>);</span></div></div><div><div><div>3</div></div><div><span>```</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>支持语法高亮和复制按钮。<strong>超过 15 行的长代码会自动折叠</strong>（显示前 8 行 + 展开按钮），也可以在语言名后面写 <code>collapse</code> 手动控制。</p></section><section><h3>提示框（callout）<a href="#提示框callout"><span>#</span></a></h3><p><code>&gt; [!类型]</code> 开头的引用块会渲染成彩色提示框，支持五种类型：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>&gt; </span><span>[</span><span>!NOTE</span><span>]</span></div></div><div><div><div>2</div></div><div><span>&gt; 补充说明，蓝色。</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>&gt; </span><span>[</span><span>!TIP</span><span>]</span></div></div><div><div><div>5</div></div><div><span>&gt; 小技巧，绿色。</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>&gt; </span><span>[</span><span>!IMPORTANT</span><span>]</span></div></div><div><div><div>8</div></div><div><span>&gt; 重要信息，紫色。</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>&gt; </span><span>[</span><span>!WARNING</span><span>]</span></div></div><div><div><div>11</div></div><div><span>&gt; 警告内容，黄色。</span></div></div><div><div><div>12</div></div><div>
</div></div><div><div><div>13</div></div><div><span>&gt; </span><span>[</span><span>!CAUTION</span><span>]</span></div></div><div><div><div>14</div></div><div><span>&gt; 危险操作，红色。</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>表格<a href="#表格"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>| 姓名 | 作用 |</span></div></div><div><div><div>2</div></div><div><span>| --- | --- |</span></div></div><div><div><div>3</div></div><div><span>| Astro | 静态站点框架 |</span></div></div><div><div><div>4</div></div><div><span>| Svelte | 前端交互框架 |</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>数学公式<a href="#数学公式"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>行内公式：$E = mc^2$</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>独立公式块：</span></div></div><div><div><div>4</div></div><div><span>$$</span></div></div><div><div><div>5</div></div><div><span>\int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi}</span></div></div><div><div><div>6</div></div><div><span>$$</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>流程图（Mermaid）<a href="#流程图mermaid"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>```mermaid</span></div></div><div><div><div>2</div></div><div><span>graph LR</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>A[写文章] --&gt; B[git push]</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>B --&gt; C[自动构建]</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>C --&gt; D[自动上线]</span></div></div><div><div><div>6</div></div><div><span>```</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>时序图（PlantUML）<a href="#时序图plantuml"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>```plantuml</span></div></div><div><div><div>2</div></div><div><span>@startuml</span></div></div><div><div><div>3</div></div><div><span>游客 -&gt; 网站 : 打开文章</span></div></div><div><div><div>4</div></div><div><span>网站 --&gt; 游客 : 返回内容</span></div></div><div><div><div>5</div></div><div><span>@enduml</span></div></div><div><div><div>6</div></div><div><span>```</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>每篇文章自带的「AI 智能总结」<a href="#每篇文章自带的ai-智能总结"><span>#</span></a></h3><p>文章页顶部有个「AI 智能总结」卡片，<strong>不需要你做任何事</strong>：</p><ul>
<li>摘要在网站构建时用免费 AI 预先生成好，直接烘进页面，访客秒开；</li>
<li>只在你改了文章内容后，下次构建才会重新生成那篇的摘要；</li>
<li>开关、接口、字数、逐字特效等参数都在 <code>src/config/index.ts</code> 的「AI 总结」模块；</li>
<li>只在构建时把正文发给 AI 一次，不收集任何访客信息。</li>
</ul></section></section>
<section><h2>第 3 步：发布上线<a href="#第-3-步发布上线"><span>#</span></a></h2><p>本地满意后，三连发布：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>add</span><span> </span><span>-A</span><span>                                        </span><span># 1. 把所有改动放进暂存区</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>commit</span><span> </span><span>-m</span><span> </span><span>"post: 新文章「我的第一篇文章」"</span><span>       </span><span># 2. 打包成一个存档点</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>push</span><span>                                          </span><span># 3. 推送到 GitHub</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>推送后 GitHub 会<strong>自动构建</strong>，构建完成后服务器<strong>自动拉取上线</strong>，约两三分钟后刷新你的线上网站就能看到新文章——不需要登录服务器做任何操作。</p><p>修改已发布的文章也一样：改文件 → 重新三连即可，链接不变（这就是建议写 <code>slug</code> 的原因）。</p></section>
<section><h2>常见问题排查<a href="#常见问题排查"><span>#</span></a></h2><p><strong>改了内容但页面没变？</strong></p><ol>
<li>先确认 <code>pnpm dev</code> 还开着、浏览器开着的是 <code>localhost:4321</code>；</li>
<li>还不行就 <code>Ctrl + C</code> 停掉，重新 <code>pnpm dev</code>（改 frontmatter 后偶尔需要）；</li>
<li>再不行删掉项目根目录的 <code>.astro</code> 文件夹后重启——这是构建缓存，删了没副作用。</li>
</ol><p><strong>线上要等才生效？</strong> 记住分界线：<code>pnpm dev</code> 里的改动只存在于你电脑；只有 <code>git push</code> 之后才会上线。另外改 <code>src/config/index.ts</code> 里的构建类配置（如 AI 总结），本地也要重新 <code>pnpm build</code> 才能验证。</p><p><strong>发布日期不对？</strong> 见上方时区坑点，时间精确到分时必须带 <code>+08:00</code>。</p><p><strong>文章不想让人看到？</strong> <code>draft: true</code> 转草稿，只在本地可见。</p></section>
<section><h2>发动态<a href="#发动态"><span>#</span></a></h2><p>动态是短内容随手记，和文章分开管理，存放在 <code>src/content/dynamic/</code>。</p><p>一行命令发一条：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>pnpm</span><span> </span><span>new-dynamic</span><span> </span><span>--</span><span> </span><span>"今天给博客加了个状态页，折腾了一下午，值！"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>会在动态目录生成一个按时间命名的 <code>.md</code> 文件，内容就是你刚才输入的文字，<code>published</code> 时间自动填好。也可以手动建文件，frontmatter 只有两三个字段：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>published</span><span>: </span><span>2026-09-21T20:30:00+08:00</span><span>   </span><span># 必填，精确到分记得带时区</span></div></div><div><div><div>3</div></div><div><span>pinned</span><span>: </span><span>false</span><span>                          </span><span># 可选，true 置顶在动态流最前</span></div></div><div><div><div>4</div></div><div><span>location</span><span>: </span><span>上海</span><span>                         </span><span># 可选，显示在动态卡片上</span></div></div><div><div><div>5</div></div><div><span>---</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span>这里写动态内容，支持 Markdown。图片地址写在正文里会自动渲染成图片墙。</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>小坑</strong>：删除或修改动态后页面没变化，是构建缓存问题——删掉 <code>.astro</code> 目录重启 <code>pnpm dev</code> 就好。</p></section>
<section><h2>添加书签导航<a href="#添加书签导航"><span>#</span></a></h2><p>书签导航页（<code>/booknav/</code>）的数据在 <code>src/config/index.ts</code> 的「书签导航」模块，按「分类组 → 书签项」两级组织。把下面示例粘进去改成自己的书签即可：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>booknavConfig</span><span>:</span><span> </span><span>BooknavGroup</span><span>[] </span><span>=</span><span> [</span></div></div><div><div><div>2</div></div><div><span><span>  </span></span><span>{</span></div></div><div><div><div>3</div></div><div><span>    </span><span>// —— 分类组 ——</span></div></div><div><div><div>4</div></div><div><span>    </span><span>id</span><span>:</span><span> </span><span>"dev"</span><span>, </span><span>// 唯一标识</span></div></div><div><div><div>5</div></div><div><span>    </span><span>name</span><span>:</span><span> </span><span>"开发"</span><span>, </span><span>// 分类名称</span></div></div><div><div><div>6</div></div><div><span>    </span><span>icon</span><span>:</span><span> </span><span>"material-symbols:code-rounded"</span><span>, </span><span>// 分类图标（Iconify 格式）</span></div></div><div><div><div>7</div></div><div><span>    </span><span>desc</span><span>:</span><span> </span><span>"写代码时离不开的站点"</span><span>, </span><span>// 分类描述，可省略</span></div></div><div><div><div>8</div></div><div><span>    </span><span>weight</span><span>:</span><span> </span><span>100</span><span>, </span><span>// 排序权重，越大越靠前</span></div></div><div><div><div>9</div></div><div><span>    </span><span>items</span><span>:</span><span> [</span></div></div><div><div><div>10</div></div><div><span><span>      </span></span><span>{</span></div></div><div><div><div>11</div></div><div><span>        </span><span>// —— 书签项 ——</span></div></div><div><div><div>12</div></div><div><span>        </span><span>title</span><span>:</span><span> </span><span>"MDN Web Docs"</span><span>,</span></div></div><div><div><div>13</div></div><div><span>        </span><span>url</span><span>:</span><span> </span><span>"https://developer.mozilla.org"</span><span>,</span></div></div><div><div><div>14</div></div><div><span>        </span><span>desc</span><span>:</span><span> </span><span>"最权威的 Web 技术文档"</span><span>, </span><span>// 可省略</span></div></div><div><div><div>15</div></div><div><span>        </span><span>weight</span><span>:</span><span> </span><span>9</span><span>, </span><span>// 组内排序</span></div></div><div><div><div>16</div></div><div><span><span>      </span></span><span>},</span></div></div><div><div><div>17</div></div><div><span><span>      </span></span><span>{</span></div></div><div><div><div>18</div></div><div><span>        </span><span>title</span><span>:</span><span> </span><span>"Astro 文档"</span><span>,</span></div></div><div><div><div>19</div></div><div><span>        </span><span>url</span><span>:</span><span> </span><span>"https://docs.astro.build"</span><span>,</span></div></div><div><div><div>20</div></div><div><span><span>      </span></span><span>},</span></div></div><div><div><div>21</div></div><div><span><span>    </span></span><span>],</span></div></div><div><div><div>22</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>23</div></div><div><span>];</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p><strong>图标不用手动找图</strong>：书签项没写 <code>icon</code> 时会自动抓取目标站点的 favicon。</p></section>
<section><h2>其他常用配置速查<a href="#其他常用配置速查"><span>#</span></a></h2>

<table><thead><tr><th>想做什么</th><th>改哪里</th></tr></thead><tbody><tr><td>站名 / 副标题 / 主题色 / 域名</td><td><code>src/config/index.ts</code> ① 站点基础（站名改 <code>SITE_TITLE</code> 一处即可）</td></tr><tr><td>头像 / 昵称 / 社交链接</td><td><code>src/config/index.ts</code> ② 用户资料（首页横幅链接同步生效）</td></tr><tr><td>开关某个页面（如相册、友链）</td><td>① 站点基础里的 <code>pages</code> 字段</td></tr><tr><td>导航栏菜单与顺序</td><td><code>src/config/index.ts</code> ④ 导航栏</td></tr><tr><td>侧边栏组件与顺序</td><td><code>src/config/index.ts</code> ⑥ 侧边栏布局（桌面与移动端共用的组件在 <code>WIDGETS</code> 里改一处）</td></tr><tr><td>首页壁纸与横幅文案</td><td><code>src/config/index.ts</code> ⑦ 背景壁纸</td></tr><tr><td>相册</td><td><code>src/config/index.ts</code> ⑮ 相册 + 图片放 <code>public/gallery/</code></td></tr><tr><td>页脚文字</td><td><code>src/config/FooterConfig.html</code></td></tr><tr><td>关于页 / 友链页</td><td><code>src/content/spec/about.md</code> / <code>friends.mdx</code></td></tr></tbody></table><p>两个通用规律：</p><ol>
<li><strong><code>pages</code> 开关是总闸</strong>：把某个页面设为 <code>false</code>，导航、sitemap 里都会自动消失，不用到处删链接；</li>
<li><strong>图标统一用 <a href="https://icon-visualizer.ntl-sync.dev/" target="_blank">Iconify</a> 语法</strong>：<code>图标集:图标名</code>，如 <code>material-symbols:home</code>、<code>fa7-brands:bilibili</code>，全站通用。</li>
</ol><hr /><p>把这篇当手册用：<strong>写文章走第 1～3 步，忘语法查第 2 步，出问题查 FAQ，改配置查速查表</strong>。后续有新的配置心得，我会持续更新这篇文章。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://li8.me/posts/1/</id>
      <title type="text">从零搭建个人博客：手把手完整教程与踩坑记录</title>
      <published>2026-09-21T01:24:17.000Z</published>
      <updated>2026-09-22T00:00:00.000Z</updated>
      <author><name>八奈奈</name></author>
      <link rel="alternate" href="https://li8.me/posts/1/"/>
      <summary type="text">手把手教程：环境准备、项目初始化、站点配置、服务器选购、Nginx 部署到 GitHub Actions 全自动发布，每一步给出可直接复制的命令，并解释背后的原理</summary>
      <content type="html"><![CDATA[<p>这篇文章把我从零搭建这个博客的<strong>全过程</strong>记录下来：每一步怎么操作、用什么命令、为什么这么做、踩过哪些坑。照着做就能复现出一模一样的系统，也当作我自己的运维手册。</p>
<p>日常使用（写文章、发动态）在<a href="/posts/2/">《博客日常使用指南》</a>，这篇只讲「怎么把站搭起来、怎么自动化」。</p>
<blockquote><p>全文涉及敏感信息的地方（服务器 IP、密码等）都用 <code>&lt;占位符&gt;</code> 代替，实际操作时替换成你自己的。</p></blockquote>
<section><h2>一、整体规划<a href="#一整体规划"><span>#</span></a></h2><p>先想清楚要什么，再动手：</p><ul>
<li><strong>静态博客</strong>：文章是 Markdown，构建后是纯 HTML。没有数据库、没有后端、没有登录入口，黑不掉也慢不了，维护成本约等于零；</li>
<li><strong>国内云服务器托管</strong>：文章推到 GitHub 仓库，GitHub Actions 构建完成后，服务器自动拉取构建产物上线，服务器只负责 Nginx 托管静态文件；</li>
<li><strong>全自动发布</strong>：<code>git push</code> 之后构建、分发、上线全部自动，我只负责写文章。</li>
</ul><p>技术栈一句话：<strong>Astro + Firefly 主题 + GitHub Actions + Git + Nginx</strong>。</p></section>
<section><h2>二、环境准备<a href="#二环境准备"><span>#</span></a></h2><p>我的本地环境是 Windows + PowerShell。需要装三样东西，每样都有验证方法。</p><section><h3>1. Node.js（≥ 22）<a href="#1-nodejs-22"><span>#</span></a></h3><p>去 <a href="https://nodejs.org/" target="_blank">Node.js 官网</a> 下载 LTS 安装包，一路下一步。装完<strong>重开一个终端</strong>验证：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>node </span><span>-</span><span>v   </span></span><span># 输出 v22.x.x 即可</span></div></div><div><div><div>2</div></div><div><span><span>npm </span><span>-</span><span>v</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><blockquote><p>老版本不够就先卸载再装新版，Node 多版本共存工具（nvm-windows）是进阶玩法，入门没必要。</p></blockquote></section><section><h3>2. pnpm（≥ 11）<a href="#2-pnpm-11"><span>#</span></a></h3><p>Node 自带 npm，用它装 pnpm：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span><span>npm install </span><span>-</span><span>g pnpm</span></span></div></div><div><div><div>2</div></div><div><span><span>pnpm </span><span>-</span><span>v</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><blockquote><p>坑点：如果项目用 <code>.npmrc</code> 锁定了 pnpm 版本，直接运行 <code>pnpm</code> 时它会自动切换到项目要求的版本，不用手动降级。</p></blockquote></section><section><h3>3. Git<a href="#3-git"><span>#</span></a></h3><p>去 <a href="https://git-scm.com/" target="_blank">git-scm.com</a> 下载安装，安装选项保持默认即可。第一次用 Git 需要配置身份（每条提交记录的署名）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--global</span><span> </span><span>user.name</span><span> </span><span>"你的用户名"</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>config</span><span> </span><span>--global</span><span> </span><span>user.email</span><span> </span><span>"你的邮箱"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><blockquote><p>坑点：Windows 下有些终端装完 Git 后找不到命令，重开一个终端窗口就好；还不行就把 Git 的 <code>cmd</code> 目录加进 <code>PATH</code> 环境变量。</p></blockquote></section></section>
<section><h2>三、初始化项目<a href="#三初始化项目"><span>#</span></a></h2><section><h3>1. 拿到主题代码<a href="#1-拿到主题代码"><span>#</span></a></h3><p>在 GitHub 上把 <a href="https://github.com/CuteLeaf/Firefly" target="_blank">Firefly 主题仓库</a> Fork（或右上角 Use this template）到自己账号下，然后克隆到本地：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>https://github.com/&lt;你的用户名&gt;/&lt;仓库名&gt;.git</span></div></div><div><div><div>2</div></div><div><span>cd</span><span> </span><span>&lt;仓库名&gt;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>建议直接在 GitHub 上把仓库设为 <strong>Private</strong>——文章草稿、服务器配置这些不需要公开。</p></section><section><h3>2. 安装依赖并启动<a href="#2-安装依赖并启动"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>pnpm</span><span> </span><span>install</span><span>    </span><span># 第一次运行，下载所有依赖，可能要几分钟</span></div></div><div><div><div>2</div></div><div><span>pnpm</span><span> </span><span>dev</span><span>        </span><span># 启动本地开发服务器</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>浏览器打开 <code>http://localhost:4321</code>，看到主题默认首页就成功了。dev server 支持热更新：改代码保存，页面自动刷新。</p></section><section><h3>3. 认识目录结构<a href="#3-认识目录结构"><span>#</span></a></h3><p>几千个文件里，你日常只需要碰这几处：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>├── astro.config.mjs        # Astro 全局配置（构建、插件、trailingSlash 等）</span></div></div><div><div><div>2</div></div><div><span>├── src/</span></div></div><div><div><div>3</div></div><div><span>│   ├── config/</span></div></div><div><div><div>4</div></div><div><span>│   │   ├── index.ts        # ★ 站点所有配置都在这一个文件（按模块分区编号）</span></div></div><div><div><div>5</div></div><div><span>│   │   └── FooterConfig.html  # 页脚文字（唯一的例外）</span></div></div><div><div><div>6</div></div><div><span>│   ├── content/</span></div></div><div><div><div>7</div></div><div><span>│   │   ├── posts/          # ★ 文章（Markdown，按分类建文件夹）</span></div></div><div><div><div>8</div></div><div><span>│   │   ├── dynamic/        # 动态（短内容随手记）</span></div></div><div><div><div>9</div></div><div><span>│   │   └── spec/           # 关于页/友链页等固定页面</span></div></div><div><div><div>10</div></div><div><span>│   └── styles/             # 样式微调</span></div></div><div><div><div>11</div></div><div><span>└── public/                 # 纯静态资源（favicon、壁纸、相册图片）</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>记住一条：<strong>想改任何功能和文案，先打开 <code>src/config/index.ts</code> 找编号注释，九成能找到</strong>。</p></section></section>
<section><h2>四、站点配置<a href="#四站点配置"><span>#</span></a></h2><p>配置都是改 TypeScript 文件，保存即时生效。所有配置集中在 <code>src/config/index.ts</code>，按模块编号分区。</p><section><h3>1. 站点基本信息（① 站点基础模块）<a href="#1-站点基本信息-站点基础模块"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>// 站点名称：站点信息、导航栏标题、首页横幅共用这一处</span></div></div><div><div><div>2</div></div><div><span>const</span><span> </span><span>SITE_TITLE</span><span> </span><span>=</span><span> </span><span>"你的博客名"</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>siteConfig</span><span>:</span><span> </span><span>SiteConfig</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>5</div></div><div><span>  </span><span>title</span><span>:</span><span> </span><span>SITE_TITLE</span><span>,</span></div></div><div><div><div>6</div></div><div><span>  </span><span>subtitle</span><span>:</span><span> </span><span>"一句话简介"</span><span>,</span></div></div><div><div><div>7</div></div><div><span>  </span><span>site_url</span><span>:</span><span> </span><span>"https://你的域名.com"</span><span>,   </span><span>// 有域名填域名，暂时没有可以先填 IP 方案</span></div></div><div><div><div>8</div></div><div><span>  </span><span>themeColor</span><span>:</span><span> {</span></div></div><div><div><div>9</div></div><div><span>    </span><span>hue</span><span>:</span><span> </span><span>220</span><span>,          </span><span>// 0-360 色相，一个数字全站变色（220 是蓝色）</span></div></div><div><div><div>10</div></div><div><span>    </span><span>defaultMode</span><span>:</span><span> </span><span>"light"</span><span>,</span></div></div><div><div><div>11</div></div><div><span><span>  </span></span><span>},</span></div></div><div><div><div>12</div></div><div><span>  </span><span>siteStartDate</span><span>:</span><span> </span><span>"2026-09-20"</span><span>,       </span><span>// 建站日期，页脚的运行天数从这里算</span></div></div><div><div><div>13</div></div><div><span>  </span><span>// ...</span></div></div><div><div><div>14</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>pages</code> 字段是各功能页的总闸，用不到的设为 <code>false</code>——导航、sitemap 里会自动消失，不用到处删链接：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>const</span><span> </span><span>pages</span><span> </span><span>=</span><span> </span><span>resolvePageToggles</span><span>({</span></div></div><div><div><div>2</div></div><div><span>  </span><span>friends</span><span>:</span><span> </span><span>true</span><span>,     </span><span>// 友链</span></div></div><div><div><div>3</div></div><div><span>  </span><span>dynamic</span><span>:</span><span> </span><span>true</span><span>,     </span><span>// 动态</span></div></div><div><div><div>4</div></div><div><span>  </span><span>gallery</span><span>:</span><span> </span><span>true</span><span>,     </span><span>// 相册</span></div></div><div><div><div>5</div></div><div><span>  </span><span>booknav</span><span>:</span><span> </span><span>true</span><span>,     </span><span>// 书签导航</span></div></div><div><div><div>6</div></div><div><span>  </span><span>bilibili</span><span>:</span><span> </span><span>true</span><span>,    </span><span>// 追番</span></div></div><div><div><div>7</div></div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>2. 作者信息（② 用户资料模块）<a href="#2-作者信息-用户资料模块"><span>#</span></a></h3><p>头像图片放进 <code>src/assets/images/</code>，然后在配置里引用：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>profileConfig</span><span>:</span><span> </span><span>ProfileConfig</span><span> </span><span>=</span><span> {</span></div></div><div><div><div>2</div></div><div><span>  </span><span>avatar</span><span>:</span><span> </span><span>"assets/images/avatar.png"</span><span>,</span></div></div><div><div><div>3</div></div><div><span>  </span><span>name</span><span>:</span><span> </span><span>"你的昵称"</span><span>,</span></div></div><div><div><div>4</div></div><div><span>  </span><span>bio</span><span>:</span><span> </span><span>"一句话介绍自己"</span><span>,</span></div></div><div><div><div>5</div></div><div><span>  </span><span>links</span><span>:</span><span> [</span></div></div><div><div><div>6</div></div><div><span>    </span><span>// 图标用 Iconify 格式，showName: false 只显示图标</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>{ </span><span>name</span><span>:</span><span> </span><span>"QQ群"</span><span>, </span><span>icon</span><span>:</span><span> </span><span>"fa7-brands:qq"</span><span>, </span><span>url</span><span>:</span><span> </span><span>"https://qm.qq.com/xxx"</span><span>, </span><span>showName</span><span>:</span><span> </span><span>false</span><span> },</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>{ </span><span>name</span><span>:</span><span> </span><span>"Email"</span><span>, </span><span>icon</span><span>:</span><span> </span><span>"fa7-solid:envelope"</span><span>, </span><span>url</span><span>:</span><span> </span><span>"mailto:你的邮箱"</span><span> },</span></div></div><div><div><div>9</div></div><div><span><span>  </span></span><span>],</span></div></div><div><div><div>10</div></div><div><span>};</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>这份 <code>links</code> 会同时用在侧边栏资料卡和首页横幅上，只改这一处。</p></section><section><h3>3. 其他常用模块<a href="#3-其他常用模块"><span>#</span></a></h3><p>配置文件里每个模块都有中文注释，改之前读一遍注释就够了。我启用了几个值得一说的：</p><ul>
<li><strong>AI 总结（⑩）</strong>：构建时调用免费 AI 接口给每篇文章生成摘要，无需密钥，参数都在注释里；</li>
<li><strong>随机封面（⑪）</strong>：文章不写 <code>image</code> 字段就自动配随机二次元封面，图源是按顺序备用的公共 API 列表；</li>
<li><strong>追番页（Bilibili UID）</strong>：<code>siteConfig</code> 的 <code>bilibili.uid</code> 填自己的 B 站 UID，追番进度自动同步。</li>
</ul></section><section><h3>4. 写一篇文章试手<a href="#4-写一篇文章试手"><span>#</span></a></h3><p>文章就是 <code>src/content/posts/</code> 下的 Markdown 文件，头部 frontmatter 是元信息：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>---</span></div></div><div><div><div>2</div></div><div><span>title</span><span>: </span><span>我的第一篇测试文章</span></div></div><div><div><div>3</div></div><div><span>published</span><span>: </span><span>2026-09-21</span></div></div><div><div><div>4</div></div><div><span>description</span><span>: </span><span>摘要，会显示在列表页</span></div></div><div><div><div>5</div></div><div><span>tags</span><span>: [</span><span>标签1</span><span>, </span><span>标签2</span><span>]</span></div></div><div><div><div>6</div></div><div><span>category</span><span>: </span><span>技术</span></div></div><div><div><div>7</div></div><div><span>draft</span><span>: </span><span>false</span></div></div><div><div><div>8</div></div><div><span>slug</span><span>: </span><span>my-first-post</span><span>   </span><span># 不填则用「文件夹+文件名」当 URL</span></div></div><div><div><div>9</div></div><div><span>---</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span>正文用 Markdown 写……</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>保存后回浏览器看效果。完整的逐字段说明、正文语法、发布流程都在<a href="/posts/2/">使用指南</a>里，这里不重复。</p></section></section>
<section><h2>五、踩坑记录<a href="#五踩坑记录"><span>#</span></a></h2><p>搭的过程中遇到的三个印象最深的坑，写下来防止后人（和未来的我）重踩。</p><section><h3>坑 1：不带斜杠的 URL 404<a href="#坑-1不带斜杠的-url-404"><span>#</span></a></h3><p>Astro 配置了 <code>trailingSlash: "always"</code> 后，<code>/posts/xxx</code>（无斜杠）直接 404。而生产环境静态托管对无斜杠请求一般会自动 301 补斜杠，本地却不行，行为不一致。</p><p><strong>解决</strong>：把配置改成 <code>"ignore"</code>，再加一个中间件统一重定向：</p><div><div><div><figure><figcaption><span>src/middleware.ts</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span>import</span><span> { </span><span>defineMiddleware</span><span> } </span><span>from</span><span> </span><span>"astro:middleware"</span><span>;</span></div></div><div><div><div>2</div></div><div>
</div></div><div><div><div>3</div></div><div><span>export</span><span> </span><span>const</span><span> </span><span>onRequest</span><span> </span><span>=</span><span> </span><span>defineMiddleware</span><span><span>((</span><span>context</span><span>, </span><span>next</span><span>) </span></span><span>=&gt;</span><span> {</span></div></div><div><div><div>4</div></div><div><span>  </span><span>const</span><span> </span><span>parsed</span><span> </span><span>=</span><span> </span><span>new</span><span> </span><span>URL</span><span><span>(</span><span>context</span><span>.</span></span><span>request</span><span>.</span><span>url</span><span>);</span></div></div><div><div><div>5</div></div><div><span>  </span><span>const</span><span> { </span><span>pathname</span><span> } </span><span>=</span><span><span> </span><span>parsed</span><span>;</span></span></div></div><div><div><div>6</div></div><div><span>  </span><span>const</span><span> </span><span>last</span><span> </span><span>=</span><span><span> </span><span>pathname</span><span>.</span></span><span>split</span><span>(</span><span>"/"</span><span>).</span><span>pop</span><span>() </span><span>??</span><span> </span><span>""</span><span>;</span></div></div><div><div><div>7</div></div><div><span>  </span><span>// 跳过：根路径 / 已带斜杠 / 带扩展名的文件 / Astro 与 Vite 内部端点</span></div></div><div><div><div>8</div></div><div><span>  </span><span>if</span><span> (</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>pathname</span><span> </span><span>===</span><span> </span><span>"/"</span><span> </span><span>||</span><span><span> </span><span>pathname</span><span>.</span></span><span>endsWith</span><span>(</span><span>"/"</span><span>) </span><span>||</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>last</span><span>.</span><span>includes</span><span>(</span><span>"."</span><span>) </span><span>||</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>pathname</span><span>.</span><span>startsWith</span><span>(</span><span>"/@"</span><span>) </span><span>||</span><span><span> </span><span>pathname</span><span>.</span></span><span>startsWith</span><span>(</span><span>"/_"</span><span>)</span></div></div><div><div><div>12</div></div><div><span><span>  </span></span><span>) </span><span>return</span><span> </span><span>next</span><span>();</span></div></div><div><div><div>13</div></div><div><span>  </span><span>return</span><span><span> </span><span>context</span><span>.</span></span><span>redirect</span><span><span>(</span><span>pathname</span><span> </span></span><span>+</span><span> </span><span>"/"</span><span> </span><span>+</span><span><span> </span><span>parsed</span><span>.</span></span><span>search</span><span>,</span></div></div><div><div><div>14</div></div><div><span>    </span><span>import</span><span>.</span><span>meta</span><span>.</span><span>env</span><span>.</span><span>DEV</span><span> </span><span>?</span><span> </span><span>302</span><span> </span><span>:</span><span> </span><span>301</span><span>);</span></div></div><div><div><div>15</div></div><div><span>});</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><blockquote><p>坑中坑：重定向状态码在开发环境要用 302，别用 301——301 会被浏览器永久缓存，万一重定向规则写错了，之后怎么改都跳旧地址，只能清缓存。</p></blockquote></section><section><h3>坑 2：主题色改了但浏览器没变<a href="#坑-2主题色改了但浏览器没变"><span>#</span></a></h3><p>改了色相值刷新还是旧颜色，其实是浏览器 <code>localStorage</code> 缓存了旧值（主题把用户的色相选择存在里面）。清站点数据或 <code>Ctrl+F5</code> 强刷即可。不是代码的问题。</p></section><section><h3>坑 3：dev server 莫名崩溃<a href="#坑-3dev-server-莫名崩溃"><span>#</span></a></h3><p>Windows 上有个系统临时文件在盘符根目录，被 Vite 的文件监听扫到时会抛 <code>EINVAL</code> 直接把 dev server 搞崩。</p><p><strong>解决</strong>两步走：在 <code>astro.config.mjs</code> 里把该文件加入 <code>server.watch.ignored</code> 忽略列表（治本），再写个循环脚本崩溃后自动重启（兜底）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>while</span><span> (</span><span>$true</span><span>) { pnpm dev; </span><span>Start-Sleep</span><span><span> </span><span>-</span><span>Seconds </span></span><span>3</span><span> }</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>教训：玄学崩溃先看完整报错栈，定位到具体文件路径，别急着怀疑自己的代码。</p></section></section>
<section><h2>六、服务器选购与初始化<a href="#六服务器选购与初始化"><span>#</span></a></h2><section><h3>1. 配置选择<a href="#1-配置选择"><span>#</span></a></h3><p>静态博客对配置要求极低，我选的是国内云厂商的入门款：</p>

<table><thead><tr><th>项</th><th>配置</th><th>说明</th></tr></thead><tbody><tr><td>CPU/内存</td><td>2核 2G</td><td>纯静态托管绰绰有余</td></tr><tr><td>带宽</td><td>固定 3 Mbps</td><td>个人博客流量足够，以后可套 CDN</td></tr><tr><td>系统盘</td><td>40G SSD</td><td>用不满</td></tr><tr><td>系统</td><td>Ubuntu 26.04 LTS</td><td>生态最省心</td></tr><tr><td>价格</td><td>百元级/年</td><td>新人活动价</td></tr></tbody></table><p><strong>注意</strong>：国内服务器绑定域名需要 ICP 备案，备案期间可以先用 IP 访问调试（备案只管域名，IP 直连不受影响）。</p><p>买完第一件事：在控制台<strong>重置服务器密码</strong>，记下来，下面 SSH 要用。</p></section><section><h3>2. 放行端口<a href="#2-放行端口"><span>#</span></a></h3><p>云服务器默认只开 SSH(22)。去控制台 <strong>安全组 → 入方向 → 添加规则</strong>，放行：</p><ul>
<li><code>80/80</code>（HTTP）</li>
<li><code>443/443</code>（HTTPS，以后用）</li>
</ul><p>协议 TCP，源 <code>0.0.0.0/0</code>。</p><blockquote><p>坑点：服务器内装好一切却访问不了，八成是安全组没放行——在服务器里 curl 是通的，外面就是连不上。系统自带的防火墙（ufw）一般是关闭的，问题基本都出在云平台的安全组这一层。</p></blockquote></section><section><h3>3. SSH 免密登录<a href="#3-ssh-免密登录"><span>#</span></a></h3><p>每次输密码太麻烦，配一下密钥登录：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 本地生成密钥对（一路回车）</span></div></div><div><div><div>2</div></div><div><span>ssh-keygen</span><span> </span><span>-t</span><span> </span><span>ed25519</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 把公钥装到服务器上（这一步需要输一次密码）</span></div></div><div><div><div>5</div></div><div><span>ssh</span><span> </span><span>&lt;用户名&gt;@&lt;服务器IP&gt;</span><span> </span><span>"mkdir -p ~/.ssh &amp;&amp; cat &gt;&gt; ~/.ssh/authorized_keys &amp;&amp; chmod 600 ~/.ssh/authorized_keys"</span><span> </span><span>&lt;</span><span> </span><span>~/.ssh/id_ed25519.pub</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 测试：不再要密码即成功</span></div></div><div><div><div>8</div></div><div><span>ssh</span><span> &lt;</span><span>用户名&gt;@&lt;服务器IP&gt;</span><span> </span><span>"echo ok"</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>原理：登录时服务器用你的公钥验证客户端持有的私钥，密码不再参与。</p></section></section>
<section><h2>七、首次部署<a href="#七首次部署"><span>#</span></a></h2><section><h3>1. 本地构建<a href="#1-本地构建"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>pnpm</span><span> </span><span>build</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>产物在 <code>dist/</code> 目录：每个页面是 <code>目录名/index.html</code>，带哈希的 CSS/JS 在 <code>_astro/</code> 下。可以本地先看看产物长什么样。</p></section><section><h3>2. 服务器装 Nginx 并上传<a href="#2-服务器装-nginx-并上传"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 服务器上安装 Nginx 并建站点目录</span></div></div><div><div><div>2</div></div><div><span>ssh</span><span> </span><span>&lt;用户名&gt;@&lt;服务器IP&gt;</span><span> </span><span>"apt update &amp;&amp; apt install -y nginx &amp;&amp; mkdir -p /var/www/blog"</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 本地打包上传（比逐个文件 scp 快得多）</span></div></div><div><div><div>5</div></div><div><span>tar</span><span> </span><span>-czf</span><span> </span><span>dist.tar.gz</span><span> </span><span>dist</span></div></div><div><div><div>6</div></div><div><span>scp</span><span> </span><span>dist.tar.gz</span><span> </span><span>&lt;用户名&gt;@&lt;服务器IP&gt;:/tmp/</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># 服务器上解压</span></div></div><div><div><div>9</div></div><div><span>ssh</span><span> </span><span>&lt;用户名&gt;@&lt;服务器IP&gt;</span><span> </span><span>"tar -xzf /tmp/dist.tar.gz -C /var/www/blog --strip-components=1"</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section><section><h3>3. Nginx 站点配置<a href="#3-nginx-站点配置"><span>#</span></a></h3><p>在服务器上创建 <code>/etc/nginx/sites-available/blog</code>：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>server</span><span> {</span></div></div><div><div><div>2</div></div><div><span>   <span> </span></span><span>listen </span><span>80</span><span> </span><span>default_server</span><span>;</span></div></div><div><div><div>3</div></div><div><span>   <span> </span></span><span>server_name </span><span>_;</span></div></div><div><div><div>4</div></div><div><span>   <span> </span></span><span>root </span><span>/var/www/blog;</span></div></div><div><div><div>5</div></div><div><span>   <span> </span></span><span>index </span><span>index.html;</span></div></div><div><div><div>6</div></div><div><span>   <span> </span></span><span>charset </span><span>utf-8;</span></div></div><div><div><div>7</div></div><div><span>   <span> </span></span><span>error_page </span><span>404</span><span> /404.html;      </span><span># Astro 会构建出 404.html</span></div></div><div><div><div>8</div></div><div>
</div></div><div><div><div>9</div></div><div><span>   <span> </span></span><span>gzip </span><span>on</span><span>;                        </span><span># 文本资源压缩，HTML/CSS/JS 体积减 70% 左右</span></div></div><div><div><div>10</div></div><div><span>   <span> </span></span><span>gzip_types </span><span>text/css application/javascript application/json image/svg+xml;</span></div></div><div><div><div>11</div></div><div>
</div></div><div><div><div>12</div></div><div><span>    </span><span># 带哈希的静态资源：内容变了文件名就变，可以放心长缓存</span></div></div><div><div><div>13</div></div><div><span>    </span><span>location</span><span> ^~ </span><span>/_astro/ </span><span>{</span></div></div><div><div><div>14</div></div><div><span>       <span> </span></span><span>expires </span><span>30d</span><span>;</span></div></div><div><div><div>15</div></div><div><span>       <span> </span></span><span>add_header </span><span>Cache-Control </span><span>"public, immutable"</span><span>;</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>17</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>启用并重载：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>ln</span><span> </span><span>-sf</span><span> </span><span>/etc/nginx/sites-available/blog</span><span> </span><span>/etc/nginx/sites-enabled/blog</span></div></div><div><div><div>2</div></div><div><span>rm</span><span> </span><span>-f</span><span> </span><span>/etc/nginx/sites-enabled/default</span><span>   </span><span># 移除默认站点，避免抢占 80 端口</span></div></div><div><div><div>3</div></div><div><span>nginx</span><span> </span><span>-t</span><span>                                  </span><span># 语法检查，必须显示 ok</span></div></div><div><div><div>4</div></div><div><span>systemctl</span><span> </span><span>reload</span><span> </span><span>nginx</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>浏览器打开 <code>http://&lt;服务器IP&gt;</code>，应该就能看到博客了。</p><p><strong>Nginx 的工作方式</strong>：收到请求后按 <code>root</code> 找文件——<code>/posts/xxx/</code> 对应 <code>/var/www/blog/posts/xxx/index.html</code>；请求目录但没带斜杠时自动 301 补斜杠；找不到文件就走 <code>error_page</code> 的 404 页。</p></section></section>
<section><h2>八、全自动发布：原理详解<a href="#八全自动发布原理详解"><span>#</span></a></h2><p>这是整套系统最核心的部分，值得讲透。</p><section><h3>1. Git 基础：一次 push 背后发生了什么<a href="#1-git-基础一次-push-背后发生了什么"><span>#</span></a></h3><p>先建立心智模型。Git 把文件的管理划分成几个「区域」，一次 push 的旅程是这样的：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>工作区                本地仓库                     远程仓库</span></div></div><div><div><div>2</div></div><div><span>(你编辑的文件)   git add→  暂存区  git commit→  (历史记录)  git push→  GitHub</span></div></div><div><div><div>3</div></div><div><span><span> </span></span><span>src/content/            (准备打包)            (.git 目录)</span></div></div></code></pre><div><div></div><div></div></div></figure></div><ul>
<li><strong>工作区</strong>：就是硬盘上看得见的文件，你用编辑器改 Markdown 就是在改工作区；</li>
<li><strong>暂存区（stage）</strong>：下次要打包进提交的文件清单。改动必须先经过它；</li>
<li><strong>本地仓库</strong>：<code>git commit</code> 会在本机生成一个不可变的快照（提交），包含改动了什么、谁改的、什么时候；</li>
<li><strong>远程仓库</strong>：GitHub 上的仓库。<code>git push</code> 把本地的新提交上传过去。</li>
</ul></section><section><h3>2. 日常发文完整命令流<a href="#2-日常发文完整命令流"><span>#</span></a></h3><p>以新写一篇文章为例，完整走一遍：</p><div><div><div><figure><figcaption><span></span><span>Terminal window</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span># ① 先看看当前状态（哪些文件改了、哪些是新增的、哪些还没跟踪）</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>status</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># ② 把改动加入暂存区</span></div></div><div><div><div>5</div></div><div><span>git</span><span> </span><span>add</span><span> </span><span>src/content/posts/my-new-post.md</span><span>   </span><span># 精确添加单个文件</span></div></div><div><div><div>6</div></div><div><span># 或 git add -A                            # 添加全部改动（含删除、改名）</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span># ③ 再看一眼：改动已进入暂存区（绿色），确认没有误加的东西</span></div></div><div><div><div>9</div></div><div><span>git</span><span> </span><span>status</span></div></div><div><div><div>10</div></div><div>
</div></div><div><div><div>11</div></div><div><span># ④ 提交：把暂存区内容固化为一个本地快照，-m 后面是提交说明</span></div></div><div><div><div>12</div></div><div><span>git</span><span> </span><span>commit</span><span> </span><span>-m</span><span> </span><span>"post: 新文章标题"</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span># ⑤ 推送到 GitHub</span></div></div><div><div><div>15</div></div><div><span>git</span><span> </span><span>push</span><span> </span><span>origin</span><span> </span><span>master</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>逐个解释 <code>git push origin master</code> 里的两个词：</p><ul>
<li><code>origin</code> 是<strong>远程仓库的别名</strong>，<code>git clone</code> 时自动指向你克隆来的那个 GitHub 地址（用 <code>git remote -v</code> 查看）；</li>
<li><code>master</code> 是<strong>分支名</strong>，可以理解成仓库的主时间线。这条命令的意思是：把本地 master 分支上的新提交，同步到 GitHub 的 master 分支。</li>
</ul><p>第一次 push 时 Git 会要求登录 GitHub。HTTPS 方式下 Windows 的凭证管理器会弹出授权窗口，登录一次之后凭证会被记住，以后不用再输。</p></section><section><h3>3. 提交前后常用的辅助命令<a href="#3-提交前后常用的辅助命令"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>git</span><span> </span><span>diff</span><span>                 </span><span># 看工作区改了什么（还没 add 的）</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>diff</span><span> </span><span>--staged</span><span>        </span><span># 看暂存区里有什么（已 add 未 commit 的）</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>log</span><span> </span><span>--oneline</span><span> </span><span>-5</span><span>     </span><span># 最近 5 条提交历史，一行一条</span></div></div><div><div><div>4</div></div><div><span>git</span><span> </span><span>show</span><span>                 </span><span># 看最新一次提交的完整改动内容</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>提交说明（<code>-m</code> 里的内容）建议写清楚这次改了什么，比如 <code>post: 新文章标题</code>、<code>fix: 修复导航样式</code>。以后翻历史时，这些说明就是你的操作日志。</p></section><section><h3>4. 出错了怎么办<a href="#4-出错了怎么办"><span>#</span></a></h3><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 情况一：忘了 add 就 commit，或者提交说明写错了</span></div></div><div><div><div>2</div></div><div><span>git</span><span> </span><span>commit</span><span> </span><span>--amend</span><span> </span><span>-m</span><span> </span><span>"正确的说明"</span><span>    </span><span># 修补最近一次提交（仅限还没 push）</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># 情况二：改动不想要了，想恢复到上次提交的样子</span></div></div><div><div><div>5</div></div><div><span>git</span><span> </span><span>checkout</span><span> </span><span>--</span><span> </span><span>文件名</span><span>                </span><span># 丢弃某文件未提交的改动（慎用）</span></div></div><div><div><div>6</div></div><div>
</div></div><div><div><div>7</div></div><div><span># 情况三：push 被拒绝（rejected），通常是 GitHub 上有你本地没有的提交</span></div></div><div><div><div>8</div></div><div><span>git</span><span> </span><span>pull</span><span> </span><span>--rebase</span><span> </span><span>origin</span><span> </span><span>master</span><span>      </span><span># 先把远端的新提交拉下来合并，再重新 push</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span># 情况四：已经 push 的提交想撤销</span></div></div><div><div><div>11</div></div><div><span>git</span><span> </span><span>revert</span><span> </span><span>&lt;提交ID&gt;</span><span>                  </span><span># 生成一个"反向提交"抵消它，安全且可追溯</span></div></div></code></pre><div><div></div><div></div></div></figure></div><blockquote><p>原则：<strong>只用安全的撤销手段</strong>（<code>revert</code>、新建提交覆盖）。<code>git push --force</code> 之类会重写历史的强操作，个人博客偶尔可救急，但一定要清楚自己在干什么。</p></blockquote><p>到这里，代码已经在 GitHub 上了。但 <code>git push</code> 本身<strong>只负责把代码传到仓库</strong>，它不知道任何部署的事。真正触发自动部署的，是下面这个机制。</p></section><section><h3>5. GitHub Actions：构建并发布到 dist 分支<a href="#5-github-actions构建并发布到-dist-分支"><span>#</span></a></h3><p>仓库里有三个 workflow 文件，各司其职：</p><ul>
<li><code>.github/workflows/deploy.yml</code> — <strong>构建站点并把产物发布到仓库的 <code>dist</code> 分支</strong>（核心）；</li>
<li><code>.github/workflows/build.yml</code> — 质量检查：Node 22/23 双版本跑 <code>astro check</code>，防手残；</li>
<li><code>.github/workflows/biome.yml</code> — 代码风格检查（biome）。</li>
</ul><p>deploy.yml 的开头声明了触发条件：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>on</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>push</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>branches</span><span>: [ </span><span>master</span><span> ]   </span><span># master 分支有新 push 就触发</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>git push</code> 后 GitHub 收到新提交，匹配到 workflow 就起一个全新的临时虚拟机（runner）按 yaml 逐步执行：安装依赖 → <code>pnpm build</code> → 把 <code>dist/</code> 目录整体提交到仓库的 <code>dist</code> 分支。</p><blockquote><p>为什么要经过 <code>dist</code> 分支中转？服务器只有 2G 内存，每次都在服务器上构建又慢又吃力；而且如果让 Actions 直接 SSH 登录服务器部署，每次构建机的境外 IP 登录都会触发阿里云的「异地登录」告警。改成「GitHub 构建 → dist 分支 → 服务器拉取」，构建交给强劲的 CI 机器，服务器只做拉取和同步，全程没有任何境外登录。</p><p>附带的好处：<code>dist</code> 分支的提交历史就是部署历史，哪次构建改了什么一目了然；出问题把服务器回退到旧的 dist 提交即可回滚。</p></blockquote></section><section><h3>6. 服务器端拉取部署<a href="#6-服务器端拉取部署"><span>#</span></a></h3><p>deploy.yml 里「发布 dist 分支」这一步的写法：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>- </span><span>name</span><span>: </span><span>Publish dist/ to dist branch</span></div></div><div><div><div>2</div></div><div><span>  </span><span>run</span><span>: </span><span>|</span></div></div><div><div><div>3</div></div><div><span><span>    </span></span><span>git config user.name "github-actions[bot]"</span></div></div><div><div><div>4</div></div><div><span><span>    </span></span><span>git config user.email "41898282+github-actions[bot]@users.noreply.github.com"</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>git fetch origin dist --depth 1 || echo "首次构建：dist 分支还不存在"</span></div></div><div><div><div>6</div></div><div><span><span>    </span></span><span>if git rev-parse -q --verify origin/dist &gt;/dev/null; then</span></div></div><div><div><div>7</div></div><div><span><span>      </span></span><span>git worktree add /tmp/publish origin/dist</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>else</span></div></div><div><div><div>9</div></div><div><span><span>      </span></span><span>git worktree add --orphan -b dist /tmp/publish</span></div></div><div><div><div>10</div></div><div><span><span>    </span></span><span>fi</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>rsync -a --delete --exclude .git dist/ /tmp/publish/</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>cd /tmp/publish</span></div></div><div><div><div>13</div></div><div><span><span>    </span></span><span>git add -A</span></div></div><div><div><div>14</div></div><div><span><span>    </span></span><span>if git diff --cached --quiet; then</span></div></div><div><div><div>15</div></div><div><span><span>      </span></span><span>echo "构建产物无变化，跳过推送"</span></div></div><div><div><div>16</div></div><div><span><span>    </span></span><span>else</span></div></div><div><div><div>17</div></div><div><span><span>      </span></span><span>git commit -m "build: ${GITHUB_SHA::7}"</span></div></div><div><div><div>18</div></div><div><span><span>      </span></span><span>git push origin HEAD:dist</span></div></div><div><div><div>19</div></div><div><span><span>    </span></span><span>fi</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>原理：<code>git worktree</code> 在不碰主工作区的前提下开一个临时工作目录指向 <code>dist</code> 分支，用本次构建产物整体替换后提交推送。推送用的 <code>GITHUB_TOKEN</code> 由 GitHub 自动注入，<strong>只对本仓库有效且永不过期</strong>，不需要在 Secrets 里配置任何密钥。</p><p>服务器这边，核心是拉取脚本 <code>scripts/deploy.sh</code>（已放在仓库里）：发现 <code>dist</code> 分支有新提交，就同步到 Nginx 站点目录——注意它<strong>只做拉取和复制，不构建</strong>，几秒内完成：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#!/usr/bin/env bash</span></div></div><div><div><div>2</div></div><div><span>set</span><span> </span><span>-euo</span><span> </span><span>pipefail</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>SITE_DIR</span><span>=</span><span>"/opt/site"</span><span>          </span><span># dist 分支的克隆（构建产物）</span></div></div><div><div><div>5</div></div><div><span>DEPLOY_DIR</span><span>=</span><span>"/var/www/blog"</span><span>    </span><span># Nginx 站点目录</span></div></div><div><div><div>6</div></div><div><span>BRANCH</span><span>=</span><span>"dist"</span></div></div><div><div><div>7</div></div><div>
</div></div><div><div><div>8</div></div><div><span>log</span><span>() { </span><span>echo</span><span> </span><span>"[$(</span><span>date</span><span> '+%F %T')] </span><span>$*</span><span>"</span><span>; }</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span># 文件锁：上一轮还没跑完就跳过本轮</span></div></div><div><div><div>11</div></div><div><span>exec</span><span> 9&gt;</span><span>/tmp/blog-deploy.lock</span></div></div><div><div><div>12</div></div><div><span>flock</span><span> </span><span>-n</span><span> </span><span>9</span><span> || </span><span>exit</span><span> </span><span>0</span></div></div><div><div><div>13</div></div><div>
</div></div><div><div><div>14</div></div><div><span>if</span><span><span> [ </span><span>!</span><span> </span><span>-d</span><span> </span></span><span>"</span><span>$SITE_DIR</span><span>/.git"</span><span> ]; </span><span>then</span></div></div><div><div><div>15</div></div><div><span>  </span><span>log</span><span> </span><span>"站点产物仓库不存在，请先执行：git clone -b dist --single-branch git@github.com:Carlinhosjoga/blog.git </span><span>$SITE_DIR</span><span>"</span></div></div><div><div><div>16</div></div><div><span>  </span><span>exit</span><span> </span><span>1</span></div></div><div><div><div>17</div></div><div><span>fi</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span>cd</span><span> </span><span>"</span><span>$SITE_DIR</span><span>"</span></div></div><div><div><div>20</div></div><div><span>git</span><span> </span><span>fetch</span><span> </span><span>origin</span><span> </span><span>"</span><span>$BRANCH</span><span>"</span></div></div><div><div><div>21</div></div><div><span>LOCAL</span><span><span>=</span><span>$(</span></span><span>git</span><span> </span><span>rev-parse</span><span> </span><span>HEAD</span><span>)</span></div></div><div><div><div>22</div></div><div><span>REMOTE</span><span><span>=</span><span>$(</span></span><span>git</span><span> </span><span>rev-parse</span><span> </span><span>"origin/</span><span>$BRANCH</span><span>"</span><span>)</span></div></div><div><div><div>23</div></div><div>
</div></div><div><div><div>24</div></div><div><span>if</span><span> [ </span><span>"</span><span>$LOCAL</span><span>"</span><span><span> </span><span>=</span><span> </span></span><span>"</span><span>$REMOTE</span><span>"</span><span> ]; </span><span>then</span></div></div><div><div><div>25</div></div><div><span>  </span><span>log</span><span> </span><span>"没有新构建产物，跳过"</span></div></div><div><div><div>26</div></div><div><span>  </span><span>exit</span><span> </span><span>0</span></div></div><div><div><div>27</div></div><div><span>fi</span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>log</span><span> </span><span>"发现新构建 </span><span>$LOCAL</span><span> -&gt; </span><span>$REMOTE</span><span>，同步上线"</span></div></div><div><div><div>30</div></div><div><span>git</span><span> </span><span>reset</span><span> </span><span>--hard</span><span> </span><span>"origin/</span><span>$BRANCH</span><span>"</span></div></div><div><div><div>31</div></div><div><span>rsync</span><span> </span><span>-az</span><span> </span><span>--delete</span><span> </span><span>--exclude</span><span> </span><span>.git</span><span> </span><span>"</span><span>$SITE_DIR</span><span>"/</span><span> </span><span>"</span><span>$DEPLOY_DIR</span><span>/"</span></div></div><div><div><div>32</div></div><div><span>log</span><span> </span><span>"部署完成"</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>安全性三件事：</p><ol>
<li><strong>只读拉取</strong>：服务器用一把「只读 Deploy Key」访问私有仓库，即使密钥泄露，别人也只能读，推不上去、删不掉仓库；</li>
<li><strong>构建在 GitHub 完成</strong>：服务器上不需要 Node 和 pnpm，也不怕构建失败——dist 分支没更新，服务器什么都不做，线上永远是最近一次<strong>成功</strong>构建的版本；</li>
<li><strong>rsync —delete</strong>：站点目录与构建产物严格一致，不会残留已删除页面的旧文件。</li>
</ol><p><strong>服务器初始化手把手</strong>（只需做一次，因为不构建，比在服务器上装 Node 的方案轻得多）：</p><div><div><div><figure><figcaption><span></span><span>Terminal window</span><span>展开</span><span>收起</span></figcaption><pre><code><div><div><div>1</div></div><div><span># ① 装 git 和 rsync（Ubuntu 一般已自带）</span></div></div><div><div><div>2</div></div><div><span>apt</span><span> </span><span>install</span><span> </span><span>-y</span><span> </span><span>git</span><span> </span><span>rsync</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span># ② 生成只读拉取密钥</span></div></div><div><div><div>5</div></div><div><span>ssh-keygen</span><span> </span><span>-t</span><span> </span><span>ed25519</span><span> </span><span>-f</span><span> </span><span>~/.ssh/blog_pull_key</span><span> </span><span>-N</span><span> </span><span>""</span><span> </span><span>-C</span><span> </span><span>"blog-server-pull"</span></div></div><div><div><div>6</div></div><div><span>cat</span><span> &gt;&gt; </span><span>~/.ssh/config</span><span> </span><span>&lt;</span><span>&lt;</span><span>'EOF'</span></div></div><div><div><div>7</div></div><div><span>Host</span><span> </span><span>github.com</span></div></div><div><div><div>8</div></div><div><span>  </span><span>IdentityFile</span><span> </span><span>~/.ssh/blog_pull_key</span></div></div><div><div><div>9</div></div><div><span>  </span><span>IdentitiesOnly</span><span> </span><span>yes</span></div></div><div><div><div>10</div></div><div><span>EOF</span></div></div><div><div><div>11</div></div><div><span>cat</span><span> </span><span>~/.ssh/blog_pull_key.pub</span></div></div><div><div><div>12</div></div><div><span># 把输出的公钥添加到 GitHub 仓库：Settings → Deploy keys → Add deploy key</span></div></div><div><div><div>13</div></div><div><span># （不要勾选 Allow write access）</span></div></div><div><div><div>14</div></div><div>
</div></div><div><div><div>15</div></div><div><span># ③ 克隆两个仓库：/opt/blog 放源码和脚本，/opt/site 放构建产物</span></div></div><div><div><div>16</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>git@github.com:Carlinhosjoga/blog.git</span><span> </span><span>/opt/blog</span></div></div><div><div><div>17</div></div><div><span>git</span><span> </span><span>clone</span><span> </span><span>-b</span><span> </span><span>dist</span><span> </span><span>--single-branch</span><span> </span><span>git@github.com:Carlinhosjoga/blog.git</span><span> </span><span>/opt/site</span></div></div><div><div><div>18</div></div><div>
</div></div><div><div><div>19</div></div><div><span># ④ 手动跑一次，确认能同步上线</span></div></div><div><div><div>20</div></div><div><span>bash</span><span> </span><span>/opt/blog/scripts/deploy.sh</span></div></div><div><div><div>21</div></div><div>
</div></div><div><div><div>22</div></div><div><span># ⑤ 加入定时任务：每 5 分钟兜底检查一次</span></div></div><div><div><div>23</div></div><div><span>(</span><span>crontab</span><span> </span><span>-l</span><span> </span><span>2&gt;/dev/null</span><span>; </span><span>\</span></div></div><div><div><div>24</div></div><div><span> </span><span>echo</span><span> </span><span>"*/5 * * * * bash /opt/blog/scripts/deploy.sh &gt;&gt; /var/log/blog-deploy.log 2&gt;&amp;1"</span><span>) | </span><span>crontab</span><span> </span><span>-</span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><blockquote><p>坑点：如果 <code>git clone</code> 或 <code>fetch</code> 报 <code>Permission denied (publickey)</code>，检查三处——公钥是否已添加为 Deploy key、<code>~/.ssh/config</code> 是否写对了 IdentityFile、<code>blog_pull_key</code> 权限是否是 600。另外国内服务器访问 GitHub 偶尔网络抖动，多试两次或换个时间段即可。</p></blockquote></section><section><h3>7. Webhook：让部署从「分钟级」变「秒级」<a href="#7-webhook让部署从分钟级变秒级"><span>#</span></a></h3><p>cron 兜底意味着最多等 5 分钟才上线。想秒级触发，给服务器加一个 webhook 接收器：GitHub 在 <code>dist</code> 分支更新时主动 POST 一个 HTTP 请求给服务器——这只是访问一个网址，<strong>不是登录</strong>，不会触发任何异地登录告警。服务器收到请求立刻执行部署脚本。</p><p>接收器是几十行 Python（放在服务器 <code>/opt/blog-webhook/hook.py</code>），只监听本机回环地址，由 Nginx 反代对外：</p><div><div><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>#!/usr/bin/env python3</span></div></div><div><div><div>2</div></div><div><span>import</span><span> hmac, hashlib, json, subprocess</span></div></div><div><div><div>3</div></div><div><span>from</span><span> http.server </span><span>import</span><span> BaseHTTPRequestHandler, ThreadingHTTPServer</span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span><span>TOKEN</span><span> </span><span>=</span><span> </span></span><span>"&lt;URL令牌&gt;"</span><span>       </span><span># 与 Nginx 反代路径一致</span></div></div><div><div><div>6</div></div><div><span><span>SECRET</span><span> </span><span>=</span><span> </span></span><span>"&lt;webhook密钥&gt;"</span><span>   </span><span># 与 GitHub webhook 的 Secret 一致</span></div></div><div><div><div>7</div></div><div><span><span>DEPLOY</span><span> </span><span>=</span><span> </span></span><span>"/opt/blog/scripts/deploy.sh"</span></div></div><div><div><div>8</div></div><div><span><span>LOG</span><span> </span><span>=</span><span> </span></span><span>"/var/log/blog-deploy.log"</span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>class</span><span><span> </span><span>Handler</span><span>(</span><span>BaseHTTPRequestHandler</span><span>)</span></span><span>:</span></div></div><div><div><div>11</div></div><div><span>    </span><span>def</span><span> </span><span>do_POST</span><span>(</span><span>self</span><span>):</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>path </span><span>=</span><span> </span><span>self</span><span>.path</span></div></div><div><div><div>13</div></div><div><span>        </span><span>if</span><span><span> path.</span><span>startswith</span><span>(</span></span><span>"/webhook/"</span><span>):</span></div></div><div><div><div>14</div></div><div><span><span>            </span></span><span>path </span><span>=</span><span> path[</span><span>len</span><span>(</span><span>"/webhook"</span><span>):]  </span><span># Nginx 反代会带上 /webhook 前缀</span></div></div><div><div><div>15</div></div><div><span>        </span><span>if</span><span><span> path </span><span>!=</span><span> </span></span><span>"/"</span><span><span> </span><span>+</span><span> </span><span>TOKEN</span><span>:</span></span></div></div><div><div><div>16</div></div><div><span>            </span><span>self</span><span><span>.</span><span>send_response</span><span>(</span></span><span>404</span><span>); </span><span>self</span><span><span>.</span><span>end_headers</span><span>(); </span></span><span>return</span></div></div><div><div><div>17</div></div><div><span><span>        </span></span><span>body </span><span>=</span><span> </span><span>self</span><span><span>.rfile.</span><span>read</span><span>(</span></span><span>int</span><span>(</span><span>self</span><span><span>.headers.</span><span>get</span><span>(</span></span><span>"Content-Length"</span><span>, </span><span>0</span><span>)))</span></div></div><div><div><div>18</div></div><div><span><span>        </span></span><span>expect </span><span>=</span><span> </span><span>"sha256="</span><span><span> </span><span>+</span><span> hmac.</span><span>new</span><span>(</span><span>SECRET</span><span>.</span><span>encode</span><span>(), body, hashlib.sha256).</span><span>hexdigest</span><span>()</span></span></div></div><div><div><div>19</div></div><div><span>        </span><span>if</span><span> </span><span>not</span><span><span> hmac.</span><span>compare_digest</span><span>(</span></span><span>self</span><span><span>.headers.</span><span>get</span><span>(</span></span><span>"X-Hub-Signature-256"</span><span>, </span><span>""</span><span>), expect):</span></div></div><div><div><div>20</div></div><div><span>            </span><span>self</span><span><span>.</span><span>send_response</span><span>(</span></span><span>403</span><span>); </span><span>self</span><span><span>.</span><span>end_headers</span><span>(); </span></span><span>return</span></div></div><div><div><div>21</div></div><div><span><span>        </span></span><span>event </span><span>=</span><span> </span><span>self</span><span><span>.headers.</span><span>get</span><span>(</span></span><span>"X-GitHub-Event"</span><span>, </span><span>""</span><span>)</span></div></div><div><div><div>22</div></div><div><span>        </span><span>if</span><span><span> event </span><span>==</span><span> </span></span><span>"ping"</span><span>:</span></div></div><div><div><div>23</div></div><div><span>            </span><span>self</span><span><span>.</span><span>send_response</span><span>(</span></span><span>200</span><span>); </span><span>self</span><span><span>.</span><span>end_headers</span><span>(); </span></span><span>return</span></div></div><div><div><div>24</div></div><div><span>        </span><span>if</span><span><span> event </span><span>==</span><span> </span></span><span>"push"</span><span> </span><span>and</span><span><span> json.</span><span>loads</span><span>(body).</span><span>get</span><span>(</span></span><span>"ref"</span><span><span>) </span><span>==</span><span> </span></span><span>"refs/heads/dist"</span><span>:</span></div></div><div><div><div>25</div></div><div><span>            </span><span>with</span><span> </span><span>open</span><span><span>(</span><span>LOG</span><span>, </span></span><span>"ab"</span><span>) </span><span>as</span><span> lf:</span></div></div><div><div><div>26</div></div><div><span><span>                </span></span><span>subprocess.</span><span>Popen</span><span>([</span><span>"bash"</span><span><span>, </span><span>DEPLOY</span><span>], </span></span><span>stdout</span><span><span>=</span><span>lf, </span></span><span>stderr</span><span><span>=</span><span>lf, </span></span><span>start_new_session</span><span>=</span><span>True</span><span>)</span></div></div><div><div><div>27</div></div><div><span>        </span><span>self</span><span><span>.</span><span>send_response</span><span>(</span></span><span>204</span><span>); </span><span>self</span><span><span>.</span><span>end_headers</span><span>()</span></span></div></div><div><div><div>28</div></div><div>
</div></div><div><div><div>29</div></div><div><span>    </span><span>def</span><span> </span><span>log_message</span><span>(</span><span>self</span><span>,</span><span> </span><span>*</span><span>args</span><span>):</span></div></div><div><div><div>30</div></div><div><span>        </span><span>pass</span><span>  </span><span># 静默访问日志</span></div></div><div><div><div>31</div></div><div>
</div></div><div><div><div>32</div></div><div><span><span>ThreadingHTTPServer</span><span>((</span></span><span>"127.0.0.1"</span><span>, </span><span>8787</span><span><span>), Handler).</span><span>serve_forever</span><span>()</span></span></div></div></code></pre><div><div></div><div></div></div></figure><div></div></div><span>展开</span><span>收起</span></div></div><p>安全校验两层：URL 路径里有随机令牌（外人猜不到入口），请求体用 GitHub webhook Secret 做 HMAC-SHA256 签名验证（伪造请求直接 403）。而且只有 <code>dist</code> 分支的 push 事件才触发部署——你平时推 master 触发的 webhook 会被它自动忽略。</p><p>用 systemd 常驻（<code>/etc/systemd/system/blog-webhook.service</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>[Unit]</span></div></div><div><div><div>2</div></div><div><span>Description</span><span><span>=</span><span>Blog deploy webhook receiver</span></span></div></div><div><div><div>3</div></div><div><span>After</span><span><span>=</span><span>network.target</span></span></div></div><div><div><div>4</div></div><div>
</div></div><div><div><div>5</div></div><div><span>[Service]</span></div></div><div><div><div>6</div></div><div><span>ExecStart</span><span><span>=</span><span>/usr/bin/python3 /opt/blog-webhook/hook.py</span></span></div></div><div><div><div>7</div></div><div><span>Restart</span><span><span>=</span><span>always</span></span></div></div><div><div><div>8</div></div><div><span>RestartSec</span><span><span>=</span><span>3</span></span></div></div><div><div><div>9</div></div><div>
</div></div><div><div><div>10</div></div><div><span>[Install]</span></div></div><div><div><div>11</div></div><div><span>WantedBy</span><span><span>=</span><span>multi-user.target</span></span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>启用：<code>systemctl daemon-reload &amp;&amp; systemctl enable --now blog-webhook</code>。</p><p>Nginx 加一段反代（<code>/etc/nginx/sites-available/blog</code>）：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>    </span><span># GitHub webhook 部署通知（路径含随机令牌，仅 dist 分支事件触发部署）</span></div></div><div><div><div>2</div></div><div><span>    </span><span>location</span><span> = </span><span>/webhook/&lt;URL令牌&gt; </span><span>{</span></div></div><div><div><div>3</div></div><div><span>       <span> </span></span><span>proxy_pass </span><span>http://127.0.0.1:8787;</span></div></div><div><div><div>4</div></div><div><span>       <span> </span></span><span>add_header </span><span>X-Robots-Tag </span><span>"noindex, nofollow"</span><span> always;</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>nginx -t &amp;&amp; systemctl reload nginx</code> 生效。最后到 GitHub 仓库：<strong>Settings → Webhooks → Add webhook</strong>，Payload URL 填 <code>http://&lt;服务器IP&gt;/webhook/&lt;URL令牌&gt;</code>，Content type 选 <code>application/json</code>，Secret 填与脚本一致的密钥，触发事件保持默认（Just the push event）。</p><p>之后每次发布的时序：<code>git push</code> → GitHub 构建（约 2 分钟）→ dist 分支更新 → webhook 秒达 → 服务器同步上线。全程无登录、无手工操作。</p></section><section><h3>8. 完整数据流<a href="#8-完整数据流"><span>#</span></a></h3><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>本地写文章 (src/content/posts/xxx.md)</span></div></div><div><div><div>2</div></div><div><span><span>        </span></span><span>│</span></div></div><div><div><div>3</div></div><div><span><span>        </span></span><span>│  git add / commit / push (master)</span></div></div><div><div><div>4</div></div><div><span><span>        </span></span><span>▼</span></div></div><div><div><div>5</div></div><div><span>GitHub 仓库 ←── build.yml / biome.yml 质量检查</span></div></div><div><div><div>6</div></div><div><span><span>        </span></span><span>│</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>│  deploy.yml：pnpm build → 产物提交到 dist 分支</span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>▼</span></div></div><div><div><div>9</div></div><div><span>dist 分支更新 ──webhook 秒级通知──▶ 服务器执行 deploy.sh</span></div></div><div><div><div>10</div></div><div><span><span>        </span></span><span>│                              │</span></div></div><div><div><div>11</div></div><div><span><span>        </span></span><span>│                              │ cron 每 5 分钟兜底</span></div></div><div><div><div>12</div></div><div><span><span>        </span></span><span>▼                              ▼</span></div></div><div><div><div>13</div></div><div><span><span>        </span></span><span>└──────▶ git 拉取 dist → rsync → /var/www/blog → Nginx → 访客</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><strong>构建永远在 GitHub 完成</strong>：服务器只做几秒钟的拉取同步，2G 内存毫无压力；产物和源码永远一一对应，出问题回滚就是把服务器 reset 到旧的 dist 提交。</p></section></section>
<section><h2>九、日常更新流程<a href="#九日常更新流程"><span>#</span></a></h2><p>配置完之后，写文章只剩三步：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span># 1. 写文章：在 src/content/posts/ 新建 .md 文件</span></div></div><div><div><div>2</div></div><div><span># 2. 提交：</span></div></div><div><div><div>3</div></div><div><span>git</span><span> </span><span>add</span><span> </span><span>-A</span><span> &amp;&amp; </span><span>git</span><span> </span><span>commit</span><span> </span><span>-m</span><span> </span><span>"post: 文章标题"</span></div></div><div><div><div>4</div></div><div><span># 3. 推送：</span></div></div><div><div><div>5</div></div><div><span>git</span><span> </span><span>push</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>约两三分钟内自动上线：GitHub 构建（约 2 分钟）→ dist 分支更新 → webhook 秒级触发服务器同步。看进度两个渠道：GitHub 仓库 <strong>Actions</strong> 标签页看构建日志（绿勾 = 构建成功）；服务器上 <code>tail -f /var/log/blog-deploy.log</code> 看部署日志。</p></section>
<section><h2>十、后续计划<a href="#十后续计划"><span>#</span></a></h2><ul>
<li>域名备案通过后：DNS 解析到服务器，Nginx 配置 <code>server_name</code>，用 certbot 免费签发 HTTPS 证书（自动续期）；</li>
<li>追番页数据是构建时拉取的，计划加一个每日定时构建（即使没有新文章也刷新一次），追番进度自动更新；</li>
<li>流量起来后在服务器前套一层国内 CDN，源站带宽压力趋近于零。</li>
</ul><hr /><p>整个搭下来最大的感受：<strong>静态博客的复杂度不在写页面，而在把「写完之后的事」自动化</strong>。一旦发布管道跑通，写文章这件事就变得毫无心理负担——这也正是坚持输出的前提。</p></section>]]></content>
    </entry>
</feed>
