<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>竹山一叶</title>
    <link>https://zsyyblog.com/</link>
    <description>竹山一叶的小家</description>
    <language>zh-CN</language>
    <copyright>All rights reserved 2026, 竹山一叶</copyright>
    <lastBuildDate>Wed, 21 Oct 2026 11:54:58 GMT</lastBuildDate>
    <generator>Hexo</generator>
    <image>
      <url>https://img.zsyyblog.com/favicon.jpg</url>
      <title>竹山一叶</title>
      <link>https://zsyyblog.com/</link>
    </image>
    <atom:link href="https://zsyyblog.com/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>依赖安装慢与超时: 从 npm 到 pnpm、镜像源与代理的完整排查</title>
      <link>https://zsyyblog.com/f8e9ba66.html</link>
      <description>npm install 卡住、超时、报ETIMEDOUT? 这篇按&quot;先定位再解决&quot;的顺序, 讲清镜像源怎么选、镜像源失效怎么发现、超时参数怎么调、代理与镜像源为什么冲突, 以及 pnpm/Docker 场景下的同类问题。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/">前端</category>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/Hexo/">Hexo</category>
      <category domain="https://zsyyblog.com/tags/Hexo/">Hexo</category>
      <pubDate>Wed, 21 Oct 2026 11:50:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p><code>npm install</code> 跑了十分钟还在转圈, 最后报错 <code>ETIMEDOUT</code> 或 <code>ECONNRESET</code>—— 这种事几乎每个国内开发者都遇到过。</p><p>网上搜一圈, 会看到各种答案: 换镜像源、清缓存、关SSL、调超时…… 但这些答案<strong>大多是碎片化的</strong>, 而且有些<strong>互相矛盾</strong>(比如让你 <code>strict-ssl=false</code>, 这实际上是在关掉安全检查)。</p><p>这篇想做的事是: <strong>给一个有序的排查框架</strong>, 让你知道每一步在解决什么问题, 而不是照抄命令碰运气。</p><h1 id="一、先定位-你到底遇到的是哪一类问题"><a href="#一、先定位-你到底遇到的是哪一类问题" class="headerlink" title="一、先定位: 你到底遇到的是哪一类问题"></a>一、先定位: 你到底遇到的是哪一类问题</h1><p>不同症状对应不同原因, 乱试只会浪费时间。</p><table><thead><tr><th align="left">症状</th><th align="left">大概率原因</th><th align="left">该跳到</th></tr></thead><tbody><tr><td align="left">一直卡住不动, 无报错</td><td align="left">源不可达 &#x2F; DNS 问题</td><td align="left">第二节</td></tr><tr><td align="left"><code>ETIMEDOUT</code></td><td align="left">网络超时, 源慢或被墙</td><td align="left">第三节</td></tr><tr><td align="left"><code>ECONNRESET</code> &#x2F; <code>ECONNREFUSED</code></td><td align="left">连接被重置, 代理冲突</td><td align="left">第五节</td></tr><tr><td align="left"><code>ERR_PNPM_META_FETCH_FAIL</code></td><td align="left">pnpm 取元数据失败</td><td align="left">第六节</td></tr><tr><td align="left"><code>CERT_HAS_EXPIRED</code></td><td align="left">证书问题</td><td align="left">第七节</td></tr><tr><td align="left"><code>ENOTFOUND</code></td><td align="left">DNS 解析失败</td><td align="left">第二节</td></tr><tr><td align="left"><code>EACCES</code> &#x2F; <code>EPERM</code></td><td align="left">权限问题</td><td align="left">第八节</td></tr></tbody></table><p><strong>先确认你属于哪一类, 再往下看。</strong> 这能省掉大半的无效尝试。</p><h2 id="三个诊断命令"><a href="#三个诊断命令" class="headerlink" title="三个诊断命令"></a>三个诊断命令</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 看当前实际用的源(不要凭记忆, 看实际配置)</span></span><br><span class="line">npm config get registry</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 测试源是否可达 + 耗时</span></span><br><span class="line">curl -s -o /dev/null -w <span class="string">&quot;HTTP码: %&#123;http_code&#125; 耗时: %&#123;time_total&#125;s\n&quot;</span> -m 20 https://registry.npmmirror.com/react</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 测试 npm 能不能拿到包元数据</span></span><br><span class="line">npm view react version</span><br></pre></td></tr></table></figure><p><strong><code>npm view react version</code> 这一条最关键</strong>—— 它只取元数据, 不下载 tarball, 所以能最快判断”源本身通不通”。</p><p>如果这条命令就卡住或报错, 问题在源和网络, 与项目无关。</p><h1 id="二、镜像源-怎么选-以及最重要的”失效检测”"><a href="#二、镜像源-怎么选-以及最重要的”失效检测”" class="headerlink" title="二、镜像源: 怎么选, 以及最重要的”失效检测”"></a>二、镜像源: 怎么选, 以及最重要的”失效检测”</h1><h2 id="常见镜像源现状-2026-年实测"><a href="#常见镜像源现状-2026-年实测" class="headerlink" title="常见镜像源现状(2026 年实测)"></a>常见镜像源现状(2026 年实测)</h2><p>我在写这篇时实测了各源的状态, 结果如下:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 老淘宝源</span></span><br><span class="line">curl -o /dev/null -w <span class="string">&quot;%&#123;http_code&#125;\n&quot;</span> -m 20 https://registry.npm.taobao.org/react</span><br><span class="line"><span class="comment"># 输出: 000  ← 已完全不可用</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># npmmirror(淘宝系新域名)</span></span><br><span class="line">curl -o /dev/null -w <span class="string">&quot;%&#123;http_code&#125;\n&quot;</span> -m 20 https://registry.npmmirror.com/react</span><br><span class="line"><span class="comment"># 输出: 200  ← 正常</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 官方源</span></span><br><span class="line">curl -o /dev/null -w <span class="string">&quot;%&#123;http_code&#125;\n&quot;</span> -m 20 https://registry.npmjs.org/react</span><br><span class="line"><span class="comment"># 输出: 200  ← 正常但国内慢</span></span><br></pre></td></tr></table></figure><p><strong>重点</strong>: <code>registry.npm.taobao.org</code> 这个老地址<strong>已经停止服务</strong>。 很多人的 <code>.npmrc</code> 里还留着它(这是几年前的教程留下的习惯), 结果所有 npm 命令都在超时。</p><blockquote><p><strong>如果你现在的 <code>npm config get registry</code> 返回的是 <code>registry.npm.taobao.org</code>, 恭喜你找到了一个大概率就是你现在问题的原因。</strong> 直接换成 <code>registry.npmmirror.com</code> 即可。</p></blockquote><h2 id="镜像源对比"><a href="#镜像源对比" class="headerlink" title="镜像源对比"></a>镜像源对比</h2><table><thead><tr><th align="left">源</th><th align="left">地址</th><th align="left">国内速度</th><th align="left">可用性</th></tr></thead><tbody><tr><td align="left"><strong>npmmirror</strong></td><td align="left"><code>https://registry.npmmirror.com</code></td><td align="left">快</td><td align="left">✅ 推荐</td></tr><tr><td align="left">腾讯云</td><td align="left"><code>https://mirrors.cloud.tencent.com/npm/</code></td><td align="left">快</td><td align="left">✅ 备选</td></tr><tr><td align="left">华为云</td><td align="left"><code>https://repo.huaweicloud.com/repository/npm/</code></td><td align="left">中</td><td align="left">✅ 备选</td></tr><tr><td align="left">官方</td><td align="left"><code>https://registry.npmjs.org</code></td><td align="left">慢</td><td align="left">✅ 保底</td></tr></tbody></table><p><strong>建议备 2 个</strong>,主源挂了自动切备用。</p><h2 id="配置方法"><a href="#配置方法" class="headerlink" title="配置方法"></a>配置方法</h2><p><strong>用户级(推荐, 全局生效)</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">npm config <span class="built_in">set</span> registry https://registry.npmmirror.com/</span><br><span class="line">npm config get registry</span><br><span class="line"><span class="comment"># 确认输出正确</span></span><br></pre></td></tr></table></figure><p><strong>项目级(团队项目建议用这个)</strong></p><p>在项目根目录建 <code>.npmrc</code>:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">registry</span>=https://registry.npmmirror.com/</span><br><span class="line"><span class="attr">fetch-retries</span>=<span class="number">3</span></span><br><span class="line"><span class="attr">fetch-retry-mintimeout</span>=<span class="number">10000</span></span><br><span class="line"><span class="attr">fetch-retry-maxtimeout</span>=<span class="number">60000</span></span><br><span class="line"><span class="attr">fetch-timeout</span>=<span class="number">120000</span></span><br></pre></td></tr></table></figure><p><strong>项目级优先于用户级</strong>, 这样新人 clone 项目后不用配就能用。</p><h2 id="检测源是否失效-建议每月做一次"><a href="#检测源是否失效-建议每月做一次" class="headerlink" title="检测源是否失效(建议每月做一次)"></a>检测源是否失效(建议每月做一次)</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 这个命令返回 000 就说明源挂了</span></span><br><span class="line">curl -s -o /dev/null -w <span class="string">&quot;%&#123;http_code&#125;\n&quot;</span> -m 15 https://registry.npmmirror.com/react</span><br></pre></td></tr></table></figure><p><strong>技巧</strong>: 很多”莫名其妙开始变慢”的情况, 都是某个镜像源悄悄挂了或限流了。 养成定期检测的习惯, 或者在 CI 里加一个健康检查。</p><h1 id="三、超时相关参数-该调哪个-调多少"><a href="#三、超时相关参数-该调哪个-调多少" class="headerlink" title="三、超时相关参数: 该调哪个, 调多少"></a>三、超时相关参数: 该调哪个, 调多少</h1><h2 id="npm-的四个关键参数"><a href="#npm-的四个关键参数" class="headerlink" title="npm 的四个关键参数"></a>npm 的四个关键参数</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 拿到这些值的当前设置</span></span><br><span class="line">npm config get fetch-retries</span><br><span class="line">npm config get fetch-retry-mintimeout</span><br><span class="line">npm config get fetch-retry-maxtimeout</span><br><span class="line">npm config get fetch-timeout</span><br></pre></td></tr></table></figure><p>含义和推荐值:</p><table><thead><tr><th align="left">参数</th><th align="left">作用</th><th align="left">推荐值</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left"><code>fetch-retries</code></td><td align="left">重试次数</td><td align="left">3</td><td align="left">太少容易失败, 太多浪费时间</td></tr><tr><td align="left"><code>fetch-retry-mintimeout</code></td><td align="left">首次重试等待</td><td align="left">10000ms</td><td align="left">太短会在网络抖动时很快放弃</td></tr><tr><td align="left"><code>fetch-retry-maxtimeout</code></td><td align="left">重试等待上限</td><td align="left">60000ms</td><td align="left"></td></tr><tr><td align="left"><code>fetch-timeout</code></td><td align="left">单次请求超时</td><td align="left">120000ms</td><td align="left">默认 5 分钟, 通常够用</td></tr></tbody></table><p><strong>一次性设好</strong>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">npm config <span class="built_in">set</span> fetch-retries 3</span><br><span class="line">npm config <span class="built_in">set</span> fetch-retry-mintimeout 10000</span><br><span class="line">npm config <span class="built_in">set</span> fetch-retry-maxtimeout 60000</span><br><span class="line">npm config <span class="built_in">set</span> fetch-timeout 120000</span><br></pre></td></tr></table></figure><h2 id="一条重要的调参原则"><a href="#一条重要的调参原则" class="headerlink" title="一条重要的调参原则"></a>一条重要的调参原则</h2><p><strong>不要一上来就疯狂调大超时。</strong> 顺序应该是:</p><ol><li>先换源(成本最低, 效果最明显)</li><li>再调重试参数</li><li>最后才考虑加超时</li></ol><p>因为如果是源本身有问题, 把超时加到 10 分钟只会让你<strong>等得更久才失败</strong>。</p><h2 id="什么时候该调大超时"><a href="#什么时候该调大超时" class="headerlink" title="什么时候该调大超时"></a>什么时候该调大超时</h2><ul><li>你在<strong>跨洲网络</strong>(比如国内访问海外 CI)</li><li>依赖里有<strong>大包</strong>(如<code>electron</code>、<code>chromium</code>)</li><li>你在<strong>低带宽环境</strong>(公司内网、虚拟机)</li></ul><p>这时候可以专门给某个命令加超时:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install --fetch-timeout=600000</span><br></pre></td></tr></table></figure><h1 id="四、DNS-与网络层排查"><a href="#四、DNS-与网络层排查" class="headerlink" title="四、DNS 与网络层排查"></a>四、DNS 与网络层排查</h1><p>如果换源后依然超时, 问题可能在更底层。</p><h2 id="判断是不是-DNS-问题"><a href="#判断是不是-DNS-问题" class="headerlink" title="判断是不是 DNS 问题"></a>判断是不是 DNS 问题</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 测试域名解析</span></span><br><span class="line">nslookup registry.npmmirror.com</span><br><span class="line"></span><br><span class="line"><span class="comment"># 测试连接耗时</span></span><br><span class="line">curl -w <span class="string">&quot;连接耗时: %&#123;time_connect&#125;s  总耗时: %&#123;time_total&#125;s\n&quot;</span> -o /dev/null https://registry.npmmirror.com/react</span><br></pre></td></tr></table></figure><p><strong><code>time_connect</code> 很大</strong>(比如超过 1 秒)说明建连本身慢, 可能需要检查网络路径。</p><h2 id="换-DNS"><a href="#换-DNS" class="headerlink" title="换 DNS"></a>换 DNS</h2><p>Windows:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看当前 DNS</span></span><br><span class="line">nslookup | more</span><br></pre></td></tr></table></figure><p>如果解析到的 IP 不合理(比如指向了失效的CDN), 可以改成公共 DNS: <code>223.5.5.5</code>(阿里)、<code>119.29.29.29</code>(腾讯)。</p><h2 id="排查是否走代理"><a href="#排查是否走代理" class="headerlink" title="排查是否走代理"></a>排查是否走代理</h2><p><strong>这是最容易漏掉的一项。</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 看环境变量里有没有代理</span></span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$HTTP_PROXY</span></span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$HTTPS_PROXY</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 看 npm 自己配置的代理</span></span><br><span class="line">npm config get proxy</span><br><span class="line">npm config get https-proxy</span><br></pre></td></tr></table></figure><p><strong>代理与镜像源的冲突是个经典坑</strong>:</p><ul><li>代理正常工作时, npm 走代理, 不需要镜像源</li><li>但如果代理<strong>部分失效</strong>(比如内网走代理、外网不走), 就会时好时坏</li><li>更糟的情况: 配置了镜像源 + 代理, 请求可能走了两次转发, 反而更慢</li></ul><p><strong>排查方法</strong>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 临时禁用代理再试</span></span><br><span class="line">npm install --no-proxy</span><br></pre></td></tr></table></figure><p>如果禁用后变快, 说明问题在代理配置。</p><h1 id="五、pnpm-报错信息完全不同"><a href="#五、pnpm-报错信息完全不同" class="headerlink" title="五、pnpm: 报错信息完全不同"></a>五、pnpm: 报错信息完全不同</h1><p>pnpm 的错误码和 npm 不一样, 而且 pnpm <strong>默认使用严格模式</strong>, 一些 npm 下能容忍的问题在 pnpm 下会直接报错。</p><h2 id="pnpm-的常见错误"><a href="#pnpm-的常见错误" class="headerlink" title="pnpm 的常见错误"></a>pnpm 的常见错误</h2><p><strong><code>ERR_PNPM_META_FETCH_FAIL</code></strong></p><p>取包元数据失败, 通常是网络或源配置问题。</p><p>排查顺序:</p><ol><li><code>pnpm config get registry</code> 确认源地址</li><li><code>curl</code> 测试该源是否可达</li><li>确认没被代理干扰(<code>pnpm config get proxy</code>)</li></ol><p><strong><code>ERR_PNPM_FETCH_404</code></strong></p><p>包在仓库里不存在。 常见于:</p><ul><li>包名拼错</li><li>包已unpublish 或改名</li><li><strong>scoped 包在镜像源上缺失</strong>(如<code>@myorg/pkg</code>)</li></ul><p><strong><code>ERR_PNPM_NO_MATCHING_VERSION</code></strong></p><p>指定版本不存在。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查包的所有版本</span></span><br><span class="line">pnpm view &lt;包名&gt; versions</span><br><span class="line"></span><br><span class="line"><span class="comment"># 如果镜像源没有某个版本, 给 scoped 包单独配源</span></span><br></pre></td></tr></table></figure><p><strong><code>ERR_PNPM_NO_OFFLINE_META</code></strong></p><p>离线模式下缓存里没有元数据。 解决: 先联网装一次, 之后才能离线。</p><h2 id="pnpm-配置镜像源"><a href="#pnpm-配置镜像源" class="headerlink" title="pnpm 配置镜像源"></a>pnpm 配置镜像源</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">pnpm config <span class="built_in">set</span> registry https://registry.npmmirror.com</span><br><span class="line">pnpm config <span class="built_in">set</span> fetch-retries 3</span><br><span class="line">pnpm config <span class="built_in">set</span> fetch-timeout 120000</span><br></pre></td></tr></table></figure><p>项目级写在 <code>package.json</code> 或 <code>.npmrc</code>:</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">registry</span>=https://registry.npmmirror.com/</span><br><span class="line"><span class="attr">fetch-retries</span>=<span class="number">3</span></span><br><span class="line"><span class="attr">fetch-timeout</span>=<span class="number">120000</span></span><br></pre></td></tr></table></figure><h2 id="scoped-包要单独配"><a href="#scoped-包要单独配" class="headerlink" title="scoped 包要单独配"></a>scoped 包要单独配</h2><p><strong>这是镜像源最常见的坑</strong>: 私有或小众的 scoped 包, 镜像源可能没有同步。</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">registry</span>=https://registry.npmmirror.com/</span><br><span class="line">@yourorg:<span class="attr">registry</span>=https://registry.npmjs.org/</span><br></pre></td></tr></table></figure><p>含义: <code>@yourorg/</code> 开头的包走官方源, 其他走镜像。</p><h2 id="pnpm-vs-npm-的选择"><a href="#pnpm-vs-npm-的选择" class="headerlink" title="pnpm vs npm 的选择"></a>pnpm vs npm 的选择</h2><p>pnpm 有一个显著优势: <strong>硬链接机制</strong>, 相同版本的包在磁盘上只存一份。</p><p>实测对比(同一项目): npm 的 <code>node_modules</code> 通常 300-500MB, pnpm 可能只有 100-200MB, 安装速度也更快。</p><p><strong>但注意</strong>: pnpm 用的是 <code>node_modules/.pnpm/</code> 结构, 有些工具(尤其是老项目)不兼容, 遇到奇怪的模块找不到问题, 可以先用 npm 验证。</p><h1 id="六、Docker-场景的同类问题"><a href="#六、Docker-场景的同类问题" class="headerlink" title="六、Docker 场景的同类问题"></a>六、Docker 场景的同类问题</h1><p>Docker 拉镜像慢或失败, 根因和 npm 高度相似。</p><h2 id="镜像拉取失败"><a href="#镜像拉取失败" class="headerlink" title="镜像拉取失败"></a>镜像拉取失败</h2><p><strong><code>pull access denied</code></strong></p><p>排查顺序:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 确认镜像名和标签存在</span></span><br><span class="line">docker search &lt;镜像名&gt;</span><br><span class="line">docker manifest inspect &lt;镜像名&gt;:&lt;标签&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 确认没超出Docker Hub 匿名拉取限额</span></span><br><span class="line"><span class="comment"># 匿名用户: 100 次/6 小时(按 IP 算)</span></span><br><span class="line"><span class="comment"># 登录免费账号: 200 次/6 小时</span></span><br><span class="line">docker login</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 确认网络能通</span></span><br><span class="line">docker pull hello-world</span><br></pre></td></tr></table></figure><p><strong>配置国内镜像加速器</strong></p><p>Docker Desktop: Settings → Docker Engine, 加:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;registry-mirrors&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="string">&quot;https://docker.m.daocloud.io&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="string">&quot;https://dockerproxy.com&quot;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>Linux 上配在 <code>/etc/docker/daemon.json</code>, 然后 <code>systemctl restart docker</code>。</p><p><strong>改完一定要重启 Docker Desktop 才生效</strong>, 这是最常见的”我配了但没用”。</p><h2 id="验证加速器是否生效"><a href="#验证加速器是否生效" class="headerlink" title="验证加速器是否生效"></a>验证加速器是否生效</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker info | grep -A3 <span class="string">&quot;Registry Mirrors&quot;</span></span><br></pre></td></tr></table></figure><p>有输出说明配置生效; 空的话说明没生效。</p><h1 id="七、证书相关问题"><a href="#七、证书相关问题" class="headerlink" title="七、证书相关问题"></a>七、证书相关问题</h1><p><code>CERT_HAS_EXPIRED</code> 或 <code>UNABLE_TO_VERIFY_LEAF_SIGNATURE</code>。</p><h2 id="正确做法-按优先级"><a href="#正确做法-按优先级" class="headerlink" title="正确做法(按优先级)"></a>正确做法(按优先级)</h2><p><strong>1. 更新根证书(推荐)</strong></p><p>老版本的 Windows 或 Node 可能没有新的根证书。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install -g nss-updates  <span class="comment"># 或在系统层面更新证书</span></span><br></pre></td></tr></table></figure><p>或直接更新 Node.js 到 LTS 版本。</p><p><strong>2. 换源</strong></p><p>如果是特定镜像的证书问题, 换个源可能就绕过了。</p><p><strong>3. 临时禁用 SSL 校验(不推荐, 但有时是唯一办法)</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm config <span class="built_in">set</span> strict-ssl <span class="literal">false</span></span><br></pre></td></tr></table></figure><p><strong>但要知道代价</strong>:</p><blockquote><p><code>strict-ssl=false</code> 会让npm <strong>不再验证 HTTPS 证书</strong>, 这意味着你<strong>无法确认下载的包是否被中间人篡改</strong>。 在公共 WiFi、公司网络或有代理的环境下, 这是实打实的供应链安全风险。</p><p>我的建议是<strong>把它当作最后的手段</strong>, 且只在临时使用, 解决后立刻改回<code>true</code>。</p><p><code>npm config set strict-ssl true</code> 改回来。</p></blockquote><p>顺带说一句: 很多老教程会让你直接关SSL, 这在当年网络环境复杂时有其道理, 但现在镜像源的证书普遍是正常的, <strong>关SSL 的弊大于利</strong>。</p><h1 id="八、权限问题"><a href="#八、权限问题" class="headerlink" title="八、权限问题"></a>八、权限问题</h1><p>Linux&#x2F;macOS 下 <code>EACCES</code> 或 <code>EPERM</code>:</p><p><strong>不要用 <code>sudo npm install</code></strong> —— 这会让文件属主变成 root, 后面更麻烦。</p><p>正确做法:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 改 Node 的全局安装目录</span></span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">mkdir</span> -p /usr/local/lib/node_modules</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">chown</span> -R $(<span class="built_in">whoami</span>) /usr/local/lib/node_modules</span><br><span class="line"></span><br><span class="line"><span class="comment"># 然后正常安装</span></span><br><span class="line">npm install -g &lt;包名&gt;</span><br></pre></td></tr></table></figure><p>或者用 nvm 管理 Node, 从根本上避免权限问题。</p><h1 id="九、一个通用的排查流程"><a href="#九、一个通用的排查流程" class="headerlink" title="九、一个通用的排查流程"></a>九、一个通用的排查流程</h1><p>把上面的内容浓缩成一张流程图:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 第 0 步: 确认症状类型</span></span><br><span class="line">npm install2&gt;&amp;1 | <span class="built_in">tee</span> install.log</span><br><span class="line">grep -oE <span class="string">&quot;ETIMEDOUT|ECONNRESET|ENOTFOUND|CERT_HAS_EXPIRED|EACCES|ERR_PNPM_[A-Z_]+&quot;</span> install.log</span><br><span class="line"></span><br><span class="line"><span class="comment"># 第 1 步: 确认源可达(最常见原因)</span></span><br><span class="line">npm config get registry</span><br><span class="line">curl -s -o /dev/null -w <span class="string">&quot;%&#123;http_code&#125;\n&quot;</span> -m 15 $(npm config get registry)react</span><br><span class="line"></span><br><span class="line"><span class="comment"># 第 2 步: 确认无代理干扰</span></span><br><span class="line"><span class="built_in">echo</span> <span class="variable">$HTTP_PROXY</span>; npm config get proxy</span><br><span class="line"></span><br><span class="line"><span class="comment"># 第 3 步: 最小化验证</span></span><br><span class="line">npm view react version</span><br><span class="line"></span><br><span class="line"><span class="comment"># 第 4 步: 清理缓存重试</span></span><br><span class="line">npm cache clean --force</span><br><span class="line">npm install</span><br><span class="line"></span><br><span class="line"><span class="comment"># 第 5 步: 换源重试</span></span><br><span class="line">npm config <span class="built_in">set</span> registry https://registry.npmmirror.com/</span><br><span class="line">npm install</span><br></pre></td></tr></table></figure><p><strong>按顺序走, 不要跳步。</strong> 大多数情况在第 1 或第 5 步就解决了。</p><h1 id="十、小结"><a href="#十、小结" class="headerlink" title="十、小结"></a>十、小结</h1><p><strong>最核心的三条</strong>:</p><ol><li><p><strong>先诊断再动手</strong>。 <code>npm config get registry</code> + <code>npm view react version</code> 这两条命令能定位八成问题。</p></li><li><p><strong>换源优先于调参数</strong>。 源不可达的时候, 把超时调到 10 分钟只是让你多等。</p></li><li><p><strong>别随手关 SSL</strong>。 那是用安全换 convenience, 记得到手改回来。</p></li></ol><p><strong>一个长期建议</strong>: 在项目里放一个 <code>.npmrc</code>(而不是只在全局配), 这样新人 clone 下来就能用, 也不会因为某台机器配置过期而卡住。</p><p><strong>最后一个实用技巧</strong>: 遇到 npm 问题时, 先试试 <code>pnpm install</code>。 有时候不是 npm 坏了, 而是 npm 和这个项目的依赖树合不来。</p><hr><p>参考:</p><ul><li>npm 官方文档配置项: <a href="https://docs.npmjs.com/configuring-npm">https://docs.npmjs.com/configuring-npm</a></li><li>npmmirror 源: <a href="https://npmmirror.com/">https://npmmirror.com</a></li><li>pnpm 配置: <a href="https://pnpm.io/config">https://pnpm.io/config</a></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>博客数据分析实战: 百度统计与 GA4 怎么配, 以及流量从哪来</title>
      <link>https://zsyyblog.com/587ac538.html</link>
      <description>装了统计代码却不知道该看什么? 本文讲清百度统计与 GA4 各自能回答什么问题、Hexo + Butterfly 下的具体配置与验证方法, 以及拿到数据后如何判断下一篇该写什么。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/">前端</category>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/Hexo/">Hexo</category>
      <category domain="https://zsyyblog.com/tags/Hexo/">Hexo</category>
      <pubDate>Wed, 21 Oct 2026 11:35:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>大部分技术博客都装了统计代码, 但装了之后呢?</p><p>我见过太多站点的状态是: 代码挂着, 后台注册了, 然后再也没打开过。 偶尔想起来看一眼, 发现数据零零散散, 看不出所以然, 最后还是不知道该写什么、该优化什么。</p><p><strong>统计工具的价值不在于”有一个数字”, 而在于”能回答具体问题”。</strong> 这篇讲怎么把工具用起来。</p><h1 id="一、先想清楚-你想回答什么问题"><a href="#一、先想清楚-你想回答什么问题" class="headerlink" title="一、先想清楚: 你想回答什么问题"></a>一、先想清楚: 你想回答什么问题</h1><p>装统计之前, 先问自己三个问题:</p><table><thead><tr><th align="left">问题</th><th align="left">主要看哪个指标</th><th align="left">用什么工具</th></tr></thead><tbody><tr><td align="left">有多少人看?</td><td align="left">PV &#x2F; UV</td><td align="left">都行</td></tr><tr><td align="left">哪些文章有人看?</td><td align="left">页面 URL 排行</td><td align="left">都行</td></tr><tr><td align="left">读者从哪来?</td><td align="left">来源网站 &#x2F; 搜索词</td><td align="left">都行</td></tr><tr><td align="left">搜索关键词能带来流量吗?</td><td align="left"><strong>搜索词报告</strong></td><td align="left"><strong>只有百度统计</strong></td></tr><tr><td align="left">海外读者有多少?</td><td align="left">国家&#x2F;地区分布</td><td align="left"><strong>只有 GA4</strong></td></tr><tr><td align="left">读者是哪个地区的?</td><td align="left">地区分布</td><td align="left">GA4 更准</td></tr></tbody></table><p><strong>关键结论先说</strong>:</p><ul><li><strong>百度统计</strong>: 唯一能拿到<strong>搜索关键词</strong>的免费工具。对中文技术博客来说, 这是最重要的功能—— 能直接告诉你读者在搜什么, 从而告诉你下一篇该写什么。</li><li><strong>GA4</strong>: 地区分布更准, 且对国内站的数据也能记录(但常被拦截), 适合有海外流量或想看地区差异的。</li></ul><h2 id="怎么选-按你的实际情况挑"><a href="#怎么选-按你的实际情况挑" class="headerlink" title="怎么选: 按你的实际情况挑"></a>怎么选: 按你的实际情况挑</h2><p>两个工具各有不可替代的部分, 选哪个取决于你的站点情况:</p><p><strong>只需要百度统计就够的情况</strong>:</p><ul><li>站是中文技术博客, 流量主要来自百度搜索</li><li>你的核心需求是<strong>搜索词报告</strong>(指导选题)</li><li>不想维护两套后台</li></ul><p><strong>建议两个都装的情况</strong>:</p><ul><li>你有海外读者(收到过英文评论, 或后台出现境外来源)</li><li>需要区分不同国家和地区的访问质量(比如想知道欧美读者占比)</li><li>你已经在用 Google 生态的其他工具(Search Console 等), 顺手一起配</li></ul><p><strong>折中做法</strong>: 先只装百度统计, 等到确实需要地区分布数据时再加 GA4。 两个工具互不冲突, 随时可以加。</p><blockquote><p>常见情况是: 国内技术站先装百度统计跑一段时间, 等有海外流量了再补 GA4。</p></blockquote><h1 id="二、百度统计配置-能拿到搜索词"><a href="#二、百度统计配置-能拿到搜索词" class="headerlink" title="二、百度统计配置(能拿到搜索词)"></a>二、百度统计配置(能拿到搜索词)</h1><h2 id="为什么百度统计值得优先了解"><a href="#为什么百度统计值得优先了解" class="headerlink" title="为什么百度统计值得优先了解"></a>为什么百度统计值得优先了解</h2><p>百度搜索是国内流量的大头, 而百度统计能看到<strong>用户在搜索框里实际输入了什么词才点到你的文章</strong>。</p><p>这个数据没有任何其他工具能提供。 Google Analytics 在国内基本收不到数据, 也拿不到百度搜索词。</p><h2 id="Hexo-Butterfly-怎么配"><a href="#Hexo-Butterfly-怎么配" class="headerlink" title="Hexo + Butterfly 怎么配"></a>Hexo + Butterfly 怎么配</h2><p><strong>第一步: 在百度统计后台添加站点</strong></p><p>拿到一个 <code>hm.baidu.com/hm.js?xxxxxxxx</code> 形式的代码。</p><p><strong>第二步: 填进 Butterfly 配置</strong></p><p><code>_config.butterfly.yml</code> 里已经有这一项, 把 ID 换成你自己的:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">baidu_analytics:</span> <span class="string">你的统计ID</span></span><br></pre></td></tr></table></figure><p>拿到 ID 的方法: 在百度统计后台点”管理” → “获取代码”, 会看到类似 <code>hm.baidu.com/hm.js?xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</code> 这样一串, <strong>取问号后面那串 32 位字符</strong>, 填到配置里。</p><p>注意这里<strong>只填 ID, 不填完整脚本 URL</strong>—— Butterfly 已经内置了 <code>hm.baidu.com/hm.js</code> 的引用逻辑, 填完整 URL 会导致脚本加载两次。</p><blockquote><p><strong>常见错误</strong>: 把 <code>https://hm.baidu.com/hm.js?xxx</code> 整段粘进去。 主题会自己拼 <code>hm.baidu.com/hm.js?</code> + 你填的内容, 结果变成 <code>hm.baidu.com/hm.js?https://hm.baidu.com/hm.js?xxx</code>, 统计失效。</p></blockquote><h2 id="验证是否装成功"><a href="#验证是否装成功" class="headerlink" title="验证是否装成功"></a>验证是否装成功</h2><p>三个检查点:</p><p><strong>1. 看页面源码</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 打开任意文章页, 查看源代码, 搜 hm.baidu.com</span></span><br></pre></td></tr></table></figure><p>应该能看到类似:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">var</span> _hmt = _hmt || [];</span></span><br><span class="line"><span class="language-javascript">(<span class="keyword">function</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">var</span> hm = <span class="variable language_">document</span>.<span class="title function_">createElement</span>(<span class="string">&quot;script&quot;</span>);</span></span><br><span class="line"><span class="language-javascript">  hm.<span class="property">src</span> = <span class="string">&quot;https://hm.baidu.com/hm.js?你的统计ID&quot;</span>;</span></span><br><span class="line"><span class="language-javascript">  ...</span></span><br><span class="line"><span class="language-javascript">&#125;)();</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>2. 实时统计页</strong></p><p>百度统计后台有”实时访客”页面。 <strong>打开自己的博客页面, 实时页面应该能看到自己</strong>。</p><p>如果看不到自己, 说明代码没生效或被拦截。</p><p><strong>3. 隔天看数据</strong></p><p>新站前24 小时数据不准(需要积累), 但第二天应该有数据进来。</p><h2 id="百度统计的隐私合规提醒"><a href="#百度统计的隐私合规提醒" class="headerlink" title="百度统计的隐私合规提醒"></a>百度统计的隐私合规提醒</h2><p><strong>在接入 GDPR 区域合规要求后, 百度统计提供符合欧盟标准的版本。</strong> 但前提是你得有隐私政策页面, 并在页面上做 Cookie 提示。</p><p>关于合规要求, 参见本站的<a href="/privacy/">隐私政策</a>一文。</p><h1 id="三、GA4-配置-地区分布更准"><a href="#三、GA4-配置-地区分布更准" class="headerlink" title="三、GA4 配置(地区分布更准)"></a>三、GA4 配置(地区分布更准)</h1><h2 id="GA4-相比百度统计的优势"><a href="#GA4-相比百度统计的优势" class="headerlink" title="GA4 相比百度统计的优势"></a>GA4 相比百度统计的优势</h2><table><thead><tr><th align="left">维度</th><th align="left">百度统计</th><th align="left">GA4</th></tr></thead><tbody><tr><td align="left">搜索关键词</td><td align="left">✅ 有</td><td align="left">❌ 无(被Google封锁)</td></tr><tr><td align="left">国家&#x2F;地区</td><td align="left">粗略</td><td align="left">✅ 精确</td></tr><tr><td align="left">设备&#x2F;浏览器</td><td align="left">✅</td><td align="left">✅</td></tr><tr><td align="left">用户行为流</td><td align="left">基础</td><td align="left">✅ 事件流</td></tr><tr><td align="left">国内数据完整性</td><td align="left">✅ 完整</td><td align="left">⚠️ 常被拦截</td></tr><tr><td align="left">实时数据延迟</td><td align="left">秒级</td><td align="left">约1 分钟</td></tr></tbody></table><p><strong>结论</strong>: 中文技术站用百度统计看搜索词, 用 GA4 看地区分布。 两个互补。</p><h2 id="配置方法"><a href="#配置方法" class="headerlink" title="配置方法"></a>配置方法</h2><p><strong>第一步: 创建 GA4 媒体资源</strong></p><p>在 <a href="https://analytics.google.com/">https://analytics.google.com</a> 里创建, 拿到 <code>G-XXXXXXXXXX</code> 格式的 ID。</p><p><strong>第二步: 填进配置</strong></p><p><code>_config.butterfly.yml</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">google_analytics:</span> <span class="string">G-XXXXXXXXXX</span></span><br></pre></td></tr></table></figure><p><strong>同样注意</strong>: 只填 ID,不要填完整脚本地址。</p><p><strong>第三步(可选): 站点验证</strong></p><p>GA4 后台要求验证网站所有权, 最简单的方式是在 <code>source/</code> 下建一个文件:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p <span class="built_in">source</span>/gtag</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;google-site-verification-xxxxxxxxxxxx.html&quot;</span> &gt; <span class="built_in">source</span>/gtag/google-site-verification-xxxxxxxxxxxx.html</span><br></pre></td></tr></table></figure><p>构建后访问 <code>https://你的域名/gtag/google-site-verification-xxxxxxxxxxxx.html</code> 即可。</p><h2 id="GA4-里值得看的报告"><a href="#GA4-里值得看的报告" class="headerlink" title="GA4 里值得看的报告"></a>GA4 里值得看的报告</h2><p><strong>1. 国家&#x2F;地区分布</strong>(<code>Reports &gt; Geography</code>)</p><p>这是 GA4 最不可替代的能力。 如果发现某个国家&#x2F;地区占比异常高, 说明你的内容在那里有意外受众。</p><p><strong>2. 流量来源</strong>(<code>Reports &gt; Traffic acquisition</code>)</p><p>按 Source&#x2F;Medium 分组, 能看出:</p><table><thead><tr><th align="left">来源</th><th align="left">含义</th></tr></thead><tbody><tr><td align="left"><code>organic / google</code></td><td align="left">Google 自然搜索</td></tr><tr><td align="left"><code>organic / baidu</code></td><td align="left">百度自然搜索</td></tr><tr><td align="left"><code>referral / xxx</code></td><td align="left">其他网站引荐</td></tr><tr><td align="left"><code>direct / (none)</code></td><td align="left">直接访问或收藏</td></tr></tbody></table><p><strong>3. 内容页表现</strong>(<code>Reports &gt; Content</code>)</p><p>按页面浏览次数排序, 看你哪篇文章最受欢迎。</p><p><strong>4. 自定义事件</strong>(<code>Reports &gt; Engagement &gt; Events</code>)**</p><p>如果你在站点里加了”复制代码”、”点击广告”之类的自定义事件, 这里能看到。</p><h1 id="四、拿到数据后-判断下一篇写什么"><a href="#四、拿到数据后-判断下一篇写什么" class="headerlink" title="四、拿到数据后: 判断下一篇写什么"></a>四、拿到数据后: 判断下一篇写什么</h1><p>这是统计工具真正的价值所在。</p><h2 id="方法一-百度搜索词报告-最重要"><a href="#方法一-百度搜索词报告-最重要" class="headerlink" title="方法一: 百度搜索词报告(最重要)"></a>方法一: 百度搜索词报告(最重要)</h2><p><strong>路径</strong>: 百度统计 → 搜索分析 → 搜索词</p><p>这个报告会列出访客<strong>实际搜索的关键词</strong>, 按展现和点击排序。</p><p><strong>典型的三种情况</strong>:</p><p><strong>情况1: 搜索词命中但点击率低</strong></p><p>比如某词展现 500, 点击 10(点击率 2%)。</p><p>说明: 你的文章<strong>有可能排到前列但标题不吸引人</strong>。<br>做法: 优化标题, 让它更贴近搜索词。</p><p><strong>情况2: 某些高频搜索词你完全没有对应文章</strong></p><p>比如某个词展现 300, 但你的站内搜索里没有相关文章。</p><p>说明: <strong>这就是下一篇该写的。</strong> 有真实需求, 搜不到就等于没接住。</p><p><strong>情况3: 词很精准但展现极低</strong></p><p>说明这个词搜索量本身小, 但<strong>转化率可能很高</strong>(目标读者更明确)。<br>判断: 如果这个词和你的核心主题高度相关, 值得写小而精的深度文。</p><h2 id="方法二-内容页表现找”潜力文章”"><a href="#方法二-内容页表现找”潜力文章”" class="headerlink" title="方法二: 内容页表现找”潜力文章”"></a>方法二: 内容页表现找”潜力文章”</h2><p>在百度统计或 GA4 里按浏览量排序, 找出:</p><p><strong>浏览量高但跳出率也高</strong> → 内容可能不够深或有误导, 值得补充完善</p><p><strong>浏览量低但停留时间长</strong> → 好内容但标题不吸引人, 可以换个更吸引人的写法重发或改标题</p><p><strong>有浏览但几乎没有评论&#x2F;互动</strong> → 缺互动引导, 可以考虑在文末加提问</p><h2 id="方法三-搜索词-站点内容做差集"><a href="#方法三-搜索词-站点内容做差集" class="headerlink" title="方法三: 搜索词 + 站点内容做差集"></a>方法三: 搜索词 + 站点内容做差集</h2><p>这个方法最能指导选题。</p><ol><li>去百度统计导出搜索词报告(CSV)</li><li>列出所有关键词</li><li>对每个词, 在自己博客里搜一下有没有相关文章</li><li><strong>没找到对应文章的词, 就是选题候选</strong></li></ol><p>我做这件事的体会是: <strong>导出一次 CSV, 顶你盯一个月后台。</strong> 因为后台的数据是分散的, 而导出的列表能让你一眼看出缺口在哪。</p><h1 id="五、几个实用技巧"><a href="#五、几个实用技巧" class="headerlink" title="五、几个实用技巧"></a>五、几个实用技巧</h1><h2 id="1-给不同页面加来源标记"><a href="#1-给不同页面加来源标记" class="headerlink" title="1. 给不同页面加来源标记"></a>1. 给不同页面加来源标记</h2><p>想知道某个具体推广渠道的效果(比如你在某平台发了链接), 可以用 URL 参数区分:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 在外链上加参数 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;https://你的域名/xxx.html?utm_source=weibo&amp;utm_medium=social&quot;</span>&gt;</span></span><br></pre></td></tr></table></figure><p>主流统计工具会自动识别 <code>utm_*</code> 参数, 在来源报告里单独列出来。</p><h2 id="2-统计代码放哪"><a href="#2-统计代码放哪" class="headerlink" title="2. 统计代码放哪"></a>2. 统计代码放哪</h2><p>Butterfly 已经帮你放好了(<code>&lt;head&gt;</code> 里)。 <strong>不要自己再手动注入一份</strong>——重复加载会让 PV 统计翻倍。</p><p>判断方法: 在页面源码里搜 <code>hm.baidu.com</code>, 如果出现两次, 就是重复了。</p><h2 id="3-不要看单日数据"><a href="#3-不要看单日数据" class="headerlink" title="3. 不要看单日数据"></a>3. 不要看单日数据</h2><p><strong>新站或低流量站的单日数据没有统计意义</strong>, 波动比信号大得多。</p><p>建议看:</p><ul><li><strong>周报</strong>: 比日报稳定</li><li><strong>月报</strong>: 看趋势和季节性</li><li><strong>同比</strong>: 和去年同期比</li></ul><h2 id="4-PV-UV-比例是个有意思的指标"><a href="#4-PV-UV-比例是个有意思的指标" class="headerlink" title="4. PV &#x2F; UV 比例是个有意思的指标"></a>4. PV &#x2F; UV 比例是个有意思的指标</h2><ul><li><strong>PV&#x2F;UV &lt; 2</strong>: 读者来了就走, 内容可能不够深或者不够对症</li><li><strong>PV&#x2F;UV 2-4</strong>: 比较健康, 说明有回访</li><li><strong>PV&#x2F;UV &gt; 4</strong>: 内容很实用, 或者站点结构让读者容易”逛”</li></ul><h2 id="5-认识”直接访问”"><a href="#5-认识”直接访问”" class="headerlink" title="5. 认识”直接访问”"></a>5. 认识”直接访问”</h2><p><code>direct / (none)</code> 这一项高, 不一定是好事。</p><p>如果很高而搜索流量很低, 说明<strong>读者基本是靠收藏或手动输入来的</strong>——说明你的站没什么自然流量, SEO 没起效, 或者内容传播主要靠熟人。</p><p>这种情况下加大 SEO 和内容推广的投入, 比继续写同类文章更值得。</p><h1 id="六、常见问题"><a href="#六、常见问题" class="headerlink" title="六、常见问题"></a>六、常见问题</h1><h2 id="装了统计但没有数据"><a href="#装了统计但没有数据" class="headerlink" title="装了统计但没有数据"></a>装了统计但没有数据</h2><p><strong>排查清单</strong>:</p><ol><li>源码里有脚本吗? → 没有就是配置没生效</li><li>有脚本但后台没数据? → 检查是否被浏览器广告拦截器屏蔽(测试用无痕模式关掉插件)</li><li>数据隔天才出来? → 正常, 实时页面能看自己</li><li>GA4 没数据? → 国内访问 Google Analytics 常被拦截, 这是预期行为; 百度统计不受影响</li></ol><h2 id="两个统计工具的数据对不上"><a href="#两个统计工具的数据对不上" class="headerlink" title="两个统计工具的数据对不上"></a>两个统计工具的数据对不上</h2><p>正常。 它们的口径不同(去重规则、时区、过滤规则), 有 10-20% 的差异不用管。</p><p><strong>看趋势就好, 不要纠结绝对值的精确。</strong></p><h2 id="统计代码会不会影响性能"><a href="#统计代码会不会影响性能" class="headerlink" title="统计代码会不会影响性能"></a>统计代码会不会影响性能</h2><p>基本不会。 统计脚本都是异步加载的, 不阻塞页面渲染。</p><p>但如果你加载了<strong>很多</strong>第三方脚本(广告、多个统计、字体 CDN), 累积起来会影响首屏时间。 这时候该考虑合并或用异步&#x2F;延迟加载。</p><h1 id="七、小结"><a href="#七、小结" class="headerlink" title="七、小结"></a>七、小结</h1><p><strong>最小可用组合</strong>:</p><ol><li><strong>百度统计</strong> —— 拿搜索词, 指导选题</li><li><strong>GA4</strong> —— 看地区分布和流量来源(需要时再加)</li><li><strong>每月导出一次搜索词报告</strong>, 和站点内容做差集</li></ol><p><strong>核心观念</strong>: 统计工具是用来<strong>指导行动</strong>的, 不是用来看好看的。</p><p>UV 从 3 涨到 30 不值得高兴; 但”发现有个高频搜索词没对应文章,赶紧写了” 这件事, 价值远大于前者。</p><hr><p>参考:</p><ul><li>百度统计: <a href="https://tongji.baidu.com/">https://tongji.baidu.com</a></li><li>Google Analytics 4: <a href="https://analytics.google.com/">https://analytics.google.com</a></li><li>GA4 文档: <a href="https://support.google.com/analytics/">https://support.google.com/analytics/</a></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hexo 博客合规与隐私: 备案到隐私政策页面, 国内外平台到底要求什么</title>
      <link>https://zsyyblog.com/1db5f3ef.html</link>
      <description>个人博客接广告前必须过的一关: ICP备案与公安备案怎么展示、隐私政策页面为什么要建、Cookie 与第三方服务怎么披露。本文给出 Hexo + Butterfly 下的完整落地步骤和可直接改用的模板。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/">前端</category>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/Hexo/">Hexo</category>
      <category domain="https://zsyyblog.com/tags/Hexo/">Hexo</category>
      <pubDate>Wed, 21 Oct 2026 11:20:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>做个人博客有件事很微妙: 前期完全没人管, 一旦想接广告变现, 合规就成了硬门槛。</p><p>我见过不少技术博主卡在这一步—— AdSense 审核反复被拒, 百度联盟一直 pending, 最后才发现缺的不是内容, 就是一个像样的隐私政策页面, 和页脚那行不起眼的备案号。</p><p>这篇把这件事一次讲清楚。<strong>只讲个人博客用得上的部分</strong>, 公司主体、法务合规这些不在范围内。</p><h1 id="一、先搞明白-到底谁在要求什么"><a href="#一、先搞明白-到底谁在要求什么" class="headerlink" title="一、先搞明白: 到底谁在要求什么"></a>一、先搞明白: 到底谁在要求什么</h1><p>不同平台的规则差异很大, 搞混了会白折腾。</p><table><thead><tr><th align="left">要求方</th><th align="left">要什么</th><th align="left">不给会怎样</th></tr></thead><tbody><tr><td align="left">工信部&#x2F;属地网信办</td><td align="left">ICP 备案(国内服务器)</td><td align="left">站点可能被阻断</td></tr><tr><td align="left">公安网安</td><td align="left">公安联网备案</td><td align="left">属地要求, 检查时会被问</td></tr><tr><td align="left">Google AdSense</td><td align="left">隐私政策页 + Cookie 披露</td><td align="left">审核不通过 &#x2F; 后续封号</td></tr><tr><td align="left">百度联盟</td><td align="left">备案号 + 网站内容</td><td align="left">审核不通过</td></tr><tr><td align="left">微信生态</td><td align="left">ICP + 备案号展示</td><td align="left">无法接入</td></tr></tbody></table><p><strong>有个关键区别</strong>: 备案是<strong>行政要求</strong>(给监管看的), 隐私政策是<strong>平台要求</strong>(给用户看的)。</p><p>前者没做是违法, 后者没做是过不了审核。</p><h1 id="二、ICP-备案与公安备案"><a href="#二、ICP-备案与公安备案" class="headerlink" title="二、ICP 备案与公安备案"></a>二、ICP 备案与公安备案</h1><h2 id="备案放在哪-页脚最显眼处"><a href="#备案放在哪-页脚最显眼处" class="headerlink" title="备案放在哪: 页脚最显眼处"></a>备案放在哪: 页脚最显眼处</h2><p>国内备案要求<strong>在网站底部显著位置展示备案号</strong>, 并且链接到备案系统。</p><p>Hexo + Butterfly 里, 页脚自定义文本在 <code>_config.butterfly.yml</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">footer:</span></span><br><span class="line">  <span class="attr">custom_text:</span> <span class="string">&gt;</span></span><br><span class="line"><span class="string">    &lt;a href=&quot;https://beian.miit.gov.cn&quot; target=&quot;_blank&quot;&gt;你的省份ICP备xxxxxxxx号-1&lt;/a&gt;</span></span><br><span class="line"><span class="string"></span>  <span class="attr">icon:</span></span><br><span class="line">    <span class="attr">height:</span> <span class="number">32</span></span><br><span class="line">    <span class="attr">width:</span> <span class="number">32</span></span><br></pre></td></tr></table></figure><p>点进去是加密串, 之后就是公安备案:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">footer:</span></span><br><span class="line">  <span class="attr">custom_text:</span> <span class="string">&gt;</span></span><br><span class="line"><span class="string">    &lt;a href=&quot;https://beian.miit.gov.cn&quot; target=&quot;_blank&quot;&gt;你的省份ICP备xxxxxxxx号-1&lt;/a&gt;&lt;br&gt;</span></span><br><span class="line"><span class="string">    &lt;a target=&quot;_blank&quot; href=&quot;http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=xxxxxxxx&quot;&gt;</span></span><br><span class="line"><span class="string">      &lt;img src=&quot;/img/beian.png&quot;&gt;你的省份公网安备 xxxxxxxxxxxx号</span></span><br><span class="line"><span class="string">    &lt;/a&gt;</span></span><br><span class="line"><span class="string"></span>  <span class="attr">icon:</span></span><br><span class="line">    <span class="attr">height:</span> <span class="number">32</span></span><br><span class="line">    <span class="attr">width:</span> <span class="number">32</span></span><br></pre></td></tr></table></figure><h2 id="公安备案的图标别乱改"><a href="#公安备案的图标别乱改" class="headerlink" title="公安备案的图标别乱改"></a>公安备案的图标别乱改</h2><p>公安备案要求<strong>同时展示图标</strong>, 而且图标不能变形、不能改颜色。常见做法是直接放一张图片。</p><p><strong>别为了美观自己重绘这个图标</strong>, 官方有明确要求, 被检查出来会扣分。</p><h2 id="备案号变更后要同步"><a href="#备案号变更后要同步" class="headerlink" title="备案号变更后要同步"></a>备案号变更后要同步</h2><p>域名备案信息变更(比如换服务器、主体变更), 要在 30 日内更新备案系统, 同时<strong>同步改页脚</strong>, 两边不一致会被判为”信息不实”。</p><h1 id="三、隐私政策页面-为什么要建-怎么建"><a href="#三、隐私政策页面-为什么要建-怎么建" class="headerlink" title="三、隐私政策页面: 为什么要建, 怎么建"></a>三、隐私政策页面: 为什么要建, 怎么建</h1><h2 id="为什么必须要有"><a href="#为什么必须要有" class="headerlink" title="为什么必须要有"></a>为什么必须要有</h2><p>两个层面:</p><p><strong>法律层面</strong>: 依据《个人信息保护法》, 处理用户个人信息(包括收集访问日志、Cookie)需要有合法基础, 并告知用户。境外平台(AdSense)更严格, 明确要求站点提供隐私政策页。</p><p><strong>平台层面</strong>: AdSense 的审核清单里, 隐私政策是硬性项。缺了会直接被拒, 而且理由往往写得很模糊, 让你猜半天。</p><h2 id="Hexo-里怎么建页面"><a href="#Hexo-里怎么建页面" class="headerlink" title="Hexo 里怎么建页面"></a>Hexo 里怎么建页面</h2><p><strong>第一步: 建文件</strong></p><p>在 <code>source/</code> 下建一个目录, 放 <code>index.md</code>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> <span class="built_in">source</span>/privacy</span><br></pre></td></tr></table></figure><p><code>source/privacy/index.md</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">title:</span> <span class="string">隐私政策</span></span><br><span class="line"><span class="attr">date:</span> <span class="number">2026-10-21</span></span><br><span class="line"><span class="attr">type:</span> <span class="string">privacy</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><p>注意 <code>permalink</code> 要匹配目录名, 这样访问 <code>/privacy/</code> 就是这个页面。</p><p><strong>第二步: 加到菜单</strong></p><p><code>_config.butterfly.yml</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">menu:</span></span><br><span class="line">  <span class="string">隐私政策:</span> <span class="string">/privacy/</span> <span class="string">||</span> <span class="string">fas</span> <span class="string">fa-shield-halved</span></span><br></pre></td></tr></table></figure><p><strong>第三步: 写内容</strong></p><p>模板见下一节, 按实际情况改。</p><h2 id="隐私政策要写哪些内容"><a href="#隐私政策要写哪些内容" class="headerlink" title="隐私政策要写哪些内容"></a>隐私政策要写哪些内容</h2><p>按主流平台的模板, 必须覆盖这几块:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">title:</span> <span class="string">隐私政策</span></span><br><span class="line"><span class="attr">date:</span> <span class="number">2026-10-21</span></span><br><span class="line"><span class="attr">type:</span> <span class="string">privacy</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="meta"></span></span><br><span class="line"><span class="comment"># 隐私政策</span></span><br><span class="line"></span><br><span class="line"><span class="string">最后更新:</span> <span class="number">2026-10-21</span></span><br><span class="line"></span><br><span class="line"><span class="string">本站(你的域名)是一个技术分享性质的静态博客。</span> <span class="string">这篇政策说明本站会收集哪些信息、为什么收集,</span> <span class="string">以及你有哪些权利。</span></span><br><span class="line"></span><br><span class="line"><span class="comment">## 收集的信息</span></span><br><span class="line"></span><br><span class="line"><span class="comment">### 访问日志</span></span><br><span class="line"><span class="string">托管服务商(你的</span> <span class="string">CDN</span> <span class="string">或服务器厂商)在处理请求时会生成访问日志,</span> <span class="string">可能包含:</span> <span class="string">IP</span> <span class="string">地址、请求时间、请求的页面、浏览器标识、来源地址。</span> <span class="string">这些日志**仅用于故障排查和统计,</span> <span class="string">不会用于识别你个人身份**。</span></span><br><span class="line"></span><br><span class="line"><span class="comment">### Cookie 与本地存储</span></span><br><span class="line"><span class="string">本站使用以下第三方服务,</span> <span class="string">它们可能在你浏览器中设置</span> <span class="string">Cookie</span> <span class="string">或使用本地存储:</span></span><br><span class="line"></span><br><span class="line"><span class="string">|</span> <span class="string">服务</span> <span class="string">|</span> <span class="string">用途</span> <span class="string">|</span> <span class="string">说明</span> <span class="string">|</span></span><br><span class="line"><span class="string">|</span> <span class="string">:--</span> <span class="string">|</span> <span class="string">:--</span> <span class="string">|</span> <span class="string">:--</span> <span class="string">|</span></span><br><span class="line"><span class="string">|</span> <span class="string">百度统计</span> <span class="string">|</span> <span class="string">访问量统计</span> <span class="string">|</span> <span class="string">境内服务,</span> <span class="string">数据在境内</span> <span class="string">|</span></span><br><span class="line"><span class="string">|</span> <span class="string">Google</span> <span class="string">AdSense</span> <span class="string">|</span> <span class="string">广告投放</span> <span class="string">|</span> <span class="string">境外服务,</span> <span class="string">涉及跨境传输</span> <span class="string">|</span></span><br><span class="line"><span class="string">|</span> <span class="string">Valine</span> <span class="string">评论系统</span> <span class="string">|</span> <span class="string">评论功能</span> <span class="string">|</span> <span class="string">评论内容会公开显示</span> <span class="string">|</span></span><br><span class="line"></span><br><span class="line"><span class="string">&gt;</span> <span class="string">这张表是**举例**,</span> <span class="string">请按自己站点实际接入的服务增删。</span> <span class="string">原则是:</span> <span class="string">页面上加载了哪个第三方服务,</span> <span class="string">政策里就要提到哪个。</span> <span class="string">用不到的删掉,</span> <span class="string">漏掉的补上。</span></span><br><span class="line"></span><br><span class="line"><span class="comment">### 评论</span></span><br><span class="line"><span class="string">如果你发表评论,</span> <span class="string">评论内容、昵称和邮箱会存储在第三方服务器(LeanCloud)并公开展示。</span> <span class="string">**请不要在评论中填写敏感个人信息。**</span></span><br><span class="line"></span><br><span class="line"><span class="comment">### RSS 订阅</span></span><br><span class="line"><span class="string">本站的RSS</span> <span class="string">订阅源不收集个人信息,</span> <span class="string">但你使用的阅读器可能有自己的数据策略,</span> <span class="string">那部分由阅读器提供方负责。</span></span><br><span class="line"></span><br><span class="line"><span class="comment">## 数据的用途与保存</span></span><br><span class="line"></span><br><span class="line"><span class="string">收集的信息仅用于:</span> <span class="string">站点运行与故障排查、访问量统计、广告投放优化。</span> <span class="string">不用于出售、不用于非法目的。</span></span><br><span class="line"></span><br><span class="line"><span class="comment">## 第三方服务与跨境传输</span></span><br><span class="line"></span><br><span class="line"><span class="string">部分第三方服务(如</span> <span class="string">Google</span> <span class="string">AdSense)的数据处理可能涉及境外。</span> <span class="string">使用本站即表示你接受该服务的数据处理方式。</span></span><br><span class="line"></span><br><span class="line"><span class="comment">## 你的权利</span></span><br><span class="line"></span><br><span class="line"><span class="string">你可以随时:</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">请求删除与你的</span> <span class="string">IP</span> <span class="string">关联的访问数据(在合理范围内)</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">拒绝非必要</span> <span class="string">Cookie(通过浏览器设置)</span></span><br><span class="line"><span class="bullet">-</span> <span class="string">撤回评论授权,</span> <span class="string">联系我删除你的评论</span></span><br><span class="line"></span><br><span class="line"><span class="string">联系邮箱见本站联系方式页面。</span></span><br><span class="line"></span><br><span class="line"><span class="comment">## 政策更新</span></span><br><span class="line"></span><br><span class="line"><span class="string">本政策可能随站点功能变化而更新,</span> <span class="string">重大变更会在本站公告。</span> <span class="string">继续使用本站视为接受更新后的政策。</span></span><br></pre></td></tr></table></figure><h2 id="关于隐私政策的几个常见误区"><a href="#关于隐私政策的几个常见误区" class="headerlink" title="关于隐私政策的几个常见误区"></a>关于隐私政策的几个常见误区</h2><p><strong>1. 不要写”我们不收集任何信息”</strong></p><p>只要接了统计和广告, 就一定有 IP 和 Cookie 被处理。 写了反而显得不真诚, 平台审核人工看得出矛盾。</p><p><strong>2. 必须有联系方式</strong></p><p>Google 要求隐私政策里有可用的联系方式。 没有的话审核可能被拒。</p><p><strong>3. 放在可达的位置</strong></p><p>不能藏在页脚小字里让人找不到。 一般做法是页脚留链接, 或菜单里直接有。 我自己的站点是放菜单。</p><p><strong>4. 声明要具体</strong></p><p>“我们可能使用第三方服务”这种太空泛的表述没有意义。 <strong>要具体到服务名称和用途</strong>, 读者才能知道你拒绝了什么。</p><h1 id="四、Cookie-与第三方脚本的披露"><a href="#四、Cookie-与第三方脚本的披露" class="headerlink" title="四、Cookie 与第三方脚本的披露"></a>四、Cookie 与第三方脚本的披露</h1><p>这是最容易被忽略的一块。 有些站点在隐私政策里写了, 但页面里实际加载的东西和声明对不上—— 这属于<strong>不实陈述</strong>, 审核和合规检查都过不去。</p><p>自查方法: 打开浏览器开发者工具的 Network 面板, 加载一个页面, 把所有第三方域名列出来, 逐个核对隐私政策里有没有提到。</p><p><strong>常见的第三方请求:</strong></p><table><thead><tr><th align="left">来源</th><th align="left">一般用途</th></tr></thead><tbody><tr><td align="left"><code>hm.baidu.com</code></td><td align="left">百度统计</td></tr><tr><td align="left"><code>google-analytics.com</code> &#x2F; <code>googletagmanager.com</code></td><td align="left">GA4</td></tr><tr><td align="left"><code>pagead2.googlesyndication.com</code></td><td align="left">AdSense</td></tr><tr><td align="left"><code>cdn.jsdelivr.net</code></td><td align="left">CDN</td></tr><tr><td align="left">字体CDN &#x2F; 图床域名</td><td align="left">静态资源</td></tr></tbody></table><p><strong>如果用的是自建图床和 CDN</strong>(比如又拍云), 也要写进去, 别以为”自己的域名就不算第三方”。</p><h1 id="五、评论区合规"><a href="#五、评论区合规" class="headerlink" title="五、评论区合规"></a>五、评论区合规</h1><p>评论功能也是个人信息处理的场景。</p><h2 id="Valine-的特别之处"><a href="#Valine-的特别之处" class="headerlink" title="Valine 的特别之处"></a>Valine 的特别之处</h2><p>如果你用 Valine 评论, 要注意两点:</p><p><strong>第一, 评论默认公开且会被搜索引擎收录。</strong> 这意味着评论里的邮箱等信息可能长期可检索。 引导用户不要在评论里留敏感信息。</p><p><strong>第二, Valine 的后台管理需要绑定邮箱。</strong> 这个邮箱是管理员的, 不会展示在前台, 但要知道它在哪里。</p><h2 id="加评论前的考虑"><a href="#加评论前的考虑" class="headerlink" title="加评论前的考虑"></a>加评论前的考虑</h2><table><thead><tr><th align="left">选项</th><th align="left">优点</th><th align="left">缺点</th></tr></thead><tbody><tr><td align="left">不开评论</td><td align="left">零合规成本</td><td align="left">少一个互动入口</td></tr><tr><td align="left">开评论但提示</td><td align="left">保留互动</td><td align="left">需要在评论框旁加提示</td></tr><tr><td align="left">自建评论系统</td><td align="left">完全可控</td><td align="left">需要维护</td></tr></tbody></table><p>很多技术博客会选择<strong>只留评论但关闭邮件通知</strong>, 减少个人信息处理环节。</p><h1 id="六、上线前自查清单"><a href="#六、上线前自查清单" class="headerlink" title="六、上线前自查清单"></a>六、上线前自查清单</h1><p>逐条确认:</p><ul><li>页脚有 ICP 备案号, 点击跳转到工信部系统</li><li>公安备案号和图标已展示</li><li>有可达的隐私政策页面</li><li>隐私政策里有<strong>具体的</strong>第三方服务列表(不是泛泛而谈)</li><li>隐私政策里有联系方式</li><li>隐私政策最后更新日期是真实的</li><li>用浏览器 Network 面板核对过第三方请求, 与政策描述一致</li><li>评论框附近有”请勿填写敏感信息”的提示</li><li>备案信息与实际主体一致, 没有过期</li></ul><h1 id="七小结"><a href="#七小结" class="headerlink" title="七小结"></a>七小结</h1><p><strong>优先级排序</strong>:</p><ol><li><strong>隐私政策页面</strong>(最紧急, 影响变现审核)</li><li><strong>页脚备案展示</strong>(合规基础, 检查时会看)</li><li><strong>第三方服务具体披露</strong>(避免不实陈述)</li><li>评论区的个人信息提示(有评论功能才需要)</li></ol><p>这些工作加起来大概一两个小时, 但换来的是审核能过、不会被检查出问题。</p><p><strong>我的体会</strong>: 很多技术人觉得合规是”务虚”, 尤其内容免费分享、不图赚钱的时候。 但一旦想让它长期稳定运行, 流量和收入都依赖平台信任, 合规就是那个最低成本的信任基础。</p><hr><p>参考:</p><ul><li>工信部 ICP&#x2F;IP 地址&#x2F;域名信息备案管理系统: <a href="https://beian.miit.gov.cn/">https://beian.miit.gov.cn</a></li><li>公安互联网站备案: <a href="https://www.beian.gov.cn/">https://www.beian.gov.cn</a></li><li>Google AdSense 政策与条款: <a href="https://support.google.com/adsense/">https://support.google.com/adsense/</a></li><li>个人信息保护法(中国): <a href="http://www.npc.gov.cn/">http://www.npc.gov.cn/</a></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hexo 博客接入广告变现: AdSense 与百度联盟的完整配置</title>
      <link>https://zsyyblog.com/6e37c488.html</link>
      <description>个人博客怎么开始赚钱? 本文记录 Hexo + Butterfly 博客接入 Google AdSense 和百度联盟的完整流程: 申请账号、Butterfly 广告位配置代码、广告展示位置选择, 以及收益现实预期与被封号的雷区。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/">前端</category>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/Hexo/">Hexo</category>
      <category domain="https://zsyyblog.com/tags/Hexo/">Hexo</category>
      <pubDate>Tue, 20 Oct 2026 13:32:56 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>域名每年要钱, 服务器(如果是静态托管)也要钱, 有人会问: <strong>个人博客靠什么覆盖成本?</strong></p><p>常见的路径有三条:</p><ol><li><strong>流量主</strong> —— 最常见, 靠展示广告</li><li><strong>联盟推广</strong> —— 靠推广产品拿佣金</li><li><strong>知识付费&#x2F;赞助</strong> —— 靠内容本身</li></ol><p>这篇先写最实际的: <strong>展示广告怎么接</strong>。同时也会说清楚收益的现实预期 —— 不吹数字,免得你按错误的期望去规划。</p><h1 id="先算一笔账"><a href="#先算一笔账" class="headerlink" title="先算一笔账"></a>先算一笔账</h1><p>假设你的硬成本是:</p><ul><li>域名: 95 元&#x2F;年</li><li>服务器&#x2F;CDN: 静态托管可以是 0, 用云服务器约 100-300 元&#x2F;年</li><li>合计约 200-400 元&#x2F;年, 折合 <strong>每天 0.5-1.1 元</strong></li></ul><p>那么问题变成: <strong>需要多少流量才能覆盖?</strong></p><h2 id="AdSense-的收益量级"><a href="#AdSense-的收益量级" class="headerlink" title="AdSense 的收益量级"></a>AdSense 的收益量级</h2><p>AdSense 的收益和 <strong>CPM(每千次展示收益)</strong> 直接相关, 而 CPM 差异极大:</p><table><thead><tr><th align="left">地区</th><th align="left">CPM(美元&#x2F;千次)</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">美国、加拿大、西欧</td><td align="left">10 - 30</td><td align="left">最高, 竞争激烈</td></tr><tr><td align="left">日本、韩国、新加坡</td><td align="left">8 - 20</td><td align="left">较高</td></tr><tr><td align="left">港澳台</td><td align="left">5 - 15</td><td align="left">中上</td></tr><tr><td align="left">中国大陆</td><td align="left">1 - 4</td><td align="left">受 AdSense 在中国大陆的投放限制影响</td></tr><tr><td align="left">东南亚、南亚</td><td align="left">0.3 - 1.5</td><td align="left">最低</td></tr></tbody></table><p>再叠加一个关键指标: <strong>RPM</strong>(每千次页面浏览的实际收益), 因为不是每个访客都会看到广告(要看展示率和点击率)。</p><p><strong>RPM 通常只有 CPM 的 1&#x2F;5 到 1&#x2F;10。</strong> 举例,CPM 假设 20 美元, RPM 大概 2-4 美元。</p><h2 id="需要多少流量"><a href="#需要多少流量" class="headerlink" title="需要多少流量"></a>需要多少流量</h2><p>按国内流量、RPM 约 0.3 美元(约 2 元人民币)算:</p><table><thead><tr><th align="left">日均 PV</th><th align="left">月 RPM 收入</th><th align="left">覆盖 300 元&#x2F;年</th></tr></thead><tbody><tr><td align="left">100</td><td align="left">6 元</td><td align="left">需要 50 个月</td></tr><tr><td align="left">500</td><td align="left">30 元</td><td align="left">需要 10 个月</td></tr><tr><td align="left">1000</td><td align="left">60 元</td><td align="left">需要 5 个月</td></tr><tr><td align="left">5000</td><td align="left">300 元</td><td align="left">需要 1 个月</td></tr></tbody></table><p><strong>结论很清楚: 靠 AdSense 覆盖成本, 需要日均几百到一两千 PV。</strong> 这不是个小数目。</p><p>如果你的站点是国内技术博客, 走百度联盟(百度的 RPM 对国内流量给得相对合理)会比 AdSense 稍好一些, 但<strong>量级是一样的</strong>。</p><blockquote><p>顺便一提: 我自己的站点还挂了又拍云的联盟推广链接(CDN 和云存储), 这种”顺带推荐”的转化率不高, 但成本是零, 可以作为补充。</p></blockquote><h1 id="AdSense-接入流程"><a href="#AdSense-接入流程" class="headerlink" title="AdSense 接入流程"></a>AdSense 接入流程</h1><h2 id="第一步-申请账号"><a href="#第一步-申请账号" class="headerlink" title="第一步: 申请账号"></a>第一步: 申请账号</h2><p>地址: <a href="https://www.google.com/adsense/">https://www.google.com/adsense/</a></p><p>需要准备:</p><ul><li>站点必须<strong>已经上线</strong>, 域名可访问</li><li>站点内容要够, <strong>一般要求 20-30 篇有实质内容的文章</strong></li><li>一个 Google 账号</li><li>收款信息(国内常见是招商银行&#x2F;中国银行的双币账户, 或电汇)</li></ul><h2 id="第二步-验证域名"><a href="#第二步-验证域名" class="headerlink" title="第二步: 验证域名"></a>第二步: 验证域名</h2><p>在 AdSense 后台添加站点, 填写:</p><ul><li>网站地址: <code>https://zsyyblog.com</code></li><li>流量来源: 填”您在博客中撰写的内容”这类</li></ul><p>然后它会给你一段验证代码, 放到网站 <code>&lt;head&gt;</code> 里。</p><p><strong>Hexo 里怎么放?</strong> 两种方式, 选一种就行:</p><p><strong>方式一(推荐): 用 Butterfly 内置的 google_adsense 配置</strong></p><p>Butterfly 主题本身就预留了 AdSense 的加载位置(<code>layout/includes/head/google_adsense.pug</code>), 直接在 <code>_config.butterfly.yml</code> 里填发布商 ID 即可, 不用手写代码:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">google_adsense:</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">client:</span> <span class="string">ca-pub-你的发布商ID</span></span><br></pre></td></tr></table></figure><p>主题会自动把脚本注入到 <code>&lt;head&gt;</code>。</p><p><strong>方式二: 手动用 inject 注入</strong></p><p>如果不想用内置配置(比如想同时接多个广告平台), 自己注入:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">head:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">|</span></span><br><span class="line">      <span class="string">&lt;script</span> <span class="string">async</span> <span class="string">src=&quot;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-你的发布商ID&quot;</span> <span class="string">crossorigin=&quot;anonymous&quot;&gt;&lt;/script&gt;</span></span><br></pre></td></tr></table></figure><blockquote><p><strong>不要两个方式同时用</strong>, 脚本会被加载两次, 导致广告重复展示或报错。</p></blockquote><h2 id="第三步-配广告位"><a href="#第三步-配广告位" class="headerlink" title="第三步: 配广告位"></a>第三步: 配广告位</h2><p>验证通过后, 后台会生成多个广告位(代码形如):</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">ins</span> <span class="attr">class</span>=<span class="string">&quot;adsbygoogle&quot;</span></span></span><br><span class="line"><span class="tag">     <span class="attr">style</span>=<span class="string">&quot;display:block&quot;</span></span></span><br><span class="line"><span class="tag">     <span class="attr">data-ad-client</span>=<span class="string">&quot;ca-pub-XXXXXXXXXXXXXXXX&quot;</span></span></span><br><span class="line"><span class="tag">     <span class="attr">data-ad-slot</span>=<span class="string">&quot;YYYYYYYYYY&quot;</span></span></span><br><span class="line"><span class="tag">     <span class="attr">data-ad-format</span>=<span class="string">&quot;auto&quot;</span></span></span><br><span class="line"><span class="tag">     <span class="attr">data-full-width-responsive</span>=<span class="string">&quot;true&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">ins</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript">(adsbygoogle = <span class="variable language_">window</span>.<span class="property">adsbygoogle</span> || []).<span class="title function_">push</span>(&#123;&#125;);</span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>关键点:</p><ul><li><code>data-ad-client</code> —— 你的发布商 ID</li><li><code>data-ad-slot</code> —— 广告位 ID(在后台创建广告单元时拿到)</li><li><code>data-ad-format=&quot;auto&quot;</code> —— 自适应尺寸, 强烈建议保留</li><li><code>data-full-width-responsive=&quot;true&quot;</code> —— 移动端适配</li></ul><p><strong>每创建一个广告单元, 就有一个新的 slot ID。</strong> 可以在同一个网站下建多个(比如”文章底部”、”侧边栏”、”首页顶部”), 分别配置不同内容。</p><h2 id="第四步-放到文章里"><a href="#第四步-放到文章里" class="headerlink" title="第四步: 放到文章里"></a>第四步: 放到文章里</h2><p>Butterfly 把广告分成<strong>两块配置</strong>, 别搞混。</p><h3 id="4-1-全局开关-google-adsense"><a href="#4-1-全局开关-google-adsense" class="headerlink" title="4.1 全局开关: google_adsense"></a>4.1 全局开关: google_adsense</h3><p>这块负责加载 AdSense 的 JS 脚本, 在 <code>_config.butterfly.yml</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">google_adsense:</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">auto_ads:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">js:</span> <span class="string">https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js</span></span><br><span class="line">  <span class="attr">client:</span> <span class="string">ca-pub-你的发布商ID</span></span><br><span class="line">  <span class="attr">enable_page_level_ads:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>字段说明:</p><table><thead><tr><th align="left">字段</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left"><code>enable</code></td><td align="left">总开关, 填了 <code>client</code> 记得打开</td></tr><tr><td align="left"><code>auto_ads</code></td><td align="left">开启自动广告, 由 Google 自动决定广告位置</td></tr><tr><td align="left"><code>js</code></td><td align="left">脚本地址, 一般不用改</td></tr><tr><td align="left"><code>client</code></td><td align="left">你的发布商 ID</td></tr><tr><td align="left"><code>enable_page_level_ads</code></td><td align="left">页面级自动广告</td></tr></tbody></table><blockquote><p><strong><code>auto_ads: true</code> 是最省事的方式。</strong> Google 的自动广告会自己挑位置, 新手直接用这个, 不用手动插代码。</p><p>但自动广告的样式不可控, 有时位置很怪。想精细控制就关掉它, 用下面的手动广告位。</p></blockquote><h3 id="4-2-手动广告位-ad"><a href="#4-2-手动广告位-ad" class="headerlink" title="4.2 手动广告位: ad"></a>4.2 手动广告位: ad</h3><p>这块是<strong>你自己贴的广告代码</strong>, 主题会把它插到指定位置:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">ad:</span></span><br><span class="line">  <span class="attr">index:</span> <span class="string">|</span>          <span class="comment"># 首页, 每三篇文章插一次</span></span><br><span class="line">    <span class="string">&lt;ins</span> <span class="string">class=&quot;adsbygoogle&quot;</span></span><br><span class="line">         <span class="string">style=&quot;display:block&quot;</span></span><br><span class="line">         <span class="string">data-ad-client=&quot;ca-pub-你的ID&quot;</span></span><br><span class="line">         <span class="string">data-ad-slot=&quot;你的广告位ID&quot;</span></span><br><span class="line">         <span class="string">data-ad-format=&quot;auto&quot;</span></span><br><span class="line">         <span class="string">data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;</span></span><br><span class="line">    <span class="string">&lt;script&gt;(adsbygoogle</span> <span class="string">=</span> <span class="string">window.adsbygoogle</span> <span class="string">||</span> []<span class="string">).push(&#123;&#125;);&lt;/script&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">aside:</span> <span class="string">|</span>          <span class="comment"># 侧边栏</span></span><br><span class="line">    <span class="string">&lt;ins</span> <span class="string">class=&quot;adsbygoogle&quot;</span></span><br><span class="line">         <span class="string">style=&quot;display:block&quot;</span></span><br><span class="line">         <span class="string">data-ad-client=&quot;ca-pub-你的ID&quot;</span></span><br><span class="line">         <span class="string">data-ad-slot=&quot;你的侧边栏广告位ID&quot;</span></span><br><span class="line">         <span class="string">data-ad-format=&quot;auto&quot;</span></span><br><span class="line">         <span class="string">data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;</span></span><br><span class="line">    <span class="string">&lt;script&gt;(adsbygoogle</span> <span class="string">=</span> <span class="string">window.adsbygoogle</span> <span class="string">||</span> []<span class="string">).push(&#123;&#125;);&lt;/script&gt;</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">post:</span> <span class="string">|</span>           <span class="comment"># 文章页, 上一篇/下一篇之前</span></span><br><span class="line">    <span class="string">&lt;ins</span> <span class="string">class=&quot;adsbygoogle&quot;</span></span><br><span class="line">         <span class="string">style=&quot;display:block&quot;</span></span><br><span class="line">         <span class="string">data-ad-client=&quot;ca-pub-你的ID&quot;</span></span><br><span class="line">         <span class="string">data-ad-slot=&quot;你的文章页广告位ID&quot;</span></span><br><span class="line">         <span class="string">data-ad-format=&quot;auto&quot;</span></span><br><span class="line">         <span class="string">data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;</span></span><br><span class="line">    <span class="string">&lt;script&gt;(adsbygoogle</span> <span class="string">=</span> <span class="string">window.adsbygoogle</span> <span class="string">||</span> []<span class="string">).push(&#123;&#125;);&lt;/script&gt;</span></span><br></pre></td></tr></table></figure><p><strong>注意这三个位置的准确含义</strong>(看主题模板源码最准):</p><table><thead><tr><th align="left">位置</th><th align="left">实际插入点</th></tr></thead><tbody><tr><td align="left"><code>ad.index</code></td><td align="left">首页文章列表, <strong>每 3 篇插一次</strong></td></tr><tr><td align="left"><code>ad.aside</code></td><td align="left">侧边栏</td></tr><tr><td align="left"><code>ad.post</code></td><td align="left">文章页, <strong>上一篇&#x2F;下一篇导航之前</strong></td></tr></tbody></table><p>不确定的话直接翻模板看:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 文章页广告插在哪</span></span><br><span class="line">grep -B3 -A5 <span class="string">&quot;ad.post&quot;</span> themes/butterfly/layout/post.pug</span><br><span class="line"></span><br><span class="line"><span class="comment"># 首页广告插在哪</span></span><br><span class="line">grep -B3 -A5 <span class="string">&quot;ad.index&quot;</span> themes/butterfly/layout/includes/mixins/indexPostUI.pug</span><br></pre></td></tr></table></figure><blockquote><p><strong>踩坑提醒</strong>: <code>ad</code> 段下面的三个字段<strong>默认都是空的</strong>, 必须整段贴 HTML 才会生效。填了 <code>enable: true</code> 之类的主题<strong>并不支持</strong>, 贴上去会被当成广告 HTML 直接输出到页面上。</p></blockquote><h2 id="第五步-手动插入自定义广告位"><a href="#第五步-手动插入自定义广告位" class="headerlink" title="第五步: 手动插入自定义广告位"></a>第五步: 手动插入自定义广告位</h2><p>内置的三个位置不够用时(比如想放文章末尾), 用 <code>inject</code> 自己插:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">bottom:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">|</span></span><br><span class="line"><span class="string">      &lt;div style=&quot;margin:20px 0;&quot;&gt;</span></span><br><span class="line"><span class="string">        &lt;ins class=&quot;adsbygoogle&quot;</span></span><br><span class="line"><span class="string">             style=&quot;display:block&quot;</span></span><br><span class="line"><span class="string">             data-ad-client=&quot;ca-pub-你的ID&quot;</span></span><br><span class="line"><span class="string">             data-ad-slot=&quot;你的广告位ID&quot;</span></span><br><span class="line"><span class="string">             data-ad-format=&quot;auto&quot;</span></span><br><span class="line"><span class="string">             data-full-width-responsive=&quot;true&quot;&gt;&lt;/ins&gt;</span></span><br><span class="line"><span class="string">        &lt;script&gt;(adsbygoogle = window.adsbygoogle || []).push(&#123;&#125;);&lt;/script&gt;</span></span><br><span class="line"><span class="string">      &lt;/div&gt;</span></span><br></pre></td></tr></table></figure><blockquote><p><strong>注意</strong>: <code>inject</code> 只有 <code>head</code> 和 <code>bottom</code> 两个键, **没有 <code>post-bottom</code>**。上面这段是全站生效的, 如果只想要文章页有广告, 还是在 <code>post.pug</code> 里加更精准。</p><p>另外 inject 的内容是纯字符串(源码里 <code>data.join(&#39;&#39;)</code>), <strong>不能在里面写模板变量</strong>。上面这段广告代码的 ID 是固定的, 所以没问题; 但如果你想动态取当前文章的 ID, 就得改主题模板。</p></blockquote><h1 id="广告放在哪里最合适"><a href="#广告放在哪里最合适" class="headerlink" title="广告放在哪里最合适"></a>广告放在哪里最合适</h1><p>这不是随便放的, 位置直接决定点击率。</p><h2 id="推荐位置"><a href="#推荐位置" class="headerlink" title="推荐位置"></a>推荐位置</h2><table><thead><tr><th align="left">位置</th><th align="left">效果</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">文章末尾</td><td align="left">最好</td><td align="left">读者已经读完, 注意力集中</td></tr><tr><td align="left">文章中段</td><td align="left">好</td><td align="left">打断阅读体验, 需谨慎</td></tr><tr><td align="left">侧边栏</td><td align="left">中</td><td align="left">常年可见, 但位置固定易被忽略</td></tr><tr><td align="left">首页顶部</td><td align="left">中</td><td align="left">首屏, 存在”点击诱饵”嫌疑</td></tr><tr><td align="left">评论区上方</td><td align="left">好</td><td align="left">互动意愿强的位置</td></tr><tr><td align="left">弹窗&#x2F;浮层</td><td align="left">短期高, 长期伤体验</td><td align="left">不建议</td></tr></tbody></table><h2 id="一个重要原则"><a href="#一个重要原则" class="headerlink" title="一个重要原则"></a>一个重要原则</h2><p><strong>Google 对”误点”非常敏感。</strong> 如果广告做得像网站原生按钮, 用户以为是点别的东西, 点错了 —— 这算”无效点击”。</p><p>后果: <strong>AdSense 会警告, 严重时直接封号, 严重的会封整个 AdSense 账号(所有站点一起完)。</strong></p><p>所以:</p><ul><li>广告要有明确的”广告”标识</li><li>不要做”继续阅读”这种伪装成功能按钮的样式</li><li>不要用会自动跳转的浮层广告</li><li><strong>不要自己点自己的广告</strong></li></ul><h1 id="百度联盟接入"><a href="#百度联盟接入" class="headerlink" title="百度联盟接入"></a>百度联盟接入</h1><h2 id="与-AdSense-的差异"><a href="#与-AdSense-的差异" class="headerlink" title="与 AdSense 的差异"></a>与 AdSense 的差异</h2><table><thead><tr><th align="left">方面</th><th align="left">AdSense</th><th align="left">百度联盟</th></tr></thead><tbody><tr><td align="left">结算</td><td align="left">美元, 需绑国际账户</td><td align="left">人民币, 绑国内银行卡</td></tr><tr><td align="left">审核</td><td align="left">相对宽松</td><td align="left">相对严格</td></tr><tr><td align="left">流量要求</td><td align="left">需一定内容量</td><td align="left">需备案(国内域名)</td></tr><tr><td align="left">覆盖</td><td align="left">全球</td><td align="left">主要是百度联盟的推广范围</td></tr><tr><td align="left">收益</td><td align="left">国外流量高</td><td align="left">国内流量相对合理</td></tr></tbody></table><p><strong>已备案的国内站点, 百度联盟往往比 AdSense 更实际。</strong></p><h2 id="配置流程"><a href="#配置流程" class="headerlink" title="配置流程"></a>配置流程</h2><ol><li>访问 <a href="https://union.baidu.com/">https://union.baidu.com/</a> 注册</li><li>提交网站信息 + 备案号</li><li>等待审核(通常 1-3 天)</li><li>审核通过后创建广告位, 拿到推广位 ID</li><li>在页面里放推广代码</li></ol><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;BAIDU_ADC_xxx&quot;</span> <span class="attr">class</span>=<span class="string">&quot;baidu_ad&quot;</span>&gt;</span><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">var</span> _HMT = _HMT || [];</span></span><br><span class="line"><span class="language-javascript">(<span class="keyword">function</span>(<span class="params"></span>)&#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">var</span> hm = <span class="variable language_">document</span>.<span class="title function_">createElement</span>(<span class="string">&quot;script&quot;</span>);</span></span><br><span class="line"><span class="language-javascript">  hm.<span class="property">src</span> = <span class="string">&quot;https://hm.baidu.com/hm.js?你的百度统计代码&quot;</span>;</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">var</span> s = <span class="variable language_">document</span>.<span class="title function_">getElementsByTagName</span>(<span class="string">&quot;script&quot;</span>)[<span class="number">0</span>];</span></span><br><span class="line"><span class="language-javascript">  s.<span class="property">parentNode</span>.<span class="title function_">insertBefore</span>(hm, s);</span></span><br><span class="line"><span class="language-javascript">&#125;)();</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">(<span class="keyword">function</span>(<span class="params"></span>)&#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">var</span> i = <span class="variable language_">document</span>.<span class="title function_">createElement</span>(<span class="string">&quot;script&quot;</span>);</span></span><br><span class="line"><span class="language-javascript">  i.<span class="property">src</span> = <span class="string">&quot;https://cpro.baidu.com/cpro/ui.js?siteid=你的站点ID&amp;callback=__cpro__&amp;use=3600&amp;referer=http://你的域名/&quot;</span>;</span></span><br><span class="line"><span class="language-javascript">  ...</span></span><br><span class="line"><span class="language-javascript">&#125;)();</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>具体的推广代码从百度联盟后台直接复制, 不用手写。</p><h1 id="被封号的几个雷区"><a href="#被封号的几个雷区" class="headerlink" title="被封号的几个雷区"></a>被封号的几个雷区</h1><p>这个话题值得单独说, 因为<strong>很多新手是”莫名其妙”被封的</strong>。</p><h2 id="1-点击率异常高"><a href="#1-点击率异常高" class="headerlink" title="1. 点击率异常高"></a>1. 点击率异常高</h2><p>CTR(点击率)如果长期超过 10% 左右, 会被判定为无效点击。正常文章博客的 CTR 通常在 1-5%。</p><h2 id="2-误点设计"><a href="#2-误点设计" class="headerlink" title="2. 误点设计"></a>2. 误点设计</h2><p>前面说过,这是最常见的原因。</p><h2 id="3-内容不足以申请"><a href="#3-内容不足以申请" class="headerlink" title="3. 内容不足以申请"></a>3. 内容不足以申请</h2><p>刚建站、没几篇文章就挂广告, 审核可能不通过; 或者通过后没有真实流量, 收益太低, 账号反而处于”低质”状态。</p><h2 id="4-流量来源不健康"><a href="#4-流量来源不健康" class="headerlink" title="4. 流量来源不健康"></a>4. 流量来源不健康</h2><p><strong>买来的垃圾流量是最大的雷。</strong> 短期能刷出可观收益, 但被检测到后基本是永久封禁, 账号里的所有站点一起没。</p><h2 id="5-广告和内容不匹配"><a href="#5-广告和内容不匹配" class="headerlink" title="5. 广告和内容不匹配"></a>5. 广告和内容不匹配</h2><p>技术博客里放游戏广告、或者放高利贷类广告, 会被判定为”低质量内容变现”, 影响账号信誉。</p><h2 id="6-用多个账号规避"><a href="#6-用多个账号规避" class="headerlink" title="6. 用多个账号规避"></a>6. 用多个账号规避</h2><p>被封后开新号继续挂, 系统会通过设备、IP、支付账户关联识别, <strong>结果是全部封禁</strong>。这是最不划算的”聪明做法”。</p><h1 id="提升收益的实际手段"><a href="#提升收益的实际手段" class="headerlink" title="提升收益的实际手段"></a>提升收益的实际手段</h1><p>广告收益的天花板不高, 想提高只能从这几个方向:</p><h2 id="提高-PV-而不是-CTR"><a href="#提高-PV-而不是-CTR" class="headerlink" title="提高 PV 而不是 CTR"></a>提高 PV 而不是 CTR</h2><p>CTR 提升空间有限(而且提太高有风险), 但<strong>流量增长是线性收益</strong>。</p><p>具体做法: 提升搜索排名(SEO)、做 RSS 分发(见 518 篇)、在技术社区分享。</p><h2 id="提高-RPM-而不只是-PV"><a href="#提高-RPM-而不只是-PV" class="headerlink" title="提高 RPM 而不只是 PV"></a>提高 RPM 而不只是 PV</h2><p>RPM 取决于访问者所在地区和广告类型。<strong>欧美访客的 RPM 是国内访客的 5-10 倍。</strong></p><p>如果你的内容是中文技术教程, 目标读者天然在国内。想接高价值广告, 可以考虑:</p><ul><li>写英文版(用翻译工具 + 人工润色)</li><li>面向海外华人的内容</li></ul><h2 id="混合变现"><a href="#混合变现" class="headerlink" title="混合变现"></a>混合变现</h2><p>只靠展示广告太单薄, 可以叠加:</p><table><thead><tr><th align="left">方式</th><th align="left">难度</th><th align="left">见效</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">联盟推广</td><td align="left">低</td><td align="left">中</td><td align="left">云服务&#x2F;CDN&#x2F;主机厂商, 按成交付费</td></tr><tr><td align="left">赞助</td><td align="left">中</td><td align="left">低但单笔高</td><td align="left">页脚赞助位, 适合有稳定粉丝的</td></tr><tr><td align="left">付费内容</td><td align="left">高</td><td align="left">高</td><td align="left">需要足够的内容深度和信任度</td></tr><tr><td align="left">买断广告位</td><td align="left">中</td><td align="left">稳定</td><td align="left">一次性卖断某个月的位置</td></tr></tbody></table><p><strong>对技术博客来说, 联盟推广其实比展示广告更划算</strong> —— 因为它不打扰阅读, 成交佣金还比 RPM 高。接云服务、域名、主机这类推广, 和内容天然相关。</p><h1 id="上线前的检查清单"><a href="#上线前的检查清单" class="headerlink" title="上线前的检查清单"></a>上线前的检查清单</h1><p>准备提交审核前, 逐条确认:</p><ul><li><input disabled="" type="checkbox"> 站点能正常访问, HTTPS 有效</li><li><input disabled="" type="checkbox"> 至少 20-30 篇有实质内容的文章</li><li><input disabled="" type="checkbox"> 没有空分类、空标签页</li><li><input disabled="" type="checkbox"> 移动端显示正常(现在大部分流量来自移动)</li><li><input disabled="" type="checkbox"> 广告位置不会误触</li><li><input disabled="" type="checkbox"> 隐私政策页面(欧美站点必备, AdSense 要求)</li><li><input disabled="" type="checkbox"> 备案号展示(国内备案要求)</li></ul><p>隐私政策页面可以用 Butterfly 自带的页面功能建一个, 简单说明用了哪些 Cookie 和第三方服务即可。</p><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><ol><li><strong>先想清楚收益预期</strong> —— 覆盖域名费需要日均几百到一两千 PV, 别指望几百 IP 就能赚钱</li><li><strong>AdSense 适合有海外流量或英文内容的站</strong>, <strong>百度联盟适合已备案的国内站</strong></li><li><strong>广告位置优先选文章末尾</strong>, 收益最好且不伤体验</li><li><strong>千万别碰”无效点击”</strong> —— 那是封号的主要原因, 而且会连累所有关联站点</li><li><strong>别买垃圾流量</strong> —— 短期收益长期封号</li><li><strong>展示广告之外, 联盟推广对技术博客更划算</strong></li></ol><p>最后一条我的体会尤其深: <strong>技术博客的用户信任度是最值钱的资产。</strong> 为了几十块钱的展示收益, 把用户体验做烂, 是很不划算的买卖。联盟推广、原创内容、良好的阅读体验, 这些才是能长期做下去的路子。</p><p>参考链接:</p><ul><li><a href="https://support.google.com/adsense/">Google AdSense 官方文档</a></li><li><a href="https://union.baidu.com/">百度联盟官网</a></li><li><a href="https://support.google.com/adsense/answer/1346295">Google 违规政策 - 无效点击</a></li><li><a href="https://butterfly.js.org/">Butterfly 主题文档 - 广告配置</a></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hexo 博客加 JSON-LD 结构化数据: 让搜索引擎读懂你的文章</title>
      <link>https://zsyyblog.com/8d3ac690.html</link>
      <description>结构化数据是把文章元信息用 JSON-LD 告诉搜索引擎的方法, 影响搜索结果的富媒体展示。本文记录 Hexo 博客接入 BlogPosting 结构化数据的完整做法: inject 注入代码、动态取值、验证与常见陷阱。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/">前端</category>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/Hexo/">Hexo</category>
      <category domain="https://zsyyblog.com/tags/Hexo/">Hexo</category>
      <pubDate>Tue, 20 Oct 2026 13:05:42 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>之前写过三篇 Hexo SEO 系列(搜索引擎原理、站内优化、提升排名), 讲的多是 meta 标签、关键词、内外链这些。</p><p>但有一个东西漏了 —— <strong>结构化数据</strong>。</p><p>它的作用是把”这是一篇文章, 作者是谁, 什么时候发的, 封面图是哪张”这类信息, 用机器能读的格式告诉搜索引擎。好处很直接: 搜索结果里可能直接显示出封面图、作者名、发布时间, 点击率比纯文字结果高。</p><p>这篇记录怎么接。</p><h1 id="原理-什么是-JSON-LD"><a href="#原理-什么是-JSON-LD" class="headerlink" title="原理: 什么是 JSON-LD"></a>原理: 什么是 JSON-LD</h1><h2 id="三种格式-选-JSON-LD"><a href="#三种格式-选-JSON-LD" class="headerlink" title="三种格式, 选 JSON-LD"></a>三种格式, 选 JSON-LD</h2><p>结构化数据有三种写法:</p><table><thead><tr><th align="left">格式</th><th align="left">写法</th><th align="left">推荐度</th></tr></thead><tbody><tr><td align="left">Microdata</td><td align="left">写在 HTML 属性里</td><td align="left">一般, 侵入性强</td></tr><tr><td align="left">RDFa</td><td align="left">写在 HTML 属性里</td><td align="left">一般</td></tr><tr><td align="left"><strong>JSON-LD</strong></td><td align="left"><strong>单独一段 <code>&lt;script&gt;</code></strong></td><td align="left"><strong>推荐</strong></td></tr></tbody></table><p><strong>选 JSON-LD 的理由: 与 HTML 完全分离。</strong> 写在 HTML 属性里的方案, 改模板容易把结构化数据改坏; JSON-LD 是一段独立的代码, 不动页面结构, 维护成本最低。</p><h2 id="长什么样"><a href="#长什么样" class="headerlink" title="长什么样"></a>长什么样</h2><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;application/ld+json&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">&#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="string">&quot;@context&quot;</span>: <span class="string">&quot;https://schema.org&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="string">&quot;@type&quot;</span>: <span class="string">&quot;BlogPosting&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="string">&quot;headline&quot;</span>: <span class="string">&quot;文章标题&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="string">&quot;image&quot;</span>: <span class="string">&quot;https://example.com/cover.png&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="string">&quot;datePublished&quot;</span>: <span class="string">&quot;2026-10-21T11:05:42+08:00&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="string">&quot;dateModified&quot;</span>: <span class="string">&quot;2026-10-21T11:05:42+08:00&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">  <span class="string">&quot;author&quot;</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="string">&quot;@type&quot;</span>: <span class="string">&quot;Person&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">    <span class="string">&quot;name&quot;</span>: <span class="string">&quot;竹山一叶&quot;</span></span></span><br><span class="line"><span class="language-javascript">  &#125;,</span></span><br><span class="line"><span class="language-javascript">  <span class="string">&quot;publisher&quot;</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="string">&quot;@type&quot;</span>: <span class="string">&quot;Organization&quot;</span>,</span></span><br><span class="line"><span class="language-javascript">    <span class="string">&quot;name&quot;</span>: <span class="string">&quot;竹山一叶的小家&quot;</span></span></span><br><span class="line"><span class="language-javascript">  &#125;</span></span><br><span class="line"><span class="language-javascript">&#125;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br></pre></td></tr></table></figure><p>搜索引擎会解析这段 JSON, 提取成知识图谱里的实体。</p><h1 id="第一步-加到-Butterfly-主题里"><a href="#第一步-加到-Butterfly-主题里" class="headerlink" title="第一步: 加到 Butterfly 主题里"></a>第一步: 加到 Butterfly 主题里</h1><p>Butterfly 提供了 <code>inject</code> 机制, 可以往页面里塞代码。</p><p><strong>先说清楚这个版本的 inject 支持哪些位置。</strong> 我查了本地主题源码, 实际只有两个注入点:</p><table><thead><tr><th align="left">位置</th><th align="left">作用范围</th><th align="left">对应模板</th></tr></thead><tbody><tr><td align="left"><code>head</code></td><td align="left">所有页面的 <code>&lt;head&gt;</code> 里</td><td align="left"><code>layout/includes/head.pug</code></td></tr><tr><td align="left"><code>bottom</code></td><td align="left">所有页面底部(<code>&lt;/body&gt;</code> 前)</td><td align="left"><code>layout/includes/additional-js.pug</code></td></tr></tbody></table><p>源码里就这两行:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">// layout/includes/head.pug</span><br><span class="line">!=fragment_cache(&#x27;injectHead&#x27;, function()&#123;return injectHtml(theme.inject.head)&#125;)</span><br><span class="line"></span><br><span class="line">// layout/includes/additional-js.pug</span><br><span class="line">!=fragment_cache(&#x27;injectBottom&#x27;, function()&#123;return injectHtml(theme.inject.bottom)&#125;)</span><br></pre></td></tr></table></figure><p><strong>这意味着没有”仅文章页”的注入点。</strong> 网上很多教程写的 <code>inject.post</code> &#x2F; <code>inject.post-bottom</code> 在这个版本里<strong>不生效</strong> —— 配了也不会被渲染。</p><h2 id="那文章级的-JSON-LD-怎么加"><a href="#那文章级的-JSON-LD-怎么加" class="headerlink" title="那文章级的 JSON-LD 怎么加?"></a>那文章级的 JSON-LD 怎么加?</h2><p>既然没有文章级注入点, 就用 <code>head</code>, 但<strong>在代码里自己判断当前是不是文章页</strong>。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">head:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">|</span></span><br><span class="line"><span class="string">      &lt;% if (page.post) &#123; %&gt;</span></span><br><span class="line"><span class="string">      &lt;script type=&quot;application/ld+json&quot;&gt;</span></span><br><span class="line"><span class="string">      &#123;</span></span><br><span class="line"><span class="string">        &quot;@context&quot;: &quot;https://schema.org&quot;,</span></span><br><span class="line"><span class="string">        &quot;@type&quot;: &quot;BlogPosting&quot;,</span></span><br><span class="line"><span class="string">        ...</span></span><br><span class="line"><span class="string">      &#125;</span></span><br><span class="line"><span class="string">      &lt;/script&gt;</span></span><br><span class="line"><span class="string">      &lt;% &#125; %&gt;</span></span><br></pre></td></tr></table></figure><p><code>page.post</code> 在文章页上存在, 在独立页面、分类页、标签页上都是 <code>undefined</code>, 用它判断最直接。</p><blockquote><p><strong>注意 inject 的格式</strong>: 源码里是 <code>data.join(&#39;&#39;)</code>, 所以<strong>每一项必须是一个字符串</strong>, 不能写成 <code>&#123; code: &#39;xxx&#39; &#125;</code> 这种对象结构。写成对象会渲染成 <code>[object Object]</code>。</p></blockquote><h2 id="顺带说明-Pug-模板里的条件语法"><a href="#顺带说明-Pug-模板里的条件语法" class="headerlink" title="顺带说明: Pug 模板里的条件语法"></a>顺带说明: Pug 模板里的条件语法</h2><p>Butterfly 用 Pug, 但 <code>inject</code> 的内容是<strong>字符串</strong>, 会被当成普通文本输出, 不会被 Pug 编译。</p><p>所以上面的 <code>&lt;% if (page.post) &#123; %&gt;</code> 这种 EJS 风格语法, <strong>在 inject 里同样不会被解析</strong> —— 它会原样输出到页面上。</p><p><strong>正确做法是改用主题的标签系统, 或者接受所有页面都注入。</strong></p><p>考虑到这一点, 更实用的方案是: <strong>给所有页面都注入 JSON-LD, 但用不同的 <code>@type</code> 区分</strong>。搜索引擎能接受一个页面里有多个 JSON-LD 块, 它会按类型分别理解:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">head:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">|</span></span><br><span class="line"><span class="string">      &lt;script type=&quot;application/ld+json&quot;&gt;</span></span><br><span class="line"><span class="string">      &#123;</span></span><br><span class="line"><span class="string">        &quot;@context&quot;: &quot;https://schema.org&quot;,</span></span><br><span class="line"><span class="string">        &quot;@type&quot;: &quot;WebSite&quot;,</span></span><br><span class="line"><span class="string">        &quot;name&quot;: &quot;...&quot;,</span></span><br><span class="line"><span class="string">        &quot;url&quot;: &quot;...&quot;</span></span><br><span class="line"><span class="string">      &#125;</span></span><br><span class="line"><span class="string">      &lt;/script&gt;</span></span><br><span class="line"><span class="string"></span>    <span class="bullet">-</span> <span class="string">|</span></span><br><span class="line"><span class="string">      &lt;script type=&quot;application/ld+json&quot;&gt;</span></span><br><span class="line"><span class="string">      &#123;</span></span><br><span class="line"><span class="string">        &quot;@context&quot;: &quot;https://schema.org&quot;,</span></span><br><span class="line"><span class="string">        &quot;@type&quot;: &quot;BlogPosting&quot;,</span></span><br><span class="line"><span class="string">        ...</span></span><br><span class="line"><span class="string">      &#125;</span></span><br><span class="line"><span class="string">      &lt;/script&gt;</span></span><br></pre></td></tr></table></figure><p><strong>如果你的站点没有独立页面(关于页等), 或者独立页面很少, 直接全站注入 <code>BlogPosting</code> 也没有大碍。</strong> 独立页面通常就那么几个, 搜索引擎不会因为一两个页面的类型标注不精确就惩罚整个站点。</p><blockquote><p><strong>另一个更稳的方案: 直接改主题模板。</strong> 主题的 <code>layout/post.pug</code> 是 Pug 文件, 里面可以用真正的 Pug&#x2F;JS 语法判断页面类型。代价是主题升级时可能要重新改(所以要先改 <code>themes/butterfly</code> 还是用 <code>extend</code>&#x2F;<code>inject</code> 覆盖, 取决于你愿不愿意接受维护成本)。</p></blockquote><h1 id="第二步-动态取值"><a href="#第二步-动态取值" class="headerlink" title="第二步: 动态取值"></a>第二步: 动态取值</h1><p>上面那段 JSON 里的字段不能写死 —— 每篇文章的标题、时间都不一样。得用模板变量动态填充。</p><h2 id="先说个坑-inject-里不能用模板变量"><a href="#先说个坑-inject-里不能用模板变量" class="headerlink" title="先说个坑: inject 里不能用模板变量"></a>先说个坑: inject 里不能用模板变量</h2><p><strong>这是最容易踩错的地方。</strong> 很多人以为 inject 里能写 <code>&lt;%- title %&gt;</code>, 实际上不行。</p><p>看主题源码里 inject 的实现:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// scripts/helpers/page.js</span></span><br><span class="line">hexo.<span class="property">extend</span>.<span class="property">helper</span>.<span class="title function_">register</span>(<span class="string">&#x27;injectHtml&#x27;</span>, <span class="function"><span class="params">data</span> =&gt;</span> &#123;</span><br><span class="line">  <span class="keyword">return</span> data ? data.<span class="title function_">join</span>(<span class="string">&#x27;&#x27;</span>) : <span class="string">&#x27;&#x27;</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><p><strong>只是 <code>join(&#39;&#39;)</code>, 没有任何模板编译。</strong> 意味着你在 inject 里写的 <code>&lt;%- title %&gt;</code> 会<strong>原封不动地作为文本输出到 HTML 里</strong>, 页面上会出现字面量 <code>&lt;%- title %&gt;</code>。</p><p><strong>结论: 需要动态取值的结构化数据, 不能靠 inject, 必须改主题模板。</strong></p><h2 id="正确做法-在-post-pug-里加"><a href="#正确做法-在-post-pug-里加" class="headerlink" title="正确做法: 在 post.pug 里加"></a>正确做法: 在 post.pug 里加</h2><p>Butterfly 的 <code>layout/post.pug</code> 是真正的 Pug 模板, 里面可以用完整语法。</p><p>在 <code>layout/post.pug</code> 的 <code>block content</code> 下面找合适位置(建议放在 <code>article#article-container</code> 之后), 加:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">if page.title</span><br><span class="line">  script(type=&quot;application/ld+json&quot;).</span><br><span class="line">    &#123;</span><br><span class="line">      &quot;@context&quot;: &quot;https://schema.org&quot;,</span><br><span class="line">      &quot;@type&quot;: &quot;BlogPosting&quot;,</span><br><span class="line">      &quot;mainEntityOfPage&quot;: &#123;</span><br><span class="line">        &quot;@type&quot;: &quot;WebPage&quot;,</span><br><span class="line">        &quot;@id&quot;: &quot;#&#123;config.url&#125;/#&#123;page.path&#125;&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &quot;headline&quot;: &quot;#&#123;page.title&#125;&quot;,</span><br><span class="line">      &quot;description&quot;: &quot;#&#123;page.description || page.excerpt || &#x27;&#x27;&#125;&quot;,</span><br><span class="line">      &quot;image&quot;: &quot;#&#123;page.cover || config.site.img || config.site.logo&#125;&quot;,</span><br><span class="line">      &quot;datePublished&quot;: &quot;#&#123;page.date.toISOString()&#125;&quot;,</span><br><span class="line">      &quot;dateModified&quot;: &quot;#&#123;page.updated ? page.updated.toISOString() : page.date.toISOString()&#125;&quot;,</span><br><span class="line">      &quot;author&quot;: &#123;</span><br><span class="line">        &quot;@type&quot;: &quot;Person&quot;,</span><br><span class="line">        &quot;name&quot;: &quot;#&#123;config.author&#125;&quot;</span><br><span class="line">      &#125;,</span><br><span class="line">      &quot;publisher&quot;: &#123;</span><br><span class="line">        &quot;@type&quot;: &quot;Organization&quot;,</span><br><span class="line">        &quot;name&quot;: &quot;#&#123;config.title&#125;&quot;,</span><br><span class="line">        &quot;logo&quot;: &#123;</span><br><span class="line">          &quot;@type&quot;: &quot;ImageObject&quot;,</span><br><span class="line">          &quot;url&quot;: &quot;#&#123;config.site.logo&#125;&quot;</span><br><span class="line">        &#125;</span><br><span class="line">      &#125;,</span><br><span class="line">      &quot;keywords&quot;: &quot;#&#123;page.tags &amp;&amp; page.tags.length ? page.tags.map(t =&gt; t.name).join(&#x27;, &#x27;) : &#x27;&#x27;&#125;&quot;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>注意 Pug 里输出 JSON 用了 <code>script(type=&quot;application/ld+json&quot;).</code> 这个写法 —— 末尾的点是 Pug 的”纯文本块”语法, 后面跟着的内容原样输出, <strong>不做 HTML 转义</strong>。</p><p>这一点很关键: 如果用 <code>script(type=&quot;application/ld+json&quot;)= #&#123;json&#125;</code>, Pug 会把引号转义成 <code>&amp;quot;</code>, JSON 就废了。</p><h2 id="几个关键点"><a href="#几个关键点" class="headerlink" title="几个关键点"></a>几个关键点</h2><h3 id="标题字段用-headline-不是-name"><a href="#标题字段用-headline-不是-name" class="headerlink" title="标题字段用 headline 不是 name"></a>标题字段用 <code>headline</code> 不是 <code>name</code></h3><p>文章类型用 <code>headline</code>。用 <code>name</code> 会被 Google 判为”不匹配预期类型”。</p><h3 id="时间要用-ISO-8601-格式"><a href="#时间要用-ISO-8601-格式" class="headerlink" title="时间要用 ISO 8601 格式"></a>时间要用 ISO 8601 格式</h3><p><code>page.date.toISOString()</code> 输出的是 <code>2026-10-21T03:05:42.000Z</code>, 这是标准格式。</p><p><strong>不要直接输出 <code>page.date</code> 本身</strong>, 默认的 <code>2026-10-21 11:05:42</code> 这种格式搜索引擎可能解析失败。</p><h3 id="URL-要拼成绝对路径"><a href="#URL-要拼成绝对路径" class="headerlink" title="URL 要拼成绝对路径"></a>URL 要拼成绝对路径</h3><p><code>#&#123;config.url&#125;/#&#123;page.path&#125;</code> 拼出的是完整 URL, 搜索引擎最喜欢这种。</p><p>如果用 <code>url_for(page.path)</code>, 得到的是带 <code>config.root</code> 的站内路径, 部署在根目录时也能用, 但<strong>部署在子目录时可能拼不完整</strong>。所以这里直接用 <code>config.url</code> 更稳。</p><h3 id="封面图要有兜底"><a href="#封面图要有兜底" class="headerlink" title="封面图要有兜底"></a>封面图要有兜底</h3><p>不是每篇文章都设了封面。所以要准备 fallback:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&quot;image&quot;: &quot;#&#123;page.cover || config.site.img || config.site.logo&#125;&quot;</span><br></pre></td></tr></table></figure><p><strong>注意 <code>page.cover</code> 可能是相对路径</strong>(比如 <code>/images/xxx.png</code>)。搜索引擎对相对路径的 <code>image</code> 支持不稳定, 稳妥起见拼成绝对 URL:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&quot;image&quot;: &quot;#&#123;page.cover &amp;&amp; !page.cover.startsWith(&#x27;http&#x27;) ? config.url + page.cover : (page.cover || config.site.img || config.site.logo)&#125;&quot;</span><br></pre></td></tr></table></figure><p><strong>注意 <code>cover</code> 可能是相对路径</strong>, 而结构化数据的 <code>image</code> 最好是绝对 URL。如果搜索引擎抓不到完整 URL, 富媒体展示就不会生效。稳妥起见可以这样写:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;image&quot;:</span> <span class="string">&quot;&lt;%- (cover &amp;&amp; cover.startsWith(&#x27;http&#x27;)) ? cover : (config.root + (cover || config.site.img || config.site.logo)) %&gt;&quot;</span></span><br></pre></td></tr></table></figure><h1 id="第三步-验证"><a href="#第三步-验证" class="headerlink" title="第三步: 验证"></a>第三步: 验证</h1><p>写完不能靠猜, 必须用官方工具验。</p><h2 id="Google-富媒体测试工具"><a href="#Google-富媒体测试工具" class="headerlink" title="Google 富媒体测试工具"></a>Google 富媒体测试工具</h2><p>地址: <a href="https://search.google.com/test/rich-results">https://search.google.com/test/rich-results</a></p><p>输入你的文章 URL, 点测试。它会告诉你:</p><ul><li>结构化数据是否被识别</li><li>哪些字段有警告或错误</li><li>预览效果长什么样</li></ul><h2 id="Schema-Markup-Validator"><a href="#Schema-Markup-Validator" class="headerlink" title="Schema Markup Validator"></a>Schema Markup Validator</h2><p>地址: <a href="https://validator.schema.org/">https://validator.schema.org/</a></p><p>比 Google 那个更宽松, 主要用来检查 JSON 语法和 schema 定义是否符合规范。<strong>两个都跑一遍</strong>, 前者看实际效果, 后者查语法细节。</p><h2 id="本地怎么验"><a href="#本地怎么验" class="headerlink" title="本地怎么验"></a>本地怎么验</h2><p>本地还没部署的话, 可以直接看渲染后的 HTML:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo clean &amp;&amp; hexo g</span><br></pre></td></tr></table></figure><p>然后在 <code>public/</code> 里找文章页面:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 找到刚生成的文章</span></span><br><span class="line">grep -l <span class="string">&quot;application/ld+json&quot;</span> public/*.html | <span class="built_in">head</span> -3</span><br></pre></td></tr></table></figure><p>确认 JSON 语法没错:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 提取 JSON 部分并校验格式</span></span><br><span class="line">grep -A20 <span class="string">&quot;application/ld+json&quot;</span> public/xxxxx.html | <span class="built_in">head</span> -25</span><br></pre></td></tr></table></figure><h1 id="常见陷阱"><a href="#常见陷阱" class="headerlink" title="常见陷阱"></a>常见陷阱</h1><h2 id="陷阱一-JSON-里的引号没转义"><a href="#陷阱一-JSON-里的引号没转义" class="headerlink" title="陷阱一: JSON 里的引号没转义"></a>陷阱一: JSON 里的引号没转义</h2><p>这是最常见的错误。<strong>如果你的标题里带了英文双引号, JSON 就会解析失败, 整段结构化数据直接作废。</strong></p><p>比如标题是 <code>Hexo 配置的 &quot;url&quot; 该怎么填</code>, 直接塞进 JSON 就崩了。</p><h2 id="解决办法-在模板里做-JSON-转义"><a href="#解决办法-在模板里做-JSON-转义" class="headerlink" title="解决办法: 在模板里做 JSON 转义"></a>解决办法: 在模板里做 JSON 转义</h2><p>Pug 的纯文本块(<code>script(...).</code>)不做 HTML 转义, 也就<strong>不会帮你转义 JSON 里的双引号</strong>。所以要在模板里手动处理。</p><p>在 <code>post.pug</code> 里定义一个变量先转义:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">- var ldTitle = (page.title || &#x27;&#x27;).replace(/\\/g, &#x27;\\\\&#x27;).replace(/&quot;/g, &#x27;\\&quot;&#x27;)</span><br><span class="line">if page.title</span><br><span class="line">  script(type=&quot;application/ld+json&quot;).</span><br><span class="line">    &#123;</span><br><span class="line">      &quot;@context&quot;: &quot;https://schema.org&quot;,</span><br><span class="line">      &quot;@type&quot;: &quot;BlogPosting&quot;,</span><br><span class="line">      &quot;headline&quot;: &quot;#&#123;ldTitle&#125;&quot;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p><strong>两个转义都要做</strong>: 先转义反斜杠 <code>\</code>, 再转义双引号 <code>&quot;</code>。顺序反了会出问题 —— 先转义双引号的话, 新产生的 <code>\</code> 会被二次处理。</p><p><code>description</code> 和 <code>keywords</code> 同理, 有引号的话也要处理。</p><blockquote><p><strong>更省事的办法</strong>: front-matter 里的标题和描述尽量用中文标点(「」『』””), 避开英文双引号。但这只能规避, 不是根治。</p></blockquote><h2 id="陷阱二-和主题自带的重复"><a href="#陷阱二-和主题自带的重复" class="headerlink" title="陷阱二: 和主题自带的重复"></a>陷阱二: 和主题自带的重复</h2><p>Butterfly 5.x 本身<strong>没有内置 JSON-LD</strong>(我查过 <code>layout/</code> 和 <code>scripts/</code>, 没有 <code>ld+json</code> 相关代码), 所以这块不会撞车。</p><p>但 <code>canonical</code> 和 <code>og:url</code> 这类标签, 主题和 SEO 插件只能有一个来源。</p><p><strong>检查方法</strong>: 看渲染后的 HTML 里有没有多个同类标签。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">grep -c <span class="string">&quot;application/ld+json&quot;</span> public/xxxxx.html</span><br><span class="line">grep -c <span class="string">&quot;rel=\&quot;canonical\&quot;&quot;</span> public/xxxxx.html</span><br></pre></td></tr></table></figure><p>大于 1 就要去掉多余的。</p><h2 id="陷阱三-忘了写-dateModified"><a href="#陷阱三-忘了写-dateModified" class="headerlink" title="陷阱三: 忘了写 dateModified"></a>陷阱三: 忘了写 <code>dateModified</code></h2><p><code>datePublished</code> 和 <code>dateModified</code> 都应该填。</p><ul><li><code>datePublished</code> —— 文章首次发布时间</li><li><code>dateModified</code> —— 最后修改时间</li></ul><p><strong>老文章更新后, <code>dateModified</code> 是重要的信号。</strong> 只写 <code>datePublished</code> 会让搜索引擎以为这篇内容一直没变过, 不给重新抓取的权重。</p><p>你可以在 front-matter 里加 <code>updated</code> 字段:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">title:</span> <span class="string">&#x27;xxx&#x27;</span></span><br><span class="line"><span class="attr">date:</span> <span class="number">2026-10-21 11:05:42</span></span><br><span class="line"><span class="attr">updated:</span> <span class="number">2026-10-25 15:30:00</span></span><br><span class="line"><span class="meta">---</span></span><br></pre></td></tr></table></figure><h2 id="陷阱四-忘记改-updated-字段"><a href="#陷阱四-忘记改-updated-字段" class="headerlink" title="陷阱四: 忘记改 updated 字段"></a>陷阱四: 忘记改 <code>updated</code> 字段</h2><p>上面那个 <code>updated</code> 不会自动更新, 得手动维护。</p><p>Hexo 有个插件可以自动根据 Git 提交时间生成, 但更简单的方式是<strong>每次改文章时顺手更新 <code>updated</code> 字段</strong>。</p><h2 id="陷阱五-结构化数据被当成作弊"><a href="#陷阱五-结构化数据被当成作弊" class="headerlink" title="陷阱五: 结构化数据被当成作弊"></a>陷阱五: 结构化数据被当成作弊</h2><p><strong>不要在结构化数据里塞关键词。</strong> 比如把 <code>keywords</code> 写成 <code>&quot;Hexo,Hexo教程,Hexo博客,Hexo搭建,hexo主题&quot;</code>, 这种堆砌反而会被判为垃圾信号。</p><p><code>keywords</code> 字段就填真实的标签, 有几个填几个, 没有就干脆不填。</p><h1 id="可以加的其他类型"><a href="#可以加的其他类型" class="headerlink" title="可以加的其他类型"></a>可以加的其他类型</h1><p><code>BlogPosting</code> 之外, 常见的还有:</p><table><thead><tr><th align="left">类型</th><th align="left">用途</th></tr></thead><tbody><tr><td align="left"><code>WebSite</code></td><td align="left">站点级信息, 全站通用</td></tr><tr><td align="left"><code>Person</code></td><td align="left">作者信息</td></tr><tr><td align="left"><code>BreadcrumbList</code></td><td align="left">面包屑导航</td></tr><tr><td align="left"><code>FAQPage</code></td><td align="left">问答页面(注意: Google 已限制该类型的富媒体展示)</td></tr><tr><td align="left"><code>HowTo</code></td><td align="left">步骤教程类文章</td></tr></tbody></table><h2 id="加站点级-WebSite"><a href="#加站点级-WebSite" class="headerlink" title="加站点级 WebSite"></a>加站点级 WebSite</h2><p><code>WebSite</code> 的字段<strong>所有页面都一样</strong>(站点名、站点 URL), 所以这条<strong>可以放心用 inject 注入</strong> —— 不需要动态取值, 也没有引号转义问题。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">head:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">|</span></span><br><span class="line"><span class="string">      &lt;script type=&quot;application/ld+json&quot;&gt;</span></span><br><span class="line"><span class="string">      &#123;</span></span><br><span class="line"><span class="string">        &quot;@context&quot;: &quot;https://schema.org&quot;,</span></span><br><span class="line"><span class="string">        &quot;@type&quot;: &quot;WebSite&quot;,</span></span><br><span class="line"><span class="string">        &quot;name&quot;: &quot;竹山一叶的小家&quot;,</span></span><br><span class="line"><span class="string">        &quot;url&quot;: &quot;https://zsyyblog.com&quot;</span></span><br><span class="line"><span class="string">      &#125;</span></span><br><span class="line"><span class="string">      &lt;/script&gt;</span></span><br></pre></td></tr></table></figure><p>站点名和 URL 写死就行 —— 站点信息本来就不会每篇文章变。</p><p>不过要注意: <strong>只有在页面真的有搜索功能时, 声明 <code>SearchAction</code> 才有意义</strong>, 否则 Google 会提示这个动作无效。</p><p>你如果开了 Butterfly 的搜索功能(<code>search.enable: true</code>), 可以加上:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;potentialAction&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;@type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;SearchAction&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;target&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://zsyyblog.com/search?q=&#123;search_term_string&#125;&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;query-input&quot;</span><span class="punctuation">:</span> <span class="string">&quot;required name=search_term_string&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h1 id="关于改主题模板的维护成本"><a href="#关于改主题模板的维护成本" class="headerlink" title="关于改主题模板的维护成本"></a>关于改主题模板的维护成本</h1><p>前面说了要改 <code>themes/butterfly/layout/post.pug</code>。这会带来一个问题: <strong>主题升级时这个改动会丢失。</strong></p><p>两个应对办法:</p><h2 id="办法一-改之前留个备份"><a href="#办法一-改之前留个备份" class="headerlink" title="办法一: 改之前留个备份"></a>办法一: 改之前留个备份</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> themes/butterfly/layout</span><br><span class="line"><span class="built_in">cp</span> post.pug post.pug.bak</span><br></pre></td></tr></table></figure><p>升级主题后对比 diff, 把改动重新补回去。</p><h2 id="办法二-用-Hexo-的主题覆盖机制"><a href="#办法二-用-Hexo-的主题覆盖机制" class="headerlink" title="办法二: 用 Hexo 的主题覆盖机制"></a>办法二: 用 Hexo 的主题覆盖机制</h2><p>Hexo 支持通过 <code>_config.yml</code> 里的 <code>theme</code> 字段配合 <code>extend</code> 生成器做覆盖, 但 Butterfly 官方文档对此支持有限。</p><p><strong>更实用的第三种</strong>: 把 JSON-LD 的生成逻辑写成一个<strong>自定义 Hexo 过滤器或脚本</strong>, 输出到 <code>source/</code> 下, 再用 inject 引入。这样升级主题不受影响。</p><p>不过说实话, 对大多数个人博客来说, <strong>办法一就够用了</strong> —— 一年升级主题一两次, 每次补一下也花不了几分钟。改之前记一下改了什么就行。</p><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>接入结构化数据, 核心就三步:</p><ol><li><strong>加代码</strong> —— 文章级改 <code>themes/butterfly/layout/post.pug</code>, 站点级用 <code>inject.head</code></li><li><strong>动态取值</strong> —— Pug 里用 <code>page.xxx</code>, 时间转 ISO 8601, URL 拼绝对路径, 引号手动转义</li><li><strong>用官方工具验证</strong> —— Google 富媒体测试 + Schema Markup Validator</li></ol><p><strong>最需要小心的三个点</strong>:</p><ul><li><strong>inject 里不能用模板变量</strong> —— 它只做 <code>join(&#39;&#39;)</code>, <code>&lt;%- title %&gt;</code> 会原样输出</li><li><strong>JSON 里的双引号要转义</strong> —— 先转 <code>\</code>, 再转 <code>&quot;</code>, 顺序不能反</li><li><strong>别和主题自带的标签重复</strong> —— canonical、og:url 这类一个来源就够了</li></ul><p>结构化数据是<strong>纯增量</strong>的 —— 加错了顶多不起作用, 不会让搜索排名变差。所以值得花半小时配好它, 换来的是搜索结果里更丰富的展示。</p><p>参考链接:</p><ul><li><a href="https://search.google.com/test/rich-results">Google 富媒体搜索结果测试</a></li><li><a href="https://validator.schema.org/">Schema Markup Validator</a></li><li><a href="https://schema.org/BlogPosting">Schema.org - BlogPosting</a></li><li><a href="https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data">Google 搜索中心 - 结构化数据通用指南</a></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hexo 博客添加 RSS 订阅: 生成 atom.xml 并推到各个平台</title>
      <link>https://zsyyblog.com/196ff3fc.html</link>
      <description>静态博客没有 RSS, 读者就没法订阅更新。本文记录 Hexo 博客接入 RSS 的完整流程: 安装 hexo-generator-feed 插件、配置输出格式、在 Butterfly 主题中添加订阅按钮, 以及把订阅源提交到常用平台的注意事项。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/">前端</category>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/Hexo/">Hexo</category>
      <category domain="https://zsyyblog.com/tags/Hexo/">Hexo</category>
      <pubDate>Tue, 20 Oct 2026 12:42:18 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>写博客有个绕不开的问题: <strong>读者怎么知道你有更新?</strong></p><p>静态博客不像公众号那样有推送, 靠的是 RSS 订阅。但很多 Hexo 博客装好之后就没管过 RSS, 结果是:</p><ul><li>搜索引擎抓不到新文章(虽然有 sitemap 兜底, 但覆盖率不如 RSS)</li><li>读者只能手动收藏首页</li><li>想订阅 RSS 的朋友发现没有 <code>atom.xml</code>, 直接放弃</li></ul><p>这篇记录 Hexo 博客接入 RSS 的完整流程。</p><h1 id="原理-RSS-是什么"><a href="#原理-RSS-是什么" class="headerlink" title="原理: RSS 是什么"></a>原理: RSS 是什么</h1><p>先说清楚原理, 后面的操作就好理解了。</p><p>RSS 本质上就是一个 <strong>XML 文件</strong>, 里面按时间顺序列着你最新的文章:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;utf-8&quot;</span>?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">feed</span> <span class="attr">xmlns</span>=<span class="string">&quot;http://www.w3.org/2005/Atom&quot;</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">title</span>&gt;</span>竹山一叶<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">link</span>&gt;</span>https://zsyyblog.com<span class="tag">&lt;/<span class="name">link</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">updated</span>&gt;</span>2026-10-21T10:42:18.000Z<span class="tag">&lt;/<span class="name">updated</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">entry</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">title</span>&gt;</span>Hexo 博客添加 RSS 订阅<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">link</span> <span class="attr">href</span>=<span class="string">&quot;https://zsyyblog.com/196ff3fc.html&quot;</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">updated</span>&gt;</span>2026-10-21T10:42:18.000Z<span class="tag">&lt;/<span class="name">updated</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">content</span> <span class="attr">type</span>=<span class="string">&quot;html&quot;</span>&gt;</span>...<span class="tag">&lt;/<span class="name">content</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">entry</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">feed</span>&gt;</span></span><br></pre></td></tr></table></figure><p>订阅者用阅读器(Feedly、Inoreader、RSSHub 等)定时拉取这个文件, 有新文章就推送。</p><p><strong>关键点: RSS 文件是一个静态文件, 每次 <code>hexo g</code> 构建时自动重新生成。</strong> 不需要额外服务, 也不需要数据库。</p><h1 id="第一步-安装-feed-插件"><a href="#第一步-安装-feed-插件" class="headerlink" title="第一步: 安装 feed 插件"></a>第一步: 安装 feed 插件</h1><p>Hexo 官方没有内置 RSS 支持, 需要装插件:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install hexo-generator-feed --save</span><br></pre></td></tr></table></figure><p>装完会自动生成 <code>atom.xml</code>(和可选的 <code>rss.xml</code>)。</p><h2 id="验证是否生效"><a href="#验证是否生效" class="headerlink" title="验证是否生效"></a>验证是否生效</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo g</span><br></pre></td></tr></table></figure><p>然后检查 <code>public/</code> 目录:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">ls</span> public/*.xml</span><br></pre></td></tr></table></figure><p>应该能看到 <code>atom.xml</code> 和 <code>rss.xml</code>。<strong>构建完不生成 XML, 就是插件没装上或者没启用。</strong></p><blockquote><p>本文写作时踩过一个坑: 习惯性写 <code>hexo clean &amp;&amp; hexo g</code>, 结果 <code>hexo clean</code> 在 Windows 上卡了十几分钟没删完。原因是 <code>hexo-fs</code> 删目录时对所有文件发起<strong>无限并发</strong>的删除调用, 文件一多句柄就排队假死。文件多的话直接手动删更快:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">rm</span> -rf public db.json &amp;&amp; hexo g</span><br></pre></td></tr></table></figure><p>89 秒 vs 卡到没下文, 差别很大。</p></blockquote><h1 id="第二步-配置输出"><a href="#第二步-配置输出" class="headerlink" title="第二步: 配置输出"></a>第二步: 配置输出</h1><p>在 <code>_config.yml</code> 里加配置:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feed:</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">type:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">atom</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">rss2</span></span><br><span class="line">  <span class="attr">path:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">atom.xml</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">rss.xml</span></span><br><span class="line">  <span class="attr">icon:</span> <span class="string">https://img.zsyyblog.com/favicon.jpg</span></span><br><span class="line">  <span class="attr">limit:</span> <span class="number">20</span></span><br><span class="line">  <span class="attr">order_by:</span> <span class="string">-date</span></span><br><span class="line">  <span class="attr">autodiscovery:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>各字段含义:</p><table><thead><tr><th align="left">配置项</th><th align="left">说明</th><th align="left">建议值</th></tr></thead><tbody><tr><td align="left"><code>type</code></td><td align="left">输出格式, <code>atom</code> 或 <code>rss2</code></td><td align="left">见下</td></tr><tr><td align="left"><code>path</code></td><td align="left">输出文件名</td><td align="left">与 type 一一对应</td></tr><tr><td align="left"><code>icon</code></td><td align="left">订阅图标</td><td align="left">图标要能被订阅源客户端直接拉到</td></tr><tr><td align="left"><code>limit</code></td><td align="left">保留最近多少篇文章</td><td align="left">20</td></tr><tr><td align="left"><code>order_by</code></td><td align="left">排序字段, <code>-date</code> 是最新在前</td><td align="left"><code>-date</code></td></tr><tr><td align="left"><code>autodiscovery</code></td><td align="left">自动在 <code>&lt;head&gt;</code> 注入 <code>&lt;link rel=&quot;alternate&quot;&gt;</code></td><td align="left"><code>true</code></td></tr></tbody></table><blockquote><p><code>limit</code> 别设太大。RSS 里塞几百篇全文, 拉取一次就好几 MB, 阅读器体验很差。20 篇左右最舒服。</p></blockquote><blockquote><p>⚠️ <strong><code>type</code> 只认 <code>atom</code> 和 <code>rss2</code> 两个值。</strong> 写 <code>rss</code> 会被插件<strong>静默过滤掉</strong> —— 不报错, 但 <code>rss.xml</code> 就是不生成。想同时输出两种格式, <code>type</code> 和 <code>path</code> 都得写成数组:</p></blockquote><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feed:</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">type:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">atom</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">rss2</span></span><br><span class="line">  <span class="attr">path:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">atom.xml</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">rss.xml</span></span><br><span class="line">  <span class="attr">limit:</span> <span class="number">20</span></span><br></pre></td></tr></table></figure><p>如果数组长度对不上(比如 <code>type</code> 写了两个但 <code>path</code> 只写一个), 插件会自动补齐, 但还是老实写全比较好。</p><h2 id="关于-atom-还是-rss"><a href="#关于-atom-还是-rss" class="headerlink" title="关于 atom 还是 rss"></a>关于 atom 还是 rss</h2><p>简单说:</p><ul><li><strong>Atom</strong> —— 格式更规范, 支持全文和摘要两种模式, 主流阅读器都支持</li><li><strong>RSS 2.0</strong> —— 更老但更通用的格式, 兼容性好一些</li></ul><p><strong>建议两个都生成</strong>。Atom 给现代阅读器, RSS 2.0 兜住那些只认老格式的工具。两种格式指向同一批内容, 不会造成重复收录。</p><h1 id="第三步-在-Butterfly-中加订阅按钮"><a href="#第三步-在-Butterfly-中加订阅按钮" class="headerlink" title="第三步: 在 Butterfly 中加订阅按钮"></a>第三步: 在 Butterfly 中加订阅按钮</h1><p>光生成 XML 还不够, 得让读者知道入口在哪。</p><h2 id="Butterfly-自带的方案-没有"><a href="#Butterfly-自带的方案-没有" class="headerlink" title="Butterfly 自带的方案: 没有"></a>Butterfly 自带的方案: 没有</h2><p>先说个坑。<strong>Butterfly 5.x 主题里根本没有 RSS 按钮这个配置项</strong> —— 不是默认关闭, 是主题源码里压根没有。你在 <code>_config.butterfly.yml</code> 里写:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">rss:</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">path:</span> <span class="string">atom.xml</span></span><br></pre></td></tr></table></figure><p>在 5.2.2 上是<strong>纯无效配置</strong>, 页面不会多出任何东西, 而且不会有任何报错提示你。网上很多教程（包括本文最初写的版本）都在讲这个 <code>rss:</code> 块, 但那是更早的版本或者别的分支。</p><p>验证方法很简单, 直接搜主题源码:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">grep -rn <span class="string">&quot;rss&quot;</span> themes/butterfly/layout/ themes/butterfly/scripts/</span><br></pre></td></tr></table></figure><p>搜不到就是真没有。别浪费时间在配置上找。</p><h2 id="零成本方案-侧边栏社交图标"><a href="#零成本方案-侧边栏社交图标" class="headerlink" title="零成本方案: 侧边栏社交图标"></a>零成本方案: 侧边栏社交图标</h2><p>改主题模板之前, 先试试这个 —— <strong>Butterfly 的侧边栏头像下面有一排社交图标</strong>(GitHub、Email 那些), 那个位置固定在侧栏顶部, <strong>每一页都在, 曝光比文章底部高得多</strong>。</p><p>它由 <code>_config.butterfly.yml</code> 的 <code>social</code> 配置驱动, <strong>加一行就行, 不用碰任何主题文件</strong>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">social:</span></span><br><span class="line">  <span class="attr">fab fa-github:</span> <span class="string">https://github.com/zsyyblog</span> <span class="string">||</span> <span class="string">Github</span></span><br><span class="line">  <span class="attr">fas fa-envelope:</span> <span class="string">mailto:zsyyblog@qq.com</span> <span class="string">||</span> <span class="string">Email</span></span><br><span class="line">  <span class="attr">fas fa-rss:</span> <span class="string">/atom.xml</span> <span class="string">||</span> <span class="string">RSS订阅</span></span><br></pre></td></tr></table></figure><p>格式是 <code>图标类名: 链接 || 悬停提示 || 颜色</code>, 三个部分都可以省。</p><p>主题的渲染源码在 <code>themes/butterfly/layout/includes/header/social.pug</code>, 一共就 8 行:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">each url, icon in theme.social</span><br><span class="line">  -</span><br><span class="line">    const [link, title, color] = url.split(&#x27;||&#x27;).map(i =&gt; trim(i))</span><br><span class="line">    const href = url_for(link)</span><br><span class="line">    const iconStyle = color ? `color: $&#123;color.replace(/[\&#x27;\&quot;]/g, &#x27;&#x27;)&#125;;` : &#x27;&#x27;</span><br><span class="line">    const iconTitle = title || &#x27;&#x27;</span><br><span class="line">  a.social-icon(href=href target=&quot;_blank&quot; title=iconTitle)</span><br><span class="line">    i(class=icon style=iconStyle)</span><br></pre></td></tr></table></figure><p>几个细节:</p><table><thead><tr><th align="left">写法</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left"><code>fas fa-rss</code></td><td align="left">图标类名。<code>fas</code> 是 Font Awesome 的<strong>实心</strong>风格前缀, 别写成 <code>fa-rss</code>(不生效)</td></tr><tr><td align="left"><code>/atom.xml</code></td><td align="left">相对站点根目录写。走 <code>url_for()</code> 处理, 部署到子目录也不会错</td></tr><tr><td align="left"><code>RSS订阅</code></td><td align="left">鼠标悬停时的 title 提示</td></tr><tr><td align="left"><code>&#39;#ee802f&#39;</code></td><td align="left">图标颜色(可选)。不写就继承主题默认色, 跟其他社交图标保持一致</td></tr></tbody></table><blockquote><p>颜色那一段是可选的, <strong>建议直接省略</strong>。侧边栏就那么几个图标, 一个是橙的另外两个是灰的反而显得跳。想跟整个站的主题色统一, 让它继承 <code>--font-color</code> 就行。</p><p>真要单独指定颜色, <strong>引号必须保留</strong> —— 少了会被 YAML 解析成别的类型, 颜色就静默丢了。</p></blockquote><blockquote><p>想同时给 RSS 2.0 留个入口? 再加一行指向 <code>/rss.xml</code> 就行, 两个图标并排。不过一般一个就够, 阅读器都认 Atom。</p></blockquote><p><strong>这个方案我认为是性价比最高的</strong>: 零代码改动、主题升级不受影响、全站每页都显示。改主题模板那种方案留给需要自定义样式的情况。</p><h2 id="更醒目的订阅入口"><a href="#更醒目的订阅入口" class="headerlink" title="更醒目的订阅入口"></a>更醒目的订阅入口</h2><p>文章底部再放一个按钮, 转化率会更高。<strong>先说清楚 inject 的位置限制。</strong> Butterfly 5.x 的 inject 实际只有两个键:</p><table><thead><tr><th align="left">位置</th><th align="left">插入点</th></tr></thead><tbody><tr><td align="left"><code>head</code></td><td align="left">所有页面的 <code>&lt;head&gt;</code> 里</td></tr><tr><td align="left"><code>bottom</code></td><td align="left">所有页面底部(<code>&lt;/body&gt;</code> 前)</td></tr></tbody></table><p><strong>没有 <code>post-bottom</code> 这样的文章级注入点。</strong> 网上教程里写的 <code>inject.post-bottom</code> 在这个版本不会被渲染。</p><p>所以想放在文章底部, 有两个办法:</p><h3 id="办法一-改主题模板-推荐"><a href="#办法一-改主题模板-推荐" class="headerlink" title="办法一: 改主题模板(推荐)"></a>办法一: 改主题模板(推荐)</h3><p>在 <code>themes/butterfly/layout/post.pug</code> 里, 找到 <code>include includes/post/post-copyright.pug</code> 那一行(它在 <code>.tag_share</code> 之前), 在这两行之间插入:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">//- RSS 订阅入口</span><br><span class="line">.rss-subscribe</span><br><span class="line">  a(href=url_for(&#x27;/atom.xml&#x27;) target=&quot;_blank&quot; rel=&quot;noopener&quot;)</span><br><span class="line">    i(class=&quot;fa-solid fa-rss&quot;)</span><br><span class="line">    span= &#x27;订阅本站更新&#x27;</span><br></pre></td></tr></table></figure><p>注意缩进是 <strong>4 个空格</strong> —— 这段代码在 <code>article#article-container</code> 块内部, 顶格写会被 Pug 当成另一个块, 直接编译报错。</p><p>对应样式加到 <code>source/css/</code> 下(比如 <code>rss.css</code>), 再在 <code>_config.butterfly.yml</code> 的 <code>inject.head</code> 里引一下:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">head:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">&lt;link</span> <span class="string">rel=&quot;stylesheet&quot;</span> <span class="string">href=&quot;/css/rss.css&quot;&gt;</span></span><br></pre></td></tr></table></figure><p><code>source/css/</code> 下的文件会被原样拷贝到 <code>public/css/</code>, 所以这里用绝对路径 <code>/css/rss.css</code> 引用就行。</p><p>用 <code>url_for(&#39;/atom.xml&#39;)</code> 而不是写死 <code>/atom.xml</code>, 这样部署在子目录时也能正确指向。图标类名 <code>fa-solid fa-rss</code> 依赖主题加载的 Font Awesome(通过 <code>asset.fontawesome</code> 配置项), Butterfly 默认带 FA6, 直接能用。</p><h3 id="办法二-放首页"><a href="#办法二-放首页" class="headerlink" title="办法二: 放首页"></a>办法二: 放首页</h3><p>如果不想改主题, 可以用 <code>inject.bottom</code> 加一段 JS, 在文章页动态插入按钮:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">bottom:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">|</span></span><br><span class="line"><span class="string">      &lt;script&gt;</span></span><br><span class="line"><span class="string">      (function () &#123;</span></span><br><span class="line"><span class="string">        var box = document.createElement(&#x27;div&#x27;);</span></span><br><span class="line"><span class="string">        box.className = &#x27;rss-subscribe&#x27;;</span></span><br><span class="line"><span class="string">        box.innerHTML = &#x27;&lt;a href=&quot;/atom.xml&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;📢 订阅本站更新&lt;/a&gt;&#x27;;</span></span><br><span class="line"><span class="string">        var c = document.getElementById(&#x27;post&#x27;);</span></span><br><span class="line"><span class="string">        if (c) c.appendChild(box);</span></span><br><span class="line"><span class="string">      &#125;)();</span></span><br><span class="line"><span class="string">      &lt;/script&gt;</span></span><br></pre></td></tr></table></figure><p><strong>注意 pjax</strong>: 上面这段只在首次加载时执行, 切换页面后按钮会消失。需要额外监听:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">document</span>.<span class="title function_">addEventListener</span>(<span class="string">&#x27;pjax:complete&#x27;</span>, <span class="keyword">function</span> (<span class="params"></span>) &#123; <span class="comment">/* 重新插入 */</span> &#125;);</span><br></pre></td></tr></table></figure><p>所以还是<strong>办法一更省心</strong>。</p><blockquote><p>这里的路径是站点根目录下的相对路径。如果你的博客部署在子目录, 需要改成 <code>/子目录/atom.xml</code>, 或者用 <code>url_for()</code> 自动处理。</p></blockquote><h1 id="第四步-提交订阅源"><a href="#第四步-提交订阅源" class="headerlink" title="第四步: 提交订阅源"></a>第四步: 提交订阅源</h1><p>生成好了就该让人订阅。以下平台的收录比较有效。</p><h2 id="提交给阅读器服务"><a href="#提交给阅读器服务" class="headerlink" title="提交给阅读器服务"></a>提交给阅读器服务</h2><table><thead><tr><th align="left">平台</th><th align="left">地址</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">Feedly</td><td align="left">feedly.com</td><td align="left">老牌阅读器, 提交后聚合全网源</td></tr><tr><td align="left">Inoreader</td><td align="left">inoreader.com</td><td align="left">功能强, 免费版够用</td></tr><tr><td align="left">RSSHub</td><td align="left">rsshub.app</td><td align="left">聚合各类网站源</td></tr><tr><td align="left">Feed43</td><td align="left">feed43.com</td><td align="left">免费, 自动发现新源</td></tr><tr><td align="left">FeedBurner</td><td align="left">feedburner.com</td><td align="left">需 Google 账号</td></tr></tbody></table><p>大部分平台的提交流程都是: 注册 → 填入你的 RSS 地址 → 验证。</p><h2 id="提交给搜索引擎"><a href="#提交给搜索引擎" class="headerlink" title="提交给搜索引擎"></a>提交给搜索引擎</h2><p>RSS 对搜索引擎的价值在于<strong>加速收录</strong>。sitemap 告诉搜索引擎”这些页面存在”, RSS 则是”这些是新的”。</p><p>RSS 和主动推送是<strong>两种不同机制</strong>, 各有各的用处:</p><table><thead><tr><th align="left">方式</th><th align="left">原理</th><th align="left">触发时机</th></tr></thead><tbody><tr><td align="left"><code>hexo-generator-baidu-sitemap</code></td><td align="left">生成 <code>baidusitemap.xml</code> 提交给百度</td><td align="left">手动在站长平台提交</td></tr><tr><td align="left"><code>hexo-baidu-url-submit</code></td><td align="left">构建后自动 POST 新 URL 到百度</td><td align="left">每次 <code>hexo deploy</code> 自动推送</td></tr></tbody></table><p>sitemap 是”定期汇报全站有哪些页面”, 主动推送是”新文章一发布就通知”。两者不冲突, 有条件的话两个都配。</p><blockquote><p>如果你的站目前只配了 sitemap 生成(也就是只生成 <code>baidusitemap.xml</code> 而没有自动推送), 那 RSS 作为另一种”告诉搜索引擎新内容”的途径, 同样有补充价值。</p></blockquote><h2 id="校验-RSS-是否有效"><a href="#校验-RSS-是否有效" class="headerlink" title="校验 RSS 是否有效"></a>校验 RSS 是否有效</h2><p>可以用在线工具检查:</p><ul><li><a href="https://validator.w3.org/feed/">https://validator.w3.org/feed/</a> —— W3C 官方校验器</li><li><a href="https://www.rssboard.org/rss-validator">https://www.rssboard.org/rss-validator</a> —— 检查是否被主流阅读器识别</li></ul><p><strong>校验报错别忽略。</strong> 常见问题是 XML 里有非法字符(比如文章里写了 <code>&amp;</code> 但没转义), 这会导致某些阅读器整个源都读不出来。</p><h1 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h1><h2 id="生成的是-atom-xml-但别人要-rss-xml"><a href="#生成的是-atom-xml-但别人要-rss-xml" class="headerlink" title="生成的是 atom.xml, 但别人要 rss.xml"></a>生成的是 atom.xml, 但别人要 rss.xml</h2><p>前面说了, <code>type</code> 可以传数组同时输出两个。</p><h2 id="订阅源里没有新文章"><a href="#订阅源里没有新文章" class="headerlink" title="订阅源里没有新文章"></a>订阅源里没有新文章</h2><p>按顺序排查:</p><ol><li>重新构建, 确认 XML 里有新 entry</li><li>检查 <code>feed.limit</code> 是不是太小(比如设成 1, 旧的被顶掉了)</li><li>确认 <code>public/atom.xml</code> 确实更新了</li><li>确认 CDN 缓存 —— 静态文件如果走了 CDN, 可能还在发旧版本</li></ol><h2 id="文章内容在订阅源里是乱码"><a href="#文章内容在订阅源里是乱码" class="headerlink" title="文章内容在订阅源里是乱码"></a>文章内容在订阅源里是乱码</h2><p>先确认是<strong>真乱码</strong>还是<strong>显示问题</strong>。Atom 的规范格式要求 <code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;</code>, 插件生成时已经写死了 utf-8, 没有 <code>charset</code> 配置项可调。</p><p>所以如果你加了 <code>charset: utf-8</code> 却没效果 —— 因为<strong>这个配置项不存在</strong>, 加了也是被忽略。</p><p>真出现乱码, 查这两处:</p><ol><li>文章源文件本身的编码是不是 UTF-8(GBK 存的中文站点很容易踩)</li><li><code>description</code> 字段里有没有非法字符 —— XML 里的 <code>&amp;</code> 必须写成 <code>&amp;amp;</code>, 直接放裸的会让整个订阅源解析失败</li></ol><h2 id="想让订阅源只显示摘要而不是全文"><a href="#想让订阅源只显示摘要而不是全文" class="headerlink" title="想让订阅源只显示摘要而不是全文"></a>想让订阅源只显示摘要而不是全文</h2><p><strong>没有 <code>excerpt</code> 这个配置项</strong>(网上部分教程写错了)。控制全文还是摘要的开关叫 <strong><code>content</code></strong>, 默认 <code>true</code>(输出全文):</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feed:</span></span><br><span class="line">  <span class="attr">content:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure><p><code>content: false</code> 时, 每篇文章只输出摘要 —— 摘要优先取文章 front-matter 里的 <code>description</code>, 没有就取 <code>excerpt</code>, 都没有就截取正文前 <code>content_limit</code> 个字符(默认 140)。</p><p>想让摘要长一点:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feed:</span></span><br><span class="line">  <span class="attr">content:</span> <span class="literal">false</span></span><br><span class="line">  <span class="attr">content_limit:</span> <span class="number">300</span></span><br></pre></td></tr></table></figure><p>全文订阅的好处是阅读器不跳转到你的站点, 体验好、也不会消耗你的服务器流量。摘要模式则相反, 能给站点导流。</p><p><strong>两种都试试, 看看哪个数据好。</strong></p><h1 id="一个容易被忽略的点-RSS-也是-SEO-利器"><a href="#一个容易被忽略的点-RSS-也是-SEO-利器" class="headerlink" title="一个容易被忽略的点: RSS 也是 SEO 利器"></a>一个容易被忽略的点: RSS 也是 SEO 利器</h1><p>前面提到 RSS 能加速收录, 这里展开说一下。</p><p>搜索引擎的爬虫会定期抓取你的 RSS。因为 RSS 里是<strong>纯文本 + 结构化的新文章链接</strong>, 比解析 HTML 页面高效得多。</p><p>而且 RSS 文件是<strong>静态的</strong>, 没有 JS 渲染的负担。百度对 JS 的处理能力本来就弱, RSS 相当于给它提供了一条不需要执行 JS 的抓取路径。</p><p>所以我的建议是:</p><ol><li><strong>务必开启 RSS</strong></li><li><strong>别忘了 <code>autodiscovery: true</code></strong> —— 插件会自动往每个页面的 <code>&lt;head&gt;</code> 里插一条 <code>&lt;link rel=&quot;alternate&quot; type=&quot;application/atom+xml&quot;&gt;</code>, 浏览器的”订阅此页”和大部分阅读器的自动发现都靠它, 这是最省事也最有效的入口</li><li><strong><code>robots.txt</code> 里指向 sitemap</strong>(RSS 是自动发现的, 不用单独写)</li><li><strong>保持 entry 的 <code>updated</code> 字段准确</strong> —— 很多搜索引擎会把它当作内容新鲜度的信号</li></ol><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>接入 RSS 其实是 5 分钟的事:</p><ol><li><code>npm install hexo-generator-feed --save</code></li><li><code>_config.yml</code> 配 <code>feed:</code> 段(<code>type</code> 和 <code>path</code> 都写成数组, 注意是 <code>rss2</code> 不是 <code>rss</code>)</li><li>重新构建, 确认 <code>public/atom.xml</code> 和 <code>public/rss.xml</code> 都生成了</li><li><strong>侧边栏加 RSS 图标</strong>(<code>_config.butterfly.yml</code> 的 <code>social</code> 加一行, 零代码改动, 全页可见)</li><li>想要更醒目的转化, 再改 <code>themes/butterfly/layout/post.pug</code> 加文章底部按钮(主题本身不带 RSS 功能, 别去找 <code>rss:</code> 配置)</li><li>提交到 Feedly &#x2F; Inoreader 等平台</li></ol><p><strong>别小看这个文件。</strong> 它是静态博客里少数几个”让读者主动回访”的通道, 而且几乎没维护成本 —— 装一次插件, 之后每次构建自动更新。</p><blockquote><p>最后提一个坑: 如果你的站点文件托管在对象存储&#x2F;CDN 上, <strong>不存在的文件可能不会返回 404,而是返回一个 200 的错误页</strong>(我们遇到的就是返回一张 16KB 的 JPEG 图片)。这种环境下, 别人提交你的 RSS 地址时, 对方会拿到图片而不是 XML, 直接判定订阅源无效。所以 RSS 上线后一定要自己 curl 一次, 确认拿回来的是 <code>&lt;?xml</code> 开头的真 XML。</p></blockquote><p>参考链接:</p><ul><li><a href="https://github.com/Hexojs/hexo-generator-feed">hexo-generator-feed - GitHub</a></li><li><a href="https://validator.w3.org/feed/">W3C Feed Validator</a></li><li><a href="https://butterfly.js.org/">Butterfly 主题文档</a></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hexo 构建失败排查: 缓存没清干净, 换个 Node 版本就报错的两个坑</title>
      <link>https://zsyyblog.com/444def77.html</link>
      <description>Hexo 报 ERR_REQUIRE_ESM、找不到标签、构建结果和预期不一致时, 大概率不是代码问题。本文整理一套固定的排查顺序: 先清缓存, 再查依赖树, 最后才改配置, 附上具体命令和判断依据。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/">前端</category>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/Hexo/">Hexo</category>
      <category domain="https://zsyyblog.com/tags/Hexo/">Hexo</category>
      <pubDate>Tue, 20 Oct 2026 12:05:47 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>写 Hexo 文章多了之后, 会遇到一个共同的问题: <strong>报错信息看不出到底哪里错了</strong>。</p><p>比如 <code>hexo s</code> 突然起不来, 或者构建出来的东西和预期不一样。网上的解决办法五花八门, 试一圈下来问题还在。</p><p>这篇不讲具体某个错误的怎么修, 而是讲<strong>一套固定的排查顺序</strong> —— 先排除掉最常见、最容易被忽略的原因, 再往深处查。按这个顺序走, 大部分问题在第 1、2 步就能定位。</p><h1 id="排查总览"><a href="#排查总览" class="headerlink" title="排查总览"></a>排查总览</h1><p>先给个全貌, 后面逐条展开:</p><table><thead><tr><th align="left">步骤</th><th align="left">做什么</th><th align="left">能解决什么</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">清干净缓存重装</td><td align="left">依赖版本残留、<code>node_modules</code> 脏了</td></tr><tr><td align="left">2</td><td align="left">查依赖树找冲突源</td><td align="left">ERR_REQUIRE_ESM、模块找不到</td></tr><tr><td align="left">3</td><td align="left">换 Node 版本验证</td><td align="left">版本错配导致的诡异报错</td></tr><tr><td align="left">4</td><td align="left">排除插件影响</td><td align="left">某个插件引发的连锁错误</td></tr><tr><td align="left">5</td><td align="left">才动配置</td><td align="left">真正的配置问题</td></tr></tbody></table><p>顺序很重要。<strong>大部分人一开始就去改配置, 结果改了半天, 其实是缓存脏了。</strong></p><h1 id="第一步-清干净缓存重装"><a href="#第一步-清干净缓存重装" class="headerlink" title="第一步: 清干净缓存重装"></a>第一步: 清干净缓存重装</h1><h2 id="为什么必须先做这步"><a href="#为什么必须先做这步" class="headerlink" title="为什么必须先做这步"></a>为什么必须先做这步</h2><p>Node 生态有个很坑的地方: <strong>改了配置但不重装, 行为不会变</strong>。</p><p><code>node_modules</code> 里装的是旧版本, 缓存里存的是旧的中间结果。这两个不清掉, 你后面所有的排查都是在验证一个错误的环境。</p><p>而且如果你之前在 Node 18 下装过依赖, 现在换到 Node 20, 那些原生模块(native module)是<strong>按旧版本编译的</strong>, 直接拿来用大概率出问题。</p><h2 id="标准操作"><a href="#标准操作" class="headerlink" title="标准操作"></a>标准操作</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 清缓存和生成产物</span></span><br><span class="line">hexo clean</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 删掉依赖锁文件和依赖目录</span></span><br><span class="line"><span class="comment"># Linux / macOS</span></span><br><span class="line"><span class="built_in">rm</span> -rf node_modules package-lock.json</span><br><span class="line">npm cache clean --force</span><br><span class="line"></span><br><span class="line"><span class="comment"># Windows PowerShell</span></span><br><span class="line">Remove-Item -Recurse -Force node_modules</span><br><span class="line">Remove-Item -Force package-lock.json</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 重新安装</span></span><br><span class="line">npm install</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. 验证</span></span><br><span class="line">hexo g</span><br><span class="line">hexo s</span><br></pre></td></tr></table></figure><p><strong>注意 <code>package-lock.json</code> 也要删</strong>。它的作用是锁定依赖版本, 但如果你要解决的问题正是”依赖版本不对”, 那这份锁定文件就是问题的固化 —— 必须删掉让它重新解析。</p><h2 id="判断-这步有没有用"><a href="#判断-这步有没有用" class="headerlink" title="判断: 这步有没有用"></a>判断: 这步有没有用</h2><p>做完这一步就好了, 说明问题出在<strong>依赖残留</strong>。以后遇到构建问题, 直接从这步开始, 能省很多时间。</p><h1 id="第二步-查依赖树找冲突源"><a href="#第二步-查依赖树找冲突源" class="headerlink" title="第二步: 查依赖树找冲突源"></a>第二步: 查依赖树找冲突源</h1><p>如果第一步做完还是报错, 说明是<strong>真实的依赖冲突</strong>, 得往下查。</p><h2 id="找出某个包到底装了几个版本"><a href="#找出某个包到底装了几个版本" class="headerlink" title="找出某个包到底装了几个版本"></a>找出某个包到底装了几个版本</h2><p>这是排查依赖冲突最有用的一条命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm <span class="built_in">ls</span> strip-ansi</span><br></pre></td></tr></table></figure><p>输出会明确告诉你哪些包依赖了它、装的是什么版本:</p><figure class="highlight mel"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">zsyyblog@</span><br><span class="line">├── hexo@8<span class="number">.1</span><span class="number">.0</span></span><br><span class="line">│   └── hexo-util@4.x</span><br><span class="line">│       └── <span class="keyword">strip</span>-ansi@7<span class="number">.1</span><span class="number">.0</span></span><br><span class="line">└── hexo-butterfly-envelope@1<span class="number">.0</span><span class="number">.13</span></span><br><span class="line">    └── some-old-plugin@0<span class="number">.8</span><span class="number">.2</span></span><br><span class="line">        └── <span class="keyword">strip</span>-ansi@6<span class="number">.0</span><span class="number">.1</span></span><br></pre></td></tr></table></figure><p>看到同一个包同时存在 7.x 和 6.x, 就知道问题在哪了。</p><h2 id="ERR-REQUIRE-ESM-怎么定位"><a href="#ERR-REQUIRE-ESM-怎么定位" class="headerlink" title="ERR_REQUIRE_ESM 怎么定位"></a>ERR_REQUIRE_ESM 怎么定位</h2><p>如果报这个错:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Error [ERR_REQUIRE_ESM]: require() of ES Module ... not supported</span><br></pre></td></tr></table></figure><p>排查步骤:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 看报错里的路径, 记下具体是哪个包</span></span><br><span class="line"><span class="comment"># 2. 查这个包在依赖树里的位置</span></span><br><span class="line">npm <span class="built_in">ls</span> &lt;包名&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 查它的版本和模块类型</span></span><br><span class="line"><span class="built_in">cat</span> node_modules/&lt;包名&gt;/package.json | grep -E <span class="string">&#x27;&quot;version&quot;|&quot;type&quot;&#x27;</span></span><br></pre></td></tr></table></figure><p>关键看 <code>&quot;type&quot;</code> 字段:</p><ul><li><code>&quot;type&quot;: &quot;module&quot;</code> —— ESM, 不能被 <code>require()</code></li><li>没有 <code>&quot;type&quot;</code> 或 <code>&quot;type&quot;: &quot;commonjs&quot;</code> —— CommonJS, 可以被 <code>require()</code></li></ul><h2 id="解决办法-强制降级传递依赖"><a href="#解决办法-强制降级传递依赖" class="headerlink" title="解决办法: 强制降级传递依赖"></a>解决办法: 强制降级传递依赖</h2><p>如果确认是某个包的新版本把老插件搞崩了, 在项目根目录强制装一个旧版本:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install &lt;包名&gt;@&lt;兼容版本&gt; --save-exact</span><br></pre></td></tr></table></figure><p><code>--save-exact</code> 会把版本号精确锁定(不带 <code>^</code>), 这样它会作为<strong>直接依赖</strong>被放在项目根目录, 所有传递依赖都会优先找到它。</p><p>Hexo 升级时最常见的组合是:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install strip-ansi@6.0.1 --save-exact</span><br></pre></td></tr></table></figure><h1 id="第三步-换-Node-版本验证"><a href="#第三步-换-Node-版本验证" class="headerlink" title="第三步: 换 Node 版本验证"></a>第三步: 换 Node 版本验证</h1><h2 id="为什么换版本能定位问题"><a href="#为什么换版本能定位问题" class="headerlink" title="为什么换版本能定位问题"></a>为什么换版本能定位问题</h2><p>有些报错很奇怪, 同一个代码在 A 机器上正常, 在 B 机器上就炸。这类问题<strong>九成是 Node 版本差异</strong>。</p><p>Hexo 各版本对 Node 的要求:</p><table><thead><tr><th align="left">Hexo 版本</th><th align="left">Node.js 最低要求</th></tr></thead><tbody><tr><td align="left">6.2+</td><td align="left">12.13.0</td></tr><tr><td align="left">7.0+</td><td align="left">14.0.0</td></tr><tr><td align="left">7.x 实际推荐</td><td align="left">18.0.0</td></tr><tr><td align="left">8.0+</td><td align="left"><strong>20.19.0</strong></td></tr></tbody></table><p><strong>如果你的 Hexo 8 项目跑在 Node 18 上, 很多报错都是版本不对导致的</strong>, 而不是配置错误。</p><h2 id="用-nvm-管理版本"><a href="#用-nvm-管理版本" class="headerlink" title="用 nvm 管理版本"></a>用 nvm 管理版本</h2><p>安装 nvm:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Linux / macOS</span></span><br><span class="line">curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash</span><br></pre></td></tr></table></figure><p>常用命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">nvm install 20        <span class="comment"># 安装 Node 20</span></span><br><span class="line">nvm install 22        <span class="comment"># 安装 Node 22</span></span><br><span class="line">nvm use 20            <span class="comment"># 切到 Node 20</span></span><br><span class="line">nvm <span class="built_in">ls</span>                <span class="comment"># 查看已安装版本</span></span><br><span class="line">nvm current           <span class="comment"># 查看当前版本</span></span><br></pre></td></tr></table></figure><p>Windows 上推荐用 <a href="https://github.com/Schniz/fnm">fnm</a>, 体验和 nvm 一致。</p><h2 id="换版本后必须重装依赖"><a href="#换版本后必须重装依赖" class="headerlink" title="换版本后必须重装依赖"></a>换版本后必须重装依赖</h2><p>这是最容易漏的一步:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">nvm use 20</span><br><span class="line"></span><br><span class="line"><span class="comment"># 必须重装, 不能直接用旧依赖</span></span><br><span class="line"><span class="built_in">rm</span> -rf node_modules package-lock.json</span><br><span class="line">npm install</span><br><span class="line"></span><br><span class="line">hexo clean &amp;&amp; hexo s</span><br></pre></td></tr></table></figure><p><strong>因为原生模块是针对特定 Node 版本编译的</strong>, 换了 Node 不重装, 大概率会报 <code>NODE_MODULE_VERSION</code> 不匹配。</p><h2 id="判断-是不是版本问题"><a href="#判断-是不是版本问题" class="headerlink" title="判断: 是不是版本问题"></a>判断: 是不是版本问题</h2><p>做法很简单: <strong>换到另一个 Node 大版本, 重新完整安装, 看问题是否消失</strong>。</p><ul><li>消失 → 确定是版本问题, 把版本固定下来</li><li>还在 → 版本不是主因, 继续第四步</li></ul><h2 id="把版本固定下来"><a href="#把版本固定下来" class="headerlink" title="把版本固定下来"></a>把版本固定下来</h2><p>确认可用后, 在 <code>package.json</code> 里声明, 避免以后重装又变:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;engines&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;node&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&gt;=22 &lt;25&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>装了 <code>nvm</code> 的话, 还可以配一个 <code>.nvmrc</code> 文件, 内容就是版本号:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">echo</span> <span class="string">&quot;22&quot;</span> &gt; .nvmrc</span><br><span class="line">nvm use</span><br></pre></td></tr></table></figure><h1 id="第四步-排除插件影响"><a href="#第四步-排除插件影响" class="headerlink" title="第四步: 排除插件影响"></a>第四步: 排除插件影响</h1><p>前三步做完还报错, 大概率是<strong>某个插件的问题</strong>。</p><h2 id="二分法定位"><a href="#二分法定位" class="headerlink" title="二分法定位"></a>二分法定位</h2><p>最朴素但最有效的办法: <strong>逐个禁用插件</strong>。</p><p>先在 <code>package.json</code> 里把可疑插件注释掉, 重新安装构建:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 禁用后重装</span></span><br><span class="line"><span class="built_in">rm</span> -rf node_modules package-lock.json</span><br><span class="line">npm install</span><br><span class="line">hexo clean &amp;&amp; hexo g</span><br></pre></td></tr></table></figure><p>如果好了, 就是这个插件的问题。再装回来。</p><h2 id="更快的办法-看报错来源"><a href="#更快的办法-看报错来源" class="headerlink" title="更快的办法: 看报错来源"></a>更快的办法: 看报错来源</h2><p>多数情况下<strong>报错信息本身就指向了来源</strong>, 比如:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Error: Cannot <span class="built_in">read</span> properties of undefined (reading <span class="string">&#x27;cover&#x27;</span>)</span><br><span class="line"><span class="comment">#                        ^^^^^^</span></span><br><span class="line"><span class="comment"># 这里读的是 cover 字段, 去 grep 谁在读 cover</span></span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">npm <span class="built_in">ls</span> &lt;报错里的包名&gt;</span><br><span class="line"><span class="comment"># 输出会告诉你这个包是被谁引入的</span></span><br></pre></td></tr></table></figure><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 直接在源码里搜</span></span><br><span class="line">grep -r <span class="string">&quot;&lt;出错的字段名&gt;&quot;</span> themes/ node_modules/&lt;可疑包&gt;/</span><br></pre></td></tr></table></figure><h2 id="临时绕过"><a href="#临时绕过" class="headerlink" title="临时绕过"></a>临时绕过</h2><p>如果确认是插件 bug 但急着发文章, 可以用垫片绕过, 而不是改插件源码。</p><p>比如老插件调用的全局函数在新版里被搬走了:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 在 _config.butterfly.yml 的 inject.head 里加</span></span><br><span class="line"><span class="variable language_">window</span>.<span class="property">getScript</span> = <span class="variable language_">window</span>.<span class="property">getScript</span> || <span class="keyword">function</span>(<span class="params">u, a</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="variable language_">window</span>.<span class="property">btf</span>.<span class="title function_">getScript</span>(u, a)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>用 <code>||</code> 做存在性判断</strong>, 这样即使主题以后又把函数放回去了, 这段代码也不会冲突。</p><h1 id="第五步-动配置"><a href="#第五步-动配置" class="headerlink" title="第五步: 动配置"></a>第五步: 动配置</h1><p>到这一步基本可以确定是配置问题了。</p><p>Hexo 配置的排查要点:</p><h2 id="配置层级要看清"><a href="#配置层级要看清" class="headerlink" title="配置层级要看清"></a>配置层级要看清</h2><p>Hexo 的配置有<strong>主题配置</strong>和<strong>站点配置</strong>两套, 同时还存在 <code>_config.yml</code>(站点)和 <code>_config.butterfly.yml</code>(主题)的区别。</p><p>常见误区: 把站点配置写到了主题配置里, 或者反过来。<strong>先确认你改的文件是哪个。</strong></p><h2 id="缩进和类型"><a href="#缩进和类型" class="headerlink" title="缩进和类型"></a>缩进和类型</h2><p>YAML 对缩进敏感, 这几个错误很常见:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 错: tab 缩进(YAML 不支持 tab)</span></span><br><span class="line"><span class="attr">inject:</span></span><br><span class="line"><span class="attr">head:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">xxx</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 错: 布尔值写成字符串</span></span><br><span class="line"><span class="attr">enable:</span> <span class="string">&#x27;true&#x27;</span>      <span class="comment"># 这是字符串 &quot;true&quot;</span></span><br><span class="line"><span class="attr">enable:</span> <span class="literal">true</span>        <span class="comment"># 这才是布尔值</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 错: 列表项缩进不对</span></span><br><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">head:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">xxx</span>             <span class="comment"># 应该再缩进两格</span></span><br></pre></td></tr></table></figure><h2 id="插件的配置节点可能缺失"><a href="#插件的配置节点可能缺失" class="headerlink" title="插件的配置节点可能缺失"></a>插件的配置节点可能缺失</h2><p>老插件升级后, 配置格式可能变了。<strong>报错里出现 <code>undefined</code> 时, 优先怀疑配置节点缺失。</strong></p><h1 id="一个通用的排查习惯"><a href="#一个通用的排查习惯" class="headerlink" title="一个通用的排查习惯"></a>一个通用的排查习惯</h1><p>最后说一个我觉得最有用的习惯 —— <strong>每次排查都记录下来</strong>。</p><p>我自己的做法是, 每解决一个新报错, 就写成一篇博客:</p><ol><li>报错现象是什么</li><li>根因是什么</li><li>怎么解决的</li><li>参考链接</li></ol><p>这样做有三个好处:</p><ul><li>下次遇到同类问题, <strong>直接搜自己的文章</strong>, 不用重新推导</li><li>一年后回头看, 这些文章正好是<strong>最有搜索价值的踩坑记录</strong></li><li>排查过程被迫理清一遍, 记忆反而更牢</li></ul><p><strong>排查本身是隐性的, 写出来才是显性的知识。</strong></p><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>一套固定的排查顺序, 记住这五步:</p><ol><li><strong>清缓存重装</strong> —— 排除依赖残留(最常见)</li><li><strong>查依赖树</strong> —— <code>npm ls &lt;包名&gt;</code> 找冲突源(第二常见)</li><li><strong>换 Node 版本</strong> —— 排除版本错配</li><li><strong>排除插件</strong> —— 二分法禁用 + 看报错来源</li><li><strong>最后动配置</strong> —— YAML 缩进、类型、节点缺失</li></ol><p><strong>核心原则: 从最常见的原因查起, 不要一上来就改配置。</strong></p><p>绝大多数构建问题在第 1、2 步就解决了, 而大部分人恰恰是从第 5 步开始的 —— 于是改了半天配置, 问题根本不在那里。</p><p>参考链接:</p><ul><li><a href="https://nodejs.org/en/about/previous-releases">Node.js Releases</a></li><li><a href="https://docs.npmjs.com/cli/v10/commands/npm-ls">npm 依赖树文档</a></li><li><a href="https://hexo.io/docs/troubleshooting.html">Hexo 文档 - 常见问题</a></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Butterfly 5.2 升级到 5.7: getScript 找不到, 说说组件加载不出来怎么解</title>
      <link>https://zsyyblog.com/64b64b1d.html</link>
      <description>Butterfly 5.x 把 getScript 等全局函数收进了 window.btf 命名空间, 3.x 时代的老插件因此报 ReferenceError。本文记录 5.2 升级到 5.7 过程中遇到的真实障碍: 命名空间迁移、插件配置节点缺失、同名标签冲突, 以及对应的解决办法。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/">前端</category>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/Hexo/">Hexo</category>
      <category domain="https://zsyyblog.com/tags/Hexo/">Hexo</category>
      <pubDate>Tue, 20 Oct 2026 11:38:15 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>上一篇文章写了 Hexo 7 → 8 的升级, 这次接着记录 Butterfly 主题的升级。</p><p>本地是 <code>5.2.2</code>, 当前最新是 <code>5.7.0</code>。中间跨了 5.3、5.4、5.5、5.6、5.7 五个版本, 官方更新日志里有大量性能优化和 bug 修复, 值得升级。</p><p>但升级过程中踩到了几个坑, 其中一个是<strong>跨大版本的破坏性变更</strong>, 老插件直接罢工。这篇把过程和解决办法记下来。</p><h1 id="先看升级收益"><a href="#先看升级收益" class="headerlink" title="先看升级收益"></a>先看升级收益</h1><p>翻了一遍 5.6 的更新日志, 对博客体验提升比较明显的有这些:</p><ul><li>TOC 目录滚动从 throttle 轮询换成 <strong>IntersectionObserver</strong>, 大幅减少滚动事件的计算量</li><li>滚动处理器换成 <code>btf.rafThrottle</code>, 跟随浏览器渲染帧率</li><li>平滑滚动从线性插值改成 <strong>easeOutQuart 缓动</strong>, 观感更自然</li><li>新增 <code>scriptCache</code> &#x2F; <code>cssCache</code> 机制, 防止脚本和样式重复加载</li><li>Mermaid 手势系统用 AbortController 重写, 修复了 Chrome 下滚动图表乱跳</li><li>修复 <code>flink.js</code> 和 <code>button.js</code> 缺少 HTML 转义导致的 <strong>XSS 漏洞</strong></li></ul><p>性能优化是实打实的, 文章多了之后尤其明显。</p><h1 id="障碍一-ReferenceError-getScript-is-not-defined"><a href="#障碍一-ReferenceError-getScript-is-not-defined" class="headerlink" title="障碍一: ReferenceError: getScript is not defined"></a>障碍一: ReferenceError: getScript is not defined</h1><p>这是最典型的报错, 说说组件加载不出来(比如”说说”模块)。</p><h2 id="报错现象"><a href="#报错现象" class="headerlink" title="报错现象"></a>报错现象</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ReferenceError: getScript is not defined</span><br></pre></td></tr></table></figure><p>页面上对应组件的位置空白, 控制台一片红。</p><h2 id="根因-全局函数搬进了命名空间"><a href="#根因-全局函数搬进了命名空间" class="headerlink" title="根因: 全局函数搬进了命名空间"></a>根因: 全局函数搬进了命名空间</h2><p>Butterfly <strong>3.x 时代</strong>, <code>getScript</code> 是直接挂在 <code>window</code> 上的:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">window</span>.<span class="property">getScript</span> = ...</span><br></pre></td></tr></table></figure><p>所以任何地方都能直接调。</p><p>从 <strong>5.x 开始</strong>, 这类函数被收进了 <code>window.btf</code> 命名空间:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">window</span>.<span class="property">btf</span>.<span class="property">getScript</span> = ...</span><br></pre></td></tr></table></figure><p>而我用的那个第三方插件(信封&#x2F;说说相关), 它注入的内联脚本里调的还是<strong>裸函数</strong>:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title function_">getScript</span>(<span class="string">&#x27;/js/xxx.js&#x27;</span>, callback)</span><br></pre></td></tr></table></figure><p>这个裸调用现在自然就找不到了。</p><h2 id="解决办法-加一段惰性垫片"><a href="#解决办法-加一段惰性垫片" class="headerlink" title="解决办法: 加一段惰性垫片"></a>解决办法: 加一段惰性垫片</h2><p>不想改插件源码的话, 可以在 <code>inject.head</code> 里加一段垫片代码。<strong>关键在于”惰性”</strong> —— 调用的时候才去取 <code>btf</code>, 这样就不依赖脚本的加载顺序:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">window</span>.<span class="property">getScript</span> = <span class="variable language_">window</span>.<span class="property">getScript</span> || <span class="keyword">function</span>(<span class="params">u, a</span>) &#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="variable language_">window</span>.<span class="property">btf</span>.<span class="title function_">getScript</span>(u, a)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>六行代码, <strong>一个插件都不用改</strong>。</p><p>具体做法是在 <code>_config.butterfly.yml</code> 的 <code>inject.head</code> 下面加:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">inject:</span></span><br><span class="line">  <span class="attr">head:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="string">|</span></span><br><span class="line"><span class="string">      window.getScript = window.getScript || function(u, a) &#123;</span></span><br><span class="line"><span class="string">        return window.btf.getScript(u, a)</span></span><br><span class="line"><span class="string">      &#125;</span></span><br></pre></td></tr></table></figure><p>这个思路挺通用的 —— 遇到”全局函数搬家”导致的报错, 都可以用同样的方式兜一层。</p><h1 id="障碍二-构建直接崩-报-cover-undefined"><a href="#障碍二-构建直接崩-报-cover-undefined" class="headerlink" title="障碍二: 构建直接崩, 报 cover  undefined"></a>障碍二: 构建直接崩, 报 cover  undefined</h1><h2 id="报错现象-1"><a href="#报错现象-1" class="headerlink" title="报错现象"></a>报错现象</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">TypeError: Cannot <span class="built_in">read</span> properties of undefined (reading <span class="string">&#x27;cover&#x27;</span>)</span><br></pre></td></tr></table></figure><p><code>hexo g</code> 直接中止, 构建不出来。</p><h2 id="根因-插件配置节点缺失"><a href="#根因-插件配置节点缺失" class="headerlink" title="根因: 插件配置节点缺失"></a>根因: 插件配置节点缺失</h2><p><code>hexo-butterfly-envelope</code> 从 <code>1.0.13+</code> 开始, 把四张图片收进了 <code>custom_pic</code> 这个子节点里, 而代码内部<strong>不做任何存在性检查</strong>就往下读。</p><p>我的旧配置里没有这一节, 于是读 <code>undefined.custom_pic</code>, 构建直接崩。</p><h2 id="解决办法-补齐配置节点"><a href="#解决办法-补齐配置节点" class="headerlink" title="解决办法: 补齐配置节点"></a>解决办法: 补齐配置节点</h2><p>按插件要求补上配置就行:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">envelope:</span></span><br><span class="line">  <span class="attr">enable:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">custom_pic:</span></span><br><span class="line">    <span class="attr">pic_1:</span> <span class="string">xxx.png</span></span><br><span class="line">    <span class="attr">pic_2:</span> <span class="string">xxx.png</span></span><br><span class="line">    <span class="attr">pic_3:</span> <span class="string">xxx.png</span></span><br><span class="line">    <span class="attr">pic_4:</span> <span class="string">xxx.png</span></span><br></pre></td></tr></table></figure><p>补的时候顺手发现, <strong>它的默认值还指向一个已经废弃的镜像地址</strong>, 一并改成了自己的图床。</p><blockquote><p>教训: 老插件升级后如果直接崩, 先去看它的 changelog, 大概率是配置格式变了。</p></blockquote><h1 id="障碍三-unknown-block-tag-timenode"><a href="#障碍三-unknown-block-tag-timenode" class="headerlink" title="障碍三: unknown block tag: timenode"></a>障碍三: unknown block tag: timenode</h1><h2 id="报错现象-2"><a href="#报错现象-2" class="headerlink" title="报错现象"></a>报错现象</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ERROR Failed to render tag: unknown block tag: timenode</span><br></pre></td></tr></table></figure><p>文章渲染失败, 但不是全部文章, 只有用了特定标签的那几篇。</p><h2 id="根因-同名标签-少注册了一个"><a href="#根因-同名标签-少注册了一个" class="headerlink" title="根因: 同名标签, 少注册了一个"></a>根因: 同名标签, 少注册了一个</h2><p>Butterfly 5 自己带了一个 <code>timeline</code> 标签, 但<strong>只注册了这一个</strong>。</p><p>而我用的第三方版本除了 <code>timeline</code>, 还注册了 <code>timenode</code>。我的文章里用了 30 次 <code>timenode</code>, 于是就报错了。</p><h2 id="解决办法-保留第三方版本"><a href="#解决办法-保留第三方版本" class="headerlink" title="解决办法: 保留第三方版本"></a>解决办法: 保留第三方版本</h2><p>让第三方的标签插件覆盖官方的, 这样两个标签都能用。</p><p>这个问题的本质是<strong>同名标签的注册冲突</strong> —— 如果你升级后遇到”某个标签突然不认识了”, 先想想是不是两个插件抢同一个标签名。</p><h1 id="升级前-先验证挂载点"><a href="#升级前-先验证挂载点" class="headerlink" title="升级前: 先验证挂载点"></a>升级前: 先验证挂载点</h1><p>这次升级前我做了件事, 差点白做。</p><p>我原本的判断是: 跨 5 个版本, 那些插件依赖的模板挂载点肯定早被删了, 升级必然失败。</p><p>于是我<strong>把 5.7.0 的包解开, 直接 grep 那几个挂载点</strong>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 解包后 grep 挂载点</span></span><br><span class="line">grep -r <span class="string">&quot;recent-posts\|sticky_layout\|aside-content\|card-widget&quot;</span> layout/</span><br></pre></td></tr></table></figure><p>结果如下:</p><table><thead><tr><th align="left">挂载点</th><th align="left">3.7.1</th><th align="left">5.7.0</th></tr></thead><tbody><tr><td align="left">recent-posts</td><td align="left">5 个布局文件</td><td align="left">6 个</td></tr><tr><td align="left">sticky_layout</td><td align="left">1</td><td align="left">1</td></tr><tr><td align="left">aside-content</td><td align="left">1</td><td align="left">1</td></tr><tr><td align="left">card-widget</td><td align="left">11</td><td align="left">13</td></tr></tbody></table><p><strong>四个挂载点在新版里一个不少</strong>, 而且 <code>card-widget</code> 还多了 2 个。</p><p>所以我那条”最重的理由”—— 证据不支持。改口之后, 升级才真正开始。</p><p>这一步花的时间不长, 但避免了”因为一个不成立的判断而放弃升级”。<strong>预感值得说出来, 但只有证据能定案。</strong></p><h1 id="完整升级流程"><a href="#完整升级流程" class="headerlink" title="完整升级流程"></a>完整升级流程</h1><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 备份 + Git 基线</span></span><br><span class="line">git add -A &amp;&amp; git commit -m <span class="string">&quot;baseline: pre-butterfly-5.7&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 记录当前版本, 方便对比</span></span><br><span class="line"><span class="built_in">cat</span> themes/butterfly/package.json | grep version</span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. 升级主题</span></span><br><span class="line">npm install hexo-theme-butterfly@5.7.0 --save</span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. 补齐插件配置节点(envelope 的 custom_pic)</span></span><br><span class="line"><span class="comment"># 5. 加 getScript 垫片到 inject.head</span></span><br><span class="line"><span class="comment"># 6. 验证</span></span><br><span class="line">hexo clean &amp;&amp; hexo g</span><br><span class="line">hexo s</span><br></pre></td></tr></table></figure><h1 id="建议的版本锁定"><a href="#建议的版本锁定" class="headerlink" title="建议的版本锁定"></a>建议的版本锁定</h1><p>升级完记得把版本锁进 <code>package.json</code>, 避免以后重装得到不同环境:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;dependencies&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;hexo&quot;</span><span class="punctuation">:</span> <span class="string">&quot;8.1.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;hexo-theme-butterfly&quot;</span><span class="punctuation">:</span> <span class="string">&quot;5.7.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;hexo-butterfly-envelope&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1.0.13&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>这次跨越 5 个小版本, <strong>全过程只撞到 3 个真实障碍</strong>, 比预期少很多:</p><ol><li><strong><code>getScript</code> 搬家</strong> —— 用惰性垫片兜住, 不改插件源码</li><li><strong>配置节点缺失</strong> —— 补配置即可</li><li><strong>同名标签冲突</strong> —— 让第三方版本覆盖官方</li></ol><p>三个问题的共同点是: **都不是主题本身的 bug, 而是”老插件跟不上新主题”**。</p><p>所以升级前做的那件事 —— <strong>grep 挂载点验证</strong> —— 比事后一个一个试错要高效得多。</p><p>参考链接:</p><ul><li><a href="https://butterfly.js.org/posts/5-6-release-notes">Butterfly 5.6 Release Notes</a></li><li><a href="https://github.com/jerryc127/hexo-theme-butterfly">hexo-theme-butterfly - GitHub</a></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>Hexo 7 升级到 Hexo 8: Node 版本卡人, 插件报 ERR_REQUIRE_ESM 怎么办</title>
      <link>https://zsyyblog.com/842c233a.html</link>
      <description>Hexo 8.0.0 正式移除了对 Node.js 16 和 18 的支持, 要求 Node 20.19.0 以上。本文记录从 Hexo 7.3.0 升级到 8.x 过程中遇到的真实问题: Node 版本不达标、ERR_REQUIRE_ESM 报错、strip-ansi 冲突的排查与解决思路。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/">前端</category>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/Hexo/">Hexo</category>
      <category domain="https://zsyyblog.com/tags/Hexo/">Hexo</category>
      <pubDate>Tue, 20 Oct 2026 11:12:33 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>最近整理博客依赖, 发现本地还是 <code>hexo 7.3.0</code>, 而官方已经发布到 <code>8.1.0</code> 了。中间隔着 <code>8.0.0</code> 这个大版本, 时隔一年多(2024-07 → 2025-09), 不少东西都变了。</p><p>这篇记录一下升级过程中踩到的坑, 供同样在使用 Hexo 的朋友参考。</p><p>先说结论: <strong>升级本身不难, 难的是 Node 版本和插件生态的错配</strong>。</p><h1 id="版本现状对照"><a href="#版本现状对照" class="headerlink" title="版本现状对照"></a>版本现状对照</h1><p>在动手之前, 先看清楚自己站在哪里:</p><table><thead><tr><th align="left">组件</th><th align="left">我的版本</th><th align="left">当前最新</th><th align="left">发布时间</th></tr></thead><tbody><tr><td align="left">Hexo</td><td align="left">7.3.0</td><td align="left">8.1.0</td><td align="left">7.3.0 是 2024-07, 8.0.0 是 2025-09</td></tr><tr><td align="left">hexo-theme-butterfly</td><td align="left">5.2.2</td><td align="left">5.7.0</td><td align="left">—</td></tr><tr><td align="left">Node.js</td><td align="left">22.22.2 LTS</td><td align="left">建议 22&#x2F;24 LTS</td><td align="left">—</td></tr></tbody></table><p>查版本和发布日期:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看本地 hexo 版本</span></span><br><span class="line">npx hexo version</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看主题版本</span></span><br><span class="line"><span class="built_in">cat</span> themes/butterfly/package.json | grep version</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看 node 版本</span></span><br><span class="line">node -v</span><br></pre></td></tr></table></figure><p>也可以直接看官方 <a href="https://hexo.io/">hexo.io</a> 首页的 Releases 列表, 版本发布时间写得很清楚。</p><h1 id="第一个坑-Node-版本不达标"><a href="#第一个坑-Node-版本不达标" class="headerlink" title="第一个坑: Node 版本不达标"></a>第一个坑: Node 版本不达标</h1><h2 id="官方-breaking-change"><a href="#官方-breaking-change" class="headerlink" title="官方 breaking change"></a>官方 breaking change</h2><p>Hexo <code>8.0.0</code> 的 release note 里, Breaking Changes 一栏只有两条, 但都很硬:</p><ul><li><code>chore: drop Node.js 16</code></li><li><code>chore: drop Node.js 18</code></li></ul><p>也就是说, <strong>Hexo 8 要求 Node.js 20.19.0 或更高版本</strong>。</p><p>如果你的环境还是 Node 16 或 18, 升级 Hexo 之后大概率会直接报错, 或者在装依赖的时候就一堆 warning。</p><h2 id="检查自己的-Node-版本"><a href="#检查自己的-Node-版本" class="headerlink" title="检查自己的 Node 版本"></a>检查自己的 Node 版本</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">node -v</span><br><span class="line">npm -v</span><br></pre></td></tr></table></figure><p>如果版本号开头是 <code>v16.</code> 或 <code>v18.</code>, 那就必须先升级 Node, 再动 Hexo。</p><p>我这里用的是 Node 22.22.2 LTS, 直接满足要求, 所以没踩这一步。<strong>如果你恰好还在 18 上, 请务必先升 Node, 不要直接升 Hexo</strong>, 否则排查方向会完全跑偏。</p><h1 id="第二个坑-ERR-REQUIRE-ESM"><a href="#第二个坑-ERR-REQUIRE-ESM" class="headerlink" title="第二个坑: ERR_REQUIRE_ESM"></a>第二个坑: ERR_REQUIRE_ESM</h1><p>这是我看到的最高频的一个报错:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">npm install</span><br><span class="line"><span class="comment"># 或者 hexo g 时</span></span><br><span class="line">Error [ERR_REQUIRE_ESM]: require() of ES Module ... from ... not supported</span><br></pre></td></tr></table></figure><h2 id="根因分析"><a href="#根因分析" class="headerlink" title="根因分析"></a>根因分析</h2><p>这不是 Hexo 本身的问题, 而是一个<strong>依赖版本错配</strong>问题。</p><p>Hexo 8 把很多底层依赖都往上提了一轮, 比如 <code>hexo-util</code> 从 3 提到 4。而一些<strong>老的第三方插件</strong>并没有跟着更新, 它们内部还依赖着旧版的 <code>strip-ansi</code>。</p><p>关键在于 <code>strip-ansi</code> 这个包的版本差异:</p><ul><li><code>strip-ansi@6.x</code> —— CommonJS, 可以被 <code>require()</code></li><li><code>strip-ansi@7.x</code> —— 改成了 ESM, <code>require()</code> 会直接报错</li></ul><p>新版 Hexo 依赖链里会带进 <code>strip-ansi@7</code>, 而老插件内部还在用 <code>require()</code> 加载它, 于是就撞上了。</p><h2 id="解决办法"><a href="#解决办法" class="headerlink" title="解决办法"></a>解决办法</h2><p>思路是<strong>在你的项目里强制降级这个传递依赖</strong>, 让老插件能找到它认识的格式:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install strip-ansi@6.0.1 --save-exact</span><br></pre></td></tr></table></figure><p>这条命令会在你的项目根目录放一个”旧版零件”, 所有插件都能拿到, 从而避开 ESM 冲突。</p><h2 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">hexo clean</span><br><span class="line">hexo s</span><br></pre></td></tr></table></figure><p>如果 <code>hexo s</code> 能正常起服务, 说明问题解决了。</p><h1 id="第三个坑-别用-npm-update"><a href="#第三个坑-别用-npm-update" class="headerlink" title="第三个坑: 别用 npm update"></a>第三个坑: 别用 npm update</h1><p>这是<strong>习惯问题</strong>, 但影响很大。</p><p>以前升级 Hexo 我习惯用 <code>npm update</code>, 这次没有继续这么做。原因是 <code>npm update</code> 会把范围内所有依赖都往最新拉, 一旦某个中间包出了不兼容的版本, 排查起来非常痛苦。</p><p>更稳妥的做法是<strong>在 <code>package.json</code> 里锁定关键依赖的主版本</strong>:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;engines&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;node&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&gt;=22 &lt;25&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;dependencies&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;hexo&quot;</span><span class="punctuation">:</span> <span class="string">&quot;8.1.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;hexo-theme-butterfly&quot;</span><span class="punctuation">:</span> <span class="string">&quot;5.7.0&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>这样以后删掉 <code>node_modules</code> 重新安装, 得到的是同一套环境, 不会因为”今天装的和上次装的不是一个东西”而困惑。</p><p>顺便说一句, <code>engines</code> 这个字段很值得加, 它能在安装阶段就提醒你 Node 版本对不对。</p><h1 id="升级前的准备-两层回退"><a href="#升级前的准备-两层回退" class="headerlink" title="升级前的准备: 两层回退"></a>升级前的准备: 两层回退</h1><p>升级这种事, <strong>备份不是可选项</strong>。</p><p>我给自己的博客目录建了两层回退:</p><h2 id="第一层-Git-基线"><a href="#第一层-Git-基线" class="headerlink" title="第一层: Git 基线"></a>第一层: Git 基线</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">git init</span><br><span class="line">git add -A</span><br><span class="line">git commit -m <span class="string">&quot;baseline: pre-Hexo-8&quot;</span></span><br><span class="line">git tag pre-hexo8</span><br><span class="line">git switch -c upgrade/hexo-8</span><br></pre></td></tr></table></figure><p>好处是任何配置和源码问题都能用 Git 直接定位和回滚。</p><h2 id="第二层-完整压缩包"><a href="#第二层-完整压缩包" class="headerlink" title="第二层: 完整压缩包"></a>第二层: 完整压缩包</h2><p>博客目录里除了 <code>source/</code>, 还有 <code>node_modules/</code>、生成的 <code>public/</code>、部署用的 <code>.deploy_git/</code>, 以及主题目录里的 Git 历史。这些东西 Git 不好管, 建议打一个完整压缩包。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 以 Linux 为例</span></span><br><span class="line">tar -czf hexo-backup-$(<span class="built_in">date</span> +%Y%m%d).tar.gz --exclude=node_modules .</span><br><span class="line"></span><br><span class="line"><span class="comment"># Windows PowerShell</span></span><br><span class="line">Compress-Archive -Path . -DestinationPath hexo-backup-20261021.zip</span><br></pre></td></tr></table></figure><p>打完包之后, <strong>建议算一下 SHA-256 校验值</strong>, 确认压缩包真的能解开, 而不是坏在那儿了。</p><h1 id="完整操作流程"><a href="#完整操作流程" class="headerlink" title="完整操作流程"></a>完整操作流程</h1><p>把上面的步骤串起来:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 1. 确认当前版本</span></span><br><span class="line">npx hexo version</span><br><span class="line">node -v</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2. 备份(别跳过这步)</span></span><br><span class="line"><span class="comment"># 打压缩包 + 算校验值</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 3. Git 基线</span></span><br><span class="line">git add -A &amp;&amp; git commit -m <span class="string">&quot;baseline: pre-Hexo-8&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 4. 切换分支, 开始升级</span></span><br><span class="line">git switch -c upgrade/hexo-8</span><br><span class="line"></span><br><span class="line"><span class="comment"># 5. 升级 Hexo</span></span><br><span class="line">npm install hexo@8.1.0 --save</span><br><span class="line"></span><br><span class="line"><span class="comment"># 6. 如果报 ERR_REQUIRE_ESM, 强制降级</span></span><br><span class="line">npm install strip-ansi@6.0.1 --save-exact</span><br><span class="line"></span><br><span class="line"><span class="comment"># 7. 验证</span></span><br><span class="line">hexo clean</span><br><span class="line">hexo s</span><br></pre></td></tr></table></figure><h1 id="关于主题的兼容问题"><a href="#关于主题的兼容问题" class="headerlink" title="关于主题的兼容问题"></a>关于主题的兼容问题</h1><p>这里要单独提一句: <strong>Hexo 升级和主题升级最好分开做, 不要一次全升</strong>。</p><p>原因是主题的模板文件可能依赖特定版本的 Hexo API。我见过 Hexo 一升级, 主题直接报模板渲染错误的。</p><p>建议的顺序是:</p><ol><li>先只升 Hexo, 主题不动, 看能不能正常构建</li><li>确认没问题了, 再单独升主题</li><li>每一步都做 Git 提交, 出问题好回退</li></ol><p>如果你和我一样用 Butterfly, 可以参考这篇 <a href="https://butterfly.js.org/posts/5-6-release-notes">[从 3.7.1 升级到 5.7.0 的真实障碍]</a>, 官方更新日志写得很细。</p><h1 id="几个已经废弃的配置项"><a href="#几个已经废弃的配置项" class="headerlink" title="几个已经废弃的配置项"></a>几个已经废弃的配置项</h1><p>顺手记一下, 升级时如果看到相关报错, 可能是这几个旧配置引起的:</p><table><thead><tr><th align="left">配置项</th><th align="left">状态</th></tr></thead><tbody><tr><td align="left"><code>auto_spacing</code></td><td align="left">已移除</td></tr><tr><td align="left"><code>multi_thread</code></td><td align="left">已移除</td></tr><tr><td align="left"><code>max_open_file</code></td><td align="left">已移除</td></tr><tr><td align="left"><code>external_link: true</code></td><td align="left">改为对象形式配置</td></tr><tr><td align="left"><code>use_date_for_updated</code></td><td align="left">已移除</td></tr></tbody></table><h1 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h1><p>这次升级的核心体会就三条:</p><ol><li><strong>先看版本, 再动手</strong>。Hexo 8 的门槛是 Node 20.19.0+, 版本不够就先升 Node。</li><li><strong>报错先看依赖树, 不要瞎改</strong>。<code>ERR_REQUIRE_ESM</code> 绝大多数是 <code>strip-ansi</code> 的锅, 降级即可。</li><li>**锁定主版本, 别用 <code>npm update</code>**。可复现的环境能省掉大量重复排查。</li></ol><p>升级本身花不了多少时间, 真正花时间的是”出问题之后不知道从哪查”。所以第 2、3 条更重要。</p><p>参考链接:</p><ul><li><a href="https://hexo.io/news/2025/09/16/hexo-8-0-0-released">hexo 8.0.0 Released</a></li><li><a href="https://github.com/hexojs/hexo/releases">Hexo Releases - GitHub</a></li><li><a href="https://butterfly.js.org/">Butterfly 更新日志</a></li></ul>]]>
      </content:encoded>
    </item>
    <item>
      <title>SVN转Git操作, 并附带当前SVN版本的操作记录</title>
      <link>https://zsyyblog.com/9c61c750.html</link>
      <description>
        <![CDATA[为了适应当下的协作开发的节奏, svn的时代渐行渐远, git的操作无疑让开发进度更加快速。 于是乎 svn->git是必然的的, 而且还要带上提交记录，进行无缝切换。]]>
      </description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/">后端</category>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/SVN/">SVN</category>
      <category domain="https://zsyyblog.com/tags/SVN/">SVN</category>
      <pubDate>Fri, 16 Aug 2024 14:52:33 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>为了适应当下的协作开发的节奏, svn的时代渐行渐远, git的操作无疑让开发进度更加快速。 于是乎 svn-&gt;git是必然的的, 而且还要带上提交记录, 进行无缝切换。</p><h1 id="基本技能"><a href="#基本技能" class="headerlink" title="基本技能"></a>基本技能</h1><p>阅读以下操作, 默认你已经具备一下技能和环境:</p><ol><li>Gitee&#x2F;Github&#x2F;Gitlab线上仓库(本文已gitee为例)</li><li>HomeBrew(这个可以使用别的, 主要是安装3-5)</li><li>git</li><li>svn</li><li>git-svn(这个是重点)</li></ol><p>可选:</p><blockquote><p>ssh公钥</p></blockquote><p>如果在安装环境和软件过程中, 出现问题, 请自行百度, 这里不做赘述。</p><h1 id="步骤概览"><a href="#步骤概览" class="headerlink" title="步骤概览"></a>步骤概览</h1><ol><li>获取svn所有提交记录的操作者名单, 替换成分配的账号</li><li>git-svn处理, 拉取svn项目,并创建git</li><li>设置git仓库地址</li><li>推送上去</li></ol><h1 id="前提说明"><a href="#前提说明" class="headerlink" title="前提说明"></a>前提说明</h1><p>本文意在指导操作步骤, 至于操作细节, 请根据项目实际操作。<br>我将进行操作, 将以前的多人项目svn移步到我的个人git上。</p><h1 id="详细步骤"><a href="#详细步骤" class="headerlink" title="详细步骤"></a>详细步骤</h1><h2 id="服务器获取svn提交者记录"><a href="#服务器获取svn提交者记录" class="headerlink" title="服务器获取svn提交者记录"></a>服务器获取svn提交者记录</h2><p>找你们的服务器给你一个, 自行需求到满足格式的形式</p><h2 id="本地获取svn提交者记录"><a href="#本地获取svn提交者记录" class="headerlink" title="本地获取svn提交者记录"></a>本地获取svn提交者记录</h2><p>如果你通过5.1步骤拿到了, 请忽略</p><ul><li><p>本地新建文件夹, 作为处理仓库</p></li><li><p>新建<code>userinfo.txt</code></p></li><li><p>cd到当前文件夹</p></li><li><p><code>git clone svn项目地址</code> 到当前文件夹下</p></li><li><p>执行命令:<br>这里注意,前言中说明了, 这里主要是转移到我的个人仓库, 所以, <code>xxx</code>的位置需要填写我的仓库邮箱地址</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">svn <span class="built_in">log</span> -r 1:HEAD --xml | grep <span class="string">&quot;&#x27; &#x27;&#123;print <span class="variable">$2</span>&#125;&#x27; | awk -F &#x27;&#x27; &#x27;&#123;print <span class="variable">$1</span>&quot;</span>=<span class="string">&quot;<span class="variable">$1</span>&quot;</span>&lt;xxx&gt;<span class="string">&quot; &#125;&#x27;  &gt; userinfo.txt</span></span><br></pre></td></tr></table></figure></li><li><p>通过上述代码得到所有提交者记录, 并进行了格式化, 可以根据实际情况做映射改动</p></li></ul><h2 id="git-svn转换"><a href="#git-svn转换" class="headerlink" title="git-svn转换"></a>git-svn转换</h2><ul><li>新建一个全新的文件夹, 作为转换的仓库</li><li>将5.1或者5.2获取到的<code>userinfo.txt</code>复制到当前文件夹下</li><li><code>cd</code>到当前文件夹</li><li>执行命令<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">// xxx为你的svn地址, 请自行填写</span><br><span class="line">git svn <span class="built_in">clone</span> xxx --no-metadata --authors-file=userinfo.txt</span><br></pre></td></tr></table></figure>这期间如果出现什么问题, 请自行百度, 大概率是git-svn的安装问题</li></ul><h2 id="创建网络仓库"><a href="#创建网络仓库" class="headerlink" title="创建网络仓库"></a>创建网络仓库</h2><p>这里采用的是gitee</p><ul><li>新建一个空仓库, 不需要选择其他的东西, <code>null null null</code></li><li>接下来你会看到仓库的提示, 这里就选择普通的https的模式</li></ul><h2 id="配置本地信息"><a href="#配置本地信息" class="headerlink" title="配置本地信息"></a>配置本地信息</h2><p>我们继续回到步骤5.2中新文件夹<br>此时该文件夹下, 一个<code>项目文件</code>, 一个<code>userinfo.txt</code></p><p>依次执行一下命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">(1) 创建readme文件</span><br><span class="line"><span class="built_in">touch</span> README.md</span><br><span class="line"></span><br><span class="line">(2) git添加提交文件</span><br><span class="line">git add README.md</span><br><span class="line"></span><br><span class="line">(3) 这里xxx是步骤3中的https的地址</span><br><span class="line">git remote add origin xxx</span><br><span class="line"></span><br><span class="line">(4) 推送上去</span><br><span class="line">git push -u origin <span class="string">&quot;master&quot;</span></span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
    <item>
      <title>代码从SVN迁移到Git代码库并保留提交记录</title>
      <link>https://zsyyblog.com/9a7984c4.html</link>
      <description>代码从SVN迁移到Git代码库并保留提交记录</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/">后端</category>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/SVN/">SVN</category>
      <category domain="https://zsyyblog.com/tags/SVN/">SVN</category>
      <pubDate>Thu, 15 Aug 2024 14:03:57 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><ol><li><p>新建文件夹SVN_TO_GIT（随意命名），进入文件夹</p></li><li><p>执行命令</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> SVN_TO_GIT</span><br><span class="line"></span><br><span class="line">git svn <span class="built_in">clone</span> --authors-file=authors.txt SVN代码库地址</span><br></pre></td></tr></table></figure><p>authors.txt文件保存svn用户与git用户的对应关系，如下所示:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">svn用户=git用户名(邮箱地址)</span><br></pre></td></tr></table></figure></li><li><p>（可选步骤）如果SVN项目有忽略文件，进入项目(project)文件夹，执行如下命令转换SVN:ignore属性为 <code>.gitignore</code>文件。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> ./project</span><br><span class="line"></span><br><span class="line">git svn show-ignore &gt; .gitignore</span><br><span class="line">git add .gitignore</span><br><span class="line">git commit -m <span class="string">&#x27;Convert svn:ignore properties to .gitignore.&#x27;</span></span><br></pre></td></tr></table></figure></li><li><p>（可选步骤）如果<code>clone</code>完成后，SVN仓库还有更新，可执行以下命令同步SVN更新</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git svn rebase</span><br></pre></td></tr></table></figure></li><li><p>添加git远程仓库地址，前提必须是空库</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git remote add origin Git代码库地址</span><br></pre></td></tr></table></figure></li><li><p>推送代码</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git push -u origin master</span><br></pre></td></tr></table></figure></li></ol>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用svnadmin减小svn版本库的大小</title>
      <link>https://zsyyblog.com/23e2b468.html</link>
      <description>使用svnadmin减小svn版本库的大小</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/">后端</category>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/SVN/">SVN</category>
      <category domain="https://zsyyblog.com/tags/SVN/">SVN</category>
      <pubDate>Wed, 14 Aug 2024 13:12:05 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h1><p>打开命令行提示符，输入以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">svnlook youngest d:/SVNRepository</span><br></pre></td></tr></table></figure><p>查看当前最新的版本号，显示最新版本记录为755。</p><h1 id="备份版本库"><a href="#备份版本库" class="headerlink" title="备份版本库"></a>备份版本库</h1><blockquote><p>很重要，在尝试过程中出现过失败，幸亏有备份，不然就over了</p></blockquote><p>把D盘的版本库，备份到C盘，同时清除历史日志，输入命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">svnadmin hotcopy --clean-logs d:/SVNRepository c:/SVNRepository</span><br></pre></td></tr></table></figure><p>这样备份后版本库从3.34G变为3.24G。</p><h1 id="dump需要保留的版本"><a href="#dump需要保留的版本" class="headerlink" title="dump需要保留的版本"></a>dump需要保留的版本</h1><p>输入以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">svnadmin dump c:/SVNRepository -r 745:755 &gt; d:/repo_dump_745_755.dmp</span><br></pre></td></tr></table></figure><p>3.24G的版本库dump出来后变成760M，苗条不少。</p><h1 id="删除旧版本库"><a href="#删除旧版本库" class="headerlink" title="删除旧版本库"></a>删除旧版本库</h1><p>输入以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">rmdir</span> /s/q d:/SVNRepository</span><br></pre></td></tr></table></figure><p>删除旧版本库，也可以直接在资源管理器里删除。</p><h1 id="创建空的版本库"><a href="#创建空的版本库" class="headerlink" title="创建空的版本库"></a>创建空的版本库</h1><p>输入以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">svnadmin create d:/SVNRepository</span><br></pre></td></tr></table></figure><p>检查空的版本库大概31.2K大小。</p><h1 id="把dump文件导入版本库"><a href="#把dump文件导入版本库" class="headerlink" title="把dump文件导入版本库"></a>把dump文件导入版本库</h1><p>输入以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">svnadmin load d:/SVNRepository &lt; d:/repo_dump_745_755.dmp</span><br></pre></td></tr></table></figure><p>这时屏幕上会显示正在载入版本库中的文件或正在提交&#x2F;装载的版本。</p><blockquote><p>完成后，用命令svnlook youngest d:&#x2F;SVNRepository 查看，显示当前版本库最新版本号是11，整个版本库大小501M。</p></blockquote><p>参考链接：<a href="https://blog.csdn.net/cfl2011/article/details/78772564">https://blog.csdn.net/cfl2011/article/details/78772564</a></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>svn 查看历史记录失败 ，并提示'Item is not readable'</title>
      <link>https://zsyyblog.com/4302c8ab.html</link>
      <description>SVN 查看版本历史时Item is not readable（show log），可以通过修改配置来解决。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/">后端</category>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/SVN/">SVN</category>
      <category domain="https://zsyyblog.com/tags/SVN/">SVN</category>
      <pubDate>Tue, 13 Aug 2024 11:45:49 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="报错信息"><a href="#报错信息" class="headerlink" title="报错信息"></a>报错信息</h1><p>svn 查看历史记录失败 ，并提示’Item is not readable’，如下图所示：</p><p><img src="https://img.zsyyblog.com/blogs/20241230194727338.png"></p><h1 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h1><ol><li><p>修改 <code>svnserve.conf</code>配置文件</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">anon-access = none</span><br></pre></td></tr></table></figure><p>设置<code>anon-access = none</code>就可以了</p></li><li><p>或者修改<code>authz</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">* = r</span><br></pre></td></tr></table></figure></li></ol>]]>
      </content:encoded>
    </item>
    <item>
      <title>VS2019/2022中解决宏定义出现宏可以转换为constexpr问题</title>
      <link>https://zsyyblog.com/d95fe258.html</link>
      <description>宏名开头显示出 “...”，并显示出“宏可以转换为constexpr”，按照下面的方法即可解决</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/IDE/">IDE</category>
      <category domain="https://zsyyblog.com/categories/IDE/Visual-Studio/">Visual Studio</category>
      <category domain="https://zsyyblog.com/tags/Visual-Studio/">Visual Studio</category>
      <pubDate>Tue, 13 Aug 2024 06:01:43 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>最近在VS中使用到了宏定义，结果宏名开头显示出 “…”，并显示出“宏可以转换为constexpr”，如果想屏蔽掉这个提示按照下面的方法操作即可解决</p><h1 id="解决办法"><a href="#解决办法" class="headerlink" title="解决办法"></a>解决办法</h1><ol><li>在“工具-&gt;选项”菜单中，导航到“文本编辑器-&gt;C&#x2F;C++-&gt;查看”</li><li>在右侧找到“代码波形曲线”其中的“可转换为constexpr的宏”中选择“无”。</li></ol><p><img src="https://img.zsyyblog.com/blogs/20241229111007206.png"></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>如何在Visual Studio 2019/2022 C++中隐藏宏名称下划线的三个点</title>
      <link>https://zsyyblog.com/1fa51d02.html</link>
      <description>每次在我的项目中使用任何宏名称时，都会出现3个点加下划线的提示，如何在Visual Studio C++中隐藏宏名称下划线的三个点呢？</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/IDE/">IDE</category>
      <category domain="https://zsyyblog.com/categories/IDE/Visual-Studio/">Visual Studio</category>
      <category domain="https://zsyyblog.com/tags/Visual-Studio/">Visual Studio</category>
      <pubDate>Tue, 13 Aug 2024 03:03:03 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h1><p>每次在我的项目中使用任何宏名称时，都会出现3个点加下划线的提示，如下图所示：</p><p><img src="https://img.zsyyblog.com/blogs/20241229110420190.png"><br>​<br>当我将鼠标悬停在它上面时，会出现一个灯泡和一个小消息框，提示在这个标识符周围浏览可能是不正确的，并建议将此宏添加到一个提示文件中，该文件似乎没有任何作用：</p><p>​<img src="https://img.zsyyblog.com/blogs/20241229110441803.png"></p><h1 id="解决办法"><a href="#解决办法" class="headerlink" title="解决办法"></a>解决办法</h1><ol><li>在“工具-&gt;选项”菜单中，导航到“文本编辑器-&gt;C&#x2F;C++-&gt;查看”</li><li>在右侧找到“代码波形曲线”其中的“已跳过的浏览区域中的宏”中选择“无”。</li></ol><p><img src="https://img.zsyyblog.com/blogs/20241229111007206.png"></p>]]>
      </content:encoded>
    </item>
    <item>
      <title>跨域请求：解决Origin null is not allowed by Access-Control-Allow-Origin错误</title>
      <link>https://zsyyblog.com/d8e587e1.html</link>
      <description>在使用JavaScript进行AJAX请求时，你可能会遇到“`Origin null is not allowed by Access-Control-Allow-Origin`”错误。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/">前端</category>
      <category domain="https://zsyyblog.com/categories/%E5%89%8D%E7%AB%AF/JavaScript/">JavaScript</category>
      <category domain="https://zsyyblog.com/tags/JavaScript/">JavaScript</category>
      <pubDate>Tue, 13 Aug 2024 01:58:36 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="问题概述"><a href="#问题概述" class="headerlink" title="问题概述"></a>问题概述</h1><p>在使用JavaScript进行AJAX请求时，你可能会遇到“<code>Origin null is not allowed by Access-Control-Allow-Origin</code>”错误。</p><p>当浏览器从文件系统URL发送请求时，它会将来源（origin）设置为“null”。</p><p>然而，许多网站限制了哪些来源可以访问它们的资源，从而导致此错误发生。</p><h1 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h1><p>为了解决这个问题，我们需要修改请求方式，以避免将来源设置为“null”。</p><h1 id="使用JSONP"><a href="#使用JSONP" class="headerlink" title="使用JSONP"></a>使用JSONP</h1><p>将你的请求更改为使用<code>JSONP（JSON with Padding）</code>格式。JSONP允许你使用 <code>&lt;script&gt;</code> 标签来请求数据，这会绕过同源策略限制。</p><p>修改后的代码如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">$.<span class="title function_">ajax</span>(&#123;</span><br><span class="line">  <span class="attr">url</span>: <span class="string">&#x27;http://www.panoramio.com/wapi/data/get_photos&#x27;</span>,</span><br><span class="line">  <span class="attr">data</span>: &#123;</span><br><span class="line">    <span class="attr">v</span>: <span class="number">1</span>,</span><br><span class="line">    <span class="attr">key</span>: <span class="string">&#x27;dummykey&#x27;</span>,</span><br><span class="line">    <span class="attr">tag</span>: <span class="string">&#x27;test&#x27;</span>,</span><br><span class="line">    <span class="attr">offset</span>: <span class="number">0</span>,</span><br><span class="line">    <span class="attr">length</span>: <span class="number">20</span>,</span><br><span class="line">    <span class="attr">minx</span>: -<span class="number">30</span>,</span><br><span class="line">    <span class="attr">miny</span>: <span class="number">0</span>,</span><br><span class="line">    <span class="attr">maxx</span>: <span class="number">0</span>,</span><br><span class="line">    <span class="attr">maxy</span>: <span class="number">150</span>,</span><br><span class="line">    <span class="attr">callback</span>: <span class="string">&#x27;processImages&#x27;</span></span><br><span class="line">  &#125;,</span><br><span class="line">  <span class="attr">dataType</span>: <span class="string">&#x27;jsonp&#x27;</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h1 id="其他注意事项"><a href="#其他注意事项" class="headerlink" title="其他注意事项"></a>其他注意事项</h1><ul><li>确保服务器允许JSONP请求。在服务器端设置CORS（跨域资源共享）标头，以允许来自你的网站的请求。</li><li>在服务器响应中，使用适当的回调函数名来处理数据。在此示例中，回调函数名为“processImages”。</li><li>如果服务器不支持JSONP，则无法解决此问题。</li></ul><h1 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h1><p>通过使用JSONP并修改请求方式，你可以解决“<code>Origin null is not allowed by Access-Control-Allow-Origin</code>”错误，并从文件系统URL向外部服务器发送AJAX请求。</p><p>常见问题解答</p><ol><li><p>为什么浏览器会将来源设置为“null”？<br>当浏览器从文件系统URL发送请求时，它会将来源设置为“null”，因为文件系统URL被认为是不安全的来源。</p></li><li><p>如何设置CORS标头？<br>在服务器端，你可以使用Access-Control-Allow-Origin标头来允许来自特定来源的请求。例如：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Access</span>-<span class="title class_">Control</span>-<span class="title class_">Allow</span>-<span class="title class_">Origin</span>: <span class="attr">https</span>:<span class="comment">//www.example.com</span></span><br></pre></td></tr></table></figure></li><li><p>JSONP是否安全？<br>与其他跨域请求方法相比，JSONP是一种相对安全的跨域请求方法。但是，它仍然存在一些潜在的安全性问题，例如：</p><ul><li>JSON劫持： 攻击者可以在服务器响应中修改回调函数的代码，从而执行恶意代码。</li><li>请求伪造： 攻击者可以创建假的JSONP请求，诱使受害者发送敏感数据到恶意服务器。</li></ul></li><li><p>除了JSONP，还有哪些其他跨域请求方法？<br>除了JSONP之外，还有其他跨域请求方法，包括：</p><ul><li>CORS： 这是当今使用最广泛的跨域请求方法。它使用服务器端标头来控制对资源的访问。</li><li>postMessage： 这种方法使用postMessage()函数在不同的窗口或iframe之间发送消息。</li></ul></li><li><p>我该如何选择最佳的跨域请求方法？<br>最佳的跨域请求方法取决于你的具体情况。一般来说，CORS是首选的方法，因为它更安全、更灵活。但是，如果服务器不支持CORS，或者你需要从不支持CORS的网站请求数据，则可以使用JSONP。</p></li></ol>]]>
      </content:encoded>
    </item>
    <item>
      <title>设备唯一标识方法（Unique Identifier）：如何在 Windows 系统上获取设备的唯一标识</title>
      <link>https://zsyyblog.com/65f458c0.html</link>
      <description>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-se]]>
      </description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/">编程语言</category>
      <category domain="https://zsyyblog.com/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/C/">C++</category>
      <category domain="https://zsyyblog.com/tags/C/">C++</category>
      <pubDate>Mon, 12 Aug 2024 14:47:03 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><p>唯一的标识一个设备是一个基本功能，可以拥有很多应用场景，比如软件授权（如何保证你的软件在授权后才能在特定机器上使用）、软件 License，设备标识，设备身份识别等。</p><p>下面列举一下各种方法的优劣：</p><h1 id="（1）网卡-MAC-地址"><a href="#（1）网卡-MAC-地址" class="headerlink" title="（1）网卡 MAC 地址"></a>（1）网卡 MAC 地址</h1><p>MAC 地址可能是最常用的标识方法，但是现在这种方法基本不可靠：一个电脑可能存在多个网卡，多个 MAC 地址，如典型的笔记本可能存在有线、无线、蓝牙等多个 MAC 地址，随着不同连接方式的改变，每次 MAC 地址也会改变。而且，当安装有虚拟机时，MAC 地址会更多。MAC 地址另外一个更加致命的弱点是，MAC 地址很容易手动更改。因此，MAC 地址基本不推荐用作设备唯一 ID。</p><h1 id="（2）CPU-ID"><a href="#（2）CPU-ID" class="headerlink" title="（2）CPU ID"></a>（2）CPU ID</h1><p>在 Windows 系统中通过命令行运行 “<code>wmic cpu get processorid</code>” 就可以查看 CPU ID。</p><p>目前 CPU ID 也无法唯一标识设备，Intel 现在可能同一批次的 CPU ID 都一样，不再提供唯一的 ID。而且经过实际测试，新购买的同一批次 PC 的 CPU ID 很可能一样。这样作为设备的唯一标识就会存在问题。</p><h1 id="（3）硬盘序列号"><a href="#（3）硬盘序列号" class="headerlink" title="（3）硬盘序列号"></a>（3）硬盘序列号</h1><p>在 Windows 系统中通过命令行运行 “<code>wmic diskdrive get serialnumber</code>” 可以查看。</p><p>硬盘序列号作为设备唯一 ID 存在的问题是，很多机器可能存在多块硬盘，特别是服务器，而且机器更换硬盘是很可能发生的事情，更换硬盘后设备 ID 也必须随之改变，不然也会影响授权等应用。因此，很多授权软件没有考虑使用硬盘序列号。而且，不一定所有的电脑都能获取到硬盘序列号。</p><h1 id="（4）自定义算法生成唯一-ID"><a href="#（4）自定义算法生成唯一-ID" class="headerlink" title="（4）自定义算法生成唯一 ID"></a>（4）自定义算法生成唯一 ID</h1><p>可以使用自制的一个特定算法（如 GUID、或者一定位数的随机数）生成唯一的 ID，然后写入到注册表或者设备上，作为其唯一 ID。</p><p>这种方法不依赖任何硬件特征，唯一性也可以自己完全控制，不过纯软件的实现缺点是这个 ID 很容易伪造，也很容易擦除；而且很可能还需要在线验证，后台存储所有 ID 的服务器必须保持在线。</p><h1 id="（5）Windows-的产品-ID（ProductId）"><a href="#（5）Windows-的产品-ID（ProductId）" class="headerlink" title="（5）Windows 的产品 ID（ProductId）"></a>（5）Windows 的产品 ID（ProductId）</h1><p>在 “控制面板 \ 系统和安全 \ 系统” 的最下面就可以看到激活的 Windows 产品 ID 信息，另外通过注册表 “<code>HKEY_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion</code>” 也可以看到看到 “ProductId” 字段。</p><p>不过这个产品 ID 并不唯一，不同系统或者机器重复的概率也比较大。虚拟机中克隆的系统，使用同一个镜像安装激活的系统，其产品 ID 就可能一模一样。经过实测，笔者在两台 Thinkpad 笔记本上发现其 ProductId 完全一样。</p><h1 id="（6）MachineGUID"><a href="#（6）MachineGUID" class="headerlink" title="（6）MachineGUID"></a>（6）MachineGUID</h1><p>Windows 安装时会唯一生成一个 GUID，可以在注册表 “<code>HKEY_MACHINE\SOFTWARE\Microsoft\Cryptography</code>” 中查看其 “MachineGuid” 字段。</p><p>这个 ID 作为 Windows 系统设备的唯一标识不错，不过值得注意的一点是，与硬件 ID 不一样，这个 ID 在重装 Windows 系统后应该不一样了。这样授权软件在重装系统后，可能就需要用户重新购买授权。</p><h1 id="（7）主板-smBIOS-UUID"><a href="#（7）主板-smBIOS-UUID" class="headerlink" title="（7）主板 smBIOS UUID"></a>（7）主板 smBIOS UUID</h1><p>在 Windows 系统中通过命令行运行 “wmic csproduct get UUID” 可以查看。</p><p>主板 UUID 是很多授权方法和微软官方都比较推崇的方法，即便重装系统 UUID 应该也不会变（笔者没有实测重装，不过在一台机器上安装双系统，获取的主板 UUID 是一样的，双系统一个 windows 一个 Linux，Linux 下用 “<code>dmidecode -s system-uuid</code>” 命令可以获取 UUID）。</p><p>但是这个方法也有缺陷，因为不是所有的厂商都提供一个 UUID，当这种情况发生时，wmic 会返回 “FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF”，即一个无效的 UUID。</p><h1 id="（8）外置密码设备提供唯一-ID"><a href="#（8）外置密码设备提供唯一-ID" class="headerlink" title="（8）外置密码设备提供唯一 ID"></a>（8）外置密码设备提供唯一 ID</h1><p>这种方法很多，比如 U 盾里面可以提供唯一的密钥标识，可信计算密码芯片里面的背书密钥 EK 等都是唯一固定在安全硬件里面的，而且通过良好的密码算法生成，唯一性和差异性都可以保证，安全性也更高。</p><p>这种方法需要在计算设备连接外置密码芯片，增加经济负担和开发成本。而且，即便这种方法也存在欺骗攻击和代理攻击等破解方法。</p><blockquote><p>当然还有很多其它方法，如可以获取声卡、CPU 模式和频率、IDE 控制器、内存等其他信息。甚至，可以收集设备的软硬件配置，通过统计方法和机器学习方法进行分类识别设备。学术上，还有各种密码算法，硬件不可克隆函数 PUF 等唯一标识的方法可以使用。</p></blockquote><p>从软件授权这个简单的应用来看，购买外置密码设备硬件太过昂贵，可以采用简单的组合方法，推荐使用主板 UUID 作为主标识，当 UUID 返回无效的值时，可以进一步采用 CPU ID、BIOS 序列号、MachineGUID 等方式作为次标识，这基本可以解决问题。</p><p>其实设备唯一标识其实也是指纹的一种，想要使用标识或者指纹时，首先必须明确自己的真实意图，是要标识一个用户（这样可以使用身份证、指纹、手机验证等方式），还是要标识一个设备（本文列举的各种设备 ID）。根据自己的真实意图才能进一步思考具体使用的方式，不忘初衷。</p><p>不过，不管使用怎样的硬件信息或者牛气的算法来进行用户或者设备的标识，还是一句老话 “道高一尺，魔高一丈”，都是可以被攻破的，即便你的标识伪造不了、克隆不了，攻击者也可以使用其它攻击方式，如逆向你的验证 check 代码，然后将其修改掉，使其 check 失灵。因此，无论设备标识或者用户标识，很多情况下可能只防君子、不防小人，甚至悲观者认为这些手段都是防止合法用户的，影响用户使用的方便性，大可以取消掉。笔者认为，没有必要这么悲观，知识产权等信息是尊敬人的价值和劳动的表现，即便不能完全防止小人，我们也要通过这些方法将一般的小人排除在技术门槛之外，并尽量增加高级小人破解时的代价。</p><hr>]]>
      </content:encoded>
    </item>
    <item>
      <title>svn cleanup命令介绍</title>
      <link>https://zsyyblog.com/dd78e826.html</link>
      <description>SVN cleanup命令是用于清理工作副本中的临时文件和未完成的操作，以修复工作副本中的错误或损坏的命令。</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/">后端</category>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/SVN/">SVN</category>
      <category domain="https://zsyyblog.com/tags/SVN/">SVN</category>
      <pubDate>Mon, 12 Aug 2024 06:10:55 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>SVN cleanup命令是用于清理工作副本中的临时文件和未完成的操作，以修复工作副本中的错误或损坏的命令。</p><h1 id="命令参数"><a href="#命令参数" class="headerlink" title="命令参数"></a>命令参数</h1><p>以下是SVN cleanup命令中常用的参数：</p><ol><li><p>–vacuum-pristines     选项：将所有临时文件清理掉、删除未使用的pristine副本。</p></li><li><p>–include-externals    选项：包含外部文件的目录和子目录中的所有工作副本。</p></li><li><p>–remove-unversioned   选项：删除所有未添加到版本控制的文件和目录。</p></li><li><p>–remove-ignored       选项：删除所有被忽略的文件和目录。</p></li><li><p>–fix-timestamps       选项：修复工作副本中的时间戳问题。</p></li><li><p>–cleanup-locks        选项：清除被锁定的工作副本。</p></li><li><p>–vacuum-free-space    选项：释放工作副本中的闲置空间。</p></li></ol><p>以上是SVN cleanup命令的常用参数，可以根据具体情况选择相应的参数来清理工作副本并修复错误。</p><h1 id="实际应用"><a href="#实际应用" class="headerlink" title="实际应用"></a>实际应用</h1><p>svn下载代码后一段时间<code>.svn</code>文件会很大，包含了各种备份文件，但是如果直接删除<code>.svn</code>文件里的内容的话再执行<code>svn</code>命令又会报错，所以我们可以采用<code>TortoiseSVN</code>自带的<code>cleanup</code>为文件夹瘦身。</p><p>操作步骤：</p><ol><li>右键<code>svn</code>文件夹</li><li>选择<code>clean up... （清理...）</code></li><li>勾选<code>Clean up working copy status（清理工作副本的状态）</code>以及<code>Vacuum pristine copies（清理原始副本）</code>，会把本地缓存的一些历史文件清除，瘦身效果十分明显！</li><li>最后点击确定即可</li></ol><p>等一会儿就能完成<code>.svn</code>目录瘦身了。</p><blockquote><p>注意事项：<br>平时使用Cleanup(清理)不建议勾选<code>清理原始副本</code>，这样清理的速度会快一些。</p></blockquote>]]>
      </content:encoded>
    </item>
    <item>
      <title>使用SVN创建本地仓库</title>
      <link>https://zsyyblog.com/899eb103.html</link>
      <description>使用SVN创建本地仓库</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/">后端</category>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/SVN/">SVN</category>
      <category domain="https://zsyyblog.com/tags/SVN/">SVN</category>
      <pubDate>Sun, 11 Aug 2024 02:28:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><ol><li><p>安装SVN – 此处略过.</p></li><li><p>设置本地仓库</p></li></ol><p>　　(1) 创建文件夹<code>mkdir svn_repository</code></p><p>　　(2) 鼠标选中文件夹，右键选择<code>TortoiseSVN</code> -&gt; <code>craete repository here（在此创建版本库）</code>  。然后在弹出框点击OK。</p><ol start="3"><li>导入文件到本地仓库.</li></ol><p>　　(1) 将<code>D:\Study\project</code>文件夹下的内容上传到<code>svn_repository</code></p><p>　　(2) 选中<code>project</code>文件夹，鼠标右键选择<code>TortoiseSVN</code> -&gt;<code>import（导入）</code></p><p>　　(3) 在弹出框的<code>URL of repository</code>填写<code>file:///D:/svn_repository</code>（之前创建的仓库地址） 然后点击OK。</p><p>　　(4) 待文件上传结束后，点击OK即可。</p><ol start="4"><li>从本地仓库下载文件</li></ol><p>　　(1) 创建<code>dev_project_src</code>文件夹，然后进入此文件夹</p><p>　　(2) 进入此文件夹，鼠标右键<code>SVN Checkout（检出）</code></p><p>　　(3) 弹出框的<code>URL of repository</code>填写本地仓库地址  – <code>file:///D:/svn_repository</code> 然后点击OK。</p><blockquote><p><strong>备注：</strong><br>其他操作和平常使用SVN没有区别。</p></blockquote>]]>
      </content:encoded>
    </item>
    <item>
      <title>执行svn sync命令做svn同步时遇到Failed to get lock on destination repos, currently held by ...错误</title>
      <link>https://zsyyblog.com/8303ba54.html</link>
      <description>执行svn sync命令做svn同步时遇到Failed to get lock on destination repos, currently held by ...错误</description>
      <author>竹山一叶</author>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/">后端</category>
      <category domain="https://zsyyblog.com/categories/%E5%90%8E%E7%AB%AF/SVN/">SVN</category>
      <category domain="https://zsyyblog.com/tags/SVN/">SVN</category>
      <pubDate>Sat, 10 Aug 2024 14:22:46 GMT</pubDate>
      <content:encoded>
        <![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/css/APlayer.min.css"><script src="/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><script class="meting-secondary-script-marker" src="\js\Meting.min.js"></script><h1 id="错误信息"><a href="#错误信息" class="headerlink" title="错误信息"></a>错误信息</h1><p>执行<code>svn sync</code>命令做svn同步时遇到<code>Failed to get lock on destination repos, currently held by ...</code>错误信息如下所示：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">C:\Users\ZSYY\Repositories&gt;svnsync init file:///C:\Users/ZSYY/Repositories/Study svn://1.14.104.184/ --allow-non-empty</span><br><span class="line">Failed to get lock on destination repos, currently held by <span class="string">&#x27;ZSYY-PC:2784d6ff-ab53-9041-9267-76f7603f08d6&#x27;</span></span><br><span class="line">Failed to get lock on destination repos, currently held by <span class="string">&#x27;ZSYY-PC:2784d6ff-ab53-9041-9267-76f7603f08d6&#x27;</span></span><br><span class="line">Failed to get lock on destination repos, currently held by <span class="string">&#x27;ZSYY-PC:2784d6ff-ab53-9041-9267-76f7603f08d6&#x27;</span></span><br><span class="line">Failed to get lock on destination repos, currently held by <span class="string">&#x27;ZSYY-PC:2784d6ff-ab53-9041-9267-76f7603f08d6&#x27;</span></span><br><span class="line">Failed to get lock on destination repos, currently held by <span class="string">&#x27;ZSYY-PC:2784d6ff-ab53-9041-9267-76f7603f08d6&#x27;</span></span><br><span class="line">Failed to get lock on destination repos, currently held by <span class="string">&#x27;ZSYY-PC:2784d6ff-ab53-9041-9267-76f7603f08d6&#x27;</span></span><br><span class="line">Failed to get lock on destination repos, currently held by <span class="string">&#x27;ZSYY-PC:2784d6ff-ab53-9041-9267-76f7603f08d6&#x27;</span></span><br><span class="line">Failed to get lock on destination repos, currently held by <span class="string">&#x27;ZSYY-PC:2784d6ff-ab53-9041-9267-76f7603f08d6&#x27;</span></span><br><span class="line">Failed to get lock on destination repos, currently held by <span class="string">&#x27;ZSYY-PC:2784d6ff-ab53-9041-9267-76f7603f08d6&#x27;</span></span><br><span class="line">Failed to get lock on destination repos, currently held by <span class="string">&#x27;ZSYY-PC:2784d6ff-ab53-9041-9267-76f7603f08d6&#x27;</span></span><br><span class="line">svnsync: E000022: Couldn<span class="string">&#x27;t get lock on destination repos after 10 attempts</span></span><br></pre></td></tr></table></figure><h1 id="出现问题的原因"><a href="#出现问题的原因" class="headerlink" title="出现问题的原因"></a>出现问题的原因</h1><p>发生以上情况的有以下几种情况：</p><ol><li>有正在同步的进程未完成，lock未释放；</li><li>上一次同步的进程异常导致中断，lock未释放。</li><li>有svn冲突</li></ol><p>SVN同步时，源库端会在目标库端加lock，成功同步后释放lock，但当SVN同步异常中断后，lock是不会自动释放的。这时在执行<code>svn sync</code>命令，就会出现如上问题。</p><h1 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h1><p>执行以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">svn propdel svn:sync-lock --revprop -r 0 http://192.168.xx.xx/仓库(svn仓库地址)</span><br></pre></td></tr></table></figure><p>将lock释放即可再次执行同步。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">svn propdel svn:sync-lock --revprop -r 0 file:///www/svndata/test</span><br></pre></td></tr></table></figure><p>我自己操作成功的命令如下所示：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">C:\Users\ZSYY\Repositories&gt;svn propdel svn:sync-lock --revprop -r 0 file:///C:\Users\ZSYY\Repositories\Study</span><br><span class="line">property <span class="string">&#x27;svn:sync-lock&#x27;</span> deleted from repository revision 0</span><br></pre></td></tr></table></figure>]]>
      </content:encoded>
    </item>
  </channel>
</rss>
