Hướng dẫn từng bước dựng blog serverless với tên miền riêng bằng Hugo + GitHub + Decap CMS + Cloudflare Pages
Trước đây trang này chạy trên Google Sites (thời Google Apps) với tên miền riêng, nhưng dịch vụ đó đã ngừng hoạt động từ lâu nên tôi cũng để https://www.ixam.net ngủ quên. Đến lúc quyết định phải tận dụng lại tên miền, tôi làm luôn một bản làm mới toàn diện. Mục tiêu là một hệ thống serverless, không phụ thuộc vào những dịch vụ dễ “đột tử” như Google Sites, và đây là kết quả của lần thử nghiệm tôi phối hợp cùng Chat GPT.
Thật lòng mà nói, tôi tưởng chỉ cần Chat GPT là làm xong ngay, nhưng thực tế lại khác. Nó bỏ sót các yêu cầu cơ bản, quay vòng vòng khi gỡ lỗi, và cuối cùng tôi phải tra thêm những kho GitHub tự tìm — như “https://github.com/patrickgrey/website/” — rồi hai bên mới hoàn thành được.
Cảm giác giống như bạn có một cấp dưới tay nghề cao nhưng liên tục hiểu sai hoặc nhầm yêu cầu, buộc bạn phải kéo lại đúng hướng từng bước. Ở góc nhìn nào đó, việc AI đạt đến mức này cũng đáng nể.
Nhưng hễ nội dung phức tạp lên là nó lại thể hiện sự vụng về tới mức gây ức chế.
Có lẽ đó cũng là một kiểu “hiện thực” chăng?
Kinh nghiệm hiện tại cho thấy: thay vì kéo dài cuộc hội thoại vô tận và bắt AI nhớ hết mọi tiền đề, tốt hơn là khi câu chuyện trở nên rối rắm, con người hãy tự thu xếp lại rồi mở một luồng mới với hướng dẫn rõ ràng hơn — hiệu quả cao hơn hẳn.
Dù vậy, nếu không có nó thì tôi khó mà kham nổi khối lượng tra cứu, công việc và cả sự kiên nhẫn cần thiết, nên phải thừa nhận năng suất của AI sinh tạo quả là đáng gờm.
Mục tiêu
- Hiển thị blog tại
https://www.example.com/
- Đăng nhập DecapCMS qua
https://www.example.com/admin/
để tạo bài viết - Mỗi bài viết được commit lên GitHub và tự động triển khai lên Cloudflare
- Hiện tại chưa phát sinh chi phí vận hành bổ sung nào (phí đăng ký tên miền vốn đã có)
0. Điều kiện tiên quyết và cấu trúc thư mục
0-1. Điều kiện tiên quyết
-
Giả định bạn đã sở hữu tên miền (
example.com
) -
Có thể dùng Windows hoặc macOS (ví dụ lệnh sẽ bao gồm cả hai)
-
Các dịch vụ cần dùng (gói miễn phí là đủ)
- Tài khoản GitHub
- Tài khoản Cloudflare (sử dụng Pages)
- Git (cài trên máy local)
- Hugo (phục vụ preview local và build trên Cloudflare)
0-2. Cấu trúc chính của repo (ví dụ)
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
Vai trò của từng phần:
hugo.toml
… cấu hình tổng thể của site (hãy thay baseURL bằng URL production)functions/api/*.js
… Cloudflare Pages Functions (xử lý GitHub OAuth cho/api/auth
và/api/callback
)layouts/_default/*.html
… template của Hugostatic/admin/
… giao diện và cấu hình DecapCMSstatic/css/main.css
… định dạng (CSS)static/_headers
… cấu hình HTTP header cho Cloudflare Pages (tùy chọn)
※ Trong tài liệu này, những chỗ cần thay đổi theo môi trường sẽ được ghi chú là “kiểm tra thay thế”.
1. Chuẩn bị ban đầu
1-1. Tạo tài khoản GitHub
- Mở GitHub trên trình duyệt và đăng ký
1-2. Tạo tài khoản Cloudflare
- Mở Cloudflare trên trình duyệt và đăng ký
1-3. Cài đặt Git và 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. Quy trình chính (đến bước deploy)
2-1. Chuẩn bị repo trên máy local
-
Tạo một repo trống trên GitHub
- Giao diện: GitHub > New repository
- Ví dụ tên:
my-hugo-blog
-
Clone repo về máy
-
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
-
-
Sao chép toàn bộ thư mục và file ở trên vào thư mục gốc của repo
Kiểm tra thay thế (phải chỉnh sửa)
-
hugo.toml
baseURL = "https://www.example.com" # ← thay bằng URL production (ví dụ: 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 # ← thay bằng repo GitHub của bạn branch: master # ← tên nhánh mặc định (main hoặc master) base_url: https://www.example.com # ← thay bằng URL production auth_endpoint: /api/auth # ← endpoint cho Functions (cố định) media_folder: static/uploads public_folder: /uploads
Ghi chú:
functions/api/auth.js
vàfunctions/api/callback.js
giữ nguyên được (chúng sử dụngurl.origin
, không cần hard-code URL theo môi trường).
-
Commit và push đầu tiên
-
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. Tạo GitHub OAuth App cho DecapCMS
-
Truy cập: GitHub > Settings > Developer settings > OAuth Apps > New OAuth App
-
Nhập thông tin:
- 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
← rất quan trọng
- Application name:
-
Sau khi tạo, bạn sẽ thấy:
- Client ID
- Client Secret (tạo mới và lưu lại)
Những giá trị này sẽ được thiết lập trong biến môi trường của Cloudflare Pages.
2-3. Tạo dự án Cloudflare Pages
-
Điều hướng: Cloudflare Dashboard > Pages > Create a project > Connect to Git
-
Kết nối GitHub và chọn repo
my-hugo-blog
-
Thiết lập build:
-
Framework preset:
None
(hoặc để Cloudflare tự nhận dạng) -
Build command:
hugo
-
Build output directory:
public
-
Biến môi trường:
HUGO_VERSION
=0.128.0
(ví dụ; nên đồng bộ với bản Hugo dùng local)GITHUB_CLIENT_ID
= (giá trị lấy ở bước trên)GITHUB_CLIENT_SECRET
= (giá trị lấy ở bước trên)
-
-
Nhấn Save and Deploy và chờ lần deploy đầu
- Sau khi thành công sẽ có URL preview dạng
*.pages.dev
- Sau khi thành công sẽ có URL preview dạng
2-4. Gắn tên miền riêng www.example.com
-
Điều hướng: Cloudflare > Pages > (project tương ứng) > Custom domains > Set up a custom domain
-
Nhập
www.example.com
và thêm vào -
Nếu tên miền không quản lý bởi Cloudflare:
- Ghi lại thông tin bản ghi DNS Cloudflare yêu cầu (ví dụ:
CNAME www -> <project>.pages.dev
) - Vào trang quản lý của nhà đăng ký tên miền hiện tại và tạo bản ghi tương ứng (quy trình tùy từng nhà cung cấp)
- Ghi lại thông tin bản ghi DNS Cloudflare yêu cầu (ví dụ:
-
Khi DNS đã cập nhật, kiểm tra
https://www.example.com/
có hoạt động bình thường
Mẹo: nếu tên miền đã nằm trong Cloudflare, chỉ cần một cú nhấp là các bản ghi DNS cần thiết sẽ được tạo sẵn.
2-5. Đăng nhập trang quản trị DecapCMS
- Mở
https://www.example.com/admin/
- Nhấn nút “Login with GitHub” và cấp quyền OAuth
- Lần đầu, GitHub sẽ hỏi bạn có cho phép ứng dụng hay không — nhấn Authorize
Nếu đăng nhập thất bại, hãy kiểm tra lại Callback URL của OAuth và biến môi trường
GITHUB_CLIENT_ID/SECRET
trên Cloudflare.
3. Vận hành (viết bài, đồng bộ local, chỉnh template)
3-1. Tạo bài viết từ CMS
- Mở
https://www.example.com/admin/
- Trong menu bên trái, chọn Blog → New blog
- Điền:
Title
,Publish Date
,Description
,Body
- Khi nhấn Publish, bài viết được commit lên GitHub → Cloudflare tự động deploy
File Markdown được tạo ra sẽ nằm trong
content/blog/
.
3-2. Đồng bộ repo local sau khi đăng bài qua CMS
-
Windows
PS C:\work\my-hugo-blog> git pull origin master
-
macOS
mac:~/work/my-hugo-blog dev$ git pull origin master
Như vậy bạn sẽ lấy về những bài mới nhất để chỉnh sửa template hay CSS một cách an toàn.
3-3. Chỉnh thiết kế và template trên máy local
-
Khởi động server phát triển local
-
Windows
PS C:\work\my-hugo-blog> hugo server -D
-
macOS
mac:~/work/my-hugo-blog dev$ hugo server -D
-
Mở
http://localhost:1313/
trên trình duyệt để kiểm tra
-
-
Một số điểm thường chỉnh (ví dụ)
layouts/_default/baseof.html
… phần<head>
, header/footerlayouts/_default/index.html
… danh sách “bài mới nhất” ở trang chủlayouts/_default/single.html
… nội dung trang bài viếtstatic/css/main.css
… màu sắc, font, khoảng cách
-
Commit và push thay đổi
-
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
-
Sau vài chục giây đến vài phút, Cloudflare sẽ tự động deploy
-
3-4. Mẹo nhỏ về branch
- Hãy đảm bảo tên branch mặc định trong
config.yml
của DecapCMS trùng với cấu hình trên Cloudflare (main
hoặcmaster
) - Nếu cần preview, tạo Pull Request trên GitHub và Cloudflare Pages sẽ sinh ra môi trường preview tự động
4. Phần bổ sung hữu ích (tùy chọn)
4-1. Ví dụ static/_headers
(không cache trang quản trị)
/admin/*
Cache-Control: no-store
4-2. robots và sitemap (nếu cần)
- Tạo
static/robots.txt
để điều khiển bot thu thập dữ liệu - Thêm
outputs
tronghugo.toml
để mở rộng việc xuất RSS và sitemap
5. Lỗi thường gặp và cách xử lý
- CMS đăng nhập mãi không xong\
→ Kiểm tra Callback URL của GitHub OAuth có phải
https://www.example.com/api/callback
không, và biến môi trườngGITHUB_CLIENT_ID/SECRET
trên Cloudflare đã đúng chưa - Trang chủ mở được nhưng trang bài viết báo 404\
→ Xác nhận
baseURL
tronghugo.toml
khớp với tên miền thực tế, đồng thời xem log build của Cloudflare để chắc rằng thư mụcpublic/
đã được tạo - /admin hiển thị trang trắng\
→ Kiểm tra việc load DecapCMS trong
static/admin/index.html
có bị chặn (ví dụ do extension) không; thử tắt extension rồi tải lại trang
6. Bảng tóm tắt các điểm cần thay thế/cấu hình
Tệp/cấu hình | Khóa | Nội dung cần thiết lập (ví dụ) |
---|---|---|
hugo.toml |
baseURL |
https://www.example.com |
static/admin/config.yml |
repo |
<YOUR_GH_USERNAME>/my-hugo-blog |
static/admin/config.yml |
branch |
Chọn main hoặc master theo nhánh thực tế |
static/admin/config.yml |
base_url |
https://www.example.com |
Cloudflare Pages | HUGO_VERSION |
Ví dụ: 0.128.0 |
Cloudflare Pages | GITHUB_CLIENT_ID |
Client ID của ứng dụng GitHub OAuth |
Cloudflare Pages | GITHUB_CLIENT_SECRET |
Client Secret của ứng dụng GitHub OAuth |
GitHub OAuth App | Callback URL | https://www.example.com/api/callback |
Cloudflare Pages | Custom domain | Thêm www.example.com |
7. Phụ lục: ví dụ tệp (thay đúng phần cần thiết trước khi dùng)
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. Gợi ý tối ưu SEO & hiệu năng
Sau khi dựng xong blog, có thể tối ưu thêm thứ hạng tìm kiếm và tốc độ tải bằng những bước dưới đây.
-
Thiết lập sitemap và robots.txt
- Đăng ký
public/sitemap.xml
(Hugo sinh tự động) với Google Search Console - Thêm quy tắc trong
static/robots.txt
- Đăng ký
-
Bổ sung OGP (Open Graph Protocol) và thẻ Twitter
- Thêm các thẻ như
<meta property="og:title">
,<meta property="og:image">
tronglayouts/_default/baseof.html
- Giúp trang trông hấp dẫn hơn khi chia sẻ lên mạng xã hội
- Thêm các thẻ như
-
Chuyển đổi hình ảnh sang WebP
- Dùng
resources/_gen/images/
và pipeline của Hugo để chuyển sang WebP tự động - Cải thiện tốc độ tải trang
- Dùng
-
Phát triển hệ thống danh mục và thẻ
- Điền
categories
vàtags
trong Front Matter củacontent/blog/
, đồng thời hiển thị đám mây thẻ trong template
- Điền
-
Thêm dữ liệu có cấu trúc
- Cung cấp thông tin bài viết bằng JSON-LD để tăng cơ hội xuất hiện rich result
8. Tổng kết
- Tạo repo trên GitHub, kết nối với Cloudflare Pages, cấu hình GitHub OAuth cho DecapCMS và gắn tên miền riêng là xong phần thiết lập
- Bài viết tạo tại
/admin/
→ commit lên GitHub → Cloudflare tự triển khai - Chỉnh sửa template và thiết kế bằng
hugo server
trên máy local rồi đẩy lên qua Git
Như vậy là bạn đã có môi trường blog serverless chạy bằng Hugo + GitHub + Decap CMS + Cloudflare.
Hiện tại chúng ta đã dựng được site chi phí thấp, nhưng khả năng tìm kiếm nội bộ, danh mục, thẻ và đám mây thẻ vẫn chưa phong phú như các nền tảng blog lâu năm. Trước mắt cứ vận hành như vậy và sẽ tiếp tục mở rộng dần cùng ChatGPT.