UTAF / 自走式ごみ箱 — 00

手元のフォルダを、
スマホから開けるURLにする

自分のPCの中にあるHTMLファイルを、世界中のブラウザから開ける状態にします。 Git・GitHub・Cloudflare の3つを順に通します。

読む時間 1〜2時間 / このページ自体が、この手順で公開されています

デプロイとは、手元にあるファイルを、他人のブラウザから届く場所に置くことである。

いまあなたがこのページを読んでいるという事実が、この教材のゴールです。 このページは C:\Users\0yory\dev\utaf-trashbot\docs\00_git-github-cloudflare.html という1つのファイルとして作られ、そこから3つの場所を経由して、あなたの画面に届いています。

この教材では、その3つの場所を順に通していきます。 手を動かす部分は、すべて実際に打ったコマンドと、実際に返ってきた文字をそのまま載せます。

WHYなぜ必要か

「ファイルを他の人に見せたい」だけなら、もっと簡単な方法がいくつもあります。 OneDrive の共有リンクを送る、Google ドライブに置く、Slack に添付する。 それで足りる場面も実際にあります。

では、なぜこのプロジェクトではわざわざ Git・GitHub・Cloudflare を通すのか。 困りごとが4つあって、そのどれも上の方法では解決しないからです。

困りごと1 — HTMLファイルは「送っても開けない」ことがある

HTMLファイルは、ブラウザが読む設計図です。 OneDrive や Slack にHTMLファイルを置いてリンクを送ると、多くの場合 ブラウザはそれを「ページ」として表示せず、ダウンロードしてしまいます。 スマホだと特にそうなります。ダウンロードしたHTMLファイルをスマホで開くのは、かなり面倒です。

「ページとして表示する」ためには、ファイルを置いた場所が 「これはHTMLです」とブラウザに宣言してくれる必要があります。 この宣言を Content-Type と言います。あとで実物を見ます。 Cloudflare のような配信サービスは、この宣言をやってくれる場所です。

困りごと2 — UTAFフォルダは大きすぎてクラウドに送れない

クラウドセッション(Anthropic 側のコンピュータで Claude を動かす仕組み)は、 手元のフォルダをアップロードして作業できます。ただし上限が100MBです。

UTAFフォルダの大きさを測ると、こうなりました。

du -sh を実行した結果(2026-10-28)
1.3G    C:\Users\0yory\OneDrive\Apps\UTAF

439M    lerobot/
403M    curriculum/
174M    urdf/
 63M    slides/
 47M    materials/
ファイル数: 27958

1.3GB。上限の13倍です。物理的に送れません。 だから「ごみ箱プロジェクトだけの、軽い別フォルダ」を作る必要がありました。 これが C:\Users\0yory\dev\utaf-trashbot です。

困りごと3 — .git を OneDrive の中に置くと壊れる

この先で使う Git は、フォルダの中に .git という隠しフォルダを作って、 そこに履歴を全部しまいます。.git の中では、 何千個もの小さなファイルが、決まった順番で書き換わります。

OneDrive は「ファイルが変わったら同期する」サービスです。 この2つを重ねると、OneDrive が同期している最中に Git が次の書き換えを始めて、 履歴が壊れることがあります。 壊れ方が分かりにくいのが厄介で、「ある日突然、過去のコミットが読めなくなる」といった形で出ます。

だから C:\Users\0yory\dev\、つまり OneDrive の外 に置きました。 「バックアップが無くなるのでは」と思うかもしれませんが、逆です。 Git リポジトリのバックアップは GitHub が担います。しかも OneDrive より丁寧に、 「いつ・誰が・どの行を・なぜ変えたか」まで残ります。

困りごと4 — 「最新版はどれ?」が分からなくなる

クラウドセッションを使うと、Claude が勝手にファイルを書き換えていきます。 同時に自分も書き換えます。何本も並列で走らせると、同じファイルを2人(2セッション)が触ります。

このとき 「最新版はどれか」を決める仕組みが無いと、 後から保存した方が前の作業を消します。 Git は、この衝突を検出して「両方残したうえで、どう混ぜるか決めろ」と言ってくれます。 実際に今日それが起きたので、あとで実例として出します。

