<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>逆行者AI博客</title>
  
  
  <link href="https://nixingzhe-blog.edgeone.dev/atom.xml" rel="self"/>
  
  <link href="https://nixingzhe-blog.edgeone.dev/"/>
  <updated>2026-09-17T03:55:41.528Z</updated>
  <id>https://nixingzhe-blog.edgeone.dev/</id>
  
  <author>
    <name>逆行者</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>零服务器搭一个「带线上后台」的静态博客</title>
    <link href="https://nixingzhe-blog.edgeone.dev/2026/09/17/zero-server-static-blog-with-admin/"/>
    <id>https://nixingzhe-blog.edgeone.dev/2026/09/17/zero-server-static-blog-with-admin/</id>
    <published>2026-09-17T03:39:06.000Z</published>
    <updated>2026-09-17T03:55:41.528Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>用 CNB 云原生构建 + EdgeOne Pages + Hexo，实现<strong>浏览器里点鼠标发文</strong>。<br>全程不需要服务器、不需要备案、不需要在本地装任何环境。</p></blockquote><hr><span id="more"></span><h2 id="0-这套方案是什么">0. 这套方案是什么</h2><h3 id="最终效果">最终效果</h3><table><thead><tr><th></th><th></th></tr></thead><tbody><tr><td>博客首页</td><td><code>https://你的域名/</code></td></tr><tr><td>线上后台</td><td><code>https://你的域名/admin/</code></td></tr><tr><td>发文方式</td><td>浏览器打开后台 → 输密码 → 写 → 点发布 → 约 1 分钟上线</td></tr><tr><td>手机能用吗</td><td>能，后台是响应式页面</td></tr><tr><td>服务器</td><td>不需要</td></tr><tr><td>花费</td><td>0</td></tr></tbody></table><h3 id="为什么这样搭">为什么这样搭</h3><p>静态博客（Hexo）本身没有后台，传统做法要么忍着手敲 Markdown，要么自己买服务器跑 WordPress。这套方案用「静态站 + 一个托管在边缘的云函数」补上了后台能力：</p><ul><li><strong>静态站</strong>（HTML/CSS/JS）放在 CDN 上 → 快、便宜、免运维、不容易被黑</li><li><strong>云函数</strong>（几十行 JS）当&quot;中介&quot;，帮你把文章写进代码仓库</li><li><strong>流水线</strong>（CNB）负责构建和发布，你完全不用管</li></ul><h3 id="你会得到什么-不会得到什么">你会得到什么 / 不会得到什么</h3><p><strong>有</strong>：文章增删改、标签、分类、归档、搜索、评论位（需另接第三方）、自定义域名、HTTPS。</p><p><strong>没有</strong>：多用户权限体系、数据库、动态交互（访问统计、登录用户）。静态博客的本质决定了这些要另接第三方服务。</p><hr><h2 id="1-准备清单">1. 准备清单</h2><h3 id="账号（3-个，都免费）">账号（3 个，都免费）</h3><table><thead><tr><th>平台</th><th>用途</th><th>地址</th><th>注意</th></tr></thead><tbody><tr><td>CNB</td><td>代码托管 + 流水线 + WebIDE</td><td><code>cnb.cool</code></td><td><strong>必须先完成实名认证</strong>才能建组织，否则创建组织会失败</td></tr><tr><td>EdgeOne Pages</td><td>静态站托管 + 云函数</td><td><code>pages.edgeone.ai</code></td><td>腾讯云国际版；没备案就选「海外可访问」</td></tr><tr><td>域名（可选）</td><td>自己的域名</td><td>任意域名商</td><td>不备案也能用，只是国内访问会绕一下</td></tr></tbody></table><h3 id="要拿到手的-3-样东西">要拿到手的 3 样东西</h3><table><thead><tr><th>名称</th><th>从哪拿</th><th>长什么样</th><th>用在哪</th></tr></thead><tbody><tr><td><strong>EdgeOne Pages API Token</strong></td><td>Pages 项目 → 设置 → API Token，创建时选<strong>永久有效</strong></td><td>一长串 base64，通常以 <code>=</code> 结尾</td><td><code>.cnb.yml</code> 里部署命令的 <code>-t</code></td></tr><tr><td><strong>CNB 访问令牌</strong></td><td>CNB → 个人设置 → 访问令牌</td><td>一串大小写字母数字</td><td>EdgeOne 环境变量 <code>CNB_TOKEN</code></td></tr><tr><td><strong>后台密码</strong></td><td>你自己定</td><td>随便定</td><td>EdgeOne 环境变量 <code>ADMIN_PASSWORD</code></td></tr></tbody></table><blockquote><p>⚠️ <strong>两个 Token 完全不同、不能混用</strong>：</p><ul><li>EdgeOne Pages API Token 用来<strong>部署静态站</strong>（<code>edgeone pages deploy</code>）</li><li>CNB 访问令牌用来<strong>读写代码仓库</strong>（相当于 git 密码）</li></ul><p>这是最容易搞混、也最容易卡住的一点。</p></blockquote><h3 id="CNB-令牌需要的权限">CNB 令牌需要的权限</h3><p>在 CNB 建访问令牌时，勾选：</p><ul><li><code>repo-code</code> → <strong>读写</strong>（流水线里要 <code>git push</code>）</li><li><code>repo-cnb-trigger</code> → <strong>读写</strong>（云函数要触发流水线）</li></ul><p>不必勾 <code>repo-basic-info</code>、<code>repo-cnb-history</code>（只有查仓库元信息和构建日志才用得到，不影响功能）。</p><hr><h2 id="2-整体架构">2. 整体架构</h2><figure class="highlight swift"><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></pre></td><td class="code"><pre><code class="hljs swift">① 你在浏览器打开  <span class="hljs-regexp">/admin/</span><br>        <span class="hljs-operator">│</span>  输入密码 <span class="hljs-operator">+</span> 文章内容<br>        <span class="hljs-operator">▼</span><br>② 云函数  <span class="hljs-type">POST</span> <span class="hljs-regexp">/api/</span>publish        <span class="hljs-operator">←</span> 跑在 <span class="hljs-type">EdgeOne</span> 边缘节点<br>        <span class="hljs-operator">│</span>  校验密码，把 <span class="hljs-type">CNB</span> 令牌从环境变量取出来用<br>        <span class="hljs-operator">│</span>  （令牌永远不下发到浏览器）<br>        <span class="hljs-operator">▼</span><br>③ <span class="hljs-type">CNB</span> <span class="hljs-type">OpenAPI</span>  <span class="hljs-type">POST</span> <span class="hljs-regexp">/&#123;repo&#125;/</span><span class="hljs-operator">-/</span>build<span class="hljs-operator">/</span>start<br>        <span class="hljs-operator">│</span>  event<span class="hljs-operator">=</span>api_trigger，文章内容通过 env 传进去<br>        <span class="hljs-operator">▼</span><br>④ <span class="hljs-type">CNB</span> 流水线  main: api_trigger<br>        <span class="hljs-operator">│</span>  stage <span class="hljs-number">1</span>: publish<span class="hljs-operator">-</span>post.sh <span class="hljs-operator">→</span> 写 md 文件 <span class="hljs-operator">→</span> git push<br>        <span class="hljs-operator">│</span>  stage <span class="hljs-number">2</span>: build<span class="hljs-operator">-</span>and<span class="hljs-operator">-</span>deploy.sh <span class="hljs-operator">→</span> hexo g <span class="hljs-operator">→</span> 组装 dist <span class="hljs-operator">→</span> edgeone pages deploy<br>        <span class="hljs-operator">▼</span><br>⑤ <span class="hljs-type">EdgeOne</span> <span class="hljs-type">Pages</span>  <span class="hljs-type">CDN</span> 更新  <span class="hljs-operator">→</span>  约 <span class="hljs-number">1</span> 分钟后线上可见<br></code></pre></td></tr></table></figure><p><strong>为什么要绕这么一圈？</strong> 有两个硬约束：</p><ol><li><strong>CNB 的 OpenAPI 对仓库文件是只读的</strong> —— 没有&quot;写文件/提交 commit&quot;的接口（详见「踩坑 1」）。</li><li><strong><code>api.cnb.cool</code> 不返回任何 CORS 头</strong> —— 浏览器不能直接调它（详见「踩坑 2」）。</li></ol><p>所以必须有一个<strong>跑在服务端、能拿密钥</strong>的中间人来转发。EdgeOne 自带的云函数正好补上这个位置，不用另外注册 Cloudflare、也不用占用你自己的服务器。</p><hr><h2 id="3-搭建步骤">3. 搭建步骤</h2><h3 id="步骤-1：CNB-建私密仓库">步骤 1：CNB 建私密仓库</h3><ol><li>打开 <code>https://cnb.cool</code> 登录</li><li>右上角 <code>+</code> → 新建仓库</li><li>填三项：<ul><li>归属组织：<code>你的组织</code></li><li>仓库名：<code>blog</code></li><li>可见性：<strong>私密</strong> ← 重要，<code>.cnb.yml</code> 里有 EdgeOne Token</li></ul></li><li><strong>不要</strong>勾&quot;初始化 README&quot;，留空仓库</li></ol><blockquote><p><strong>为什么必须私密</strong>：<code>.cnb.yml</code> 里要写 EdgeOne API Token 才能部署。虽然更安全的做法是用平台的密钥管理功能，但最省事的方案是&quot;仓库私密 + Token 只放在这一个文件里&quot;。</p></blockquote><h3 id="步骤-2：EdgeOne-Pages-建项目">步骤 2：EdgeOne Pages 建项目</h3><ol><li>打开 <code>https://pages.edgeone.ai</code>，登录腾讯云国际账号</li><li>新建项目 → 选「<strong>直接上传</strong>」（不要选 Git 集成，我们走 CLI 部署）</li><li>项目名：<code>你的项目名</code>（比如 <code>myblog</code>）—— 记住它，后面多处要填</li><li>访问区：没备案选「<strong>海外可访问</strong>」；有备案可选国内</li><li>进项目设置 → API Token → <strong>新建一个永久有效的 Token</strong> → 复制保存</li></ol><h3 id="步骤-3：初始化-Hexo-工程">步骤 3：初始化 Hexo 工程</h3><p>在本地（或 CNB WebIDE 里）执行：</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><code class="hljs bash"><span class="hljs-comment"># 建目录</span><br><span class="hljs-built_in">mkdir</span> Blog &amp;&amp; <span class="hljs-built_in">cd</span> Blog<br><br><span class="hljs-comment"># 装依赖（package.json 见下方&quot;文件清单&quot;）</span><br>npm install --registry=https://registry.npmmirror.com<br><br><span class="hljs-comment"># 从 node_modules 里把主题拷进 themes/（Fluid 主题的标准装法）</span><br><span class="hljs-built_in">mkdir</span> -p themes scaffolds <span class="hljs-built_in">source</span>/_posts<br><span class="hljs-built_in">cp</span> -R node_modules/hexo-theme-fluid themes/fluid<br><br><span class="hljs-comment"># 主题配置文件</span><br><span class="hljs-built_in">cp</span> themes/fluid/_config.yml ../_config.fluid.yml<br></code></pre></td></tr></table></figure><p><strong><code>Blog/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><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><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;blog&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;private&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;hexo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;7.3.0&quot;</span> <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;dependencies&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;hexo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^7.3.0&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;hexo-generator-archive&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^2.0.0&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;hexo-generator-category&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^2.0.0&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;hexo-generator-index&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^4.0.0&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;hexo-generator-tag&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^2.0.0&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;hexo-renderer-ejs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^2.0.0&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;hexo-renderer-marked&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^7.0.0&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;hexo-renderer-stylus&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^3.0.0&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;hexo-server&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^3.0.0&quot;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-attr">&quot;hexo-theme-fluid&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^1.9.8&quot;</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p><strong><code>Blog/_config.yml</code> 要改的几处</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><code class="hljs yaml"><span class="hljs-attr">title:</span> <span class="hljs-string">你的博客名</span><br><span class="hljs-attr">subtitle:</span> <span class="hljs-string">&#x27;&#x27;</span><br><span class="hljs-attr">description:</span> <span class="hljs-string">一句话描述</span><br><span class="hljs-attr">author:</span> <span class="hljs-string">你的名字</span><br><span class="hljs-attr">language:</span> <span class="hljs-string">zh-CN</span><br><span class="hljs-attr">timezone:</span> <span class="hljs-string">&#x27;Asia/Shanghai&#x27;</span><br><br><span class="hljs-comment"># 换成你的正式域名，影响生成的绝对链接和 RSS</span><br><span class="hljs-attr">url:</span> <span class="hljs-string">https://你的域名</span><br><br><span class="hljs-attr">theme:</span> <span class="hljs-string">fluid</span><br></code></pre></td></tr></table></figure><h3 id="步骤-4：写根目录的配置文件">步骤 4：写根目录的配置文件</h3><h4 id="4-0-ide-Dockerfile-——-WebIDE-环境（可选）">4.0 <code>.ide/Dockerfile</code> —— WebIDE 环境（可选）</h4><p>配了它，就能在<strong>浏览器里</strong>打开一个完整的 VSCode（带终端、Git 面板、语法高亮），不用在本地装任何环境。改完文件直接在 WebIDE 里 commit + push。</p><figure class="highlight dockerfile"><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><code class="hljs dockerfile"><span class="hljs-keyword">FROM</span> node:<span class="hljs-number">20</span><br><br><span class="hljs-comment"># code-server + 常用插件</span><br><span class="hljs-keyword">RUN</span><span class="language-bash"> curl -fsSL https://code-server.dev/install.sh | sh \</span><br><span class="language-bash">  &amp;&amp; code-server --install-extension cnbcool.cnb-welcome \</span><br><span class="language-bash">  &amp;&amp; code-server --install-extension redhat.vscode-yaml \</span><br><span class="language-bash">  &amp;&amp; code-server --install-extension dbaeumer.vscode-eslint \</span><br><span class="language-bash">  &amp;&amp; code-server --install-extension waderyan.gitblame \</span><br><span class="language-bash">  &amp;&amp; code-server --install-extension mhutchie.git-graph \</span><br><span class="language-bash">  &amp;&amp; code-server --install-extension donjayamanne.githistory \</span><br><span class="language-bash">  &amp;&amp; code-server --install-extension tencent-cloud.coding-copilot \</span><br><span class="language-bash">  &amp;&amp; <span class="hljs-built_in">echo</span> <span class="hljs-keyword">done</span></span><br><br><span class="hljs-comment"># git 等基础工具 + ssh（支持用本地 VSCode Remote-SSH 连进来）</span><br><span class="hljs-keyword">RUN</span><span class="language-bash"> apt-get update &amp;&amp; apt-get install -y git wget unzip openssh-server</span><br><br><span class="hljs-keyword">ENV</span> LANG C.UTF-<span class="hljs-number">8</span><br><span class="hljs-keyword">ENV</span> LANGUAGE C.UTF-<span class="hljs-number">8</span><br></code></pre></td></tr></table></figure><p>用法：CNB 仓库页点「云原生开发」，第一次会按这个 Dockerfile 构建环境（约 2 分钟），之后直接拉起 WebIDE。</p><h4 id="4-1-cnb-yml-——-流水线定义">4.1 <code>.cnb.yml</code> —— 流水线定义</h4><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></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-comment"># ---------- ① 云原生开发（WebIDE）----------</span><br><span class="hljs-string">$:</span><br>  <span class="hljs-attr">vscode:</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-attr">docker:</span><br>        <span class="hljs-attr">build:</span> <span class="hljs-string">./.ide/Dockerfile</span><br>      <span class="hljs-attr">runner:</span><br>        <span class="hljs-attr">cpus:</span> <span class="hljs-number">4</span><br>      <span class="hljs-attr">services:</span><br>        <span class="hljs-bullet">-</span> <span class="hljs-string">vscode</span><br>      <span class="hljs-attr">stages:</span><br>        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">预装依赖</span><br>          <span class="hljs-attr">script:</span> <span class="hljs-string">|</span><br><span class="hljs-string">            npm install -g hexo-cli || true</span><br><span class="hljs-string">            if [ -d Blog ]; then cd Blog &amp;&amp; npm install || true; fi</span><br><span class="hljs-string"></span>        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">拉起</span> <span class="hljs-string">WebIDE</span><br>          <span class="hljs-attr">script:</span> <span class="hljs-string">vscode:go</span><br><br><span class="hljs-comment"># ---------- ② 代码推送 → 构建并部署 ----------</span><br><span class="hljs-attr">main:</span><br>  <span class="hljs-attr">push:</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">构建并部署博客</span><br>      <span class="hljs-attr">docker:</span><br>        <span class="hljs-attr">image:</span> <span class="hljs-string">node:20</span><br>        <span class="hljs-attr">volumes:</span><br>          <span class="hljs-bullet">-</span> <span class="hljs-string">/root/.npm:copy-on-write</span><br>      <span class="hljs-attr">env:</span><br>        <span class="hljs-attr">EDGEONE_API_TOKEN:</span> <span class="hljs-string">&lt;EDGEONE_PAGES_API_TOKEN&gt;</span><br>        <span class="hljs-attr">EDGEONE_PROJECT:</span> <span class="hljs-string">你的项目名</span><br>      <span class="hljs-attr">stages:</span><br>        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">渲染并部署</span><br>          <span class="hljs-attr">script:</span> <span class="hljs-string">|</span><br><span class="hljs-string">            sh ./scripts/build-and-deploy.sh</span><br><span class="hljs-string"></span><br>  <span class="hljs-comment"># ---------- ③ 线上后台「发布」按钮 → 写入文章 ----------</span><br>  <span class="hljs-attr">api_trigger:</span><br>    <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">发布新文章</span><br>      <span class="hljs-attr">docker:</span><br>        <span class="hljs-attr">image:</span> <span class="hljs-string">node:20</span><br>        <span class="hljs-attr">volumes:</span><br>          <span class="hljs-bullet">-</span> <span class="hljs-string">/root/.npm:copy-on-write</span><br>      <span class="hljs-attr">env:</span><br>        <span class="hljs-attr">EDGEONE_API_TOKEN:</span> <span class="hljs-string">&lt;EDGEONE_PAGES_API_TOKEN&gt;</span><br>        <span class="hljs-attr">EDGEONE_PROJECT:</span> <span class="hljs-string">你的项目名</span><br>      <span class="hljs-attr">stages:</span><br>        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">写入文章并推送</span><br>          <span class="hljs-attr">script:</span> <span class="hljs-string">|</span><br><span class="hljs-string">            sh ./scripts/publish-post.sh</span><br><span class="hljs-string"></span>        <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">渲染并部署</span><br>          <span class="hljs-attr">script:</span> <span class="hljs-string">|</span><br>            <span class="hljs-string">sh</span> <span class="hljs-string">./scripts/build-and-deploy.sh</span><br></code></pre></td></tr></table></figure><p>要点：</p><ul><li><code>api_trigger</code> 是挂在<strong>分支名下面</strong>的合法事件，和 <code>push</code> 同级</li><li>流水线内置的 <code>CNB_TOKEN</code> 在 <code>api_trigger</code> 下拥有 <code>repo-code:rw</code>，所以脚本里能 <code>git push</code></li><li>构建镜像用 <code>node:20</code></li></ul><h4 id="4-2-build-sh-——-组装部署产物">4.2 <code>build.sh</code> —— 组装部署产物</h4><figure class="highlight sh"><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><code class="hljs sh"><span class="hljs-meta">#!/bin/sh</span><br><span class="hljs-comment"># 组装部署产物 dist/</span><br><span class="hljs-built_in">set</span> -e<br><br>SCRIPT_DIR=$(<span class="hljs-built_in">cd</span> <span class="hljs-string">&quot;<span class="hljs-subst">$(dirname <span class="hljs-string">&quot;<span class="hljs-variable">$0</span>&quot;</span>)</span>&quot;</span> &amp;&amp; <span class="hljs-built_in">pwd</span>)<br><span class="hljs-built_in">cd</span> <span class="hljs-string">&quot;<span class="hljs-variable">$SCRIPT_DIR</span>&quot;</span><br><br><span class="hljs-keyword">if</span> [ ! -d Blog/public ]; <span class="hljs-keyword">then</span><br>  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;找不到 Blog/public，请先执行 hexo generate&quot;</span><br>  <span class="hljs-built_in">exit</span> 1<br><span class="hljs-keyword">fi</span><br><br><span class="hljs-comment"># 旧产物改名而不是删除，避免误删</span><br><span class="hljs-keyword">if</span> [ -d dist ]; <span class="hljs-keyword">then</span><br>  <span class="hljs-built_in">mv</span> dist <span class="hljs-string">&quot;dist_stale_<span class="hljs-subst">$(date +%s)</span>&quot;</span><br><span class="hljs-keyword">fi</span><br><br><span class="hljs-built_in">mkdir</span> -p dist<br><span class="hljs-built_in">cp</span> -R Blog/public/. dist/            <span class="hljs-comment"># 博客本体</span><br><span class="hljs-built_in">mkdir</span> -p dist/admin<br><span class="hljs-built_in">cp</span> admin/index.html dist/admin/index.html   <span class="hljs-comment"># 线上后台</span><br><span class="hljs-built_in">cp</span> -R cloud-functions dist/cloud-functions  <span class="hljs-comment"># 云函数</span><br><span class="hljs-built_in">cp</span> <span class="hljs-built_in">functions</span>/package.json dist/package.json<br><span class="hljs-built_in">cp</span> edgeone.json dist/edgeone.json<br><br><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;===== dist 组装完成 =====&quot;</span><br><span class="hljs-built_in">du</span> -sh dist<br></code></pre></td></tr></table></figure><h4 id="4-3-scripts-build-and-deploy-sh-——-渲染-部署">4.3 <code>scripts/build-and-deploy.sh</code> —— 渲染 + 部署</h4><figure class="highlight sh"><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></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-meta">#!/bin/sh</span><br><span class="hljs-built_in">set</span> -e<br><br>SCRIPT_DIR=$(<span class="hljs-built_in">cd</span> <span class="hljs-string">&quot;<span class="hljs-subst">$(dirname <span class="hljs-string">&quot;<span class="hljs-variable">$0</span>&quot;</span>)</span>&quot;</span> &amp;&amp; <span class="hljs-built_in">pwd</span>)<br>ROOT=$(<span class="hljs-built_in">cd</span> <span class="hljs-string">&quot;<span class="hljs-variable">$SCRIPT_DIR</span>/..&quot;</span> &amp;&amp; <span class="hljs-built_in">pwd</span>)<br><span class="hljs-built_in">cd</span> <span class="hljs-string">&quot;<span class="hljs-variable">$ROOT</span>&quot;</span><br><br><span class="hljs-keyword">if</span> [ ! -d Blog/source ]; <span class="hljs-keyword">then</span><br>  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;Blog/ 还没初始化，跳过构建。&quot;</span><br>  <span class="hljs-built_in">exit</span> 0<br><span class="hljs-keyword">fi</span><br><br><span class="hljs-keyword">if</span> ! <span class="hljs-built_in">command</span> -v hexo &gt;/dev/null 2&gt;&amp;1; <span class="hljs-keyword">then</span><br>  npm install -g hexo-cli<br><span class="hljs-keyword">fi</span><br><br><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;===== 1/3 渲染静态文件 =====&quot;</span><br>(<br>  <span class="hljs-built_in">cd</span> Blog<br>  npm install --no-audit --no-fund<br>  hexo clean<br>  hexo generate<br>)<br><br><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;===== 2/3 组装 dist =====&quot;</span><br>sh ./build.sh<br><br><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;===== 3/3 部署到 EdgeOne Pages =====&quot;</span><br><span class="hljs-keyword">if</span> [ -z <span class="hljs-string">&quot;<span class="hljs-variable">$EDGEONE_API_TOKEN</span>&quot;</span> ]; <span class="hljs-keyword">then</span><br>  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;缺少 EDGEONE_API_TOKEN，跳过部署（只构建不发布）。&quot;</span><br>  <span class="hljs-built_in">exit</span> 0<br><span class="hljs-keyword">fi</span><br><br>npx --<span class="hljs-built_in">yes</span> edgeone pages deploy dist \<br>  -n <span class="hljs-string">&quot;<span class="hljs-variable">$&#123;EDGEONE_PROJECT:-你的项目名&#125;</span>&quot;</span> \<br>  -t <span class="hljs-string">&quot;<span class="hljs-variable">$EDGEONE_API_TOKEN</span>&quot;</span><br></code></pre></td></tr></table></figure><h4 id="4-4-scripts-publish-post-sh-——-把文章写进仓库">4.4 <code>scripts/publish-post.sh</code> —— 把文章写进仓库</h4><figure class="highlight sh"><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><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br></pre></td><td class="code"><pre><code class="hljs sh"><span class="hljs-meta">#!/bin/sh</span><br><span class="hljs-built_in">set</span> -e<br><br>SCRIPT_DIR=$(<span class="hljs-built_in">cd</span> <span class="hljs-string">&quot;<span class="hljs-subst">$(dirname <span class="hljs-string">&quot;<span class="hljs-variable">$0</span>&quot;</span>)</span>&quot;</span> &amp;&amp; <span class="hljs-built_in">pwd</span>)<br>ROOT=$(<span class="hljs-built_in">cd</span> <span class="hljs-string">&quot;<span class="hljs-variable">$SCRIPT_DIR</span>/..&quot;</span> &amp;&amp; <span class="hljs-built_in">pwd</span>)<br><span class="hljs-built_in">cd</span> <span class="hljs-string">&quot;<span class="hljs-variable">$ROOT</span>&quot;</span><br><br>: <span class="hljs-string">&quot;<span class="hljs-variable">$&#123;PUB_TITLE:?没有收到 PUB_TITLE（文章标题）&#125;</span>&quot;</span><br>: <span class="hljs-string">&quot;<span class="hljs-variable">$&#123;PUB_CONTENT_B64:?没有收到 PUB_CONTENT_B64（文章正文）&#125;</span>&quot;</span><br><br>TARGET_BRANCH=<span class="hljs-string">&quot;<span class="hljs-variable">$&#123;CNB_BRANCH:-main&#125;</span>&quot;</span><br>POST_DIR=<span class="hljs-string">&quot;Blog/source/_posts&quot;</span><br><br><span class="hljs-comment"># 去掉前后空白 / 换行 / 引号，避免破坏 YAML front-matter</span><br><span class="hljs-function"><span class="hljs-title">clean</span></span>() &#123; <span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;%s&#x27;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$1</span>&quot;</span> | <span class="hljs-built_in">tr</span> -d <span class="hljs-string">&#x27;\r\n&#x27;</span> | sed <span class="hljs-string">&#x27;s/^[[:space:]]*//; s/[[:space:]]*$//&#x27;</span> | <span class="hljs-built_in">tr</span> <span class="hljs-string">&#x27;&quot;&#x27;</span> <span class="hljs-string">&quot;&#x27;&quot;</span>; &#125;<br><br><span class="hljs-comment"># 文件名清洗：保留中文，只把路径危险字符和空白替换掉</span><br>SLUG=$(<span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;%s&#x27;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$PUB_SLUG</span>&quot;</span> \<br>  | <span class="hljs-built_in">tr</span> -d <span class="hljs-string">&#x27;\r\n&#x27;</span> \<br>  | sed <span class="hljs-string">&#x27;s#[/\\:*?&quot;&lt;&gt;|[:space:]]#-#g; s/-\&#123;2,\&#125;/-/g; s/^[-.]*//; s/[-.]*$//&#x27;</span>)<br>[ -n <span class="hljs-string">&quot;<span class="hljs-variable">$SLUG</span>&quot;</span> ] || SLUG=<span class="hljs-string">&quot;post-<span class="hljs-subst">$(date +%Y%m%d%H%M%S)</span>&quot;</span><br><br><span class="hljs-built_in">mkdir</span> -p <span class="hljs-string">&quot;<span class="hljs-variable">$POST_DIR</span>&quot;</span><br><br><span class="hljs-comment"># 正文经 base64 传输，换行、引号、中文都不会被破坏</span><br><span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;%s&#x27;</span> <span class="hljs-string">&quot;<span class="hljs-variable">$PUB_CONTENT_B64</span>&quot;</span> | <span class="hljs-built_in">base64</span> -d &gt; /tmp/post_body.md<br><br>&#123;<br>  <span class="hljs-built_in">printf</span> -- <span class="hljs-string">&#x27;---\n&#x27;</span><br>  <span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;title: &quot;%s&quot;\n&#x27;</span> <span class="hljs-string">&quot;<span class="hljs-subst">$(clean <span class="hljs-string">&quot;<span class="hljs-variable">$PUB_TITLE</span>&quot;</span>)</span>&quot;</span><br>  <span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;date: %s\n&#x27;</span> <span class="hljs-string">&quot;<span class="hljs-subst">$(date &#x27;+%Y-%m-%d %H:%M:%S&#x27;)</span>&quot;</span><br>  <span class="hljs-keyword">if</span> [ -n <span class="hljs-string">&quot;<span class="hljs-variable">$PUB_TAGS</span>&quot;</span> ]; <span class="hljs-keyword">then</span> <span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;tags: [%s]\n&#x27;</span> <span class="hljs-string">&quot;<span class="hljs-subst">$(clean <span class="hljs-string">&quot;<span class="hljs-variable">$PUB_TAGS</span>&quot;</span>)</span>&quot;</span>; <span class="hljs-keyword">fi</span><br>  <span class="hljs-keyword">if</span> [ -n <span class="hljs-string">&quot;<span class="hljs-variable">$PUB_CATEGORIES</span>&quot;</span> ]; <span class="hljs-keyword">then</span> <span class="hljs-built_in">printf</span> <span class="hljs-string">&#x27;categories: [%s]\n&#x27;</span> <span class="hljs-string">&quot;<span class="hljs-subst">$(clean <span class="hljs-string">&quot;<span class="hljs-variable">$PUB_CATEGORIES</span>&quot;</span>)</span>&quot;</span>; <span class="hljs-keyword">fi</span><br>  <span class="hljs-built_in">printf</span> -- <span class="hljs-string">&#x27;---\n\n&#x27;</span><br>  <span class="hljs-built_in">cat</span> /tmp/post_body.md<br>&#125; &gt; <span class="hljs-string">&quot;<span class="hljs-variable">$POST_DIR</span>/<span class="hljs-variable">$SLUG</span>.md&quot;</span><br><br><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;===== 已写入 <span class="hljs-variable">$POST_DIR</span>/<span class="hljs-variable">$SLUG</span>.md =====&quot;</span><br><br>git config user.name  <span class="hljs-string">&quot;<span class="hljs-variable">$&#123;CNB_BUILD_USER_NICKNAME:-博客&#125;</span>&quot;</span><br>git config user.email <span class="hljs-string">&quot;<span class="hljs-variable">$&#123;CNB_BUILD_USER_EMAIL:-blog@example.com&#125;</span>&quot;</span><br><br>git add <span class="hljs-string">&quot;<span class="hljs-variable">$POST_DIR</span>/<span class="hljs-variable">$SLUG</span>.md&quot;</span><br><span class="hljs-keyword">if</span> git diff --cached --quiet; <span class="hljs-keyword">then</span><br>  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;内容与仓库中的版本完全一致，无需提交。&quot;</span><br>  <span class="hljs-built_in">exit</span> 0<br><span class="hljs-keyword">fi</span><br><br>git commit -m <span class="hljs-string">&quot;post: <span class="hljs-subst">$(clean <span class="hljs-string">&quot;<span class="hljs-variable">$PUB_TITLE</span>&quot;</span>)</span>&quot;</span><br><br><span class="hljs-comment"># CNB_TOKEN 是流水线内置临时令牌，api_trigger 下拥有 repo-code:rw</span><br><span class="hljs-keyword">if</span> [ -n <span class="hljs-string">&quot;<span class="hljs-variable">$CNB_REPO_URL_HTTPS</span>&quot;</span> ]; <span class="hljs-keyword">then</span><br>  REPO_HOST_PATH=<span class="hljs-string">&quot;<span class="hljs-variable">$&#123;CNB_REPO_URL_HTTPS#https://&#125;</span>&quot;</span><br><span class="hljs-keyword">elif</span> [ -n <span class="hljs-string">&quot;<span class="hljs-variable">$CNB_REPO_SLUG</span>&quot;</span> ]; <span class="hljs-keyword">then</span><br>  REPO_HOST_PATH=<span class="hljs-string">&quot;cnb.cool/<span class="hljs-variable">$CNB_REPO_SLUG</span>&quot;</span><br><span class="hljs-keyword">else</span><br>  <span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;错误：拿不到仓库地址&quot;</span><br>  <span class="hljs-built_in">exit</span> 1<br><span class="hljs-keyword">fi</span><br><br>PUSH_URL=<span class="hljs-string">&quot;https://cnb:<span class="hljs-variable">$&#123;CNB_TOKEN&#125;</span>@<span class="hljs-variable">$&#123;REPO_HOST_PATH&#125;</span>&quot;</span><br>git push <span class="hljs-string">&quot;<span class="hljs-variable">$PUSH_URL</span>&quot;</span> <span class="hljs-string">&quot;HEAD:refs/heads/<span class="hljs-variable">$TARGET_BRANCH</span>&quot;</span><br><br><span class="hljs-built_in">echo</span> <span class="hljs-string">&quot;===== 已推送到 <span class="hljs-variable">$TARGET_BRANCH</span> 分支 =====&quot;</span><br></code></pre></td></tr></table></figure><p><strong>两个设计要点</strong>：</p><ul><li>正文用 <strong>base64</strong> 传，避免中文、换行、引号在多层 shell 传递中被破坏</li><li><code>publish-post.sh</code> 里 <code>git push</code> 会再次触发 <code>main: push</code> 流水线 → 部署。<strong>这是故意的</strong>，形成闭环</li></ul><h3 id="步骤-5：写云函数（4-个文件）">步骤 5：写云函数（4 个文件）</h3><p>放到 <code>cloud-functions/</code> 目录。<strong>目录名必须是 <code>cloud-functions</code></strong>（EdgeOne 已把旧的 <code>node-functions</code> 改名）。</p><h4 id="5-1-cloud-functions-cnb-js-——-公共工具（不会注册成路由）">5.1 <code>cloud-functions/_cnb.js</code> —— 公共工具（不会注册成路由）</h4><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><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><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-variable constant_">DEFAULT_API_BASE</span> = <span class="hljs-string">&#x27;https://api.cnb.cool&#x27;</span>;<br><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-variable constant_">DEFAULT_REPO</span> = <span class="hljs-string">&#x27;你的组织/你的仓库&#x27;</span>;<br><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> <span class="hljs-variable constant_">DEFAULT_BRANCH</span> = <span class="hljs-string">&#x27;main&#x27;</span>;<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">json</span>(<span class="hljs-params">body, status = <span class="hljs-number">200</span></span>) &#123;<br>  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>(<span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(body), &#123;<br>    status,<br>    <span class="hljs-attr">headers</span>: &#123;<br>      <span class="hljs-string">&#x27;content-type&#x27;</span>: <span class="hljs-string">&#x27;application/json; charset=utf-8&#x27;</span>,<br>      <span class="hljs-string">&#x27;cache-control&#x27;</span>: <span class="hljs-string">&#x27;no-store&#x27;</span>,<br>    &#125;,<br>  &#125;);<br>&#125;<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">base64Utf8</span>(<span class="hljs-params">str</span>) &#123;<br>  <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> <span class="hljs-title class_">Buffer</span> !== <span class="hljs-string">&#x27;undefined&#x27;</span>) &#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-title class_">Buffer</span>.<span class="hljs-title function_">from</span>(str, <span class="hljs-string">&#x27;utf8&#x27;</span>).<span class="hljs-title function_">toString</span>(<span class="hljs-string">&#x27;base64&#x27;</span>);<br>  &#125;<br>  <span class="hljs-keyword">const</span> bytes = <span class="hljs-keyword">new</span> <span class="hljs-title class_">TextEncoder</span>().<span class="hljs-title function_">encode</span>(str);<br>  <span class="hljs-keyword">let</span> bin = <span class="hljs-string">&#x27;&#x27;</span>;<br>  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; bytes.<span class="hljs-property">length</span>; i += <span class="hljs-number">1</span>) bin += <span class="hljs-title class_">String</span>.<span class="hljs-title function_">fromCharCode</span>(bytes[i]);<br>  <span class="hljs-keyword">return</span> <span class="hljs-title function_">btoa</span>(bin);<br>&#125;<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">slugify</span>(<span class="hljs-params">input</span>) &#123;<br>  <span class="hljs-keyword">const</span> s = <span class="hljs-title class_">String</span>(input || <span class="hljs-string">&#x27;&#x27;</span>)<br>    .<span class="hljs-title function_">trim</span>()<br>    .<span class="hljs-title function_">toLowerCase</span>()<br>    .<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/[^a-z0-9\u4e00-\u9fa5]+/g</span>, <span class="hljs-string">&#x27;-&#x27;</span>)<br>    .<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/^-+|-+$/g</span>, <span class="hljs-string">&#x27;&#x27;</span>)<br>    .<span class="hljs-title function_">slice</span>(<span class="hljs-number">0</span>, <span class="hljs-number">60</span>);<br>  <span class="hljs-keyword">return</span> s || <span class="hljs-string">`post-<span class="hljs-subst">$&#123;<span class="hljs-built_in">Date</span>.now()&#125;</span>`</span>;<br>&#125;<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">config</span>(<span class="hljs-params">env = &#123;&#125;</span>) &#123;<br>  <span class="hljs-keyword">return</span> &#123;<br>    <span class="hljs-attr">apiBase</span>: <span class="hljs-title class_">String</span>(env.<span class="hljs-property">CNB_API_BASE</span> || <span class="hljs-variable constant_">DEFAULT_API_BASE</span>).<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/\/+$/</span>, <span class="hljs-string">&#x27;&#x27;</span>),<br>    <span class="hljs-attr">repo</span>: <span class="hljs-title class_">String</span>(env.<span class="hljs-property">CNB_REPO</span> || <span class="hljs-variable constant_">DEFAULT_REPO</span>),<br>    <span class="hljs-attr">branch</span>: <span class="hljs-title class_">String</span>(env.<span class="hljs-property">CNB_BRANCH</span> || <span class="hljs-variable constant_">DEFAULT_BRANCH</span>),<br>    <span class="hljs-attr">token</span>: <span class="hljs-title class_">String</span>(env.<span class="hljs-property">CNB_TOKEN</span> || <span class="hljs-string">&#x27;&#x27;</span>),<br>    <span class="hljs-attr">password</span>: <span class="hljs-title class_">String</span>(env.<span class="hljs-property">ADMIN_PASSWORD</span> || <span class="hljs-string">&#x27;&#x27;</span>),<br>  &#125;;<br>&#125;<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">cnb</span>(<span class="hljs-params">cfg, path, init = &#123;&#125;</span>) &#123;<br>  <span class="hljs-keyword">const</span> url = <span class="hljs-string">`<span class="hljs-subst">$&#123;cfg.apiBase&#125;</span>/<span class="hljs-subst">$&#123;cfg.repo&#125;</span>/-/<span class="hljs-subst">$&#123;<span class="hljs-built_in">String</span>(path).replace(/^\/+/, <span class="hljs-string">&#x27;&#x27;</span>)&#125;</span>`</span>;<br>  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(url, &#123;<br>    ...init,<br>    <span class="hljs-attr">headers</span>: &#123;<br>      <span class="hljs-title class_">Authorization</span>: <span class="hljs-string">`Bearer <span class="hljs-subst">$&#123;cfg.token&#125;</span>`</span>,<br>      <span class="hljs-comment">// CNB 的内容协商只认单一值：</span><br>      <span class="hljs-comment">// &quot;application/vnd.cnb.api+json, application/json&quot; 会直接 406，</span><br>      <span class="hljs-comment">// 默认的 */* 也一样被拒。</span><br>      <span class="hljs-title class_">Accept</span>: <span class="hljs-string">&#x27;application/vnd.cnb.api+json&#x27;</span>,<br>      <span class="hljs-string">&#x27;Content-Type&#x27;</span>: <span class="hljs-string">&#x27;application/json&#x27;</span>,<br>      ...(init.<span class="hljs-property">headers</span> || &#123;&#125;),<br>    &#125;,<br>  &#125;);<br>  <span class="hljs-keyword">const</span> text = <span class="hljs-keyword">await</span> res.<span class="hljs-title function_">text</span>();<br>  <span class="hljs-keyword">let</span> data = <span class="hljs-literal">null</span>;<br>  <span class="hljs-keyword">try</span> &#123;<br>    data = text ? <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(text) : <span class="hljs-literal">null</span>;<br>  &#125; <span class="hljs-keyword">catch</span> &#123;<br>    data = &#123; <span class="hljs-attr">raw</span>: text.<span class="hljs-title function_">slice</span>(<span class="hljs-number">0</span>, <span class="hljs-number">500</span>) &#125;;<br>  &#125;<br>  <span class="hljs-keyword">return</span> &#123; <span class="hljs-attr">ok</span>: res.<span class="hljs-property">ok</span>, <span class="hljs-attr">status</span>: res.<span class="hljs-property">status</span>, data &#125;;<br>&#125;<br></code></pre></td></tr></table></figure><blockquote><p><strong>反直觉但很重要</strong>：EdgeOne 的函数是<strong>按文件名注册路由</strong>的。<code>_cnb.js</code> 没有导出 <code>onRequest</code>，所以它只是个普通模块，不会变成 <code>/api/_cnb</code> 这样的接口。</p></blockquote><h4 id="5-2-cloud-functions-api-publish-js-——-发布接口">5.2 <code>cloud-functions/api/publish.js</code> —— 发布接口</h4><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><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><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">import</span> &#123; json, base64Utf8, slugify, config, cnb &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;../_cnb.js&#x27;</span>;<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">onRequestPost</span>(<span class="hljs-params">context</span>) &#123;<br>  <span class="hljs-keyword">const</span> &#123; request, env &#125; = context;<br>  <span class="hljs-keyword">const</span> cfg = <span class="hljs-title function_">config</span>(env);<br><br>  <span class="hljs-keyword">if</span> (!cfg.<span class="hljs-property">password</span>) <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123; <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">&#x27;服务端还没配置 ADMIN_PASSWORD 环境变量&#x27;</span> &#125;, <span class="hljs-number">500</span>);<br>  <span class="hljs-keyword">if</span> (!cfg.<span class="hljs-property">token</span>) <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123; <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">&#x27;服务端还没配置 CNB_TOKEN 环境变量&#x27;</span> &#125;, <span class="hljs-number">500</span>);<br><br>  <span class="hljs-keyword">let</span> body;<br>  <span class="hljs-keyword">try</span> &#123;<br>    body = <span class="hljs-keyword">await</span> request.<span class="hljs-title function_">json</span>();<br>  &#125; <span class="hljs-keyword">catch</span> &#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123; <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">&#x27;请求体不是合法的 JSON&#x27;</span> &#125;, <span class="hljs-number">400</span>);<br>  &#125;<br><br>  <span class="hljs-keyword">if</span> (<span class="hljs-title class_">String</span>(body.<span class="hljs-property">password</span> || <span class="hljs-string">&#x27;&#x27;</span>) !== cfg.<span class="hljs-property">password</span>) &#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123; <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">&#x27;管理密码不正确&#x27;</span> &#125;, <span class="hljs-number">401</span>);<br>  &#125;<br><br>  <span class="hljs-keyword">const</span> title = <span class="hljs-title class_">String</span>(body.<span class="hljs-property">title</span> || <span class="hljs-string">&#x27;&#x27;</span>).<span class="hljs-title function_">trim</span>();<br>  <span class="hljs-keyword">const</span> content = <span class="hljs-title class_">String</span>(body.<span class="hljs-property">content</span> || <span class="hljs-string">&#x27;&#x27;</span>);<br>  <span class="hljs-keyword">if</span> (!title) <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123; <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">&#x27;文章标题不能为空&#x27;</span> &#125;, <span class="hljs-number">400</span>);<br>  <span class="hljs-keyword">if</span> (!content.<span class="hljs-title function_">trim</span>()) <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123; <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">&#x27;文章正文不能为空&#x27;</span> &#125;, <span class="hljs-number">400</span>);<br>  <span class="hljs-keyword">if</span> (content.<span class="hljs-property">length</span> &gt; <span class="hljs-number">60000</span>) &#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123; <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">`正文太长（<span class="hljs-subst">$&#123;content.length&#125;</span> 字），请控制在 6 万字以内`</span> &#125;, <span class="hljs-number">400</span>);<br>  &#125;<br><br>  <span class="hljs-keyword">const</span> slug = <span class="hljs-title function_">slugify</span>(body.<span class="hljs-property">slug</span> || title);<br><br>  <span class="hljs-keyword">const</span> payload = &#123;<br>    <span class="hljs-attr">event</span>: <span class="hljs-string">&#x27;api_trigger&#x27;</span>,<br>    <span class="hljs-attr">branch</span>: cfg.<span class="hljs-property">branch</span>,<br>    <span class="hljs-attr">title</span>: <span class="hljs-string">`发布文章: <span class="hljs-subst">$&#123;title&#125;</span>`</span>,<br>    <span class="hljs-attr">sync</span>: <span class="hljs-string">&#x27;false&#x27;</span>,<br>    <span class="hljs-attr">env</span>: &#123;<br>      <span class="hljs-attr">PUB_TITLE</span>: title,<br>      <span class="hljs-attr">PUB_SLUG</span>: slug,<br>      <span class="hljs-attr">PUB_CONTENT_B64</span>: <span class="hljs-title function_">base64Utf8</span>(content),<br>      <span class="hljs-attr">PUB_TAGS</span>: <span class="hljs-title class_">String</span>(body.<span class="hljs-property">tags</span> || <span class="hljs-string">&#x27;&#x27;</span>),<br>      <span class="hljs-attr">PUB_CATEGORIES</span>: <span class="hljs-title class_">String</span>(body.<span class="hljs-property">categories</span> || <span class="hljs-string">&#x27;&#x27;</span>),<br>    &#125;,<br>  &#125;;<br><br>  <span class="hljs-keyword">const</span> r = <span class="hljs-keyword">await</span> <span class="hljs-title function_">cnb</span>(cfg, <span class="hljs-string">&#x27;build/start&#x27;</span>, &#123; <span class="hljs-attr">method</span>: <span class="hljs-string">&#x27;POST&#x27;</span>, <span class="hljs-attr">body</span>: <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(payload) &#125;);<br><br>  <span class="hljs-keyword">if</span> (!r.<span class="hljs-property">ok</span>) &#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123;<br>      <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>,<br>      <span class="hljs-attr">error</span>: <span class="hljs-string">`触发 CNB 构建失败（HTTP <span class="hljs-subst">$&#123;r.status&#125;</span>）`</span>,<br>      <span class="hljs-attr">hint</span>: <span class="hljs-string">&#x27;检查 CNB_TOKEN 是否有效，以及令牌是否有 repo-cnb-trigger:rw 权限&#x27;</span>,<br>      <span class="hljs-attr">detail</span>: r.<span class="hljs-property">data</span>,<br>    &#125;, <span class="hljs-number">502</span>);<br>  &#125;<br><br>  <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123;<br>    <span class="hljs-attr">ok</span>: <span class="hljs-literal">true</span>,<br>    slug,<br>    <span class="hljs-attr">file</span>: <span class="hljs-string">`Blog/source/_posts/<span class="hljs-subst">$&#123;slug&#125;</span>.md`</span>,<br>    <span class="hljs-attr">build</span>: r.<span class="hljs-property">data</span>,<br>    <span class="hljs-attr">message</span>: <span class="hljs-string">&#x27;已触发发布，约 1~2 分钟后线上更新&#x27;</span>,<br>  &#125;);<br>&#125;<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">onRequestGet</span>(<span class="hljs-params"></span>) &#123;<br>  <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123; <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">&#x27;请用 POST 提交文章&#x27;</span> &#125;, <span class="hljs-number">405</span>);<br>&#125;<br></code></pre></td></tr></table></figure><h4 id="5-3-cloud-functions-api-posts-js-——-文章列表（只读）">5.3 <code>cloud-functions/api/posts.js</code> —— 文章列表（只读）</h4><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><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></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">import</span> &#123; json, config, cnb &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;../_cnb.js&#x27;</span>;<br><br><span class="hljs-keyword">function</span> <span class="hljs-title function_">normalize</span>(<span class="hljs-params">data</span>) &#123;<br>  <span class="hljs-keyword">let</span> arr = [];<br>  <span class="hljs-keyword">if</span> (<span class="hljs-title class_">Array</span>.<span class="hljs-title function_">isArray</span>(data)) &#123;<br>    arr = data;<br>  &#125; <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (data &amp;&amp; <span class="hljs-keyword">typeof</span> data === <span class="hljs-string">&#x27;object&#x27;</span>) &#123;<br>    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> key <span class="hljs-keyword">of</span> [<span class="hljs-string">&#x27;contents&#x27;</span>, <span class="hljs-string">&#x27;files&#x27;</span>, <span class="hljs-string">&#x27;entries&#x27;</span>, <span class="hljs-string">&#x27;list&#x27;</span>, <span class="hljs-string">&#x27;data&#x27;</span>, <span class="hljs-string">&#x27;items&#x27;</span>]) &#123;<br>      <span class="hljs-keyword">if</span> (<span class="hljs-title class_">Array</span>.<span class="hljs-title function_">isArray</span>(data[key])) &#123; arr = data[key]; <span class="hljs-keyword">break</span>; &#125;<br>    &#125;<br>  &#125;<br>  <span class="hljs-keyword">return</span> arr<br>    .<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">it</span>) =&gt;</span> it &amp;&amp; <span class="hljs-keyword">typeof</span> it === <span class="hljs-string">&#x27;object&#x27;</span>)<br>    .<span class="hljs-title function_">map</span>(<span class="hljs-function">(<span class="hljs-params">it</span>) =&gt;</span> &#123;<br>      <span class="hljs-keyword">const</span> path = it.<span class="hljs-property">path</span> || it.<span class="hljs-property">name</span> || <span class="hljs-string">&#x27;&#x27;</span>;<br>      <span class="hljs-keyword">return</span> &#123;<br>        <span class="hljs-attr">name</span>: it.<span class="hljs-property">name</span> || path.<span class="hljs-title function_">split</span>(<span class="hljs-string">&#x27;/&#x27;</span>).<span class="hljs-title function_">pop</span>() || <span class="hljs-string">&#x27;&#x27;</span>,<br>        path,<br>        <span class="hljs-attr">sha</span>: it.<span class="hljs-property">sha</span> || it.<span class="hljs-property">object_sha</span> || <span class="hljs-string">&#x27;&#x27;</span>,<br>        <span class="hljs-attr">size</span>: <span class="hljs-keyword">typeof</span> it.<span class="hljs-property">size</span> === <span class="hljs-string">&#x27;number&#x27;</span> ? it.<span class="hljs-property">size</span> : <span class="hljs-literal">null</span>,<br>        <span class="hljs-attr">type</span>: it.<span class="hljs-property">type</span> || (it.<span class="hljs-property">is_dir</span> ? <span class="hljs-string">&#x27;dir&#x27;</span> : <span class="hljs-string">&#x27;file&#x27;</span>),<br>      &#125;;<br>    &#125;)<br>    .<span class="hljs-title function_">filter</span>(<span class="hljs-function">(<span class="hljs-params">it</span>) =&gt;</span> it.<span class="hljs-property">name</span> &amp;&amp; <span class="hljs-regexp">/\.(md|markdown)$/i</span>.<span class="hljs-title function_">test</span>(it.<span class="hljs-property">name</span>))<br>    .<span class="hljs-title function_">sort</span>(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> a.<span class="hljs-property">name</span>.<span class="hljs-title function_">localeCompare</span>(b.<span class="hljs-property">name</span>));<br>&#125;<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">onRequestGet</span>(<span class="hljs-params">context</span>) &#123;<br>  <span class="hljs-keyword">const</span> cfg = <span class="hljs-title function_">config</span>(context.<span class="hljs-property">env</span>);<br>  <span class="hljs-keyword">if</span> (!cfg.<span class="hljs-property">token</span>) <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123; <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">error</span>: <span class="hljs-string">&#x27;服务端还没配置 CNB_TOKEN 环境变量&#x27;</span> &#125;, <span class="hljs-number">500</span>);<br><br>  <span class="hljs-keyword">const</span> r = <span class="hljs-keyword">await</span> <span class="hljs-title function_">cnb</span>(cfg, <span class="hljs-string">`git/contents/Blog/source/_posts?ref=<span class="hljs-subst">$&#123;<span class="hljs-built_in">encodeURIComponent</span>(cfg.branch)&#125;</span>`</span>, &#123; <span class="hljs-attr">method</span>: <span class="hljs-string">&#x27;GET&#x27;</span> &#125;);<br><br>  <span class="hljs-keyword">if</span> (!r.<span class="hljs-property">ok</span>) &#123;<br>    <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123;<br>      <span class="hljs-attr">ok</span>: <span class="hljs-literal">false</span>,<br>      <span class="hljs-attr">error</span>: <span class="hljs-string">`读取文章列表失败（HTTP <span class="hljs-subst">$&#123;r.status&#125;</span>）`</span>,<br>      <span class="hljs-attr">hint</span>: <span class="hljs-string">&#x27;如果返回 404，通常是 Blog/source/_posts 目录还不存在——先在后台发布一篇就会有&#x27;</span>,<br>      <span class="hljs-attr">detail</span>: r.<span class="hljs-property">data</span>,<br>    &#125;, <span class="hljs-number">502</span>);<br>  &#125;<br><br>  <span class="hljs-keyword">const</span> posts = <span class="hljs-title function_">normalize</span>(r.<span class="hljs-property">data</span>);<br>  <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(&#123; <span class="hljs-attr">ok</span>: <span class="hljs-literal">true</span>, <span class="hljs-attr">count</span>: posts.<span class="hljs-property">length</span>, posts &#125;);<br>&#125;<br></code></pre></td></tr></table></figure><h4 id="5-4-cloud-functions-api-ping-js-——-自检接口">5.4 <code>cloud-functions/api/ping.js</code> —— 自检接口</h4><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><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></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">import</span> &#123; json, config, cnb &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;../_cnb.js&#x27;</span>;<br><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">onRequestGet</span>(<span class="hljs-params">context</span>) &#123;<br>  <span class="hljs-keyword">const</span> cfg = <span class="hljs-title function_">config</span>(context.<span class="hljs-property">env</span>);<br><br>  <span class="hljs-keyword">const</span> out = &#123;<br>    <span class="hljs-attr">ok</span>: <span class="hljs-literal">true</span>,<br>    <span class="hljs-attr">server</span>: &#123;<br>      <span class="hljs-attr">hasPassword</span>: <span class="hljs-title class_">Boolean</span>(cfg.<span class="hljs-property">password</span>),<br>      <span class="hljs-attr">hasToken</span>: <span class="hljs-title class_">Boolean</span>(cfg.<span class="hljs-property">token</span>),<br>      <span class="hljs-attr">apiBase</span>: cfg.<span class="hljs-property">apiBase</span>,<br>      <span class="hljs-attr">repo</span>: cfg.<span class="hljs-property">repo</span>,<br>      <span class="hljs-attr">branch</span>: cfg.<span class="hljs-property">branch</span>,<br>    &#125;,<br>    <span class="hljs-attr">cnb</span>: <span class="hljs-literal">null</span>,<br>  &#125;;<br><br>  <span class="hljs-keyword">if</span> (!cfg.<span class="hljs-property">token</span>) &#123;<br>    out.<span class="hljs-property">ok</span> = <span class="hljs-literal">false</span>;<br>    out.<span class="hljs-property">hint</span> = <span class="hljs-string">&#x27;还没配置 CNB_TOKEN 环境变量&#x27;</span>;<br>    <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(out, <span class="hljs-number">200</span>);<br>  &#125;<br>  <span class="hljs-keyword">if</span> (!cfg.<span class="hljs-property">password</span>) &#123;<br>    out.<span class="hljs-property">ok</span> = <span class="hljs-literal">false</span>;<br>    out.<span class="hljs-property">hint</span> = <span class="hljs-string">&#x27;还没配置 ADMIN_PASSWORD 环境变量&#x27;</span>;<br>    <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(out, <span class="hljs-number">200</span>);<br>  &#125;<br><br>  <span class="hljs-keyword">const</span> r = <span class="hljs-keyword">await</span> <span class="hljs-title function_">cnb</span>(cfg, <span class="hljs-string">&#x27;git/head&#x27;</span>, &#123; <span class="hljs-attr">method</span>: <span class="hljs-string">&#x27;GET&#x27;</span> &#125;);<br>  out.<span class="hljs-property">cnb</span> = &#123; <span class="hljs-attr">ok</span>: r.<span class="hljs-property">ok</span>, <span class="hljs-attr">status</span>: r.<span class="hljs-property">status</span>, <span class="hljs-attr">data</span>: r.<span class="hljs-property">data</span> &#125;;<br><br>  <span class="hljs-keyword">if</span> (!r.<span class="hljs-property">ok</span>) &#123;<br>    out.<span class="hljs-property">ok</span> = <span class="hljs-literal">false</span>;<br>    out.<span class="hljs-property">hint</span> = <span class="hljs-string">&#x27;CNB_TOKEN 无效，或这个令牌没有该仓库的读取权限&#x27;</span>;<br>  &#125; <span class="hljs-keyword">else</span> &#123;<br>    out.<span class="hljs-property">hint</span> = <span class="hljs-string">&#x27;配置正常，可以进 /admin/ 写文章了&#x27;</span>;<br>  &#125;<br>  <span class="hljs-keyword">return</span> <span class="hljs-title function_">json</span>(out, <span class="hljs-number">200</span>);<br>&#125;<br></code></pre></td></tr></table></figure><h4 id="5-5-functions-package-json-——-函数运行时声明">5.5 <code>functions/package.json</code> —— 函数运行时声明</h4><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><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;your-blog&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1.0.0&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;private&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;type&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;module&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;description&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;静态博客 + 云函数&quot;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;scripts&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span><br>    <span class="hljs-attr">&quot;build&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;echo \&quot;静态文件已由流水线构建完成\&quot;&quot;</span><br>  <span class="hljs-punctuation">&#125;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><p>注意 <code>&quot;type&quot;: &quot;module&quot;</code> —— 云函数用的是 ESM 语法（<code>export</code> / <code>import</code>）。</p><h3 id="步骤-6：写线上后台页面-admin-index-html">步骤 6：写线上后台页面 <code>admin/index.html</code></h3><p>一个<strong>单文件</strong>页面（HTML + CSS + JS 全在里面，零外部依赖），部署后通过 <code>https://你的域名/admin/</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><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><code class="hljs js"><span class="hljs-comment">// ① 登录：密码只存本机 localStorage，真正校验在服务端</span><br><span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">setItem</span>(<span class="hljs-string">&#x27;admin_pw&#x27;</span>, pw);<br><br><span class="hljs-comment">// ② 实时预览：把 Markdown 转成 HTML 显示在右侧</span><br><span class="hljs-keyword">function</span> <span class="hljs-title function_">render</span>(<span class="hljs-params">md</span>) &#123;<br>  <span class="hljs-keyword">return</span> md<br>    .<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/^### (.*)$/gm</span>, <span class="hljs-string">&#x27;&lt;h3&gt;$1&lt;/h3&gt;&#x27;</span>)<br>    .<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/^## (.*)$/gm</span>, <span class="hljs-string">&#x27;&lt;h2&gt;$1&lt;/h2&gt;&#x27;</span>)<br>    .<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/^# (.*)$/gm</span>, <span class="hljs-string">&#x27;&lt;h1&gt;$1&lt;/h1&gt;&#x27;</span>)<br>    .<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/\*\*(.+?)\*\*/g</span>, <span class="hljs-string">&#x27;&lt;strong&gt;$1&lt;/strong&gt;&#x27;</span>)<br>    .<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/`([^`]+)`/g</span>, <span class="hljs-string">&#x27;&lt;code&gt;$1&lt;/code&gt;&#x27;</span>)<br>    .<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/^- (.*)$/gm</span>, <span class="hljs-string">&#x27;&lt;li&gt;$1&lt;/li&gt;&#x27;</span>)<br>    .<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/\[(.+?)\]\((.+?)\)/g</span>, <span class="hljs-string">&#x27;&lt;a href=&quot;$2&quot;&gt;$1&lt;/a&gt;&#x27;</span>);<br>&#125;<br><br><span class="hljs-comment">// ③ 发布：POST 到同源接口，不涉及跨域</span><br><span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-string">&#x27;/api/publish&#x27;</span>, &#123;<br>  <span class="hljs-attr">method</span>: <span class="hljs-string">&#x27;POST&#x27;</span>,<br>  <span class="hljs-attr">headers</span>: &#123; <span class="hljs-string">&#x27;content-type&#x27;</span>: <span class="hljs-string">&#x27;application/json&#x27;</span> &#125;,<br>  <span class="hljs-attr">body</span>: <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(&#123; password, title, slug, tags, categories, content &#125;),<br>&#125;);<br></code></pre></td></tr></table></figure><p>页面提供 3 个标签页：<strong>写文章</strong>（标题/文件名/标签/分类/正文 + 实时预览）、<strong>文章列表</strong>（读仓库）、<strong>设置</strong>（接口地址、退出登录）。</p><blockquote><p>完整代码较长（含样式），建议直接抄仓库里的 <code>admin/index.html</code>。写的时候注意：如果用 <code>const $ = id =&gt; document.getElementById(id)</code> 这种简写，调用时<strong>不要</strong>带 <code>#</code>（<code>$('#title')</code> 会把 <code>#</code> 当成 id 的一部分，返回 null）。</p></blockquote><h3 id="步骤-7：站点配置-edgeone-json">步骤 7：站点配置 <code>edgeone.json</code></h3><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><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><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;headers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>    <span class="hljs-punctuation">&#123;</span><br>      <span class="hljs-attr">&quot;source&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/admin/*&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;headers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>        <span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;X-Robots-Tag&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;noindex, nofollow&quot;</span> <span class="hljs-punctuation">&#125;</span><br>      <span class="hljs-punctuation">]</span><br>    <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>    <span class="hljs-punctuation">&#123;</span><br>      <span class="hljs-attr">&quot;source&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/api/*&quot;</span><span class="hljs-punctuation">,</span><br>      <span class="hljs-attr">&quot;headers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>        <span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;key&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Cache-Control&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;value&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;no-store, max-age=0&quot;</span> <span class="hljs-punctuation">&#125;</span><br>      <span class="hljs-punctuation">]</span><br>    <span class="hljs-punctuation">&#125;</span><br>  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;rewrites&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br>    <span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;source&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/admin&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;destination&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/admin/index.html&quot;</span> <span class="hljs-punctuation">&#125;</span><br>  <span class="hljs-punctuation">]</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><ul><li><code>/admin/*</code> 加 <code>noindex</code>，不让搜索引擎收录你的后台</li><li><code>/api/*</code> 禁缓存，否则自检和发布结果会读到旧值</li><li><code>rewrites</code> 让 <code>/admin</code>（不带斜杠）也能打开后台</li></ul><h3 id="步骤-8：推代码-配环境变量-首次部署">步骤 8：推代码 + 配环境变量 + 首次部署</h3><p><strong>8.1 推代码到 CNB</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></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">cd</span> 你的项目目录<br>git init<br>git add -A<br>git commit -m <span class="hljs-string">&quot;初始化博客&quot;</span><br>git remote add origin https://cnb.cool/你的组织/你的仓库.git<br>git push -u origin main<br></code></pre></td></tr></table></figure><p>（如果本机没装 git，也可以用 CNB 网页的&quot;上传文件&quot;功能，或 WebIDE 里的终端。）</p><p><strong>8.2 配 4 个环境变量</strong></p><p>EdgeOne Pages → 你的项目 → 设置 → 环境变量 → 生产环境，添加：</p><table><thead><tr><th>变量名</th><th>值</th></tr></thead><tbody><tr><td><code>ADMIN_PASSWORD</code></td><td>你定的后台密码</td></tr><tr><td><code>CNB_TOKEN</code></td><td>你的 CNB 访问令牌</td></tr><tr><td><code>CNB_REPO</code></td><td><code>你的组织/你的仓库</code></td></tr><tr><td><code>CNB_BRANCH</code></td><td><code>main</code></td></tr></tbody></table><p><strong>也可以用 CLI 配</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></pre></td><td class="code"><pre><code class="hljs bash">npx edgeone pages <span class="hljs-built_in">link</span> -n 你的项目名 -t &lt;EDGEONE_PAGES_API_TOKEN&gt;<br>npx edgeone pages <span class="hljs-built_in">env</span> <span class="hljs-built_in">set</span> ADMIN_PASSWORD <span class="hljs-string">&#x27;你的密码&#x27;</span> -t &lt;EDGEONE_PAGES_API_TOKEN&gt; -e production<br>npx edgeone pages <span class="hljs-built_in">env</span> <span class="hljs-built_in">set</span> CNB_TOKEN <span class="hljs-string">&#x27;你的CNB令牌&#x27;</span> -t &lt;EDGEONE_PAGES_API_TOKEN&gt; -e production<br>npx edgeone pages <span class="hljs-built_in">env</span> <span class="hljs-built_in">set</span> CNB_REPO <span class="hljs-string">&#x27;你的组织/你的仓库&#x27;</span> -t &lt;EDGEONE_PAGES_API_TOKEN&gt; -e production<br>npx edgeone pages <span class="hljs-built_in">env</span> <span class="hljs-built_in">set</span> CNB_BRANCH <span class="hljs-string">&#x27;main&#x27;</span> -t &lt;EDGEONE_PAGES_API_TOKEN&gt; -e production<br></code></pre></td></tr></table></figure><blockquote><p>⚠️ <strong>CLI 的 <code>env set</code> 会把变量写进本地 <code>.env</code> 文件</strong>，务必确保 <code>.gitignore</code> 里有 <code>.env</code> 和 <code>.edgeone/</code>（见「踩坑 4」）。</p></blockquote><p><strong>8.3 <code>.gitignore</code>（必须）</strong></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></pre></td><td class="code"><pre><code class="hljs gitignore">node_modules/<br>dist/<br>dist_stale_*/<br>_stale/<br>Blog/db.json<br>Blog/public/<br>Blog/.deploy*/<br><br># 密钥相关，绝对不能进仓库<br>.env<br>.env.*<br>.edgeone/<br></code></pre></td></tr></table></figure><p><strong>8.4 触发首次部署</strong></p><p>推一次代码（或第一次提交就会触发 <code>main: push</code> 流水线），流水线会：</p><ol><li>渲染 Hexo</li><li>组装 dist</li><li><code>edgeone pages deploy</code> 到你的项目</li></ol><p>等 1~2 分钟，打开 <code>https://你的项目名.edgeone.dev/</code> 看效果。</p><p><strong>8.5 自检</strong></p><p>浏览器打开 <code>https://你的域名/api/ping</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></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br>  <span class="hljs-attr">&quot;ok&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;server&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;hasPassword&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;hasToken&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;apiBase&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://api.cnb.cool&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;repo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;你的组织/你的仓库&quot;</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;branch&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;main&quot;</span> <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;cnb&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">&#123;</span> <span class="hljs-attr">&quot;ok&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">true</span></span><span class="hljs-punctuation">,</span> <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">200</span> <span class="hljs-punctuation">&#125;</span><span class="hljs-punctuation">,</span><br>  <span class="hljs-attr">&quot;hint&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;配置正常，可以进 /admin/ 写文章了&quot;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure><ul><li><code>hasPassword: false</code> → <code>ADMIN_PASSWORD</code> 没配上</li><li><code>hasToken: false</code> → <code>CNB_TOKEN</code> 没配上</li><li><code>cnb.ok: false</code> 且 status 是 401/403 → 令牌无效或权限不够</li><li><code>cnb.ok: false</code> 且 status 是 406 → 代码里的 <code>Accept</code> 头写成了多个值（见「踩坑 3」）</li></ul><p><strong>8.6 绑自己的域名（可选）</strong></p><ol><li>域名商处加一条 CNAME，指向 <code>你的项目名.edgeone.dev</code></li><li>EdgeOne Pages 项目里添加自定义域名</li><li>改 <code>Blog/_config.yml</code> 的 <code>url</code>，重新推送</li></ol><hr><h2 id="4-发文">4. 发文</h2><h3 id="方式-A：网页后台（推荐）">方式 A：网页后台（推荐）</h3><figure class="highlight awk"><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><code class="hljs awk">打开 https:<span class="hljs-regexp">//</span>你的域名<span class="hljs-regexp">/admin/</span><br>  → 输入密码<br>  → 写文章页：填标题 → 写正文（右侧实时预览）<br>  → 标签/分类可留空；文件名留空会自动生成<br>  → 点「发布到线上」<br>  → 等约 <span class="hljs-number">1</span> 分钟，刷新博客首页即见<br></code></pre></td></tr></table></figure><p><strong>几个细节</strong>：</p><ul><li><strong>草稿</strong>：点「存草稿」把内容存在你本机浏览器（localStorage），换设备看不到</li><li><strong>文件名</strong>：留空时按标题生成，中文会保留；纯符号标题会退化成时间戳</li><li><strong>正文长度上限</strong>：6 万字</li><li><strong>发布后能不能改</strong>：可以，同样的文件名再发一次会覆盖；换文件名则是新文章</li><li><strong>删文章</strong>：后台不支持删除，需要到 CNB 网页里把那篇 <code>.md</code> 删掉（或在 WebIDE 里删），删完会自动触发重新部署</li></ul><h3 id="方式-B：让-AI-代发">方式 B：让 AI 代发</h3><p>如果你在用 AI 助手协作，也可以把标题 + 正文给它，让它调发布接口推上去，效果和后台点发布一样。</p><h3 id="Markdown-速查">Markdown 速查</h3><table><thead><tr><th>想要的效果</th><th>写法</th></tr></thead><tbody><tr><td>一级标题</td><td><code># 标题</code></td></tr><tr><td>二级标题</td><td><code>## 标题</code></td></tr><tr><td>加粗</td><td><code>**加粗**</code></td></tr><tr><td>斜体</td><td><code>*斜体*</code></td></tr><tr><td>无序列表</td><td><code>- 项目</code></td></tr><tr><td>有序列表</td><td><code>1. 项目</code></td></tr><tr><td>链接</td><td><code>[文字](https://...)</code></td></tr><tr><td>图片</td><td><code>![说明](图片地址)</code></td></tr><tr><td>行内代码</td><td><code>`code`</code></td></tr><tr><td>代码块</td><td><code>```js</code> 换行 代码 换行 <code>```</code></td></tr><tr><td>引用</td><td><code>&gt; 引用内容</code></td></tr><tr><td>表格</td><td>用 <code>|</code> 分隔的 Markdown 表格语法</td></tr><tr><td>分割线</td><td><code>---</code></td></tr></tbody></table><p><strong>发文时最常用的三条</strong>：<code>##</code> 小标题、<code>**加粗**</code>、代码块。其余可以慢慢学。</p><hr><h2 id="5-日常维护">5. 日常维护</h2><h3 id="改后台密码">改后台密码</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash">npx edgeone pages <span class="hljs-built_in">env</span> <span class="hljs-built_in">set</span> ADMIN_PASSWORD <span class="hljs-string">&#x27;新密码&#x27;</span> -t &lt;EDGEONE_PAGES_API_TOKEN&gt; -e production<br></code></pre></td></tr></table></figure><p>不用重新部署，环境变量立即生效（在后台「设置」里点「退出登录」清掉本机旧密码再重进）。</p><h3 id="换域名">换域名</h3><ol><li>域名商加 CNAME → 指向 EdgeOne 项目域名</li><li>EdgeOne Pages 里绑定自定义域名</li><li>改 <code>Blog/_config.yml</code> 的 <code>url</code> 为新域名</li><li>推送代码（或后台发一篇）触发重新部署</li></ol><h3 id="手动本地部署">手动本地部署</h3><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><code class="hljs bash"><span class="hljs-built_in">cd</span> Blog &amp;&amp; hexo clean &amp;&amp; hexo generate &amp;&amp; <span class="hljs-built_in">cd</span> ..<br>sh ./build.sh<br>npx edgeone pages deploy dist -n 你的项目名 -t &lt;EDGEONE_PAGES_API_TOKEN&gt; --skip-ai-gateway-sync --json<br></code></pre></td></tr></table></figure><h3 id="看构建状态">看构建状态</h3><ul><li>CNB 仓库页 → 构建 → 构建历史</li><li>触发构建后可以用 CNB OpenAPI 查状态：<code>GET /&#123;repo&#125;/-/build/status/&#123;构建号&#125;</code></li></ul><h3 id="每次发文的实际耗时">每次发文的实际耗时</h3><p>实测一次发布全程 <strong>56~68 秒</strong>：</p><table><thead><tr><th>阶段</th><th>耗时</th></tr></thead><tbody><tr><td>写入文章并推送</td><td>0.3 秒</td></tr><tr><td>渲染并部署</td><td>56~62 秒</td></tr></tbody></table><hr><h2 id="6-踩坑记录">6. 踩坑记录</h2><p>这一节是全文最值钱的部分 —— 下面每一个坑都是实测撞出来的。</p><h3 id="坑-1：CNB-OpenAPI-只能读不能写">坑 1：CNB OpenAPI 只能读不能写</h3><p><strong>现象</strong>：想直接在浏览器里调 CNB API 写文章，接口返回 404。</p><p><strong>实测结果</strong>：</p><table><thead><tr><th>接口</th><th>方法</th><th>结果</th></tr></thead><tbody><tr><td>读分支 / 读目录 / 读文件 / 读 commit</td><td>GET</td><td>✅ 200</td></tr><tr><td><code>/-/git/blobs</code>（创建散对象）</td><td>POST</td><td>403（有路由，但组不成 commit）</td></tr><tr><td><code>/-/git/commits</code>（建提交）</td><td>POST</td><td>❌ <strong>404 路由不存在</strong></td></tr><tr><td><code>/-/git/contents/&#123;path&#125;</code>（写文件）</td><td>PUT</td><td>❌ <strong>404 路由不存在</strong></td></tr></tbody></table><p>扫完全量 OpenAPI 文档，唯一和写相关的操作是 <code>CreateBlob</code>，<strong>没有</strong> <code>CreateCommit</code> / <code>CreateTree</code> / <code>UpdateRef</code> / <code>PutContent</code>。</p><p><strong>结论</strong>：CNB 上写仓库只有一条路 —— <strong>git push over HTTPS</strong>（用户名 + 令牌当密码）。</p><p><strong>解法</strong>：把&quot;写文件&quot;这一步交给 CNB 流水线做（流水线里有 git 环境），云函数只负责&quot;触发流水线&quot;。</p><h3 id="坑-2：api-cnb-cool-没有-CORS-头">坑 2：<code>api.cnb.cool</code> 没有 CORS 头</h3><p><strong>现象</strong>：浏览器直接 <code>fetch('https://api.cnb.cool/...')</code> 必被拦。</p><p><strong>实测</strong>：OPTIONS 预检、带 <code>Origin</code> 的 200 响应、甚至把 <code>Origin</code> 换成 <code>https://cnb.cool</code> 自己 —— <strong>全都没有 <code>Access-Control-Allow-Origin</code></strong>。</p><p><strong>结论</strong>：浏览器不能直连 CNB API。</p><p><strong>解法</strong>：必须有一个<strong>同源的</strong>服务端代理。EdgeOne 云函数正好挂在同一个域名下（<code>/api/*</code>），天然同源，零跨域。</p><h3 id="坑-3：CNB-只认单一的-Accept-头（406）">坑 3：CNB 只认单一的 <code>Accept</code> 头（406）</h3><p><strong>现象</strong>：自检接口一直返回 <code>cnb.ok: false, status: 406</code>，但用 curl 直接调 CNB 是好的。</p><p><strong>根因</strong>：云函数里写成了</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-title class_">Accept</span>: <span class="hljs-string">&#x27;application/vnd.cnb.api+json, application/json&#x27;</span>   <span class="hljs-comment">// ❌ 406</span><br></code></pre></td></tr></table></figure><p>CNB 的内容协商<strong>不接受逗号分隔的合并值</strong>。实测：</p><table><thead><tr><th>Accept 值</th><th>结果</th></tr></thead><tbody><tr><td><code>application/vnd.cnb.api+json, application/json</code></td><td>❌ 406</td></tr><tr><td><code>application/vnd.cnb.api+json</code></td><td>✅ 200</td></tr><tr><td><code>application/json</code></td><td>✅ 200</td></tr><tr><td>不带 Accept（默认 <code>*/*</code>）</td><td>❌ 406</td></tr></tbody></table><p><strong>解法</strong>：只给一个值。</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-title class_">Accept</span>: <span class="hljs-string">&#x27;application/vnd.cnb.api+json&#x27;</span>   <span class="hljs-comment">// ✅</span><br></code></pre></td></tr></table></figure><h3 id="坑-4：EdgeOne-CLI-会把密钥写进本地-env">坑 4：EdgeOne CLI 会把密钥写进本地 <code>.env</code></h3><p><strong>这是一次真实的安全事故，务必重视。</strong></p><p><strong>经过</strong>：用 <code>edgeone pages env set</code> 配环境变量时，CLI 顺手把 4 个变量写进了本地 <code>.env</code>。因为 <code>.gitignore</code> 当时<strong>没有</strong>排除 <code>.env</code>，结果这个文件被推送脚本一起提交进了仓库 —— 后台密码和 CNB 令牌<strong>明文进过仓库历史</strong>。</p><p><strong>影响</strong>：即使之后把文件删掉，旧提交对象可能仍存在于服务端。</p><p><strong>处理</strong>：</p><ol><li><code>.gitignore</code> 补上 <code>.env</code>、<code>.env.*</code>、<code>.edgeone/</code></li><li>重建历史 + 强推，让仓库只剩一个干净提交</li><li><strong>轮换所有泄露过的密钥</strong>（换 CNB 令牌 + 换后台密码）</li><li>确认仓库是<strong>私密</strong>的</li></ol><p><strong>教训</strong>：</p><ul><li>用任何 CLI 配密钥后，<strong>第一件事是检查它有没有在本地留下明文文件</strong></li><li><code>.gitignore</code> 要在放密钥<strong>之前</strong>就配好，不能事后补</li></ul><h3 id="坑-5：isomorphic-git-的-快进-不会检出文件">坑 5：isomorphic-git 的&quot;快进&quot;不会检出文件</h3><p><strong>背景</strong>：本机没装 git，用纯 JS 的 <code>isomorphic-git</code> 做推送工具。</p><p><strong>现象</strong>：线上后台发了一篇文章后（远端比本地新），本地再同步时，那篇文章被当成&quot;已删除&quot;提交上去了。</p><p><strong>根因</strong>：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">await</span> git.<span class="hljs-title function_">merge</span>(&#123; fs, dir, ours, theirs, <span class="hljs-attr">fastForwardOnly</span>: <span class="hljs-literal">true</span> &#125;);   <span class="hljs-comment">// ❌ 只移动分支指针</span><br></code></pre></td></tr></table></figure><p><code>merge(&#123;fastForwardOnly:true&#125;)</code> <strong>只更新分支指针，不会把新文件检出到工作区</strong>。所以工作区里没有那个文件，<code>statusMatrix</code> 就认为它是&quot;被删除了&quot;。</p><p><strong>正确写法</strong>：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">await</span> git.<span class="hljs-title function_">writeRef</span>(&#123; fs, dir, <span class="hljs-attr">ref</span>: <span class="hljs-string">`refs/heads/<span class="hljs-subst">$&#123;BRANCH&#125;</span>`</span>, <span class="hljs-attr">value</span>: remoteSha, <span class="hljs-attr">force</span>: <span class="hljs-literal">true</span> &#125;);<br><span class="hljs-keyword">await</span> git.<span class="hljs-title function_">checkout</span>(&#123; fs, dir, <span class="hljs-attr">ref</span>: <span class="hljs-variable constant_">BRANCH</span>, <span class="hljs-attr">force</span>: <span class="hljs-literal">true</span> &#125;);   <span class="hljs-comment">// ✅ 真正检出</span><br></code></pre></td></tr></table></figure><h3 id="坑-6：函数目录名是-cloud-functions">坑 6：函数目录名是 <code>cloud-functions</code></h3><p>EdgeOne 早期文档写的是 <code>node-functions</code>，后来改名为 <strong><code>cloud-functions</code></strong>。写错目录名，函数不会生效（表现为 <code>/api/ping</code> 返回静态站的 404 页面）。</p><p>另外：<strong>手动构建时，要把函数文件和 <code>package.json</code> 一起放进部署产物目录</strong>，CLI 才能识别。</p><h3 id="坑-7：api-trigger-要挂在分支名下">坑 7：<code>api_trigger</code> 要挂在分支名下</h3><p><code>.cnb.yml</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></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">main:</span><br>  <span class="hljs-attr">push:</span> [<span class="hljs-string">...</span>]          <span class="hljs-comment"># ✅</span><br>  <span class="hljs-attr">api_trigger:</span> [<span class="hljs-string">...</span>]   <span class="hljs-comment"># ✅</span><br></code></pre></td></tr></table></figure><p>写成顶层 <code>api_trigger:</code> 不会触发。</p><p>另外 <code>POST /&#123;repo&#125;/-/build/start</code> 的 <code>event</code> 字段只能是 <code>api_trigger</code>（或它的前缀），<strong>没法用它触发 <code>push</code> 流水线</strong>。</p><h3 id="附：沙箱-网络相关（仅影响本地调试）">附：沙箱/网络相关（仅影响本地调试）</h3><ul><li>某些受限网络环境下访问不了 <code>*.edgeone.dev</code>（curl 直连超时），但 <code>api.cnb.cool</code> 和 EdgeOne 的部署 API 是通的</li><li>验证线上内容可以换个通道（比如用能出网的抓取工具），测 POST 类接口可以本地 <code>import</code> 云函数模块直接调用处理器</li></ul><hr><h2 id="7-安全清单">7. 安全清单</h2><p>搭完自查一遍：</p><ul><li>[ ] CNB 仓库设为<strong>私密</strong></li><li>[ ] <code>.gitignore</code> 里有 <code>.env</code>、<code>.env.*</code>、<code>.edgeone/</code></li><li>[ ] <code>.cnb.yml</code> 里的 EdgeOne Token 只在这一个文件里，且仓库私密</li><li>[ ] EdgeOne 环境变量里配的是 <code>ADMIN_PASSWORD</code> / <code>CNB_TOKEN</code>，<strong>不要</strong>把这些值写进任何前端文件</li><li>[ ] <code>edgeone.json</code> 里 <code>/admin/*</code> 有 <code>noindex</code></li><li>[ ] CNB 令牌权限<strong>最小化</strong>：只给 <code>repo-code</code> 和 <code>repo-cnb-trigger</code>，不勾其他</li><li>[ ] 用 CLI 配完密钥后，检查本地有没有生成 <code>.env</code> 之类的明文文件</li><li>[ ] 后台密码别用弱密码；怀疑泄露就换（换密码不用重新部署）</li><li>[ ] 定期轮换 CNB 访问令牌</li></ul><p><strong>关于后台安全性的说明</strong>：后台密码校验发生在服务端云函数里，密码不会下发到浏览器；CNB 令牌只存在 EdgeOne 环境变量中，前端代码里没有任何密钥。因此即使有人看到你的 <code>/admin/</code> 页面，没有密码也做不了任何事。</p><hr><h2 id="附录：文件清单">附录：文件清单</h2><h3 id="项目根目录">项目根目录</h3><table><thead><tr><th>文件</th><th>作用</th></tr></thead><tbody><tr><td><code>.cnb.yml</code></td><td>流水线定义（WebIDE / push 构建 / api_trigger 发布）</td></tr><tr><td><code>.gitignore</code></td><td>排除依赖、产物、密钥文件</td></tr><tr><td><code>build.sh</code></td><td>组装部署产物 <code>dist/</code></td></tr><tr><td><code>edgeone.json</code></td><td>站点配置（响应头、路由重写）</td></tr><tr><td><code>TUTORIAL.md</code></td><td>本文的 Markdown 全文</td></tr><tr><td><code>.ide/Dockerfile</code></td><td>WebIDE 的容器环境（Node 20 + code-server）</td></tr><tr><td><code>Blog/</code></td><td>Hexo 工程（源码 + 主题 + 文章）</td></tr><tr><td><code>admin/index.html</code></td><td>线上后台页面（单文件）</td></tr><tr><td><code>cloud-functions/</code></td><td>云函数（<code>_cnb.js</code> + <code>api/&#123;publish,posts,ping&#125;.js</code>）</td></tr><tr><td><code>functions/package.json</code></td><td>函数运行时声明（<code>type: module</code>）</td></tr><tr><td><code>scripts/build-and-deploy.sh</code></td><td>渲染 + 部署</td></tr><tr><td><code>scripts/publish-post.sh</code></td><td>写文章 + git push</td></tr></tbody></table><h3 id="Blog-里的关键文件"><code>Blog/</code> 里的关键文件</h3><table><thead><tr><th>文件</th><th>作用</th></tr></thead><tbody><tr><td><code>_config.yml</code></td><td>站点配置（标题、作者、url、theme）</td></tr><tr><td><code>_config.fluid.yml</code></td><td>主题配置（样式、导航、评论等，注释很全）</td></tr><tr><td><code>source/_posts/*.md</code></td><td>文章</td></tr><tr><td><code>themes/fluid/</code></td><td>主题本体</td></tr><tr><td><code>scaffolds/post.md</code></td><td><code>hexo new</code> 时的模板（定义 front-matter 结构）</td></tr></tbody></table><h3 id="三个核心文件的关系">三个核心文件的关系</h3><figure class="highlight nix"><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></pre></td><td class="code"><pre><code class="hljs nix">管理员点发布<br>    │<br>    ├─ admin<span class="hljs-symbol">/index.html</span>  ──POST──▶  cloud-functions<span class="hljs-symbol">/api/publish.js</span><br>    │                                      │<br>    │                                      ▼<br>    │                          CNB OpenAPI build<span class="hljs-symbol">/start</span> (api_trigger)<br>    │                                      │<br>    │                                      ▼<br>    │                          scripts<span class="hljs-symbol">/publish-post.sh</span>  → 写 md <span class="hljs-operator">+</span> git push<br>    │                                      │<br>    └──────────────────────────────────────┤<br>                                           ▼<br>                                scripts<span class="hljs-symbol">/build-and-deploy.sh</span><br>                                   → hexo generate → build.sh → dist<span class="hljs-symbol">/</span><br>                                   → edgeone pages deploy<br></code></pre></td></tr></table></figure><hr><h2 id="成本与限制">成本与限制</h2><table><thead><tr><th>项目</th><th>情况</th></tr></thead><tbody><tr><td>费用</td><td>0（CNB 免费额度 + EdgeOne Pages 免费额度）</td></tr><tr><td>服务器</td><td>不需要</td></tr><tr><td>备案</td><td>不需要（用海外访问区）</td></tr><tr><td>国内访问速度</td><td>走海外节点，首屏约 1~3 秒，偶有波动</td></tr><tr><td>想要国内秒开</td><td>需要域名备案 + 用 EdgeOne 国内版选「国内可访问」</td></tr><tr><td>单篇正文上限</td><td>6 万字</td></tr><tr><td>发布延迟</td><td>约 1 分钟</td></tr></tbody></table><hr><p><em>本文记录的每一步都经过实际运行验证。文中所有密钥均为占位符，照做时请替换成你自己的。</em></p>]]></content>
    
    
    <summary type="html">&lt;blockquote&gt;
