独自ドメインでサーバレスかつCMS具備のブログを、Hugo + GitHub + Decap CMS + Cloudflare Pagesのスタックで作る手順
独自ドメイン+Google Sites(Google APPS時代の)で運用していたこのサイト、相当前に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
)は取得済みとする -
OS は Windows / macOS どちらでもよい(コマンド例は両方記載)
-
使うサービス(無料枠でOK)
- 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
はそのままで OK(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 App を作る(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. ブランチ運用のワンポイント
- 既定ブランチが
main
かmaster
かを DecapCMS のconfig.yml
と Cloudflare の設定で揃える - プレビューしたい場合は、GitHub の Pull Request を作ると Cloudflare Pages が Preview 環境を自動発行
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 App の Client ID |
Cloudflare Pages | GITHUB_CLIENT_SECRET |
GitHub OAuth App の 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">
を追加- SNSでのシェア時に魅力的に見せる
-
画像の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と二人三脚で拡充していきたい。