まとめると ── HTMLをページとして配信するため(1)、クラウドセッションに送れる大きさにするため(2)、 履歴を壊さないため(3)、同時編集の衝突を解決するため(4)。 この4つのために、3つの場所を通します。

OVERVIEW全体像

通る場所は3つです。それぞれ役割がはっきり違います。

① 手元のPC dev\utaf-trashbot docs\*.html .git git push ② GitHub 履歴の保管庫 private 誰が何を変えたか 自動 ③ Cloudflare 配信する場所 learn.utaf.dev 東京のサーバー スマホ・PC ブラウザ クラウドセッション ここも②を読む
手元で書く(①)→ 履歴ごと預ける(②)→ 配信される(③)。 クラウドセッションは手元のPCではなく②を読むので、push していないものは見えない。
場所役割これが無いと
① Git
(手元)
変更の履歴を記録する いつ何を変えたか分からない。衝突したとき直せない
② GitHub
(ネット上)
履歴を預ける。他の人やクラウドセッションがそこを読む 自分のPCを開いている間しか作業できない
③ Cloudflare
(ネット上)
HTMLをページとして配信する URLが存在しない。スマホで開けない

PART 1Git ── 履歴を記録する

リポジトリとは

リポジトリ(repository)
ファイルと、そのファイルの変更履歴を全部抱えたフォルダのこと。 「作業フォルダ + 履歴」がセットになったもの、と考えてください。 履歴は .git という隠しフォルダに入っています。
コミット(commit)
ある時点のファイルの状態を、丸ごと記録したもの。スナップショットです。 「この行を変えた」という差分ではなく、「このときフォルダはこうだった」という写真が残ります。

ここで引っかかりやすいのが、「保存」と「コミット」は別物だという点です。 エディタで Ctrl+S を押すのは「保存」です。ファイルが書き換わります。 でもその時点では、Git は何も記録していません。

コミットは、自分で「いまの状態を記録しろ」と命令したときだけ作られます。 つまり Git の履歴は、自動では増えません。 これは不便に見えますが、意図があります。 「意味のある区切り」でだけ記録したいからです。

実際に打ったコマンド

今日このリポジトリを作ったときのコマンドを、全部そのまま載せます。 1行ずつ意味を見ていきます。

リポジトリを作って、最初のコミットを作るまで
cd /c/Users/0yory/dev/utaf-trashbot

git init -b main

git config user.name  "Half-and-Half-run"
git config user.email "263915778+Half-and-Half-run@users.noreply.github.com"

git add -A

git commit -m "プロジェクトの土台を作る"

git init -b main

いまいるフォルダをリポジトリにします。.git フォルダがここで作られます。 実行前は、こう言われます。

git init をする前に git のコマンドを打つと出るエラー
fatal: not a git repository (or any of the parent directories): .git

「ここはリポジトリじゃない」という意味です。 .git が無いので履歴を記録する場所が無い、と言っています。 このエラーを見たら git init を忘れています。

-b main は「最初のブランチの名前を main にする」という指定です。

ブランチ(branch)
履歴の枝のこと。main は「本線」という意味の慣習的な名前です。 ブランチが複数あると、本線を壊さずに実験できます。 クラウドセッションは作業ごとに別のブランチを作って、 あとで本線に合流させる(これをマージと言います)形で動きます。

-b main を付けないと、Git のバージョンによって master という名前になります。 GitHub 側が main を標準にしているので、合わせておかないと後で名前の食い違いが起きます。

git config user.name / user.email

コミットには「誰が作ったか」が必ず刻まれます。その名前とメールアドレスの設定です。

ここで実際に問題が起きました。設定を確認したら、こうなっていました。

設定の確認結果
$ git config --global user.name
Half-and-Half-run

$ git config --global user.email
your-email@example.com

メールアドレスが your-email@example.com、つまり初期値のままでした。 このままコミットすると、履歴に存在しないアドレスが永久に残ります。

