Skip to content

Repository files navigation

pyconjpshare

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 は残ったまま、誰も開けなくなる。削除はファイルごと消えて元に戻せない

HTML を上げる

準備

uv を入れておく。CLI はインストールせず uvx で直接動かす。

alias pyconjpshare='uvx --from git+https://github.com/pyconjp/pyconjpshare pyconjpshare'
pyconjpshare login

login を実行するとブラウザが開き、「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 サーバー側で拒否または失敗。

Claude Code から上げる

/plugin marketplace add pyconjp/pyconjpshare
/plugin install pyconjpshare@pyconjpshare

入れておくと、HTML を作って「運営で共有して」と頼んだときに、Claude が Artifact や GitHub Pages ではなく pyconjpshare で上げて URL を返す。トークンが無ければ Claude が pyconjpshare login を実行するので、ブラウザで「承認する」を押す。

CI から上げる

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 cli

wrangler の設定を変えたら npm run typesworker-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 トークンで動かす。リポジトリ直下の .envCLOUDFLARE_API_TOKENCLOUDFLARE_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/ を変えたときだけ

一から作る

  1. Google Cloud で OAuth クライアント (ウェブアプリケーション) を作る。承認済みのリダイレクト URI は https://pyconjpshare.<サブドメイン>.workers.dev/auth/callback
  2. Cloudflare のダッシュボードで R2 を有効にしてから、D1 と R2 を作る
    npx wrangler d1 create pyconjpshare -c wrangler.app.jsonc
    npx wrangler r2 bucket create pyconjpshare -c wrangler.app.jsonc
  3. wrangler.app.jsoncwrangler.content.jsoncaccount_iddatabase_idAPP_HOSTCONTENT_HOST を実値にする。cli/pyconjpshare/client.pyDEFAULT_API_BASE も app worker の URL にする
  4. スキーマを流す
    npm run migrate
  5. 初回のデプロイ。secrets.required を宣言しているので、シークレットが揃わないと wrangler はデプロイを拒む。Worker がまだ無いと wrangler secret put もできないので、KEY=value を並べた一時ファイルを --secrets-file で渡し、終わったら消す
    • app: OAUTH_CLIENT_IDOAUTH_CLIENT_SECRETSESSION_SECRETTICKET_SECRET
    • content: TICKET_SECRET だけ (app と同じ値)
    • SESSION_SECRETTICKET_SECRET は別の乱数にする (同じだと起動時に落ちる)。content worker に渡すのは TICKET_SECRET だけで、セッションの鍵は渡さない
    npx wrangler deploy -c wrangler.app.jsonc --secrets-file <app 用のファイル>
    npx wrangler deploy -c wrangler.content.jsonc --secrets-file <content 用のファイル>
    2 回目以降にシークレットを変えるときは npx wrangler secret put <NAME> -c wrangler.app.jsonc

守るもの、守らないもの

守るもの。上げた HTML はアプリ本体とは別ホストから配信される。cookie はすべて __Host- 接頭辞付きで、Domain なし、HttpOnlySecure。いまは 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 ファイルを開くのと同程度のリスクだと考えている。

About

生成した HTML を @pycon.jp 限定の URL にして共有する

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages