住民が「どんな首長になってほしいか」をポスターの形で表現し、集まった言葉を並べて公開するウェブアプリ。 サーバー側のプログラムを持たない。 生成した静的ファイルを置くだけで動く。
横浜市長選にあわせ、Code for YOKOHAMA のメンバーを中心とした市民有志がつくったもの。 横浜の内容は設定ファイルに追い出してあり、どの自治体でもそのまま使えるように公開している。
横浜市長選にあわせて公開した、実際の稼働例。
試すだけなら「ギャラリーに公開する」のチェックを外したまま保存してください。 チェックを入れて保存すると、実際の投稿として記録されます。
ビルドすると、2つのHTMLができる。
| ジェネレーター | 言葉・レイアウト・配色・背景写真を選ぶとポスターがその場で描かれる。PNGで保存し、SNSへシェアできる |
| ギャラリー | 投稿された言葉を一覧する。言語・レイアウトで絞り込める |
日本語・やさしい日本語・英語・中国語(簡体)・韓国語の5言語に対応し、 選択肢もポスターに描かれる文字もすべて切り替わる。
| 誰向けか | |
|---|---|
| docs/GUIDE.md | はじめての人のための導入ガイド。 自分のまちで立ち上げたい非エンジニア向け。Node.js を入れるところから公開まで |
| docs/DESIGN.md | なぜこう作ってあるか。 考え方と、割り切ったこと。改造・移植の前に読む |
| docs/SETUP.md | 移植手順(技術者向け)。 要点だけを短く |
| docs/OPERATION.md | 公開してからの運営。 荒らし対応、問い合わせ、承認制、終わり方 |
| docs/USER-GUIDE.md | 利用者向けの使い方。 そのまま配れる下敷き |
- ポスターはすべて Canvas 2D で描いている。外部ライブラリを一切使わない。
CDNもnpmも不要で、生成される
.htmlはそれ1枚で動く - 投稿はGoogleフォームにPOSTし、回答はスプレッドシートに溜まる。 データベースもサーバーサイドのコードも要らない
- ギャラリーはそのスプレッドシートをCSVで読み、同じ描画コードでポスターを組み直す。 画像は保存していない
- 背景写真はウィキメディア・コモンズの pageid だけを記録し、表示時にAPIで取り直す。 写真そのものは送信しない
- 保存したPNGには投稿内容をメタデータとして焼き込んであり、生成側に読み戻して続きを編集できる
これらを選んだ理由と、その代わりに諦めたことは docs/DESIGN.md に書いてある。
必要なのは Node.js だけ(テストで使う jsdom の要件で 20.19 以上、22 系なら 22.13 以上)。
ビルド自体に依存ライブラリは無く、npm i はテストとOGP画像の生成に使う。
npm i # 初回のみ
npm test # dist/ を作り直してから、jsdom で2ページを動かして確認npm test は、走る前に node build.js と OGP画像2枚の生成を自動で済ませる。
クローンした直後でも、この2つだけで最後まで通る。
自分のまちの設定を入れるときは、次を足す。
cp config.sample.json config.json # 公開先URL・地名・フォーム・シートを書く
npm run build # src/ から dist/ を作り直すconfig.json が無ければ config.sample.json(サンプル市・example.org のダミー)で動く。
設定の書き方は docs/SETUP.md。
ローカル確認は file:// だと通信が弾かれることがあるので、簡易サーバー経由がよい。
cd dist && python3 -m http.server 8000 # Windows なら python -m http.server 8000SNSのカード画像(dist/ogp.png と dist/ogp-gallery.png)を作り直すときは次を叩く。
npm test の中でも同じものが走っている。
npm run ogpdist/ の2つのHTMLとOGP画像2枚を、同じフォルダにアップロードすれば公開できる。
config.sample.json |
公開先URL・地名と首長の呼び名・フォームID・entry番号・シートID。複製して config.json にする |
build.js |
src/ から dist/ を生成。設定を差し込むのもここ。依存ライブラリなし |
place.js |
地名から「市長/町長」「市民/町民」などを組み立てる。build.js と OGP生成が使う |
src/i18n.js |
5言語の辞書。文言・選択肢はすべてここ |
src/app.tmpl |
ジェネレーター本体。ポスター描画コードの正本 |
src/gallery.tmpl |
ギャラリー |
tools/mkqr.js |
QRを事前計算してJSONに落とす。書き出したQRは読み返して検証する |
tools/mkogp.js / mkogp-gallery.js |
OGP画像の生成 |
test/ |
jsdom + @napi-rs/canvas で全ボタンを押して回すテスト |
地名は src/ に直接書かず、config.json の place から差し込んでいる。
"full": "葉山町" と書けば、ページも辞書も「葉山町長・町民・役場・町長選」に変わる
(市・町・村・区・都道府県に対応。英語・中国語・韓国語は自動では決められないので設定に書く)。
描画関数は src/app.tmpl にしかなく、ビルド時にギャラリーへ機械的にコピーされる。
ポスターのデザインを変えれば両方に反映される。dist/ を手で直しても次のビルドで消える。
config.json と、そこから作るQRコード(tools/qr*.json)、dist/ は .gitignore 済み。
共有するのは *.sample.json のほう。
ブラウザを使わず、jsdom + @napi-rs/canvas で dist/*.html を実際に動かす。
node build.js のあとに npm test を実行する。
ページ内の例外は jsdom が黙って飲み込むので、test/env.js が拾って必ず失敗にしている。
通信はすべて止め、Googleフォームとコモンズは差し替えて中身を検査する。
見ているのは、5言語 × 6レイアウト × 3サイズの描画、地名の差し替え、フォーム送信の13項目、 ギャラリーのフィルタ、コモンズのライセンス絞り込み、保存PNGの焼き込みと読み戻し、 下書きと履歴、XSS、壊れた記録への耐性、localStorage が使えない端末での動作、 そして2ページとも全ボタンを機械的にクリックしても例外が出ないこと。
初期化順を入れ替えただけでページの半分が動かなくなることがあるので、 UIに手を入れたら全ボタンを押すテストまで通すこと。
コードは MIT なので、改変も再配布も商用利用も自由。 連絡も報告も要らない。 以下はお願いではなく、外すと事故になるところの共有。理由は docs/DESIGN.md。
- 背景写真はウィキメディア・コモンズから、CC0・パブリックドメイン・CC BY だけを検索している。 SA は継承条件ができあがったポスター全体に及び、ND はトリミング・色調変更が許されないため外してある。 絞り込みを広げるなら、できたポスターのライセンスをどうするかまで含めて決めること
- 撮影者・作品名・ライセンス・URL・改変の有無は、画像に自動で焼き込んでいる。 CC BY の写真でこの表示を外すと、ライセンス違反になる。 消せるように改造するなら、同時に CC BY を検索対象から外す必要がある
いまの画面は、投稿する人にこう約束している。約束の中身は運営する人が決めてよい。 ただし文言と実装のどちらか片方だけを変えると、投稿者への嘘になる。
| 画面での約束 | それを支えている実装 |
|---|---|
| チェックを入れたときだけ送信する | 「ギャラリーに公開する」(consent)が ON のときだけ POST する |
| 写真そのものは送らない | コモンズの pageid だけを記録し、表示時にAPIで取り直す |
| 下書きと履歴は端末から出ない | localStorage のみ。消す導線(「この端末の記録を消す」)も置いてある |
| 投稿された言葉は CC0 で公開する | 注意書き n4 に明記。集計・公開の前提 |
集めているのはニックネーム・年代・性別・地域(すべて任意)。項目を増やすときは、
注意書き(n4b)と、保存した画像に焼き込まれる内容も一緒に直すこと。
- ポスターに載る統計(
src/i18n.jsのstats)は、出典を確認してから入れた。 他都市・他国との比較は、統計年次が変わっても逆転しない差のものを選んでいる - 選挙が絡む時期には公職選挙法の制限があり、生成物をどう使うかは使う人の責任になる。 そのため画面に「選挙運動には使わないでください」と書き、特定の候補者・政党には触れなかった
このリポジトリのコードは MIT License(LICENSE)。 Copyright (c) 2026 Code for YOKOHAMA。
改変も再配布も商用利用も自由だが、著作権表示とライセンス文は残すこと。
生成される dist/*.html にはこの表示が自動で入る(build.js が差し込む)。
投稿された言葉のCC0とは別の話。 あれは集まったデータを誰でも分析できるようにするための 取り決めで、コードのライセンスとは対象が違う。