ではどのアドレスを入れるか。ここに落とし穴があります。 コミットに刻まれたメールアドレスは、リポジトリを公開した瞬間に誰でも読めます。 しかも履歴なので、あとから消すのが非常に面倒です。

そのための仕組みが GitHub にあります。noreply アドレスです。

noreply アドレスの形
263915778+Half-and-Half-run@users.noreply.github.com
   ↑                ↑
 ユーザーID      ユーザー名

これは GitHub が用意している「あなた専用だが、個人のメールではない」アドレスです。 GitHub 側ではあなたのアカウントと紐づくので、コミットは正しくあなたのものとして表示されます。 でも個人の Gmail は漏れません。

なぜ --global を付けなかったか。 --global を付けると、そのPCの全リポジトリに適用されます。 付けないと、いまいるリポジトリだけの設定になります。 今回は「このリポジトリだけ noreply にする」という判断なので、付けていません。

git add -A

ここが、Git を初めて触る人が一番つまずく場所です。

git add は「次のコミットに含めるファイルを選ぶ」命令です。 -A は「全部」という意味です。 この選ぶ場所のことを ステージング(staging) と言います。

作業フォルダ 書き換えたファイル (Ctrl+S した状態) add ステージング 記録するものを選ぶ台 まだ履歴ではない commit 履歴 スナップショット もう消えない
保存しただけでは左端。add で中央へ、commit で右端へ移る。 2段階になっているのは、変更の一部だけを記録できるようにするため。

なぜ2段階に分かれているのか。 10個のファイルを直したけれど、そのうち3個だけを「この修正」として記録したい、 ということが実際にあります。残り7個は別の作業なので、別のコミットにしたい。 ステージングがあると、それができます。

1段階だと「直したもの全部が、まとめて1つの記録になる」しかなく、 あとから履歴を読んだときに「このコミット、何をしたやつだ?」が分からなくなります。

git commit -m "…"

ステージングに乗っているものを、履歴に刻みます。 -m はメッセージの指定です。このメッセージが履歴に残り、 あとから「何をしたコミットか」を読む唯一の手がかりになります。

実行すると、こう返ってきました。

コミットしたときの出力
warning: in the working copy of 'CLAUDE.md',
         LF will be replaced by CRLF the next time Git touches it

04a4c61 プロジェクトの土台を作る

04a4c61 がコミットの名前です。コミットハッシュと言います。 中身から機械的に計算された文字列で、世界中で重複しません。 「あのときの状態に戻したい」と言うとき、この7文字で指定します。

その警告は何か ── LF と CRLF

上に出ている warning は、放っておいて構いません。 ただ必ず出るので、意味を知っておくと驚かずに済みます。

テキストファイルの「改行」は、見えない文字で表されています。 そしてこの文字が、OS によって違います。

書き方中身使うOS
LF改行を1文字で表すLinux / macOS
CRLF改行を2文字で表すWindows

Git は履歴の中では LF で統一し、Windows のフォルダに取り出すときは CRLF に戻します。 その変換が起きることを教えてくれているだけです。 ファイルが壊れるという警告ではありません。

PART 2GitHub ── 履歴を預ける

リモートとは

リモート(remote)
ネット上にあるリポジトリの置き場所のこと。 手元のリポジトリと、ネット上のリポジトリを結びつけておくと、 push で送り、fetch で受け取れます。 慣習で origin という名前を付けます。

ここで大事なのは、GitHub は「バックアップ置き場」ではないということです。 履歴ごと預かる場所なので、GitHub 側でも「誰がいつ何の行を変えたか」が全部読めます。 そしてクラウドセッションは、あなたのPCではなくGitHub を読んで作業します。

これが重要な帰結を持ちます。 push していない変更は、クラウドセッションには存在しません。 手元でどれだけ書いても、push するまでクラウドの Claude には見えません。

リポジトリを作る

