使用 Hugo + GitHub + Decap CMS + Cloudflare Pages 建置支持自定義域名的無伺服器部落格步驟
這個網站過去是用獨立域名加上(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 均可(命令示例兩者都會寫)
-
使用的服務(免費額度即可)
- GitHub 賬號
- Cloudflare 賬號(使用 Pages)
- Git(本地)
- Hugo(本地預覽用,Cloudflare 建置時也會用到)
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. 在本地準備儲存庫
-
在 GitHub 建立一個空儲存庫
- 界面:GitHub > New repository
- 名稱示例:
my-hugo-blog
-
克隆到本地
-
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
-
-
將上面的文件夾和文件整體放到儲存庫根目錄
替換檢查(務必修改這裏)
-
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.js
與functions/api/callback.js
無需修改(因爲使用url.origin
,不需要寫死環境)。
-
第一次送出併推送
-
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 登入)
-
界面:GitHub > Settings > Developer settings > OAuth Apps > New OAuth App
-
輸入:
- 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
← 非常重要
- Application name:
-
建立後會顯示:
- Client ID
- Client Secret(產生新值並保存)
這些信息要配置到 Cloudflare Pages 的環境變數裏。
2-3. 建立 Cloudflare Pages 專案
-
界面:Cloudflare 儀表板 > Pages > Create a project > Connect to Git
-
關聯 GitHub,並選擇儲存庫
my-hugo-blog
-
建置設置:
-
Framework preset:
None
(或交給 Cloudflare 自動識別) -
Build command:
hugo
-
Build output directory:
public
-
環境變數:
HUGO_VERSION
=0.128.0
(示例,和本地 Hugo 版本保持一致更穩妥)GITHUB_CLIENT_ID
= (上一節取得的值)GITHUB_CLIENT_SECRET
= (上一節取得的值)
-
-
點擊 Save and Deploy,等待首次部署
- 成功後會產生一個
*.pages.dev
的預覽 URL
- 成功後會產生一個
2-4. 綁定獨立域名 www.example.com
-
界面:Cloudflare > Pages >(對應專案)> Custom domains > Set up a custom domain
-
輸入
www.example.com
並新增 -
如果沒有把域名轉移到 Cloudflare:
- 記錄 Cloudflare 界面顯示的 DNS 記錄信息(例:
CNAME www -> <project>.pages.dev
) - 到當前域名註冊商的管理頁面,按照同樣的設置加入該記錄(具體步驟依各註冊商而定)
- 記錄 Cloudflare 界面顯示的 DNS 記錄信息(例:
-
生效後,確認
https://www.example.com/
能正常瀏覽網站
小提示:如果域名已經在 Cloudflare 中,只需點擊按鈕就能自動配置所需的 DNS。
2-5. 進入 DecapCMS 管理界面
- 在瀏覽器打開
https://www.example.com/admin/
- 點擊 “Login with GitHub” 之類的按鈕,授權 GitHub OAuth
- 第一次會出現 GitHub 的“是否授權該應用?”提示,點擊 Authorize
如果登入失敗,請重新確認 OAuth 的 Callback URL 以及 Cloudflare 的環境變數(
GITHUB_CLIENT_ID/SECRET
)。
3. 營運維護步驟(撰寫文章・本地同步・模板編輯)
3-1. 通過 CMS 建立文章
- 界面:
https://www.example.com/admin/
- 左側導航:點擊 Blog → New blog
- 輸入:
Title
、Publish Date
、Description
、Body
- 點擊 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. 在本地調整設計與模板
-
在本地啓動開發伺服器
-
Windows
PS C:\work\my-hugo-blog> hugo server -D
-
macOS
mac:~/work/my-hugo-blog dev$ hugo server -D
-
在瀏覽器瀏覽
http://localhost:1313/
查看
-
-
可能的修改點(示例)
layouts/_default/baseof.html
…<head>
、頁眉/頁腳layouts/_default/index.html
… 首頁“最新文章”列表layouts/_default/single.html
… 文章正文頁面static/css/main.css
… 顏色、字體、間距等
-
送出併推送修改
-
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. 分支運作小技巧
- 請確保預設分支名稱在 DecapCMS 的
config.yml
與 Cloudflare 設置中保持一致(main
或master
) - 如果想要預覽,可在 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.toml
的baseURL
是否與實際域名一致,並查看 Cloudflare 建置日誌裏是否產生了public/
- /admin 空白一片\
→ 檢查
static/admin/index.html
中 DecapCMS 的加載(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 |
根據實際情況使用 main 或 master |
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 與性能優化要點
在完成部落格搭建後,可以通過以下措施進一步提升搜索排名和加載速度。
-
配置網站地圖與 robots.txt
- 將 Hugo 自動產生的
public/sitemap.xml
送到 Google Search Console - 在
static/robots.txt
中加入爬蟲控制
- 將 Hugo 自動產生的
-
設置 OGP(Open Graph Protocol)與 Twitter 卡片
- 在
layouts/_default/baseof.html
中加入<meta property="og:title">
、<meta property="og:image">
等 - 讓社交媒體分享時更具吸引力
- 在
-
將圖片轉換爲 WebP
- 利用
resources/_gen/images/
,通過 Hugo 管線自動轉換爲 WebP - 改善頁面加載速度
- 利用
-
增加分類與標籤功能
- 在
content/blog/
的 Front Matter 中加入categories
、tags
,並在模板裏顯示標籤雲
- 在
-
引入結構化數據
- 用 JSON-LD 形式向搜索引擎明確文章信息,爭取展示富結果
8. 總結
- 在 GitHub 建立儲存庫、連接 Cloudflare Pages、爲 DecapCMS 配置 GitHub OAuth、綁定獨立域名就能完成搭建
- 文章通過
/admin/
建立 → 自動送到 GitHub → Cloudflare 自動部署 - 模板和設計可以在本地執行
hugo server
進行修改,然後用 Git 送出
至此,就可以營運一套由 Hugo + GitHub + Decap CMS + Cloudflare 組成的無伺服器部落格環境了。
雖然這樣已經搭好了低維護成本的網站,但站內搜索、分類、標籤與標籤雲等功能相比現有部落格還是差了一截,只能暫時先這樣。之後還想繼續和 ChatGPT 兩人三腳地慢慢擴充。