<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="/atom.xsl"?><feed xmlns="http://www.w3.org/2005/Atom"><title>博客魔改</title><subtitle>克喵的小站，记录着我的一些文字~</subtitle><link href="https://blog.518339.xyz/categories/%E5%8D%9A%E5%AE%A2%E9%AD%94%E6%94%B9/" rel="alternate"/><link href="https://blog.518339.xyz/categories/%E5%8D%9A%E5%AE%A2%E9%AD%94%E6%94%B9/atom.xml" rel="self" type="application/atom+xml"/><id>https://blog.518339.xyz/categories/%E5%8D%9A%E5%AE%A2%E9%AD%94%E6%94%B9/</id><updated>2026-09-29T15:56:00+08:00</updated><generator uri="https://gohugo.io/">Hugo 0.166.0</generator><author><name>克喵Moretti</name><email>mcy@kemiaosw.top</email></author><entry><title>用 Pages CMS 写博客：从连接仓库到 AI 摘要</title><link href="https://blog.518339.xyz/p/pages-cms-writing-guide.html" rel="alternate"/><link rel="enclosure" href="https://cdn.jsdmirror.com/gh/kmoretti/piclist-img@main/img/6abcb99f8d703.webp" type="image/webp"/><id>https://blog.518339.xyz/p/pages-cms-writing-guide.html</id><published>2026-09-29T15:56:00+08:00</published><updated>2026-09-29T15:56:00+08:00</updated><author><name>克喵Moretti</name><email>mcy@kemiaosw.top</email></author><summary type="text">最近把这个博客接到了 Pages CMS。
之前写文章，通常是打开本地编辑器，找到 content/posts，新建一个 Markdown 文件，再手动检查 front matter、图片路径和构建结果。这个流程没有什么问题，只是每次想在别的电脑上改两句话，都要先找到仓库，再把环境准备好。
Pages CMS 解决的不是“怎么写 Markdown”这个问题，而是把这件事搬到了一个可以直接登录使用的网页里。文章仍然保存到 …</summary><content type="html"><![CDATA[<p>最近把这个博客接到了 Pages CMS。</p>
<p>之前写文章，通常是打开本地编辑器，找到 <code>content/posts</code>，新建一个 Markdown 文件，再手动检查 front matter、图片路径和构建结果。这个流程没有什么问题，只是每次想在别的电脑上改两句话，都要先找到仓库，再把环境准备好。</p>
<p>Pages CMS 解决的不是“怎么写 Markdown”这个问题，而是把这件事搬到了一个可以直接登录使用的网页里。文章仍然保存到 GitHub，Hugo 仍然负责构建，Cloudflare Pages 仍然负责发布，只是中间多了一个更适合写作的入口。</p>
<p>这篇文章把整个流程记下来：怎么连接仓库，怎么写第一篇文章，什么时候要切到 Source mode，以及文章保存以后，AI 摘要是怎么自动生成的。</p>
<h2 id="先看一遍发布链路">先看一遍发布链路</h2>
<p>这个项目没有把 CMS 当成一个独立的数据库。Pages CMS 改的还是仓库里的 Markdown 文件，保存后直接提交到 <code>main</code> 分支。</p>
<div class="solitude-tag tag-mermaid mermaid">flowchart LR
    A[Pages CMS 编辑文章] --&gt; B[提交到 GitHub main]
    B --&gt; C[GitHub Actions]
    C --&gt; D[补齐 AI 摘要]
    D --&gt; E[Hugo 构建]
    E --&gt; F[Cloudflare Pages 发布]</div>

<p>所以，一次正常发布大致会经过这些步骤：</p>
<ol>
<li>在 Pages CMS 中编辑或新建文章。</li>
<li>CMS 把修改提交到 <code>main</code>。</li>
<li>GitHub Actions 运行 <code>Deploy to Cloudflare Pages</code>。</li>
<li>对没有摘要的文章生成 <code>ai_summary</code>。</li>
<li>使用 Hugo Extended 构建站点并检查产物。</li>
<li>将站点部署到 Cloudflare Pages。</li>
</ol>
<p>Pages CMS 不需要再点击一个单独的“部署”按钮。文章提交成功后，后面的事情交给 Actions 就可以了。</p>




  

<aside class="solitude-tag note info flat no-icon has-icon">
  <span class="note-icon" aria-hidden="true"><i class="fas fa-circle-info"></i></span>
  <div class="note-content">这篇文章以仓库 <code>kemiao-moretti/meowloge</code> 为例。Pages CMS 连接的是这个 GitHub 仓库，Hugo 文章目录是 <code>content/posts</code>，不是仓库外层同名的目录。</div>
</aside>

<h2 id="连接-pages-cms">连接 Pages CMS</h2>
<h3 id="第一步登录">第一步：登录</h3>
<p>打开 <a href="https://app.pagescms.org/">Pages CMS</a>，使用 GitHub 登录。第一次进入时，需要授权 Pages CMS 访问 GitHub 仓库，按页面提示完成即可。</p>
<p>登录之后，选择博客对应的仓库：</p>
<figure class="code-block" data-code-block data-language="text">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">text</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-text">kemiao-moretti/meowloge</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>进入仓库后，确认当前查看的是 <code>main</code> 分支。这个项目采用直接提交 <code>main</code> 的方式，没有另外维护用于写作的 <code>edit</code> 分支。</p>
<h3 id="第二步确认内容集合">第二步：确认内容集合</h3>
<p>仓库根目录的 <code>.pages.yml</code> 是 Pages CMS 的配置文件。当前项目只开放两个内容集合：</p>
<ul>
<li><strong>Posts / 文章</strong>：对应 <code>content/posts</code>；</li>
<li><strong>Changelog / 更新日志</strong>：对应 <code>content/changelog</code>。</li>
</ul>
<p>第一次连接后，先确认这两个集合能正常显示。如果看到了 <code>_index.md</code>，不要把它当成普通文章编辑。它是 Hugo section 的索引文件，负责文章列表页的标题和描述。</p>
<details class="solitude-tag tag-fold" ><summary class="tag-fold-summary"><span class="tag-fold-title">为什么不直接在 CMS 里开放整个仓库？</span><span class="tag-fold-icon" aria-hidden="true"></span></summary><div class="tag-fold-content">因为文章只是这个项目的一部分。主题、Hugo 配置、工作流和脚本都属于站点实现，误改它们可能会让整站构建失败。CMS 只开放 Posts 和 Changelog，既能完成日常写作，也能把配置文件留在代码管理流程里。</div></details>

<h3 id="第三步配置自动发布所需的变量">第三步：配置自动发布所需的变量</h3>
<p>如果只是编辑已经有 <code>ai_summary</code> 的文章，CMS 本身不需要 AI Key。但新文章发布时，Actions 可能需要调用摘要接口，所以 GitHub 仓库里要提前配置好变量。</p>
<p>进入 GitHub 仓库的：</p>
<figure class="code-block" data-code-block data-language="text">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">text</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-text">Settings → Secrets and variables → Actions</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>在 <strong>Secrets</strong> 中配置：</p>
<table>
	<thead>
			<tr>
					<th>Secret</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>AI_SUMMARY_API_KEY</code></td>
					<td>AI 摘要服务的 API Key</td>
			</tr>
			<tr>
					<td><code>CLOUDFLARE_API_TOKEN</code></td>
					<td>部署 Cloudflare Pages</td>
			</tr>
			<tr>
					<td><code>CLOUDFLARE_ACCOUNT_ID</code></td>
					<td>Cloudflare 账户 ID</td>
			</tr>
	</tbody>
</table>
<p><code>AI_SUMMARY_API</code> 也可以作为 Secret 配置，用来覆盖默认的 OpenAI 兼容接口地址。</p>
<p>在 <strong>Variables</strong> 中可以配置：</p>
<table>
	<thead>
			<tr>
					<th>Variable</th>
					<th>用途</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>AI_SUMMARY_MODEL</code></td>
					<td>摘要使用的模型</td>
			</tr>
			<tr>
					<td><code>AI_SUMMARY_MAX_INPUT</code></td>
					<td>发送给模型的最大正文长度</td>
			</tr>
			<tr>
					<td><code>AI_SUMMARY_PROMPT</code></td>
					<td>自定义摘要提示词</td>
			</tr>
	</tbody>
</table>
<p>密钥只放在 GitHub Secrets 或本地 <code>.env</code>，不要写进 <code>.pages.yml</code>、<code>hugo.yaml</code>、文章 front matter 或正文代码块里。</p>
<h2 id="在-pages-cms-里写第一篇文章">在 Pages CMS 里写第一篇文章</h2>
<p>进入 <strong>Posts / 文章</strong> 集合，点击新建文章。这里的字段基本对应文章 front matter。</p>
<h3 id="这些字段怎么填">这些字段怎么填</h3>
<ul>
<li><code>title</code>：文章标题。</li>
<li><code>slug</code>：文章地址使用的小写英文标识，只允许小写字母、数字和连字符，例如 <code>pages-cms-writing-guide</code>。</li>
<li><code>date</code>：发布日期和时间，站点时区是 <code>Asia/Shanghai</code>。</li>
<li><code>description</code>：文章列表、搜索和分享时使用的描述。</li>
<li><code>cover</code>：可以填写 <code>/img/...</code> 的本地图片，也可以填写外部 HTTPS 图片地址。</li>
<li><code>categories</code>、<code>tags</code>、<code>series</code>：按需要填写数组。</li>
<li><code>toc</code>：文章较长、需要目录时打开。</li>
<li><code>comment</code>：是否开启评论。</li>
<li><code>locate</code>：文章位置说明，不需要时可以留空。</li>
<li><code>recommend</code>：只有确实要推荐文章时才打开。</li>
</ul>
<p>新文章的文件名由 <code>slug</code> 生成：</p>
<figure class="code-block" data-code-block data-language="text">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">text</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-text">content/posts/&lt;slug&gt;.md</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>这个博客的文章链接由 Hugo 配置为 <code>/p/:slug</code>，所以 slug 不是一个随手填写的内部字段。文章发布以后，尽量不要再改它，否则公开 URL 也会跟着变化。</p>




  

<aside class="solitude-tag note warning simple no-icon has-icon">
  <span class="note-icon" aria-hidden="true"><i class="fas fa-triangle-exclamation"></i></span>
  <div class="note-content">已有文章不要修改 slug。Pages CMS 已经关闭了文章重命名操作，但填写新文章时仍然建议使用简短、稳定、全小写的英文 slug。</div>
</aside>

<h3 id="普通内容用-editor-mode">普通内容用 Editor mode</h3>
<p>标题、段落、列表、普通链接和普通图片，都可以直接使用 Pages CMS 的 Editor mode。这个模式适合日常写作，写完之后看起来和普通 Markdown 编辑器差不多。</p>
<p>例如下面这段内容不需要特殊处理：</p>
<figure class="code-block" data-code-block data-language="markdown">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">markdown</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-markdown">## 这是一节标题

这里是一段普通正文，里面可以放一个[站外链接](https://gohugo.io/)。

- 第一项
- 第二项</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>写作时可以先不用考虑 AI 摘要。正文保存后，后面的工作流会自动处理它。</p>
<h3 id="遇到-shortcode-就切换-source-mode">遇到 shortcode 就切换 Source mode</h3>
<p>这个主题提供了不少 Hugo shortcode，比如提示框、折叠面板、标签页、Mermaid 和卡片。Pages CMS 不负责解析它们，只是把文章里的 Markdown 保存回 GitHub。</p>
<p>因此，正文里出现下面这些内容时，建议切换到 <strong>Source mode</strong>：</p>
<ul>
<li><code>{{&lt; note &gt;}}</code>、<code>{{&lt; fold &gt;}}</code> 这类 Hugo shortcode；</li>
<li>Mermaid 流程图或 Chart.js 数据；</li>
<li>复杂 HTML；</li>
<li>shortcode 的示例代码；</li>
<li>需要保留精确缩进、空格和成对标签的内容。</li>
</ul>
<p>比如这段提示框就应该在 Source mode 中编辑：</p>
<figure class="code-block" data-code-block data-language="go-html-template">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">go-html-template</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-go-html-template">{{&lt; note type=&#34;success&#34; &gt;}}
文章已经保存，接下来等待 GitHub Actions 构建。
{{&lt; /note &gt;}}</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>这里代码块里的 <code>/*</code> 和 <code>*/</code> 是为了让示例原样显示，实际文章中使用 shortcode 时不需要加这层转义。</p>
<p>如果把包含大量 shortcode 的文章交给 Editor mode，编辑器可能会重新排列 Markdown，或者把 shortcode 当成普通文本处理。文章看起来可能没问题，但构建时才发现标签没有闭合。遇到这类文章，Source mode 更稳妥。</p>
<h3 id="图片怎么上传">图片怎么上传</h3>
<p>Pages CMS 的媒体配置指向 <code>static/img</code>，文章正文图片建议放在：</p>
<figure class="code-block" data-code-block data-language="text">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">text</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-text">static/img/posts/</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>上传后，在文章中使用公开路径：</p>
<figure class="code-block" data-code-block data-language="markdown">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">markdown</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-markdown">![文章示例](/img/posts/example.webp)</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>提交前检查三件事：</p>
<ol>
<li>GitHub 中的文件确实位于 <code>static/img/posts/</code>；</li>
<li>Markdown 里的文件名大小写和实际文件一致；</li>
<li>引用使用 <code>/img/posts/...</code>，而不是 <code>static/img/posts/...</code>。</li>
</ol>
<p>图片不会放进 <code>content/posts</code>。文章目录存 Markdown，静态资源目录存图片，这是 Hugo 处理静态文件的方式。</p>
<h2 id="保存之后会发生什么">保存之后会发生什么</h2>
<p>在 CMS 中保存文章以后，先确认页面提示已经提交成功，再打开 GitHub Actions 查看 <code>Deploy to Cloudflare Pages</code>。</p>
<p>Actions 会先运行摘要脚本。它会读取 <code>content/posts</code> 下的文章，清理 front matter、代码块、图片地址、HTML 标签和 Markdown 符号，然后把正文发送给配置好的 OpenAI 兼容接口。</p>
<p>生成结果会写回 front matter：</p>
<figure class="code-block" data-code-block data-language="yaml">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">yaml</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-yaml">ai_summary: &gt;-
  这里是根据文章内容生成的一段中文摘要。</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>随后 Hugo 使用带有摘要的工作树继续构建，Cloudflare Pages 部署的也是这次构建结果。</p>







<article
  class="solitude-tag card_box tag-card no-cover has-action"
  style="width:100%;"
>
  
  <div class="card_body">
    <div class="card_top">
      
      <span class="card_tag">已有说明</span>
    </div>
    <div class="card_mask card_info">
      <div class="card_content">
        <span class="card_title">AI 摘要只在构建期生成</span>
        <span class="card_description">浏览器只负责展示 front matter 中已经存在的摘要，不会在访客打开文章时请求 AI 接口。API Key 不会进入前端。</span>
      </div>
      <div class="card_footer">
        <div class="card_rating" aria-label="评分 5 / 5">
          <i class="fa-solid fa-star" aria-hidden="true"></i>
        </div>
        
          <a
            class="card_action"
            href="/p/ai-summary"
            aria-label="查看详情：AI 摘要只在构建期生成"
            
          ><i class="fa-solid fa-arrow-right" aria-hidden="true"></i></a>
        
      </div>
    </div>
  </div>
</article>

<h3 id="普通发布不会重复生成">普通发布不会重复生成</h3>
<p>摘要脚本默认只处理没有 <code>ai_summary</code> 的文章。这样修改一篇已有文章时，不会因为普通发布再次消耗模型额度，也不会让摘要在每次构建时发生变化。</p>
<p>如果确实需要重新生成已有文章的摘要，可以在 GitHub Actions 页面手动运行工作流，并把：</p>
<figure class="code-block" data-code-block data-language="text">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">text</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-text">force_summaries = true</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>设为 <code>true</code>。</p>
<p>有一种情况会始终跳过：文章明确写了：</p>
<figure class="code-block" data-code-block data-language="yaml">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">yaml</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-yaml">ai_summary: false</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>这表示作者不希望这篇文章生成摘要。普通发布和强制重新生成都不会覆盖这个停用标记。</p>
<h3 id="本地也可以先试跑">本地也可以先试跑</h3>
<p>如果想在提交前检查哪些文章会被处理，可以在博客仓库根目录执行：</p>
<figure class="code-block" data-code-block data-language="bash">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">bash</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-bash">node scripts/generate-ai-summary.mjs --dry-run</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>这条命令只列出候选文章，不调用 AI 接口，也不会修改文件。确认配置和文章都没有问题后，再运行完整命令：</p>
<figure class="code-block" data-code-block data-language="bash">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">bash</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-bash">node scripts/generate-ai-summary.mjs
hugo --gc --minify</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>本地摘要配置放在 <code>.env</code> 中：</p>
<figure class="code-block" data-code-block data-language="dotenv">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">dotenv</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-dotenv">AI_SUMMARY_API=https://api.siliconflow.cn/v1/chat/completions
AI_SUMMARY_API_KEY=你的 API Key
AI_SUMMARY_MODEL=Qwen/Qwen3-8B</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p><code>.env</code> 已经被 Git 忽略。不要把真实 Key 填进 <code>.env.example</code>，更不要把它放进文章里作为示例提交。</p>
<h2 id="几个容易踩到的地方">几个容易踩到的地方</h2>
<h3 id="source-mode-和-editor-mode-混着用">Source mode 和 Editor mode 混着用</h3>
<p>普通段落用 Editor mode 没问题，但一篇文章只要包含多个 shortcode，最好从头到尾在 Source mode 中检查一次。尤其要确认：</p>
<ul>
<li>每个块级 shortcode 都有对应的结束标签；</li>
<li>Mermaid、Chart.js 的内容仍然是合法格式；</li>
<li>代码块中的 shortcode 示例没有被提前解析；</li>
<li>HTML 标签的引号和缩进没有被编辑器改坏。</li>
</ul>
<h3 id="ai-摘要失败后继续等部署">AI 摘要失败后继续等部署</h3>
<p>摘要接口返回错误、Key 缺失、模型名称错误或接口超时，Actions 会失败，后面的 Hugo 构建和 Cloudflare 部署不会继续执行。</p>
<p>这时先看 Actions 日志中的失败文件和错误信息，检查 <code>AI_SUMMARY_API_KEY</code>、接口地址、模型名称以及服务商状态。配置修好后，在原来的 workflow 页面重新运行即可。</p>
<h3 id="把摘要当成-seo-描述">把摘要当成 SEO 描述</h3>
<p><code>ai_summary</code> 是文章正文前的展示内容，<code>description</code> 才是文章的描述字段。两者用途不同，不要为了显示摘要而删掉 <code>description</code>，也不要把 API 返回结果直接塞进站点全局配置。</p>




  

<aside class="solitude-tag note success modern no-icon has-icon">
  <span class="note-icon" aria-hidden="true"><i class="fas fa-circle-check"></i></span>
  <div class="note-content">写作时只需要关注标题、正文、图片和文章元数据。保存以后，让 Pages CMS 提交 GitHub，再让 Actions 负责摘要、构建和部署，整个流程就串起来了。</div>
</aside>

<h2 id="最后检查一下">最后检查一下</h2>
<p>现在这套流程可以概括成一句话：Pages CMS 负责写，GitHub 负责存，Actions 负责补齐摘要和构建，Cloudflare Pages 负责发布。</p>
<p>第一次使用时，我建议按下面的顺序走一遍：</p>
<ol>
<li>连接 <code>kemiao-moretti/meowloge</code>，确认分支是 <code>main</code>。</li>
<li>检查 Posts 集合是否对应 <code>content/posts</code>。</li>
<li>创建一篇测试文章，使用英文 slug。</li>
<li>普通正文用 Editor mode，包含 shortcode 时切到 Source mode。</li>
<li>上传一张图片，确认它最终位于 <code>static/img/posts/</code>。</li>
<li>保存后查看 GitHub commit 和 Actions 运行结果。</li>
<li>打开文章，确认摘要显示在正文前，图片、代码块和 shortcode 都正常。</li>
</ol>
<p>这样以后想写文章时，不必先打开本地开发环境。打开 Pages CMS，写完，保存，等它跑完发布流程就可以了。偶尔需要改主题或调脚本时，再回到仓库里处理，两套入口各自负责擅长的事情，反而更清楚。</p>
]]></content><category term="博客魔改" scheme="category"/><category term="博客魔改" scheme="series"/><category term="Pages CMS" scheme="tag"/><category term="Hugo" scheme="tag"/><category term="GitHub Actions" scheme="tag"/><category term="AI" scheme="tag"/></entry><entry><title>给博客文章加上 AI 摘要</title><link href="https://blog.518339.xyz/p/ai-summary.html" rel="alternate"/><link rel="enclosure" href="https://openlist.081531.xyz/d/yidong/imgbed/lsky/2026/09/22/6ab2876c65ce0.webp" type="image/webp"/><id>https://blog.518339.xyz/p/ai-summary.html</id><published>2026-09-22T18:30:00+08:00</published><updated>2026-09-22T18:30:00+08:00</updated><author><name>克喵Moretti</name><email>mcy@kemiaosw.top</email></author><summary type="text">最近给这个博客加了一张 AI 摘要卡。它会出现在文章正文前面，读者打开文章后，可以先扫一眼文章讲了什么，再决定要不要继续往下看。
这篇文章把接入过程记下来。以后重装主题，或者想给别的 Hugo 站点照着改时，不用再翻聊天记录。
先说最终效果 文章的 front matter 里有 ai_summary 时，页面会在正文前显示摘要卡：
yaml 复制 ai_summary: 这篇文章介绍了如何给 Hugo 站点接入 AI 摘要，并说明了构 …</summary><content type="html"><![CDATA[<p>最近给这个博客加了一张 AI 摘要卡。它会出现在文章正文前面，读者打开文章后，可以先扫一眼文章讲了什么，再决定要不要继续往下看。</p>
<p>这篇文章把接入过程记下来。以后重装主题，或者想给别的 Hugo 站点照着改时，不用再翻聊天记录。</p>
<h2 id="先说最终效果">先说最终效果</h2>
<p>文章的 front matter 里有 <code>ai_summary</code> 时，页面会在正文前显示摘要卡：</p>
<figure class="code-block" data-code-block data-language="yaml">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">yaml</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-yaml">ai_summary: 这篇文章介绍了如何给 Hugo 站点接入 AI 摘要，并说明了构建脚本、前端组件和密钥管理方式。</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>没有这个字段的文章不会显示空卡片。摘要卡的颜色、边框和阴影都跟着 Solitude 的主题变量走，所以切换明暗模式时不需要另外维护两套样式。</p>
<p>摘要文字默认会有一个很短的打字效果。浏览器开启了“减少动态效果”后，组件会直接显示完整内容。</p>
<h2 id="组件放在哪里">组件放在哪里</h2>
<p>文章模板是：</p>
<figure class="code-block" data-code-block data-language="text">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">text</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-text">themes/solitude/layouts/posts/page.html</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>在文章元信息和正文之间加一行 partial：</p>
<figure class="code-block" data-code-block data-language="html">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">html</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-html">{{ partial &#34;ai-summary.html&#34; . }}</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>摘要模板在：</p>
<figure class="code-block" data-code-block data-language="text">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">text</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-text">themes/solitude/layouts/_partials/ai-summary.html</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>模板只做一件事：读取 <code>.Params.ai_summary</code>。字段为空就什么也不输出，这样旧文章不需要批量修改，也不会出现一张写着“暂无摘要”的占位卡。</p>
<h2 id="主题配置">主题配置</h2>
<p>站点的 <code>hugo.yaml</code> 里有一段摘要配置：</p>
<figure class="code-block" data-code-block data-language="yaml">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">yaml</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-yaml">params:
  solitude:
    ai_summary:
      enable: true
      title: AI 摘要
      model: AI generated
      loading: 正在整理文章重点……</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p><code>title</code> 是卡片标题，<code>model</code> 会显示在右侧的小标签里，<code>loading</code> 是脚本还没有开始打字时的备用文字。</p>
<p>这些配置只是展示层设置。真正请求 AI 的接口和密钥不放在这里。</p>
<h2 id="配置根目录-env">配置根目录 <code>.env</code></h2>
<p>接口信息放在博客根目录的 <code>.env</code> 文件里，也就是：</p>
<figure class="code-block" data-code-block data-language="text">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">text</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-text">blog/.env</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>可以先复制示例文件：</p>
<figure class="code-block" data-code-block data-language="bash">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">bash</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-bash">cp .env.example .env</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>Windows PowerShell 可以这样复制：</p>
<figure class="code-block" data-code-block data-language="powershell">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">powershell</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-powershell">Copy-Item .env.example .env</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>然后打开 <code>.env</code>，填入自己的接口信息：</p>
<figure class="code-block" data-code-block data-language="dotenv">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">dotenv</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-dotenv">AI_SUMMARY_API=https://api.siliconflow.cn/v1/chat/completions
AI_SUMMARY_API_KEY=你的 SiliconFlow API Key
AI_SUMMARY_MODEL=Qwen/Qwen3-8B</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p><code>.env</code> 已经被加入 <code>.gitignore</code>，不会提交到 Git。不要把真实密钥写进 <code>.env.example</code>，也不要把 <code>.env</code> 发给别人。</p>
<p>脚本会自动读取博客根目录的 <code>.env</code>。如果当前终端已经设置了同名环境变量，环境变量优先，不会被 <code>.env</code> 覆盖。</p>
<p>SiliconFlow 的 Qwen3 默认可能开启思考模式。摘要脚本已经固定发送 <code>enable_thinking: false</code>，并设置 <code>stream: false</code>，这样会直接返回普通 JSON 摘要，不会等待或解析流式响应。</p>
<h2 id="用脚本生成摘要">用脚本生成摘要</h2>
<p>生成脚本位于：</p>
<figure class="code-block" data-code-block data-language="text">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">text</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-text">scripts/generate-ai-summary.mjs</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>它使用 OpenAI 兼容的 Chat Completions 接口，整体行为参考了 <code>hexo-ai-summary</code>：只在构建准备阶段处理 Markdown，不在访客浏览器里请求 AI；默认跳过已有摘要的文章，也支持并发、输入清洗、摘要字段自定义、覆盖控制和分级日志。脚本会遍历 <code>content/posts/</code> 下的 Markdown 文章，然后把生成结果写回 front matter。</p>
<p>先做一次 dry-run 比较稳妥：</p>
<figure class="code-block" data-code-block data-language="bash">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">bash</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-bash">node scripts/generate-ai-summary.mjs --dry-run</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>这一步只会列出准备处理的文章，不会修改文件。</p>
<p>确认 <code>.env</code> 已经填好后，直接生成：</p>
<figure class="code-block" data-code-block data-language="bash">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">bash</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-bash">node scripts/generate-ai-summary.mjs</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>如果不想创建 <code>.env</code>，也可以临时用 PowerShell 设置配置：</p>
<figure class="code-block" data-code-block data-language="powershell">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">powershell</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-powershell">$env:AI_SUMMARY_API = &#34;https://api.siliconflow.cn/v1/chat/completions&#34;
$env:AI_SUMMARY_API_KEY = &#34;你的 SiliconFlow API Key&#34;
$env:AI_SUMMARY_MODEL = &#34;Qwen/Qwen3-8B&#34;
node scripts/generate-ai-summary.mjs</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>如果只想临时覆盖 <code>.env</code> 里的配置，仍然可以使用环境变量：</p>
<figure class="code-block" data-code-block data-language="bash">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">bash</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-bash">AI_SUMMARY_API=&#34;https://另一个接口/v1/chat/completions&#34; \\
AI_SUMMARY_API_KEY=&#34;临时密钥&#34; \\
AI_SUMMARY_MODEL=&#34;临时模型&#34; \\
node scripts/generate-ai-summary.mjs</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>PowerShell 写法：</p>
<figure class="code-block" data-code-block data-language="powershell">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">powershell</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-powershell">$env:AI_SUMMARY_API = &#34;https://另一个接口/v1/chat/completions&#34;
$env:AI_SUMMARY_API_KEY = &#34;临时密钥&#34;
$env:AI_SUMMARY_MODEL = &#34;临时模型&#34;
node scripts/generate-ai-summary.mjs</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>脚本默认只生成缺少摘要的文章。想重新生成已有摘要时，加上 <code>--force</code>：</p>
<figure class="code-block" data-code-block data-language="bash">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">bash</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-bash">node scripts/generate-ai-summary.mjs --force</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>生成完成后，再运行 Hugo：</p>
<figure class="code-block" data-code-block data-language="bash">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">bash</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-bash">hugo --gc</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>目前这两个步骤是分开的。也就是说，<code>hugo</code> 不会偷偷调用 AI 接口；需要生成摘要时，先手动运行脚本，再构建站点。这样构建过程更可控，也不容易因为接口临时超时而卡住发布。</p>
<p>脚本还支持这些和插件思路对应的配置：</p>
<figure class="code-block" data-code-block data-language="dotenv">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">dotenv</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-dotenv">AI_SUMMARY_SUMMARY_FIELD=ai_summary
AI_SUMMARY_COVER_ALL=false
AI_SUMMARY_MAX_INPUT_TOKEN=5000
AI_SUMMARY_MAX_OUTPUT_TOKEN=2000
AI_SUMMARY_CONCURRENCY=2
AI_SUMMARY_SLEEP_TIME=0
AI_SUMMARY_LOGGER=1
AI_SUMMARY_THINKING=false</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p><code>AI_SUMMARY_COVER_ALL=true</code> 或运行时加 <code>--force</code> 会覆盖已有摘要；<code>AI_SUMMARY_CONCURRENCY</code> 控制同时请求的文章数，建议不要设得太大；<code>AI_SUMMARY_SLEEP_TIME</code> 可以在每篇文章处理后增加间隔。推理模型可以打开 <code>AI_SUMMARY_THINKING=true</code>，脚本会发送 <code>reasoning_effort=medium</code>，但摘要场景默认关闭更稳妥。</p>
<h2 id="脚本会怎么处理正文">脚本会怎么处理正文</h2>
<p>发送给模型前，脚本会先做一些清理：</p>
<ul>
<li>去掉 front matter</li>
<li>去掉代码块</li>
<li>去掉图片和链接地址</li>
<li>去掉 HTML 标签和 Markdown 符号</li>
<li>把过长的正文截断到输入长度上限</li>
</ul>
<p>默认输入上限是 12000 个字符，可以在 <code>.env</code> 里调整，也可以用环境变量临时覆盖：</p>
<figure class="code-block" data-code-block data-language="bash">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">bash</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-bash">AI_SUMMARY_MAX_INPUT=16000 node scripts/generate-ai-summary.mjs --dry-run</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>提示词也可以通过 <code>.env</code> 里的 <code>AI_SUMMARY_PROMPT</code> 自定义。默认提示词要求模型只返回一段中文摘要，不要输出标题、列表或 Markdown。</p>
<h2 id="为什么不在浏览器里直接生成">为什么不在浏览器里直接生成</h2>
<p>最开始也想过在文章页放一个“生成摘要”按钮，但那样需要把 API Key 交给浏览器。只要打开开发者工具，Key 就能被看到，后面还可能被别人拿去刷接口额度。</p>
<p>所以现在的做法是：</p>
<ol>
<li>在本地或 CI 构建环境里调用模型。</li>
<li>把摘要写进文章 front matter。</li>
<li>网站前端只负责展示已经生成好的文字。</li>
</ol>
<p>访客不会接触到 API Key，文章页面也不依赖 AI 接口才能打开。对博客来说，这种方式简单一点，但更踏实。</p>
<h2 id="生成失败怎么办">生成失败怎么办</h2>
<p>脚本遇到某篇文章请求失败时，会在终端打印文件名和错误原因，并继续处理后面的文章。失败的文章不会被写入半截摘要。</p>
<p>如果最后有失败项，脚本会返回非零退出码。发布前看一眼终端输出就能知道有没有文章漏掉，不需要专门去页面里排查。</p>
<p>常见问题通常是：</p>
<ul>
<li><code>AI_SUMMARY_API_KEY</code> 没有设置</li>
<li>接口地址少了 <code>/v1/chat/completions</code></li>
<li>模型名称写错</li>
<li>接口暂时超时或返回限流</li>
</ul>
<h2 id="最后检查一下">最后检查一下</h2>
<p>改完后，我一般会按这个顺序检查：</p>
<figure class="code-block" data-code-block data-language="bash">
  <figcaption class="code-block__toolbar">
    <span class="code-block__language">bash</span>
      <button class="code-block__copy" type="button" data-code-copy>复制</button>
  </figcaption>
  <div class="code-block__viewport">
    <pre class="code-block__fallback" tabindex="0"><code class="language-bash">node scripts/generate-ai-summary.mjs --dry-run
hugo --gc
hugo server --port 1313</code></pre>
  </div>
  <button class="code-block__expand" type="button" data-code-expand hidden>展开全部</button>
</figure>
<p>然后打开一篇有 <code>ai_summary</code> 的文章，看四件事：摘要是不是在正文前、暗色模式下文字是否清楚、手机宽度有没有横向滚动，以及没有摘要的旧文章是否保持原样。</p>
<p>这套组件现在只负责摘要展示，没有把摘要当成 SEO 的 <code>description</code>，也没有把请求逻辑塞进访客浏览器。以后如果觉得摘要质量不够，优先改提示词；如果觉得卡片太抢眼，再调整组件 CSS，不需要动文章正文。</p>
]]></content><category term="博客魔改" scheme="category"/><category term="博客魔改" scheme="series"/></entry></feed>