这个站点过去是用独立域名加上(Google Apps 时代的)Google Sites 运营的,但早就不能继续使用 Google Site,我也让 https://www.ixam.net 这个 URL 闲置了很久。终于下定决心好好利用它,于是趁机进行重建。既然如此,就想做成无服务器而且不受 Google Site 那样随意终止的服务所束缚的形式,于是和 Chat GPT 君一起尝试出了现在的成果。

老实说我以为只靠 Chat GPT 就能轻松完成,结果并没有那么简单。它会忽略基本需求,排查问题时还会原地打转,最后还是参考了我自己搜到的 GitHub 仓库 “https://github.com/patrickgrey/website/” 等资料,才总算合作完成。

过程中的感觉就像带着一个技术力很强却老爱误解或搞错需求的部下,一边纠偏一边推进项目。从某种意义上说,AI 已经到了这种程度也挺厉害的。

但是,一旦内容复杂起来,它又会暴露出让人抓狂的愚蠢之处。

这或许也算是另一种真实?

目前看来,与其让对话无限延伸并要求它理解全部前提,不如当对话变得复杂时由人类整理一遍,再在全新的线程里重新下达指令,实际效果更好。

不过如果没有它,无论是调查、工作量还是耐心,我一个人明显无法完成,所以生成式 AI 的生产力真是惊人。


目标

  • 能在 https://www.example.com/ 上显示博客
  • 能从 https://www.example.com/admin/ 登录 DecapCMS 创建文章
  • 创建的文章会提交到 GitHub,并自动部署到 Cloudflare
  • 顺便说,目前额外的运行成本为 0(域名注册费本来就要付)

0. 前提与文件夹结构

