Claude Code や Codex が生成した HTML を、@pycon.jp のアカウントを持つ人だけが開ける URL にする。
レポートやダッシュボードを作って、コマンドを一つ実行すると URL が返る。Slack に貼れば共有は終わり。
https://htmlshare.in.2027.pycon.jp
- 閲覧できるのは @pycon.jp の Google アカウントを持つ全員。共有ごとに閲覧者を絞る機能は無い。応募者や参加者の個人情報など、全スタッフに見せられないものは上げない
- 共有を変更・削除できるのは、最初に上げた人だけ
- 上限は 1 ファイル 25MB、1 共有あたり合計 100MB、500 ファイル
- 見えるのは最新の版だけ。過去の版には戻せない
共有の URL (https://pyconjpshare-content.contact-2b3.workers.dev/s/<slug>/) を開くと、初回だけ Google のログインを経由してページが表示される。@pycon.jp 以外のアカウントでは開けない。
https://htmlshare.in.2027.pycon.jp を開くと、上がっている共有の一覧が見られる。
- 「すべての共有」「自分の共有」で切り替え、タイトルと説明の検索、タグでの絞り込みができる
- 共有のタイトルを押すと詳細が開く。自分の共有なら、タイトル、説明、タグの編集、公開・非公開の切り替え、削除ができる
- 非公開にすると URL は残ったまま、誰も開けなくなる。削除はファイルごと消えて元に戻せない
uv を入れておく。CLI はインストールせず uvx で直接動かす。
alias pyconjpshare='uvx --from git+https://github.com/pyconjp/pyconjpshare pyconjpshare'
pyconjpshare loginlogin を実行するとブラウザが開き、「pyconjpshare CLI を承認しますか」の画面が出るので「承認する」を押す。120 秒で時間切れになる。トークンは ~/.config/pyconjpshare/token に保存され、次からは要らない。
pyconjpshare push ./out --slug q3-report --title "Q3 レポート" --tag program- ディレクトリを渡すときは、直下に
index.htmlが要る。HTML ファイルを 1 つだけ渡してもよい - 返ってきた URL を Slack などに貼れば共有は終わり。閲覧範囲の設定は要らない
--slugを省くとディレクトリ名 (単一ファイルならファイル名) から作る。--titleを省くとindex.htmlの<title>を使う- 同じ
--slugで上げ直すと、URL はそのままで中身が入れ替わる。変わっていないファイルは送られない - タイトル、説明、タグは上げるたびに上書きされる。上げ直すときに
--titleや--tagを省くと前回の値が消えるので、毎回同じ指定を付ける - 他の人が使っている slug には上げられない
| コマンド | 用途 |
|---|---|
login |
ブラウザで認証してトークンを保存 |
push <path> |
上げる。--slug --title --description --tag (複数可) --json |
list |
一覧。--mine --q --tag --json |
rm <slug> |
削除。端末でないときは --yes が必須 |
open <slug> |
ブラウザで開く |
whoami |
認証されているアカウント |
--json を付けると {"url": ..., "slug": ..., "rev": ..., "files": ..., "bytes": ...} を返す。終了コードは 0 成功、2 引数や入力の誤り、3 認証エラー (login し直す)、4 サーバー側で拒否または失敗。
/plugin marketplace add pyconjp/pyconjpshare
/plugin install pyconjpshare@pyconjpshare
入れておくと、HTML を作って「運営で共有して」と頼んだときに、Claude が Artifact や GitHub Pages ではなく pyconjpshare で上げて URL を返す。トークンが無ければ Claude が pyconjpshare login を実行するので、ブラウザで「承認する」を押す。
CI ではブラウザを開けないので、https://htmlshare.in.2027.pycon.jp/tokens で名前を付けてトークンを発行し、環境変数 PYCONJPSHARE_TOKEN に入れる。トークンは発行した画面でしか表示されない。要らなくなったら同じ画面で失効させる。
Cloudflare Workers を 2 本立てる。
- app worker (
src/app): Google ログイン、共有の一覧と詳細、CLI 用 API、トークン管理 - content worker (
src/content): アップロードされた HTML の配信だけ。API を一本も持たない
上げた HTML は任意の JavaScript を実行するので、配信を別ホストに分けて、そのホストには叩ける経路を置かない。ファイル実体は R2、メタデータは D1。設計は PyCon JP 運営リポジトリの docs/superpowers/specs/2026-09-08-pyconjpshare-design.md、API は docs/api.md。
無料プランでは 1 リクエストあたり R2 と D1 の呼び出しが 50 回までなので、どの処理もファイル数に比例して R2 や D1 を叩かない。push し直したときに変わらないファイルは R2 でコピーせず、D1 の rev_files.object_rev で前の rev の実体を指す。
npm ci
npm test # サーバー (vitest + workers pool)
npm run typecheck
uv run pytest # CLI
uv run ruff check cliwrangler の設定を変えたら npm run types で worker-configuration.d.ts を作り直す。
いまのデプロイ先は次のとおり。個人アカウントに置くと、年度が替わったときに触れる人がいなくなるので、PyCon JP の共有アカウント配下に置いている。
| 対象 | 置き場所 |
|---|---|
| Workers / D1 / R2 | Cloudflare の「PyCon JP」アカウント (account_id は wrangler の設定に書いてある) |
| app | https://htmlshare.in.2027.pycon.jp (Route 53 の CNAME → Pages pyconjp-htmlshare → service binding → Worker pyconjpshare。SHE-4) |
| content worker | https://pyconjpshare-content.contact-2b3.workers.dev |
| OAuth クライアント | Google Cloud の組織 pycon.jp 配下のプロジェクト pyconjpshare (同意画面は内部) |
wrangler は API トークンで動かす。リポジトリ直下の .env に CLOUDFLARE_API_TOKEN と CLOUDFLARE_ACCOUNT_ID を置き (gitignore 済み)、set -a; source .env; set +a してからコマンドを打つ。トークンの権限は Workers スクリプト、Workers R2 ストレージ、D1、Cloudflare Pages の編集と、アカウント設定の読み取り。
pycon.jp の DNS は AWS の Route 53 にあり、Workers のカスタムドメインは付けられない。Pages は外部 DNS からの CNAME でカスタムドメインを付けられるので、中身が pages/public/_worker.js の数行だけの Pages を前に置いている。app worker は workers_dev: false で、Pages からしか届かない。
npm run migrate # migrations/ を足したときだけ
npm run deploy:app
npm run deploy:content
npm run deploy:pages # pages/ を変えたときだけ- Google Cloud で OAuth クライアント (ウェブアプリケーション) を作る。承認済みのリダイレクト URI は
https://pyconjpshare.<サブドメイン>.workers.dev/auth/callback - Cloudflare のダッシュボードで R2 を有効にしてから、D1 と R2 を作る
npx wrangler d1 create pyconjpshare -c wrangler.app.jsonc npx wrangler r2 bucket create pyconjpshare -c wrangler.app.jsonc
wrangler.app.jsoncとwrangler.content.jsoncのaccount_id、database_id、APP_HOST、CONTENT_HOSTを実値にする。cli/pyconjpshare/client.pyのDEFAULT_API_BASEも app worker の URL にする- スキーマを流す
npm run migrate
- 初回のデプロイ。
secrets.requiredを宣言しているので、シークレットが揃わないと wrangler はデプロイを拒む。Worker がまだ無いとwrangler secret putもできないので、KEY=valueを並べた一時ファイルを--secrets-fileで渡し、終わったら消す- app:
OAUTH_CLIENT_ID、OAUTH_CLIENT_SECRET、SESSION_SECRET、TICKET_SECRET - content:
TICKET_SECRETだけ (app と同じ値) SESSION_SECRETとTICKET_SECRETは別の乱数にする (同じだと起動時に落ちる)。content worker に渡すのはTICKET_SECRETだけで、セッションの鍵は渡さない
2 回目以降にシークレットを変えるときはnpx wrangler deploy -c wrangler.app.jsonc --secrets-file <app 用のファイル> npx wrangler deploy -c wrangler.content.jsonc --secrets-file <content 用のファイル>
npx wrangler secret put <NAME> -c wrangler.app.jsonc - app:
守るもの。上げた HTML はアプリ本体とは別ホストから配信される。cookie はすべて __Host- 接頭辞付きで、Domain なし、HttpOnly、Secure。いまは app が pycon.jp、content が workers.dev で別サイトだが、両方を workers.dev に置いた場合は same-site になり SameSite では分離できない。どちらの配置でも効くように、cookie 認証で状態を変える経路には Origin 検査を入れてある。content を pycon.jp の配下に置かないのは、上げたページの JavaScript から Domain=pycon.jp の cookie に触れさせないため。配信時は nosniff を付け、Content-Type は拡張子から決めて保存時に焼き込む。
守らないもの。CSP は入れない。上げたページは任意の JavaScript を実行できるので、外部への通信も、閲覧者が元から見られる他の共有の読み取りも止まらない。同僚が Slack で送ってきた HTML ファイルを開くのと同程度のリスクだと考えている。