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(