diff --git a/.github/workflows/sync-listings-bot.yml b/.github/workflows/sync-listings-bot.yml
new file mode 100644
index 0000000..6cc8b0b
--- /dev/null
+++ b/.github/workflows/sync-listings-bot.yml
@@ -0,0 +1,48 @@
+name: Listings Sync Bot
+
+# main への push 後に同期ブロックを再生成し、差分があれば
+# github-actions[bot] が main に直接 commit する。PR 段階のチェック
+# (sync-listings-check.yml) を通過していれば通常は no-op で終わる。
+#
+# ループ防止: bot 自身の push では走らせない。
+
+on:
+ push:
+ branches: [main]
+
+permissions:
+ contents: write
+
+concurrency:
+ group: listings-sync-main
+ cancel-in-progress: false
+
+jobs:
+ sync:
+ if: github.actor != 'github-actions[bot]'
+ name: Auto-commit listing sync
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.x'
+
+ - name: Run sync_listings --write
+ run: python3 scripts/sync_listings.py --write
+
+ - name: Commit if changed
+ run: |
+ if [[ -z "$(git status --porcelain)" ]]; then
+ echo "No changes — listings already in sync."
+ exit 0
+ fi
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ git add sitemap.xml llms.txt index.html README.md
+ git commit -m "chore(listings): auto-sync $(date +%F)"
+ git push
diff --git a/.github/workflows/sync-listings-check.yml b/.github/workflows/sync-listings-check.yml
new file mode 100644
index 0000000..38aa042
--- /dev/null
+++ b/.github/workflows/sync-listings-check.yml
@@ -0,0 +1,30 @@
+name: Listings Sync Check
+
+# PR 用の verify-only。NN-slug/index.html の追加・変更があった際に
+# sitemap.xml / llms.txt / index.html / README.md の自動生成ブロックが
+# 同期されていなければ fail させる。
+# ローカルでの修正コマンド: python3 scripts/sync_listings.py --write
+#
+# 詳細: scripts/README.md
+
+on:
+ pull_request:
+
+permissions:
+ contents: read
+
+jobs:
+ check:
+ name: Verify auto-generated listing blocks
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.x'
+
+ - name: Run sync_listings --check
+ run: python3 scripts/sync_listings.py --check
diff --git a/CLAUDE.md b/CLAUDE.md
index d8cf586..e08fdc1 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -49,6 +49,12 @@
- AI 周辺は移り変わりが激しい。古くなったら直す、ではなく、**最初から "更新される前提" で設計**
- HTML × Git × PR ベースの運用は、その変化への対応手段
+### 5. 講義一覧は自動同期する(手で更新しない)
+- 新しい講義 `NN-slug/index.html` を追加するだけで、`sitemap.xml` / `llms.txt` / ルート `index.html` / `README.md` の講義一覧ブロックが自動同期される
+- 真実源は各 `NN-slug/index.html` の `
` と ``
+- 同期スクリプト: `python3 scripts/sync_listings.py --write`(PR では `--check` が CI 強制)
+- 詳細: [`scripts/README.md`](./scripts/README.md)
+
---
## 期待する効果(長期)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 469fa35..7a0d7d6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -100,7 +100,26 @@ commit に `Co-Authored-By: Name ` trailer を付けると、GitHub の C
### CI
- **Prompt Injection Guard** が HTML/CSS/JS の追加行を自動スキャン
-- 失敗時は job summary に検出箇所が表示される
+- **Listings Sync Check** が `sitemap.xml` / `llms.txt` / `index.html` / `README.md` の講義一覧ブロックを検証
+- 失敗時は job summary に検出箇所・修正コマンドが表示される
+
+### 新しい Lecture を追加するとき
+
+`sitemap.xml` / `llms.txt` / `index.html`(ルート)/ `README.md` の講義一覧は **自動同期** されます。`NN-slug/index.html` の `` と `` から自動生成されるので、これらのファイルを手で編集する必要はありません。
+
+```bash
+# 講義ディレクトリを作って書く
+cp -r _template NN-slug
+# ... NN-slug/index.html を編集 ...
+
+# 4 つの listing ファイルを同期
+python3 scripts/sync_listings.py --write
+
+# 確認用
+python3 scripts/sync_listings.py --check
+```
+
+PR で同期し忘れた場合は CI(`sync-listings-check.yml`)が落とします。詳細は [`scripts/README.md`](./scripts/README.md)。
### 大きな変更の場合
diff --git a/README.md b/README.md
index 98f41fb..73bd418 100644
--- a/README.md
+++ b/README.md
@@ -27,8 +27,13 @@ AI コーディング(Claude Code 等)を中心に、散在しがちな情
| 章 | 内容 | スライド |
|---|---|---|
-| [`00-about/`](./00-about/index.html) | この資料の歩き方(Read me) | 10 枚 |
-| [`01-claude-code-intro/`](./01-claude-code-intro/index.html) | Claude Code 入門 | 13 枚 |
+
+
+| [`00-about/`](./00-about/index.html) | Read me | 10 枚 |
+| [`01-claude-code-intro/`](./01-claude-code-intro/index.html) | なぜ今、Claude Code を学ぶのか | 13 枚 |
+| [`02-setup/`](./02-setup/index.html) | Claude Code 環境構築 | 13 枚 |
+| [`03-claude-md/`](./03-claude-md/index.html) | CLAUDE.md でプロジェクトを記憶させる | 13 枚 |
+
各章は単体の HTML として完結しているので、ブラウザで開けばそのまま読めます。
@@ -39,8 +44,13 @@ AI コーディング(Claude Code 等)を中心に、散在しがちな情
### 方法 1: 公開サイト(GitHub Pages)
```
+
+
https://co-lect.github.io/lectures/00-about/
https://co-lect.github.io/lectures/01-claude-code-intro/
+https://co-lect.github.io/lectures/02-setup/
+https://co-lect.github.io/lectures/03-claude-md/
+
```
### 方法 2: クローンしてローカルで開く
diff --git a/_template/index.html b/_template/index.html
index 3863b1d..3e32081 100644
--- a/_template/index.html
+++ b/_template/index.html
@@ -2,6 +2,9 @@
+
{{ 講義タイトル }} — Lectures
diff --git a/index.html b/index.html
index e690a72..a6b70ca 100644
--- a/index.html
+++ b/index.html
@@ -102,14 +102,25 @@ Lectures
講義
+
+
-
00 — Read me
-
教材シリーズの設計思想と歩き方。1 年で陳腐化する世界で、資料をどう作るか(9 枚)
+ Lectures シリーズの歩き方 — 1 年で陳腐化する世界で、AI と共同編集する資料はどう作るべきか。教材全体の設計思想を 10 枚で示す Read me。
-
01 — なぜ今、Claude Code を学ぶのか
-
AI コーディングの全体像、ツールの正体、人の立ち位置の変化(12 枚)
+ なぜ今 Claude Code を学ぶのか — AI コーディングの全体像、ツールの正体、人の立ち位置の変化を 13 枚で概観する HTML スライド教材。
+ -
+ 02 — Claude Code 環境構築
+
Claude Code を動かすまでの手順を 13 枚で解説。プランの選択からインストール・認証・初起動まで。
+
+ -
+ 03 — CLAUDE.md でプロジェクトを記憶させる
+
毎回の説明をゼロにする CLAUDE.md の使い方。書き方・置き場所・効果的な3パターンを13枚で解説。
+
+
鮮度について
diff --git a/llms.txt b/llms.txt
index 6120444..a751f5f 100644
--- a/llms.txt
+++ b/llms.txt
@@ -6,8 +6,13 @@
## 講義
-- [00 Read me](https://co-lect.github.io/lectures/00-about/): 教材シリーズの設計思想と歩き方。1 年で陳腐化する世界で資料をどう作るか(10 枚)
-- [01 なぜ今、Claude Code を学ぶのか](https://co-lect.github.io/lectures/01-claude-code-intro/): AI コーディングの全体像、ツールの正体、人の立ち位置の変化(13 枚)
+
+
+- [00 Read me](https://co-lect.github.io/lectures/00-about/): Lectures シリーズの歩き方 — 1 年で陳腐化する世界で、AI と共同編集する資料はどう作るべきか。教材全体の設計思想を 10 枚で示す Read me。
+- [01 なぜ今、Claude Code を学ぶのか](https://co-lect.github.io/lectures/01-claude-code-intro/): なぜ今 Claude Code を学ぶのか — AI コーディングの全体像、ツールの正体、人の立ち位置の変化を 13 枚で概観する HTML スライド教材。
+- [02 Claude Code 環境構築](https://co-lect.github.io/lectures/02-setup/): Claude Code を動かすまでの手順を 13 枚で解説。プランの選択からインストール・認証・初起動まで。
+- [03 CLAUDE.md でプロジェクトを記憶させる](https://co-lect.github.io/lectures/03-claude-md/): 毎回の説明をゼロにする CLAUDE.md の使い方。書き方・置き場所・効果的な3パターンを13枚で解説。
+
## メタ
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644
index 0000000..cb372e1
--- /dev/null
+++ b/scripts/README.md
@@ -0,0 +1,63 @@
+# scripts/
+
+リポジトリ運用のためのユーティリティスクリプト。
+
+---
+
+## `sync_listings.py` — 講義一覧の自動同期
+
+`NN-slug/index.html` を真実源(source of truth)として、以下の自動生成ブロックを同期する:
+
+| ファイル | 同期される場所 |
+|---|---|
+| `sitemap.xml` | ` ... :end -->` — 講義 URL の `` ブロック |
+| `llms.txt` | 同上 — 「## 講義」の箇条書き |
+| `index.html`(ルート) | 同上 — `` の中身 |
+| `README.md` | `` 〜 — 章テーブル |
+| `README.md` | `` 〜 — 公開 URL 一覧 |
+
+各 `NN-slug/index.html` の `` と `` と `` 数が抽出元。
+
+### 使い方
+
+```bash
+# 変更を書き戻す(ローカル開発で使う)
+python3 scripts/sync_listings.py --write
+
+# ずれていないか確認(CI 用、exit 1 で fail)
+python3 scripts/sync_listings.py --check
+```
+
+### 新しい講義を追加する流れ
+
+1. `_template/` をコピーして `NN-slug/` を作る
+2. `` / `` / スライド本文を書く
+3. `python3 scripts/sync_listings.py --write` で 4 ファイルを同期
+4. `git add -A && git commit && PR`
+
+PR を上げ忘れても CI の `sync-listings-check.yml` が落とすので発見できる。
+main にマージ後は念のため `sync-listings-bot.yml` が再生成する二重防衛。
+
+### 依存
+
+- Python 3.8+(標準ライブラリのみ。`pip install` 不要)
+- `git`(lastmod の取得に使用)
+
+### マーカー設計
+
+各ファイルに HTML/XML コメント形式でマーカーを埋める。マーカー間の本文のみ
+スクリプトが再生成し、マーカー外は触らない。
+
+```
+
+... 自動生成 ...
+
+```
+
+同じファイルに 2 か所以上ある場合は名前付きにする:
+
+```
+
+... 自動生成 ...
+
+```
diff --git a/scripts/sync_listings.py b/scripts/sync_listings.py
new file mode 100644
index 0000000..cfce89b
--- /dev/null
+++ b/scripts/sync_listings.py
@@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""sync_listings.py — keep listing files in sync with NN-slug/index.html.
+
+Discovers lecture directories matching ``NN-slug/`` at the repo root,
+extracts metadata (title / meta description / count / git lastmod),
+and rewrites the auto-generated blocks (delimited by HTML comment markers)
+inside sitemap.xml, llms.txt, index.html, and README.md.
+
+Usage:
+ python3 scripts/sync_listings.py --check # exit 1 if any block drifts
+ python3 scripts/sync_listings.py --write # apply drift back to disk
+
+Marker format:
+ ...
+ ...
+
+No third-party deps; Python 3.8+ standard library only.
+"""
+from __future__ import annotations
+
+import argparse
+import datetime
+import html
+import os
+import re
+import subprocess
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+
+SITE_BASE = "https://co-lect.github.io/lectures/"
+LECTURE_DIR_RE = re.compile(r"^([0-9]{2})-([a-z0-9-]+)$")
+TITLE_RE = re.compile(r"(.*?)", re.DOTALL)
+META_DESC_RE = re.compile(
+ r']")
+
+
+def repo_root() -> Path:
+ out = subprocess.run(
+ ["git", "rev-parse", "--show-toplevel"],
+ capture_output=True, text=True, check=True,
+ )
+ return Path(out.stdout.strip())
+
+
+def git_lastmod(rel_path: str, root: Path) -> str:
+ try:
+ out = subprocess.run(
+ ["git", "log", "-1", "--format=%cs", "--", rel_path],
+ capture_output=True, text=True, check=True, cwd=root,
+ )
+ date = out.stdout.strip()
+ if date:
+ return date
+ except subprocess.CalledProcessError:
+ pass
+ return datetime.date.today().isoformat()
+
+
+@dataclass
+class Lecture:
+ num: str
+ slug: str
+ raw_title: str
+ desc: str
+ slide_count: int
+ lastmod: str
+
+ @property
+ def dir_name(self) -> str:
+ return f"{self.num}-{self.slug}"
+
+ @property
+ def url(self) -> str:
+ return f"{SITE_BASE}{self.dir_name}/"
+
+ @property
+ def topic(self) -> str:
+ """Short topic title for listing display.
+
+ Handles two patterns:
+ "Lecture 01 — Foo" → "Foo"
+ "Read me — 1 年で..." → "Read me"
+ (anything else) → full title (after stripping " — Lectures")
+ """
+ t = re.sub(r"\s+—\s+Lectures$", "", self.raw_title.strip())
+ m = re.match(r"^Lecture\s+\d{2}\s+—\s+(.+)$", t)
+ if m:
+ return m.group(1).strip()
+ m = re.match(r"^(.+?)\s+—\s+.+$", t)
+ if m:
+ return m.group(1).strip()
+ return t
+
+
+def _extract(pattern: re.Pattern, page: str, label: str, path: Path) -> str:
+ m = pattern.search(page)
+ if not m:
+ raise SystemExit(f"ERROR: missing {label} in {path}")
+ # HTML entities in the source decode to their characters so that
+ # renderers can re-escape per output format without double-encoding.
+ return html.unescape(m.group(1).strip())
+
+
+def discover_lectures(root: Path) -> list[Lecture]:
+ lectures: list[Lecture] = []
+ for child in sorted(root.iterdir()):
+ if not child.is_dir():
+ continue
+ m = LECTURE_DIR_RE.match(child.name)
+ if not m:
+ continue
+ idx = child / "index.html"
+ if not idx.exists():
+ raise SystemExit(
+ f"ERROR: {child.name}/ matches the lecture naming pattern "
+ f"but has no index.html"
+ )
+ page = idx.read_text(encoding="utf-8")
+ lectures.append(Lecture(
+ num=m.group(1),
+ slug=m.group(2),
+ raw_title=_extract(TITLE_RE, page, "", idx),
+ desc=_extract(META_DESC_RE, page, '', idx),
+ slide_count=len(SECTION_RE.findall(page)),
+ lastmod=git_lastmod(f"{child.name}/index.html", root),
+ ))
+ return lectures
+
+
+# Pre-flight validation. Reject metadata characters that would break the
+# rendered output: newlines collapse listings, ""
+ end_tag = f""
+ s = text.find(start_tag)
+ if s < 0:
+ raise SystemExit(f"ERROR: '{start_tag}' not found")
+ after_start = s + len(start_tag)
+ e = text.find(end_tag, after_start)
+ if e < 0:
+ raise SystemExit(f"ERROR: '{end_tag}' not found (after start marker)")
+ line_start = text.rfind("\n", 0, e) + 1
+ end_indent = text[line_start:e]
+ if end_indent.strip():
+ end_indent = "" # not pure whitespace; do not preserve
+ current = text[after_start:e]
+ new = f"\n{replacement}\n{end_indent}"
+ if current == new:
+ return text, False
+ return text[:after_start] + new + text[e:], True
+
+
+# --- main --------------------------------------------------------------------
+
+def main() -> int:
+ ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
+ g = ap.add_mutually_exclusive_group(required=True)
+ g.add_argument("--check", action="store_true",
+ help="exit 1 if any block drifts")
+ g.add_argument("--write", action="store_true",
+ help="write drift back to disk")
+ args = ap.parse_args()
+
+ root = repo_root()
+ lectures = discover_lectures(root)
+ if not lectures:
+ print("No lectures discovered.", file=sys.stderr)
+ return 2
+ validate_metadata(lectures)
+
+ file_text: dict[str, str] = {}
+ drifted: list[tuple[str, str]] = []
+
+ for path_str, marker, key in TARGETS:
+ path = root / path_str
+ if path_str not in file_text:
+ file_text[path_str] = path.read_text(encoding="utf-8")
+ replacement = RENDERERS[key](lectures)
+ new_text, changed = replace_block(file_text[path_str], marker, replacement)
+ file_text[path_str] = new_text
+ if changed:
+ drifted.append((path_str, marker))
+
+ if args.check:
+ if not drifted:
+ print(f"OK: all listings in sync ({len(lectures)} lectures).")
+ return 0
+ print(f"Drift detected in {len(drifted)} block(s):", file=sys.stderr)
+ for path_str, marker in drifted:
+ print(f" - {path_str} [{marker}]", file=sys.stderr)
+ print(
+ "\nTo fix:\n python3 scripts/sync_listings.py --write\n"
+ "then commit the result.",
+ file=sys.stderr,
+ )
+ summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
+ if summary_path:
+ with open(summary_path, "a", encoding="utf-8") as f:
+ f.write("### Listings out of sync\n\n")
+ f.write("The following auto-generated blocks need regeneration:\n\n")
+ for path_str, marker in drifted:
+ f.write(f"- `{path_str}` (`{marker}`)\n")
+ f.write(
+ "\n**Fix locally:**\n"
+ "```\npython3 scripts/sync_listings.py --write\n```\n"
+ "then commit the result.\n"
+ )
+ return 1
+
+ # --write
+ changed_files = {p for p, _ in drifted}
+ for path_str in changed_files:
+ (root / path_str).write_text(file_text[path_str], encoding="utf-8")
+ if changed_files:
+ print(f"Wrote {len(changed_files)} file(s): "
+ f"{', '.join(sorted(changed_files))}")
+ else:
+ print(f"OK: nothing to write ({len(lectures)} lectures, all in sync).")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/sitemap.xml b/sitemap.xml
index 7b89909..3c8d36f 100644
--- a/sitemap.xml
+++ b/sitemap.xml
@@ -2,20 +2,35 @@
https://co-lect.github.io/lectures/
- 2026-05-17
+ 2026-05-20
monthly
1.0
+
+
https://co-lect.github.io/lectures/00-about/
- 2026-05-17
+ 2026-05-19
monthly
0.8
https://co-lect.github.io/lectures/01-claude-code-intro/
- 2026-05-17
+ 2026-05-19
monthly
0.8
+
+ https://co-lect.github.io/lectures/02-setup/
+ 2026-05-19
+ monthly
+ 0.8
+
+
+ https://co-lect.github.io/lectures/03-claude-md/
+ 2026-05-19
+ monthly
+ 0.8
+
+