リポジトリをネット上に作って、履歴を送るコマンド
gh repo create utaf-trashbot --private --source=. --push
部分意味
ghGitHub を操作するコマンド。GitHub CLI という道具
repo createネット上に新しいリポジトリを作る
utaf-trashbotリポジトリの名前
--private非公開にする。これが無いと公開になる
--source=.いまいるフォルダを中身として使う
--push作ったあと、履歴をすぐ送る

private と public の判断

このリポジトリは --private にしました。理由は、 プロジェクトの仕様書に予算と部品単価とメンバーの名前が入っているからです。 コード自体は公開して困るものではありませんが、公開する理由も特にありません。

判断の基準は「公開して困るものが1つでも入っているか」です。1つでもあれば private です。 後から gh repo edit --visibility public で公開に切り替えられるので、 迷ったら private にしておくのが安全です。逆向き(公開→非公開)は手遅れです。 一度公開されたものは、誰かが既にコピーしている可能性を消せません。

実際に起きた事故 ── 「すでに存在します」

上のコマンドを実行したら、こう返ってきました。

実際に出たエラー
GraphQL: Name already exists on this account (createRepository)

「その名前はこのアカウントに既にあります」。 つまり utaf-trashbot がすでに作られていたということです。

ここで絶対にやってはいけないのが、無理に上書きすることです。 中に何が入っているか分からないまま上書きすると、他人の作業を消します。 なので、まず中を見ます。

リモートの中身と履歴を確認する
$ gh api repos/Half-and-Half-run/utaf-trashbot/commits \
    --jq '.[] | "\(.commit.author.date)  \(.commit.author.name)  \(.commit.message)"'

2026-10-28T10:29:25Z  Ryouji Kawashima    Update project name in CLAUDE.md
2026-10-28T10:21:51Z  Half-and-Half-run   プロジェクトの土台を作る

読めたことはこうです。

つまり 手元とネット上で、履歴が枝分かれしていた状態です。

枝分かれした履歴をどう合わせるか

状況を図にすると、こうです。

04a4c61 共通の土台 61e8def GitHub 側(本人の手編集) The University of Tokyo に直した cd84baf 手元側(Cloudflare 対応) vercel.json を消して _headers を足した
同じ土台から2方向に伸びている。どちらも消したくない。 この形を「枝分かれした(diverged)」と言う。

やってはいけないのは git push --force です。 これは「手元の履歴で、ネット上の履歴を上書きしろ」という命令です。 実行すると 上の枝(本人の手編集)が消えます。

正しい手順は、まず相手の枝を取ってきて、自分の変更をその上に載せ直すことです。 これを リベース(rebase) と言います。

相手の枝を取って、自分の変更を上に載せ直す
git remote add origin https://github.com/Half-and-Half-run/utaf-trashbot.git

git fetch origin

git merge-base HEAD origin/main
04a4c61b3cc1f94792389e6b4955ba6b6c209958

git rebase origin/main
Successfully rebased and updated refs/heads/main
コマンドやっていること
git remote add origin … ネット上の置き場所を origin という名前で登録する
git fetch origin 相手の履歴を取ってくる。手元のファイルはまだ変わらない
git merge-base HEAD origin/main 「どこから枝分かれしたか」を調べる。共通の土台を教えてくれる
git rebase origin/main 自分のコミットを、相手の枝の先に載せ直す

fetch で手元のファイルが変わらない、という点は覚えておく価値があります。 「取ってくるだけ」と「手元に反映する」が分かれているので、 取ってきて中を見てから、どうするか決められます。

リベース後の履歴を確認すると、こうなりました。

リベース後の履歴と、本人の修正が残っているかの確認
$ git log --oneline
bc3b384 公開先を Cloudflare Pages にする      ← 自分の変更が上に乗った
61e8def Update project name in CLAUDE.md     ← 本人の手編集が残っている
04a4c61 プロジェクトの土台を作る

$ grep -n "Autonomy Forge" CLAUDE.md
5:UTAF(The University of Tokyo Autonomy Forge)のプロジェクト。

一直線になりました。本人が直した The University of Tokyo も残っています。 どちらの作業も失われていません。 これがリベースの目的です。

