From cc62942bb950b477714dc8f3a712f11f8b512ef6 Mon Sep 17 00:00:00 2001
From: watanabe-kohei-jp <283722319+watanabe-kohei-jp@users.noreply.github.com>
Date: Fri, 22 May 2026 10:56:55 +0900
Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=85=A8=E7=89=88=20HTML=20=E3=82=92?=
=?UTF-8?q?=20GitHub=20Pages=20=E3=81=AE=20/dist/=20=E3=81=A7=E9=85=8D?=
=?UTF-8?q?=E4=BF=A1=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## 何をしているか
main への push で `python build.py` を CI で実行し、生成された
完全版 HTML (`dist/*.html`) を既存リポと一緒に Pages にデプロイ。
これまで「ローカルでビルドして手動配布」だった完全版が、Pages 上の
URL(`co-lect.github.io/lectures/dist/<章名>.html`)として常に
最新版が公開され、右クリック → 保存でダウンロードできるようになる。
## 変更
- `.github/workflows/pages-deploy.yml` (新規)
- main push でビルド + デプロイ。PR では build-only で破損検出
- `_site/` に編集用(リポ全体)+ 完全版(`dist/*.html`)を rsync で詰める
- 既存の編集用 URL(章フォルダ、`shared/`、`sitemap.xml` 等)は維持
- `build.py`
- 完全版 HTML の `` 直前に `` と
`` を自動挿入
- 重複コンテンツ対策。検索エンジンには編集用 URL を正本として扱わせる
- `README.md`
- 「完全版を入手する」セクションを Pages 経由のダウンロード手順に拡張
- 取り扱い注意(dist/ を直接編集しない・force-add しない)を明文化
## 設計判断
- **`_site` パターン採用**: Pages 設定を「Actions」に切り替えると、
artifact に含めなかった既存 URL は消える。rsync で `.git` `.github`
`dist` `_site` のみ除外して残りを全部 `_site/` に入れることで、
編集用 URL の互換性を保つ
- **`dist/` は引き続き git 管理外**: CI が生成・配信するため、リポに
入れる必要なし。PR #12 で確立した不変条件を維持
- **canonical/noindex を build.py 側で注入**: ワークフローではなく
ビルドスクリプトで仕込むことで、ローカルビルドした完全版にも同じ
メタが入る(手元配布版でも検索エンジン的に安全)
- **PR ジョブは build-only**: 外部 PR で deploy 権限を持たせない
## デプロイ前に必要な手動作業
Settings → Pages → Source を「GitHub Actions」に切り替える必要あり
(現状は「Deploy from a branch」想定)。切替後は本ワークフローが
唯一のデプロイ経路になる。
## 動作確認(ローカル)
- `python build.py` で 4 章すべて [OK]、size 妥当
- 全章に canonical / robots=noindex が `` 直前に挿入されることを確認
Closes #33
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.github/workflows/pages-deploy.yml | 73 ++++++++++++++++++++++++++++++
README.md | 27 +++++++++--
build.py | 27 +++++++++++
3 files changed, 124 insertions(+), 3 deletions(-)
create mode 100644 .github/workflows/pages-deploy.yml
diff --git a/.github/workflows/pages-deploy.yml b/.github/workflows/pages-deploy.yml
new file mode 100644
index 0000000..eaee50f
--- /dev/null
+++ b/.github/workflows/pages-deploy.yml
@@ -0,0 +1,73 @@
+name: Deploy GitHub Pages
+
+# main への push で Pages を自動デプロイする。
+#
+# 配信物は 2 系統:
+# - 編集用: 章フォルダ・shared/・sitemap.xml 等(リポをそのまま)
+# - 完全版: `python build.py` で生成した dist/<章名>.html
+#
+# 「Deploy from a branch」から「GitHub Actions」へ切り替える必要がある
+# (Settings → Pages → Source)。切替後は本ワークフローが唯一のデプロイ経路。
+#
+# PR では build-only ジョブだけ走らせ、build.py の破損を merge 前に検出する。
+# 詳細: Issue #33
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ branches: ["main"]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# 同時デプロイは 1 件まで。in-progress を中断しないことで、進行中の deploy を守る。
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Build standalone HTML
+ run: python3 build.py
+
+ - name: Stage site
+ # _site/ に編集用(リポ全体)+ 完全版(dist/)の両方を詰める。
+ # .git/.github/dist/_site は site 配信から除外。
+ run: |
+ mkdir -p _site
+ rsync -a --delete \
+ --exclude '.git' \
+ --exclude '.github' \
+ --exclude 'dist' \
+ --exclude '_site' \
+ ./ _site/
+ mkdir -p _site/dist
+ cp dist/*.html _site/dist/
+
+ - name: Upload Pages artifact
+ # PR では artifact をアップロードしない(deploy しないため)。
+ if: github.event_name != 'pull_request'
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: _site
+
+ deploy:
+ # PR では deploy しない。main への push / workflow_dispatch のみ。
+ if: github.event_name != 'pull_request'
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/README.md b/README.md
index 51008ad..98f41fb 100644
--- a/README.md
+++ b/README.md
@@ -63,15 +63,36 @@ cd lectures
---
-## 完全版(1 ファイル)をビルドする
+## 完全版(1 ファイル)を入手する
-各章を 1 つの HTML にまとめた「完全版」を生成できます。`shared/` の CSS・JS と画像をすべて埋め込むので、**リポジトリもネット接続もなしで開けます**。メール添付・USB 配布・オフライン閲覧に。
+各章を 1 つの HTML にまとめた「完全版」が用意されています。`shared/` の CSS・JS と画像をすべて埋め込むので、**リポジトリもネット接続もなしで開けます**。メール添付・USB 配布・オフライン閲覧に。
+
+### 方法 1: ダウンロードする(推奨)
+
+CI が自動更新する常に最新版を Pages から取得できます。
+
+```
+https://co-lect.github.io/lectures/dist/00-about.html
+https://co-lect.github.io/lectures/dist/01-claude-code-intro.html
+https://co-lect.github.io/lectures/dist/02-setup.html
+https://co-lect.github.io/lectures/dist/03-claude-md.html
+```
+
+ブラウザで開いて右クリック →「名前を付けて保存」で `.html` 1 ファイルが手元に残ります。
+
+### 方法 2: 自分でビルドする
```bash
python build.py
```
-`dist/00-about.html` などが生成されます(`dist/` は git 管理外のビルド成果物)。正本は各章の `index.html`(`shared/` 参照版)のままなので、`shared/` や本文を変更したら `python build.py` で作り直します。依存は Python 3 標準ライブラリのみ。
+`dist/00-about.html` などが生成されます(`dist/` は git 管理外のビルド成果物)。依存は Python 3 標準ライブラリのみ。
+
+### 注意
+
+- `dist/` は **ビルドの度に上書きされる**。直接編集しないこと
+- 配布前は **必ず `python build.py` を実行**するか、Pages の最新版を取得する
+- `dist/` は git 管理外(`.gitignore` 設定済み)。`git add -f` で強制追加しないこと
> Web フォント(Google Fonts)だけは埋め込みません。完全オフラインではシステムフォントにフォールバックします(レイアウトは崩れません)。
diff --git a/build.py b/build.py
index fe2d1f8..ebf0a2e 100644
--- a/build.py
+++ b/build.py
@@ -8,6 +8,12 @@
正本は各章の index.html(shared/ 参照版)のまま。dist/ はビルド成果物で
git 管理外。内容や shared/ を変更したら `python build.py` で再生成する。
+SEO / 重複コンテンツ対策:
+ 完全版 HTML には `` と
+ `` を自動挿入する。
+ Pages 上で 2 系統の URL(章フォルダ / dist/)が同一コンテンツを返すため、
+ 検索エンジンには編集用 URL を正本として扱わせる。
+
既知の制約:
theme.css は Google Fonts を @import している。完全オフラインでは
Web フォントが読めず、システムフォントにフォールバックする
@@ -27,6 +33,9 @@
ROOT = Path(__file__).resolve().parent
DIST = ROOT / "dist"
+# Pages 上の編集用 URL のベース。完全版 HTML の canonical に使う。
+CANONICAL_BASE = "https://co-lect.github.io/lectures"
+
# 外部 / data URI は埋め込み対象外(そのまま残す)
_SKIP_PREFIXES = ("http://", "https://", "data:", "//")
@@ -97,6 +106,23 @@ def img_repl(m: re.Match) -> str:
return html
+def inject_seo_meta(html: str, chapter_name: str) -> str:
+ """完全版 HTML に canonical と robots=noindex を 直前に注入する。
+
+ 編集用 URL(章フォルダ)が正本であることを検索エンジンに伝え、
+ 完全版 URL(dist/<章名>.html)の重複 indexing を避ける。
+ """
+ canonical = f"{CANONICAL_BASE}/{chapter_name}/"
+ tags = (
+ f'\n'
+ f'\n'
+ )
+ if "" in html:
+ return html.replace("", tags + "", 1)
+ # が無いケースは想定外。head 解析より、検出して失敗させる方が安全
+ raise RuntimeError(f"{chapter_name}: が見つからず canonical を挿入できません")
+
+
def main() -> int:
# Windows の既定コンソール(cp932)でも日本語を出せるようにする
try:
@@ -116,6 +142,7 @@ def main() -> int:
for ch in chapters:
html = (ch / "index.html").read_text(encoding="utf-8-sig")
built = inline(html, ch)
+ built = inject_seo_meta(built, ch.name)
# 未解決のローカル参照(../ や _assets/)が残っていないか検証
leftover = sorted(set(re.findall(