&lt;p&gt;用 CNB 云原生构建 + EdgeOne Pages + Hexo，实现&lt;strong&gt;浏览器里点鼠标发文&lt;/strong&gt;。&lt;br&gt;
全程不需要服务器、不需要备案、不需要在本地装任何环境。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;</summary>
    
    
    
    <category term="教程" scheme="https://nixingzhe-blog.edgeone.dev/categories/%E6%95%99%E7%A8%8B/"/>
    
    
    <category term="建站" scheme="https://nixingzhe-blog.edgeone.dev/tags/%E5%BB%BA%E7%AB%99/"/>
    
    <category term="Hexo" scheme="https://nixingzhe-blog.edgeone.dev/tags/Hexo/"/>
    
    <category term="CNB" scheme="https://nixingzhe-blog.edgeone.dev/tags/CNB/"/>
    
    <category term="EdgeOne" scheme="https://nixingzhe-blog.edgeone.dev/tags/EdgeOne/"/>
    
  </entry>
  
  <entry>
    <title>这个博客是怎么搭起来的</title>
    <link href="https://nixingzhe-blog.edgeone.dev/2026/09/17/how-this-blog-works/"/>
    <id>https://nixingzhe-blog.edgeone.dev/2026/09/17/how-this-blog-works/</id>
    <published>2026-09-17T03:14:30.000Z</published>
    <updated>2026-09-17T03:14:33.164Z</updated>
    
    <content type="html"><![CDATA[<h2 id="为什么写这一篇">为什么写这一篇</h2><p>这是站点的第一篇正式文章，顺便记录一下这套博客的搭建方式 —— 因为它和常见的「买服务器 + 装 WordPress」完全不一样。</p><h2 id="整体结构">整体结构</h2><p>整套东西没有任何服务器，也不需要备案：</p><table><thead><tr><th>环节</th><th>由谁负责</th></tr></thead><tbody><tr><td>代码托管 + 自动构建</td><td>CNB 云原生构建</td></tr><tr><td>静态页面托管 + CDN</td><td>EdgeOne Pages</td></tr><tr><td>写文章</td><td>线上后台，浏览器里点鼠标</td></tr></tbody></table><h2 id="一篇文章的旅程">一篇文章的旅程</h2><figure class="highlight objectivec"><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><code class="hljs objectivec">在后台填标题、写正文，点「发布」<br>   ↓<br>云函数收到请求，用服务端保存的令牌去调 <span class="hljs-built_in">CNB</span> 的构建接口<br>   ↓<br><span class="hljs-built_in">CNB</span> 启动一条流水线：把文章写成 Markdown 提交进仓库<br>   ↓<br>提交动作又触发一条构建流水线：渲染 → 打包 → 部署<br>   ↓<br>大约一到两分钟后，文章出现在网站上<br></code></pre></td></tr></table></figure><h2 id="这样做的好处">这样做的好处</h2><ul><li><strong>不用管服务器</strong>：不用买、不用配、不用担心被攻击，也没有续费问题</li><li><strong>不用备案</strong>：静态站托管在境外节点，开通即用</li><li><strong>有完整历史</strong>：每篇文章都是一次代码提交，改错了随时回滚</li><li><strong>随时随地写</strong>：只要有个浏览器，手机也能发文</li></ul><h2 id="代价">代价</h2><ul><li>访问速度取决于 CDN 节点，国内打开会绕一下</li><li>后台功能比不上成熟的动态博客系统，没有草稿箱、定时发布这些</li></ul><p>不过对一个「记录和分享」为主的站点来说，够用了。</p><hr><p>以后就在这里更新了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;h2 id=&quot;为什么写这一篇&quot;&gt;为什么写这一篇&lt;/h2&gt;
&lt;p&gt;这是站点的第一篇正式文章，顺便记录一下这套博客的搭建方式 —— 因为它和常见的「买服务器 + 装 WordPress」完全不一样。&lt;/p&gt;
&lt;h2 id=&quot;整体结构&quot;&gt;整体结构&lt;/h2&gt;
&lt;p&gt;整套东西没有任何服务</summary>
      
    
    
    
    <category term="站务" scheme="https://nixingzhe-blog.edgeone.dev/categories/%E7%AB%99%E5%8A%A1/"/>
    
    
    <category term="站务" scheme="https://nixingzhe-blog.edgeone.dev/tags/%E7%AB%99%E5%8A%A1/"/>
    
  </entry>
  
  <entry>
    <title>你好，逆行者AI博客</title>
    <link href="https://nixingzhe-blog.edgeone.dev/2026/09/17/hello-world/"/>
    <id>https://nixingzhe-blog.edgeone.dev/2026/09/17/hello-world/</id>
    <published>2026-09-17T02:50:00.000Z</published>
    <updated>2026-09-17T03:02:49.551Z</updated>
    
    <content type="html"><![CDATA[<p>欢迎来到 <strong>逆行者AI博客</strong>。这里记录 AI 时代的学习、思考与实践。</p><h2 id="这个博客是怎么搭起来的">这个博客是怎么搭起来的</h2><p>整套东西跑在两条流水线上，没有服务器、没有备案，也不需要本地装任何环境：</p><table><thead><tr><th>环节</th><th>承担者</th></tr></thead><tbody><tr><td>代码托管 + 自动构建</td><td>CNB 云原生构建（<code>cnb.cool</code>）</td></tr><tr><td>静态站托管 + CDN</td><td>EdgeOne Pages（项目 <code>nixingzhe-blog</code>）</td></tr><tr><td>写文章</td><td>线上后台 <code>/admin/</code>，浏览器里点鼠标</td></tr></tbody></table><h2 id="发文长什么样">发文长什么样</h2><p>打开后台，填标题、写正文，点「发布」，剩下的全自动：</p><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs dart">后台点发布 → 触发 CNB 流水线 → 写入文章 → <span class="hljs-title class_">Hexo</span> 渲染 → 部署 <span class="hljs-title class_">EdgeOne</span> → 上线<br></code></pre></td></tr></table></figure><p>大概 1~2 分钟就能在网站上看到新文章。</p><h2 id="一篇示例：Markdown-都支持什么">一篇示例：Markdown 都支持什么</h2><p><strong>加粗</strong>、<em>斜体</em>、<s>删除线</s>、<a href="https://cnb.cool">链接</a>、行内代码 <code>npm run build</code>。</p><p>无序列表：</p><ul><li>第一项</li><li>第二项<ul><li>嵌套一项</li></ul></li></ul><p>有序列表：</p><ol><li>第一步</li><li>第二步</li></ol><p>引用：</p><blockquote><p>逆行者，走的是少有人走的路，看的是多数人看不到的风景。</p></blockquote><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></pre></td><td class="code"><pre><code class="hljs js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">hello</span>(<span class="hljs-params">name</span>) &#123;<br>  <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`你好，<span class="hljs-subst">$&#123;name&#125;</span>`</span>);<br>&#125;<br><span class="hljs-title function_">hello</span>(<span class="hljs-string">&#x27;逆行者&#x27;</span>);<br></code></pre></td></tr></table></figure><p>图片：</p><figure class="highlight markdown"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs markdown">![<span class="hljs-string">图片说明</span>](<span class="hljs-link">/images/example.png</span>)<br></code></pre></td></tr></table></figure><p>水平线：</p><hr><p>就写到这里。以后就在这里更新了。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;p&gt;欢迎来到 &lt;strong&gt;逆行者AI博客&lt;/strong&gt;。这里记录 AI 时代的学习、思考与实践。&lt;/p&gt;
&lt;h2 id=&quot;这个博客是怎么搭起来的&quot;&gt;这个博客是怎么搭起来的&lt;/h2&gt;
&lt;p&gt;整套东西跑在两条流水线上，没有服务器、没有备案，也不需要本地装任何环境：&lt;/p&gt;
</summary>
      
    
    
    
    <category term="站务" scheme="https://nixingzhe-blog.edgeone.dev/categories/%E7%AB%99%E5%8A%A1/"/>
    
    
    <category term="随笔" scheme="https://nixingzhe-blog.edgeone.dev/tags/%E9%9A%8F%E7%AC%94/"/>
    
    <category term="建站" scheme="https://nixingzhe-blog.edgeone.dev/tags/%E5%BB%BA%E7%AB%99/"/>
    
  </entry>
  
</feed>