原則として覚えておくこと。 人が手で直したものは、機械が作ったものより優先します。 --force で押し流す前に、必ず fetch して中を見る。 これは Git の作法というより、共同作業の作法です。

private リポジトリには、もう1つ許可が必要

ここが見落としやすい点です。claude.ai と GitHub を連携しても、 private リポジトリはそれだけでは読めません。

理由は、GitHub の権限の仕組みにあります。 「あなたのアカウントと繋がっている」ことと、 「このリポジトリを読んでよい」ことは別の許可だからです。 後者は GitHub App という単位で、リポジトリごとに与えます。

だから private リポジトリでクラウドセッションを使うには、 Claude GitHub App をそのリポジトリに入れる必要があります。 同じ理由で、Cloudflare にも Cloudflare 用の GitHub App を入れました。 サービスごとに別のアプリで、別に許可します。

All repositories と Only select repositories

アプリを入れるとき、どのリポジトリに許可するか選びます。

選択意味向いている場面
Only select
repositories
選んだリポジトリだけ private リポジトリが複数あり、片方だけ見せたいとき
All repositories 全部。今後作るものも自動で含む private が1つだけ、または全部そのサービスで使うとき

判断の材料は 「private リポジトリがいくつあるか」 です。 公開リポジトリはもともと誰でも読めるので、許可しても実質的に何も増えません。 今回のアカウントは private が utaf-trashbot の1つだけだったので、 All repositories でも実質的な差がありませんでした。

PART 3Cloudflare ── ページとして配信する

なぜ Pages ではなく Workers なのか

Cloudflare で静的なサイトを公開する方法は、長く Cloudflare Pages でした。 UTAF の既存の教材サイト(utaf-curriculum.pages.dev)もこれです。

ところが今回 Pages を選ぼうとしたら、画面に legacy と出ました。 調べると、Cloudflare は新規プロジェクトには Workers を勧めるようになっていました。 Pages は既存プロジェクトのために維持されるものの、新しい機能は追加されません。 廃止日は発表されていませんが、今から新しく作るなら Workers 側が正解です。

PagesWorkers(Static Assets)
立場既存プロジェクトの維持新規はこちら
公開するフォルダの指定画面で Build output directory に入力 リポジトリの wrangler.jsonc に書く
_headers使える使える
料金無料枠あり静的ファイルの配信は無料

この違いが、画面を見たときの混乱の原因になります。 Workers の画面には Build output directory という欄がありません。 代わりに Deploy command が出ます。 「設定する場所が、画面からリポジトリの中のファイルに移った」と理解してください。

wrangler.jsonc ── 公開するフォルダを宣言する

これがリポジトリ直下に置いたファイルの全文です。これだけです。

wrangler.jsonc(全文)
{
  // docs/ の中身をそのまま配信するだけの設定。
  // Worker のコード(main)は書かない。静的ファイルだけを公開する。
  "name": "utaf-trashbot",
  "compatibility_date": "2026-10-28",
  "assets": {
    "directory": "./docs"
  }
}
行意味間違えると
"name" Worker の名前。URL になる(utaf-trashbot.〇〇.workers.dev) 画面で作った名前と違うと、Worker が2つできる
"compatibility_date" 「この日の Cloudflare の挙動で動かす」という固定。作った日を入れる 無いとデプロイが拒否される
"assets".
"directory"
配信するフォルダ。./docs を指定 ./ にすると README.md や CLAUDE.md まで公開される

拡張子が .jsonc である理由

ふつうの JSON ファイルには コメントが書けません。 // から始まる行を入れると、読み込みに失敗します。

.jsonc は「コメントを書ける JSON」です(c は comment)。 上のファイルの1行目と2行目がコメントです。 設定ファイルは、半年後の自分が読んで意味が分からなくなりがちなので、 コメントが書ける形式を選んでいます。

main を書かないという選択

Workers は本来、プログラムを動かす仕組みです。 リクエストが来たら何か計算して返す、という処理を書けます。 その処理を書いたファイルを main で指定します。

今回は main を書いていません。 教材はただのHTMLファイルで、計算する必要が無いからです。 assets だけを書くと、「ファイルをそのまま返すだけの Worker」になります。