0-1. 前提

  • 假设已经取得域名(example.com

  • 操作系统使用 Windows 或 macOS 均可(命令示例两者都会写)

  • 使用的服务(免费额度即可)

0-2. 仓库的主要结构(示例)

hugo.toml
content/
  blog/
  posts/
data/
functions/
  api/
    auth.js
    callback.js
layouts/
  _default/
    baseof.html
    index.html
    list.html
    single.html
static/
  _headers
  admin/
    config.yml
    index.html
  css/
    main.css

这个结构的作用如下:

  • hugo.toml … 网站整体设置(baseURL 要改成正式 URL
  • functions/api/*.js … Cloudflare Pages Functions(用于 GitHub OAuth 的 /api/auth/api/callback
  • layouts/_default/*.html … Hugo 模板
  • static/admin/DecapCMS 的界面与设置
  • static/css/main.css … 样式(CSS)
  • static/_headers … Cloudflare Pages 的 HTTP 头设置(可选)

※ 本指南会把需要按照环境替换的位置标注为“替换检查”。


1. 事前准备

1-1. 创建 GitHub 账号

  • 在浏览器打开 GitHub 并注册

1-2. 创建 Cloudflare 账号

  • 在浏览器打开 Cloudflare 并注册

1-3. 安装 Git 和 Hugo

  • Windows(PowerShell)

    PS C:\Users\alice> winget install Git.Git
    PS C:\Users\alice> winget install Hugo.Hugo.Extended
    
  • macOS(Terminal)

    mac:~ dev$ brew install git
    mac:~ dev$ brew install hugo
    

2. 正式步骤(直到部署)

2-1. 在本地准备仓库

  1. 在 GitHub 创建一个空仓库

    • 界面:GitHub > New repository
    • 名称示例:my-hugo-blog
  2. 克隆到本地

    • Windows

      PS C:\work> git clone https://github.com/<YOUR_GH_USERNAME>/my-hugo-blog.git
      PS C:\work> cd .\my-hugo-blog
      
    • macOS

      mac:~/work dev$ git clone https://github.com/<YOUR_GH_USERNAME>/my-hugo-blog.git
      mac:~/work dev$ cd my-hugo-blog
      
  3. 将上面的文件夹和文件整体放到仓库根目录

替换检查(务必修改这里)

  • hugo.toml

    baseURL = "https://www.example.com"   # ← 替换为正式环境的 URL(例:https://www.example.com)
    languageCode = "ja-jp"
    title = "Example Blog"
    publishDir = "public"
    
    [permalinks]
      blog = "/blog/:year/:month/:slug/"
    
  • static/admin/config.yml

    backend:
      name: github
      repo: <YOUR_GH_USERNAME>/my-hugo-blog   # ← 替换为你的 GitHub 仓库
      branch: master                          # ← 仓库的默认分支名(main 或 master)
      base_url: https://www.example.com       # ← 替换为正式环境 URL
      auth_endpoint: /api/auth                # ← Functions 的端点(固定)
    media_folder: static/uploads
    public_folder: /uploads
    

参考:functions/api/auth.jsfunctions/api/callback.js 无需修改(因为使用 url.origin,不需要写死环境)。

  1. 第一次提交并推送

    • Windows

      PS C:\work\my-hugo-blog> git add -A
      PS C:\work\my-hugo-blog> git commit -m "Initial commit: Hugo + [DecapCMS](https://decapcms.org/) + CF Pages"
      PS C:\work\my-hugo-blog> git push -u origin master
      
    • macOS

      mac:~/work/my-hugo-blog dev$ git add -A
      mac:~/work/my-hugo-blog dev$ git commit -m "Initial commit: Hugo + [DecapCMS](https://decapcms.org/) + CF Pages"
      mac:~/work/my-hugo-blog dev$ git push -u origin master
      

2-2. 创建 GitHub OAuth 应用(供 DecapCMS 登录)

  1. 界面:GitHub > Settings > Developer settings > OAuth Apps > New OAuth App

  2. 输入:

    • Application name:[DecapCMS](https://decapcms.org/) for my-hugo-blog
    • Homepage URL:https://www.example.com
    • Authorization callback URL:https://www.example.com/api/callback非常重要
  3. 创建后会显示:

    • Client ID
    • Client Secret(生成新值并保存)

这些信息要配置到 Cloudflare Pages 的环境变量里。


2-3. 创建 Cloudflare Pages 项目

  1. 界面:Cloudflare 仪表盘 > Pages > Create a project > Connect to Git

  2. 关联 GitHub,并选择仓库 my-hugo-blog

  3. 构建设置:

    • Framework preset:None(或交给 Cloudflare 自动识别)

    • Build command:hugo

    • Build output directory:public

    • 环境变量:

      • HUGO_VERSION = 0.128.0(示例,和本地 Hugo 版本保持一致更稳妥)
      • GITHUB_CLIENT_ID = (上一节取得的值)
      • GITHUB_CLIENT_SECRET = (上一节取得的值)
  4. 点击 Save and Deploy,等待首次部署

    • 成功后会生成一个 *.pages.dev 的预览 URL

2-4. 绑定独立域名 www.example.com

  1. 界面:Cloudflare > Pages >(对应项目)> Custom domains > Set up a custom domain

  2. 输入 www.example.com 并新增

  3. 如果没有把域名转移到 Cloudflare:

    • 记录 Cloudflare 界面显示的 DNS 记录信息(例:CNAME www -> <project>.pages.dev
    • 到当前域名注册商的管理页面,按照同样的设置添加该记录(具体步骤依各注册商而定)
  4. 生效后,确认 https://www.example.com/ 能正常访问站点

小提示:如果域名已经在 Cloudflare 中,只需点击按钮就能自动配置所需的 DNS。


2-5. 进入 DecapCMS 管理界面

  1. 在浏览器打开 https://www.example.com/admin/
  2. 点击 “Login with GitHub” 之类的按钮,授权 GitHub OAuth
  3. 第一次会出现 GitHub 的“是否授权该应用?”提示,点击 Authorize

如果登录失败,请重新确认 OAuth 的 Callback URL 以及 Cloudflare 的环境变量(GITHUB_CLIENT_ID/SECRET)。


3. 运维步骤(撰写文章・本地同步・模板编辑)

3-1. 通过 CMS 创建文章

  1. 界面:https://www.example.com/admin/
  2. 左侧导航:点击 BlogNew blog
  3. 输入:TitlePublish DateDescriptionBody
  4. 点击 Publish,即可自动提交到 GitHub → Cloudflare 自动部署

生成的 Markdown 会放在 content/blog/ 中。

3-2. 在本地同步最新仓库(通过 CMS 发布文章后)

  • Windows

    PS C:\work\my-hugo-blog> git pull origin master
    
  • macOS

    mac:~/work/my-hugo-blog dev$ git pull origin master
    

这样就能把最新的文章拉到本地,安全地继续修改模板或 CSS。

3-3. 在本地调整设计与模板

  1. 在本地启动开发服务器

    • Windows

      PS C:\work\my-hugo-blog> hugo server -D
      
    • macOS

      mac:~/work/my-hugo-blog dev$ hugo server -D
      
    • 在浏览器访问 http://localhost:1313/ 查看

  2. 可能的修改点(示例)

    • layouts/_default/baseof.html<head>、页眉/页脚
    • layouts/_default/index.html … 首页“最新文章”列表
    • layouts/_default/single.html … 文章正文页面
    • static/css/main.css … 颜色、字体、间距等
  3. 提交并推送修改

    • Windows

      PS C:\work\my-hugo-blog> git add -A
      PS C:\work\my-hugo-blog> git commit -m "Update theme/layouts"
      PS C:\work\my-hugo-blog> git push
      
    • macOS

      mac:~/work/my-hugo-blog dev$ git add -A
      mac:~/work/my-hugo-blog dev$ git commit -m "Update theme/layouts"
      mac:~/work/my-hugo-blog dev$ git push
      
    • 数十秒到几分钟后,Cloudflare 会自动部署

3-4. 分支运作小技巧

  • 请确保默认分支名称在 DecapCMSconfig.yml 与 Cloudflare 设置中保持一致(mainmaster
  • 如果想要预览,可在 GitHub 建立 Pull Request,Cloudflare Pages 会自动生成预览环境

4. 实用补充(可选)

4-1. static/_headers 示例(让管理界面不被缓存)

/admin/*
  Cache-Control: no-store

4-2. robots 与站点地图(按需配置)

  • 准备 static/robots.txt 控制爬虫
  • hugo.toml 中追加 outputs,就能扩展 RSS 与 sitemap 的输出

5. 常见问题与处理

  • CMS 登录一直在转圈\ → 检查 GitHub OAuth 的 Callback URL 是否是 https://www.example.com/api/callback,以及 Cloudflare 环境变量 GITHUB_CLIENT_ID/SECRET 是否正确
  • 首页能打开但文章页面 404\ → 确认 hugo.tomlbaseURL 是否与实际域名一致,并查看 Cloudflare 构建日志里是否生成了 public/
  • /admin 空白一片\ → 检查 static/admin/index.htmlDecapCMS 的加载(CDN)是否被拦截,关闭浏览器扩展后再刷新

6. 替换与设置检查点一览(复习)

文件/设置 替换或设置内容(示例)
hugo.toml baseURL https://www.example.com
static/admin/config.yml repo <YOUR_GH_USERNAME>/my-hugo-blog
static/admin/config.yml branch 根据实际情况使用 mainmaster
static/admin/config.yml base_url https://www.example.com
Cloudflare Pages HUGO_VERSION 例:0.128.0
Cloudflare Pages GITHUB_CLIENT_ID GitHub OAuth 应用的 Client ID
Cloudflare Pages GITHUB_CLIENT_SECRET GitHub OAuth 应用的 Client Secret
GitHub OAuth App Callback URL https://www.example.com/api/callback
Cloudflare Pages Custom domain 添加 www.example.com

7. 附录:文件示例(按需替换后使用)

7-1. functions/api/auth.js

// Cloudflare Pages Functions(/api/auth)
export async function onRequest(context) {
  const { request, env } = context;
  const client_id = env.GITHUB_CLIENT_ID;
  try {
    const url = new URL(request.url);
    const redirectUrl = new URL('https://github.com/login/oauth/authorize');
    redirectUrl.searchParams.set('client_id', client_id);
    redirectUrl.searchParams.set('redirect_uri', `${url.origin}/api/callback`);
    redirectUrl.searchParams.set('scope', 'repo user');
    redirectUrl.searchParams.set('state', crypto.getRandomValues(new Uint8Array(12)).join(''));
    return Response.redirect(redirectUrl.href, 302);
  } catch (err) {
    return new Response(String(err?.message || err), { status: 500 });
  }
}

7-2. functions/api/callback.js

function renderBody(status, content) {
  const html = `
  <script>
    const receiveMessage = (message) => {
      window.opener.postMessage(
        'authorization:github:${status}:${JSON.stringify(content)}',
        message.origin
      );
      window.removeEventListener("message", receiveMessage, false);
    }
    window.addEventListener("message", receiveMessage, false);
    window.opener.postMessage("authorizing:github", "*");
  </script>`;
  return new Blob([html]);
}

export async function onRequest(context) {
  const { request, env } = context;
  const client_id = env.GITHUB_CLIENT_ID;
  const client_secret = env.GITHUB_CLIENT_SECRET;
  try {
    const url = new URL(request.url);
    const code = url.searchParams.get('code');
    const response = await fetch('https://github.com/login/oauth/access_token', {
      method: 'POST',
      headers: { 'content-type': 'application/json', 'user-agent': 'cf-pages-oauth', 'accept': 'application/json' },
      body: JSON.stringify({ client_id, client_secret, code }),
    });
    const result = await response.json();
    if (result.error) {
      return new Response(renderBody('error', result), { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 401 });
    }
    const token = result.access_token;
    const provider = 'github';
    return new Response(renderBody('success', { token, provider }), { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 200 });
  } catch (error) {
    return new Response(error.message, { headers: { 'content-type': 'text/html;charset=UTF-8' }, status: 500 });
  }
}

7-3. static/admin/config.yml

backend:
  name: github
  repo: <YOUR_GH_USERNAME>/my-hugo-blog
  branch: main
  base_url: https://www.example.com
  auth_endpoint: /api/auth

media_folder: static/uploads
public_folder: /uploads

collections:
  - name: 'blog'
    label: 'Blog'
    folder: 'content/blog'
    create: true
    slug: '{{slug}}'
    editor:
      preview: false
    fields:
      - { label: 'Title', name: 'title', widget: 'string' }
      - { label: 'Publish Date', name: 'date', widget: 'datetime' }
      - { label: 'Description', name: 'description', widget: 'string' }
      - { label: 'Body', name: 'body', widget: 'markdown' }


9. SEO 与性能优化要点

在完成博客搭建后,可以通过以下措施进一步提升搜索排名和加载速度。

  1. 配置站点地图与 robots.txt

    • 将 Hugo 自动生成的 public/sitemap.xml 提交到 Google Search Console
    • static/robots.txt 中添加爬虫控制
  2. 设置 OGP(Open Graph Protocol)与 Twitter 卡片

    • layouts/_default/baseof.html 中添加 <meta property="og:title"><meta property="og:image">
    • 让社交媒体分享时更具吸引力
  3. 将图片转换为 WebP

    • 利用 resources/_gen/images/,通过 Hugo 管线自动转换为 WebP
    • 改善页面加载速度
  4. 增加分类与标签功能

    • content/blog/ 的 Front Matter 中添加 categoriestags,并在模板里显示标签云
  5. 引入结构化数据

    • 用 JSON-LD 形式向搜索引擎明确文章信息,争取展示富结果

8. 总结

  • 在 GitHub 创建仓库、连接 Cloudflare Pages、为 DecapCMS 配置 GitHub OAuth、绑定独立域名就能完成搭建
  • 文章通过 /admin/ 创建 → 自动提交到 GitHub → Cloudflare 自动部署
  • 模板和设计可以在本地运行 hugo server 进行修改,然后用 Git 提交

至此,就可以运营一套由 Hugo + GitHub + Decap CMS + Cloudflare 组成的无服务器博客环境了。

虽然这样已经搭好了低维护成本的网站,但站内搜索、分类、标签与标签云等功能相比现有博客还是差了一截,只能暂时先这样。之后还想继续和 ChatGPT 两人三脚地慢慢扩充。