Colophon
このサイトの仕組み
このサイトは、記事の入力に WordPress を使い、表示は前もって組み立てた静的なファイルで配っています。どう作り、何を守っているかを、技術に関心のある方に向けてまとめました。
- 配り方
- 静的なファイル(HTML・CSS・画像)
- WordPress への要求
- GET のみ
- Web フォントの読み込み
- 0 バイト
- インラインのスクリプト・style 属性
- 0
- 組み立てた時点の活動報告
- 17 本
- 最後に組み立てた日
01原稿と表示を分ける
活動報告は、これまでどおり WordPress の管理画面で書きます。このサイトは、その記事を WordPress の公開 REST API から読み出し、ページの形に組み立てて配ります。
読み出しに使う部品は GET しか送れないように作ってあり、WordPress のデータを書き換える経路を持ちません。前回の取得結果は手元に残し、If-None-Match と If-Modified-Since を付けて問い合わせるので、変わっていない記事は取り直しません。
トップや寄付のページなど、記事以外の文面は、事実関係を確かめた文章をまとめたファイル(Design Layer)が唯一の原本です。ページはそこから文字を一字も変えずに読み込みます。団体の電話番号は掲載しない決まりがあり、組み立ての最後に出力するすべての文書を検索して、番号が見つかれば組み立て自体を失敗させます。
02組み立ての流れ
- Design Layer の文面と写真、WordPress の記事・カテゴリ・寄付の集計を読み込む
- 記事の HTML を、許可した要素と属性だけを残す方式で無害化する(スクリプト・埋め込み・style 属性は落とす)
- 写真を AVIF と WebP の複数の幅に変換し、ファイル名に中身のハッシュを付ける
- ページごとに HTML と、同じ内容の Markdown を書き出す
- フィード、サイトマップ、検索用の索引、llms.txt、応答ヘッダーの設定を書き出す
- 電話番号の検査、Brotli の事前圧縮、全ファイルの SHA-256 の記録
組み立ては Node.js が TypeScript をそのまま実行する形です。外部のパッケージは、画像の変換に使う sharp だけです。
03表示の速さ
- 画像:AVIF と WebP を
srcsetで幅ごとに用意し、ブラウザが画面に合うものを選びます。幅と高さを先に宣言するので、読み込み中にレイアウトがずれません。読み込む前は、写真の色を 2×2 に縮めたグラデーションを CSS のクラスとして下に敷きます(data: URI は使いません)。 - 文字:Web フォントを読み込まず、端末に入っている書体で表示します。文字のための通信は 0 バイトです。
- 次のページの先読み:Speculation Rules を HTTP の応答ヘッダーから配り、リンクにポインターを重ねたときなどに、次のページの準備を始めます。インラインのスクリプトを置かずに済む方法です。
- ページの移り変わり:同じサイトの中の移動は、ブラウザのページ間 View Transitions でつなぎます。活動報告の一覧で記事の写真を押すと、その写真が記事の先頭の写真へ形を変えながら移ります。
- CSS と JavaScript:スタイルシートはカスケードレイヤーで順序を固定した 1 本です。スクリプトはページに必要なものだけをモジュールとして読み込みます。
Speculation-Rules: "/speculation.json"
04キーウへの道のり
トップの地図では、水島港からキーウまでの経路の SVG に pathLength="10000" を与えています。経路の長さを 10,000 と数え直すことで、経路上の 1 単位がそのまま 1km になります。
救急車の位置、ここまで進んだ黄色い線、寄付の金額を動かしたときの青い線は、型を宣言したカスタムプロパティ(--km-now と --you)から CSS が計算します。JavaScript は金額の入力に合わせて --you を書き換えるだけで、救急車の座標を計算しません。
.map .vehicle {
offset-path: var(--route);
offset-distance: calc(
min(10000, (var(--km-now) + var(--you)) * var(--drive)) / 100 * 1%
);
}
地図が画面に入ってくるにつれて救急車が港から今日の地点まで走る動きは、スクロールに連動するアニメーション(animation-timeline: view())で、これも CSS だけで描いています。
05動きと、動きを減らす設定
各節が現れる動きや写真のゆるやかな拡大は、スクロールの位置に連動する CSS のアニメーションです。端末で「視差効果を減らす」などの設定をしている方には、動きを止めて表示します。ページ間の移り変わりも同じ設定で止まります。
06安全のための決まり
- Content Security Policy:スクリプトもスタイルも、このサイトのファイルだけを読み込みます。インラインのスクリプト、
style属性、<style>要素はひとつも置いていません。動きのある値も CSS のクラスとカスタムプロパティで渡します。 - Trusted Types:
require-trusted-types-for 'script'を有効にし、ポリシーを 1 つも許可していません。スクリプトからinnerHTMLで HTML を書き込むことはできず、サイト内検索の結果も DOM の要素を 1 つずつ組み立てて表示しています。 - Subresource Integrity:CSS と JavaScript には、中身の SHA-384 を
integrity属性として付けています。配信の途中で書き換えられたファイルは、ブラウザが実行しません。 - そのほかの応答ヘッダー:Cross-Origin-Opener-Policy と Cross-Origin-Resource-Policy で他のサイトとの関わりを絞り、Permissions-Policy でカメラ・マイク・位置情報などの利用を拒否しています。
- 記事の本文:WordPress から受け取った HTML は、許可した要素・属性・URL の形式(http・https・mailto とサイト内のパス)だけを残してから載せます。
07文字組
和文と欧文の間の空き(text-autospace)、約物の詰め(text-spacing-trim)、見出しの文節での改行(word-break: auto-phrase)は、ブラウザの機能に任せています。対応していないブラウザでは、普通の組み方で表示されます。
08機械にも読める形
どのページも、アドレスの末尾に index.md を付けると、同じ内容を Markdown で読めます(例:このページの Markdown)。ページには構造化データ(JSON-LD)も入れています。
09JavaScript が無くても
本文、寄付の入口、メニュー、よくある質問の開閉は、JavaScript を切っても使えます。メニューは HTML の popover 属性、質問の開閉は <details> 要素で作っています。JavaScript は、地図の金額の操作やサイト内検索のように、あると便利な部分にだけ使っています。