_headers ── まだ検索に出したくない

教材が8枚とも「未作成」の状態で Google の検索結果に出てしまうと、 困ります。中身が無いページが「UTAF 教材」として見つかってしまいます。

これを防ぐのが _headers です。 docs/ の中、つまり配信するフォルダの直下に置きます。

docs/_headers(全文)
/*
  X-Robots-Tag: noindex
行意味
/*どのURLに適用するか。* は「何でも」なので全ページ
(字下げした行)付けるヘッダ。字下げが必須。無いとURL指定と解釈される
X-Robots-Tag: noindex 検索エンジンに「この内容を検索結果に載せないで」と伝える

大事なのは、これは「見られなくする」設定ではないということです。 URL を知っている人は普通に読めます。検索から見つからなくなるだけです。 この状態を「限定公開」と呼んだりします。

完成して人に広めてよくなったら、このファイルを消します。 それだけで検索に載るようになります。

ヘッダ(header)
ページの本体とは別に、ブラウザへ送られる「この応答についての情報」のこと。 HTMLの中身ではなく、中身に付いてくる荷札です。 「これはHTMLです」「文字コードはUTF-8です」「検索に載せないで」といった指示が入ります。

画面に入れた値

Cloudflare の Workers Builds 画面に入れた値
Worker name      : utaf-trashbot        ← wrangler.jsonc の name と揃える
Branch           : main
Build command    : (空欄)
Deploy command   : npx wrangler deploy
Root directory   : /                    ← docs にしてはいけない

Root directory を docs にしてしまうのが、ありがちな間違いです。 「公開したいのは docs だから」と思ってしまいますが、逆です。

Root directory は 「wrangler.jsonc がどこにあるか」の指定です。 設定ファイルはリポジトリ直下にあるので / です。 そして公開するフォルダは、その設定ファイルの中で ./docs と指定済みです。 役割が二重になっているように見えて、実は別の話です。

npx
「必要な道具をその場で取ってきて実行する」コマンド。 npx wrangler deploy は、wrangler(Cloudflare を操作する道具)を 事前にインストールしていなくても、その場で取ってきて実行してくれます。 初回だけ時間がかかるのはこのためです。

PART 4ドメイン ── 覚えられる名前を付ける

ここまでで utaf-trashbot.kubokubo456z.workers.dev が動きました。 読めますが、人に口で伝えるには長すぎます。 learn.utaf.dev にします。

DNS とは

DNS(Domain Name System)
learn.utaf.dev のような名前を、 サーバーの住所(IPアドレス)に変換する仕組み。 電話帳のようなものです。ブラウザは、名前を打たれたらまず DNS に住所を聞きに行きます。
サブドメイン
utaf.dev の前に付ける名前。 learn.utaf.dev の learn の部分です。 ドメインを1つ持っていれば、サブドメインは好きな数だけ無料で作れます。

同じアカウント内だと自動になる、という話

UTAF のウェブサイト(www.utaf.dev)を設定したときのメモが残っていました。

website/cloudflare_dns.txt(既存のメモ、抜粋)
Type:   CNAME
Name:   www
Target: 1fdfc120acf8c45d.vercel-dns-017.com
Proxy status: DNS only   ← グレーの雲にする

このときは、Vercel という別のサービスが出した住所を、 手でコピーして Cloudflare に入力しました。 CNAME は「この名前は、別の名前と同じ場所を指す」という種類のレコードです。

今回はこの作業が要りませんでした。理由は、 ドメインと配信先が同じ Cloudflare アカウントの中にあるからです。 Cloudflare 側で「このドメインは自分が管理している」と分かるので、 カスタムドメインを追加した瞬間に、必要なレコードを自分で作ります。

実際、追加した直後に確認したらこうなっていました。

DNS が引けるかの確認
$ nslookup learn.utaf.dev

Name:      learn.utaf.dev
Addresses: 2606:4700:3033::6815:3fa4
           2606:4700:3034::ac43:ab10

住所が返ってきました。手で何も入力していません。

証明書 ── https の鍵マーク

証明書(certificate)
「この名前のサイトは、確かにこのサーバーのものです」を第三者が保証する電子的な書類。 ブラウザのアドレス欄に鍵マークが出るのは、これが有効なときです。 これが無いと、通信が暗号化されず、途中で書き換えられても気づけません。

新しいサブドメインを足した直後は、証明書の発行がまだ終わっていないことがあります。 その間は https で繋がりません。数分から十数分で発行されます。 実際に確認したら、こうなっていました。

証明書の中身を確認する
$ echo | openssl s_client -connect learn.utaf.dev:443 \
    -servername learn.utaf.dev

depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R4
depth=1 C=US, O=Google Trust Services, CN=WE1
depth=0 CN=utaf.dev
verify return:1

verify return:1 は「検証が通った」という意味です。 発行は終わっていました。

実際に起きた勘違い ── status=000

ところが、同じタイミングでページを取ろうとすると失敗しました。

失敗した確認
$ curl -s -o /dev/null -w "status=%{http_code}\n" https://learn.utaf.dev/
status=000

status は、サーバーが返す3桁の番号です。 200 なら成功、404 ならページが無い、500 ならサーバーの故障。 ところが 000 は、そのどれでもありません。

000 は「サーバーまで届かなかった」という意味です。 番号を返してもらう前の段階で失敗しています。 つまりサイトの問題ではなく、繋ぐ経路の問題です。

原因を切り分けるために、経路を変えて試しました。

IPv4 を指定して試す
$ curl -s -4 -o /dev/null -w "status=%{http_code}\n" https://learn.utaf.dev/
status=200

通りました。-4 は「IPv4 で繋げ」という指定です。

IPv4 と IPv6
インターネットの住所の書き方が2種類あります。 IPv4 は 192.0.2.1 のような形、IPv6 は 2606:4700:… のような形です。 住所が足りなくなったので新しい書き方(IPv6)が作られましたが、 両方が混在して使われています。

先の nslookup の結果を見返すと、返ってきた住所は IPv6 だけでした。 そして確認に使った環境は IPv6 の経路が通っていなかった。 サイトは最初から正常で、こちらの環境の問題だったというだけの話です。

ここから学べること。 エラーが出たとき「何が壊れたか」を決めつける前に、条件を1つだけ変えて試す。 今回変えたのは経路(IPv6 → IPv4)だけです。 それで結果が変わったので、「サイトではなく経路が原因」と確定できました。 これは切り分け(isolation)といって、あらゆるデバッグの基本です。

最終確認 ── 荷札を読む

公開されたページのヘッダを全部見る
$ curl -sI -4 https://learn.utaf.dev/

HTTP/1.1 200 OK
Content-Type: text/html
x-robots-tag: noindex
CF-RAY: a5196c1ec86de358-NRT
行何が分かるか
200 OK正常に返っている
Content-Type: text/html 「これはHTMLです」という宣言。 これがあるからブラウザがページとして表示する。困りごと1の答え
x-robots-tag: noindex docs/_headers が効いている。検索に出ない
CF-RAY: …-NRT どこのサーバーが返したか。NRT は成田、つまり東京。 だから速い(0.1秒)

この4行が揃っていれば、公開は完了しています。

DRILL1行だけ変えてみる

読むだけでは身に付かないので、実際に1行変えて、結果を見てください。 どれも元に戻せます。

演習1 ── push が本当に自動で反映されるか確かめる

docs/index.html を開いて、見出しの下の1行を探します。

<p class="lede">TurtleBot3 を土台に、呼ぶと来るごみ箱ロボットを作る。…</p>

この文の最後に、自分の名前でも何でもいいので1単語足してください。そして:

git add -A
git commit -m "確認用に1行変える"
git push

1〜2分待って、スマホで learn.utaf.dev を開き直します。 足した単語が出ていれば、自動反映が効いています。 出てこないなら Git 連携ができていないので、Cloudflare の Settings → Build を見てください。

これは演習というより、仕組みが繋がっているかの検査です。 一番最初にやる価値があります。

演習2 ── 配信するフォルダを間違えるとどうなるか

wrangler.jsonc の directory を "./" に変えて push します。

"assets": {
  "directory": "./"      ← ./docs から変える
}

反映されたら learn.utaf.dev/CLAUDE.md を開いてみてください。 作業ルールが全世界に読めるようになっています。

これが docs/ を切り分けている理由です。 「公開するもの」と「リポジトリに置くもの」は別だという前提で作られています。 確認したら "./docs" に戻して push してください。

演習3 ── 荷札を自分で足してみる

docs/_headers に1行足します。

/*
  X-Robots-Tag: noindex
  X-UTAF-Test: hello

push して反映されたら、こう確認します。

curl -sI https://learn.utaf.dev/

x-utaf-test: hello が出てくれば、 自分でヘッダを足せたということです。 字下げを忘れると効かないので、そこも試してみてください。 確認したら足した行を消します。

PITFALLSつまずきやすいところ

今日実際に出たものを、全部そのまま載せます。

ここはリポジトリじゃない

fatal: not a git repository (or any of the parent directories): .git

git init をしていないフォルダで git のコマンドを打つと出ます。 あるいは、別のフォルダにいます。 pwd(いまどこにいるか表示)で場所を確かめてください。

その名前はもうある

GraphQL: Name already exists on this account (createRepository)

同じ名前のリポジトリが既にあります。 消す前に中を見てください。自分で作ったのを忘れているか、 誰かが作っています。確認は gh repo view 名前 と gh api repos/所有者/名前/commits です。

status=307 が返る ── URLから .html が落ちる

$ curl -s -o /dev/null -w "status=%{http_code}\n" \
    https://learn.utaf.dev/00_git-github-cloudflare.html
status=307

これは失敗ではありません。 307 は「その住所は別の住所に移った」という意味の返事です。 Cloudflare Workers は既定で、URLから .html を外した形を正式なURLにします。 つまり本当のURLはこちらです。

https://learn.utaf.dev/00_git-github-cloudflare

.html を付けて開いても、自動でこちらに連れて行かれるので問題ありません。 ただし curl で確認するときは -L (リダイレクトを追う)を付けないと 307 で止まります。 これを知らずに「200が返らない=デプロイ失敗」と判断すると、 動いているものを壊しに行くことになります。

$ curl -sS -L -o /dev/null -w "%{http_code} %{url_effective}\n" \
    https://learn.utaf.dev/00_git-github-cloudflare.html
200 https://learn.utaf.dev/00_git-github-cloudflare

返事の番号は3桁の最初の数字で大きく分かれます。 2xx は成功、3xx は移動、4xx はこちらの間違い、 5xx は向こうの故障。3xx を失敗と数えないこと。

status=000 が返る

status=000

サーバーまで届いていません。サイトの故障ではなく経路の問題です。 -4 を付けて IPv4 で試す、別の回線で試す、数分待つ。 証明書の発行待ちでも同じ症状になります。

Build output directory という欄が無い

壊れていません。Workers の画面を見ています。 その役目は wrangler.jsonc の assets.directory が引き受けています。 画面には Deploy command が出ているはずなので、 そこに npx wrangler deploy と入れてください。

Root directory に docs と入れてしまった

wrangler.jsonc が見つからず、デプロイが失敗します。 Root directory は「設定ファイルの場所」で、 「公開するフォルダ」ではありません。/ に戻してください。

LF will be replaced by CRLF という警告

warning: in the working copy of 'CLAUDE.md',
         LF will be replaced by CRLF the next time Git touches it

無視してよい警告です。改行文字の書き方が Windows と Linux で違うので、変換が起きることを教えているだけです。 ファイルは壊れません。Windows で Git を使うと、ほぼ毎回出ます。

クラウドセッションが、書いたはずのファイルを知らない

push していないからです。 クラウドセッションはあなたのPCではなく GitHub を読みます。 手元にしか無いコミットは、存在しないものとして扱われます。 クラウドに作業を投げる前に git push してください。

RELATED関連