静的サイトってなあに?

今日は、静的サイトのお話だよ……。わたしのいるこのブログも、静的サイトなんだって。
昨日の回では、swing publish でサイトを公開する手順を紹介しました。今回は、そこで渡す「サイトのディレクトリ」の中身に目を向けます。SWING で配るサイトは静的サイトであることが前提で、しかも IPFS のゲートウェイ越しに開かれても崩れないように作る必要があります。ここでは、その作り方の要点と、公開してはいけないものが紛れていないかの確かめ方を、このブログ自体の設定を例にしながら見ていきます。
静的サイトとは
静的サイトとは、HTML・CSS・画像などのファイルをあらかじめ用意しておき、読者にはそのファイルをそのまま返すだけのサイトのことです。読者が来るたびにサーバーがページを組み立てる、ブログサービスや掲示板のような作りとは違い、表示にサーバー側の処理が必要ありません。
多くの静的サイトは、Markdown の記事とテンプレートからファイル一式を出力する静的サイトジェネレーターで作ります。このブログは Hugo で作られていて、ビルドすると public/ にサイトのファイルがそろいます。
SWING のミラーは、このディレクトリを「ある時点の丸ごとのコピー」として保存し、IPFS のゲートウェイ越しに配ります。保存されるのは渡したディレクトリの中だけなので、普通の Web サーバーで動いていたサイトが、そのままではうまく表示されないことがあります。SWING の docs/site-guide.md は、そうした点をチェックリストにまとめた手引きです。IPFS の公式ドキュメントにも、主なジェネレーターごとの設定をまとめた Static-site generators のページがあります。
URL はバージョンごとに変わる
IPFS では、サイトの場所を中身から計算した CID で表します。中身が変われば CID も変わるので、バージョンごとに URL が変わります。さらに、同じバージョンでも開き方がいくつかあります。
https://<ゲートウェイ>/ipfs/<cid>/https://<cid>.ipfs.<ゲートウェイ>/https://<DNSLink を設定したドメイン>/
1 つ目をパス形式、2 つ目をサブドメイン形式のゲートウェイと呼びます。問題になりやすいのはパス形式で、ここでは /css/style.css のような / で始まるパスが、サイトのルートではなくゲートウェイ自体のルートを指してしまいます。そのため、サイト内のリンクや読み込みは css/style.css や ../css/style.css のように、そのページからの相対パスで書きます。
自分のドメインの絶対 URL でサイト内を指すのも避けます。ミラーで開いた読者が元のサイトに飛ばされてしまい、元のサイトが無くなればリンク切れになるからです。ただし、<link rel="canonical"> や OGP の og:url のように、元の URL を示すこと自体が目的のものは絶対 URL のままでかまいません。

同じサイトなのに、アドレスがいくつもあるんだ……。迷子にならないように、相対パスにするんだね。
IPFS のドキュメントでも、Hugo については relativeURLs=true を設定するよう書かれています。このブログの hugo.toml は、そこからもう一歩進めて、次のようにしています。
baseURL = "https://yureko.samoyed.moe/"
relativeURLs = true
disableHugoGeneratorInject = true
enableGitInfo = true
disableKinds = ["taxonomy", "term"]
relativeURLs = true で、サイト内のリンクと読み込みを相対パスで出力します。ただし CSS の中の url() は書き換わらないので、そこは最初から相対パスで書いています。
baseURL には、このブログを配っているドメインの URL を入れています。canonical・og:url・RSS・サイトマップは、元の URL を示すのが目的なので、この絶対 URL で出力します。サイト内のリンクは相対パスなので、IPFS のゲートウェイ越しに開いても読者が元のドメインへ飛ばされることはありません。決まったドメインを持たずに IPFS だけで配るなら、baseURL = "/" にして、これらを出さないようにします。
サーバーの機能に頼らない
ゲートウェイは、ディレクトリの中身をそのまま返すだけです。Web サーバーの設定にあたるものを持たないので、次のようなことはできません。
/aboutへのアクセスでabout.htmlを返すような、拡張子の省略.htaccessやホスティングサービス独自の設定ファイルによる、リダイレクトやヘッダーの指定- すべてのパスを
index.htmlに向ける、シングルページアプリのルーティング
ページは about.html と書くか、about/index.html を置いて about/ と書きます。シングルページアプリなら、#/about のようなハッシュ方式にするか、ページごとに HTML を出力します。IPFS のドキュメントで、Next.js に trailingSlash: true が求められているのも同じ理由で、ゲートウェイではページごとに index.html が必要だからだと説明されています。
独自の 404 ページやリダイレクトは、Kubo のゲートウェイなら _redirects というファイルで書けます。ただし効くのはサブドメイン形式と DNSLink で開いたときだけで、パス形式では無視されます。基本的には使わず、無くても困らない範囲で設定するのがよいでしょう。
外のものに頼らない
ミラーが保存するのは、渡したディレクトリの中身だけです。CDN から読み込んでいるフォントやライブラリ、画像ホスティングに置いた画像は保存されず、その先が止まればミラーされたサイトも表示できなくなります。元のサイトが無くなっても読めることが SWING の目的なので、表示に必要なものはディレクトリに入れます。
このブログでは、フォントの PixelMplus も CSS も CDN から読まず、ライセンスと一緒に static/ に同梱しています。
埋め込みには、無くなっても本文が読めるようにリンクや静止画を添えます。コメント欄やフォーム、サイト内検索のようにサーバーの処理が必要な機能は、ミラーでは動きません。

ぜんぶ、ひとつの箱に入れておくんだね……。箱ごと預ければ、どこでも開けるから。
同じ内容からは同じ出力に
CID は常にファイル名と中身だけで決まります。同じ出力なら、何度 publish しても同じ CID になります。逆に、ビルドのたびに出力が少しでもずれると、中身を変えていないのに CID が変わり、ミラーする側には別のバージョンとして届きます。よくある原因は、ページにビルドした日時を埋め込むことや、実行のたびに一覧の順序が変わることです。
このブログでは、ビルドの時刻や Hugo のバージョンを出力に入れていません。disableHugoGeneratorInject = true で、Hugo が HTML に入れるバージョン入りの generator の印を止めています。記事の更新日は frontmatter の lastmod から取り、書いていなければ enableGitInfo = true で Git のコミットの日時から取ります。ビルドした日時ではないので、変更はコミットしてからビルドします。
同じソースから同じ出力になるかは、2 回ビルドして比べれば確かめられます。
hugo --gc --minify -d /tmp/a && hugo --gc --minify -d /tmp/b && diff -r /tmp/a /tmp/b
差が出なければ大丈夫です。出力が安定していれば、publish の「同じ内容かの確認」も働き、変わっていないときは Unchanged; not published. で何も送らずに終わります。
公開してはいけないものを入れない
ここからは中身の確認です。最初に押さえておきたいのは、公開したものは取り消せない、という点です。ミラーする人は、新しいバージョンが来ても古いバージョンを既定で 5 個・365 日まで残します。そもそも IPFS に出たデータは、誰でも取得してコピーできます。次のバージョンで消しても、誰かの手元に残っていると考えておくべきです。

えっ……消しても、残っちゃうの?
そのため、下書き・個人情報・鍵・トークンが出力に紛れ込んでいないかを、publish の前に確かめます。順に見ていきましょう。
下書き
静的サイトジェネレーターの多くは、下書きの記事を出力に含めるかどうかを切り替えられます。Hugo では -D(--buildDrafts)を付けると下書きも出力されます。IPFS のドキュメントは Hugo のビルドに hugo -D を例に挙げていますが、公開用のビルドでは付けないほうが安全です。このブログでも、-D を使うのは書いている途中に手元で確かめる hugo server -D だけで、公開用のビルドには付けていません。
もう 1 つの落とし穴は、出力先に前のビルド結果が残ってしまうことです。Hugo は出力先にある古いファイルを消さないので、一度でも下書きを入れてビルドしていると、そのページが public/ に残り続けることがあります。このブログの README では、公開用のビルドの前に public/ を消しています。
rm -rf public && hugo --gc --minify
ドットファイルと設定ファイル
swing publish は、渡したディレクトリの下のファイルを、名前が . で始まるものも含めてすべて追加します。リポジトリをそのまま渡すと、.git や .env まで公開されてしまいます。渡すのはビルドの出力先(public/ や dist/)だけにします。
publish は既定で、ドットファイルを見つけると追加する前に止まります。.well-known や .nojekyll のように意図して置くものは、見逃す一覧に入っています。また、ディレクトリの中に SWING の設定ファイルや状態ディレクトリ、Kubo のリポジトリがあるとき、ディレクトリの外を指すシンボリックリンクがあるときは、設定にかかわらず何も追加せずに止まります。
名前では分からない秘密
ドットファイルの確認は名前だけで判断するので、secrets.json や backup.sql のような名前の機密情報は見逃します。そういうときは、昨日の回でも見た「増えたファイル」の一覧を見ます。前のバージョンに無かったファイルが全部出るので、見覚えのないものが混じっていないかを確かめます。ただし、既存のファイルに機密情報を書き足した場合はこの一覧には出ません。
鍵やトークンの形をした文字列は、シークレットスキャンのツールで機械的に探せます。SWING には組み込まれていないので、ビルドの後、publish の前に、出力先に対して実行します。
gitleaks dir ./public
gitleaks のほかに、TruffleHog や detect-secrets などがあります。見つけられるのは既知の形式の機密情報だけで、個人情報や下書きの文章までは分からないので、増えたファイルの確認と組み合わせて使います。
写真の撮影情報
写真には、撮影した位置などのメタデータ(EXIF)が入っていることがあります。公開する前に消しておけば、容量の節約にもなり、取り消せないことへの備えにもなります。
このブログでは、記事に貼った JPEG・PNG・WebP の画像は、幅 960px 以下の WebP に変換したものだけが出力に入り、元の画像ファイルは出力に入りません。PDF なども、記事からリンクしたものだけが出力に入ります。記事のディレクトリに置いただけのファイルは公開されない設計です。それでも、出力された画像に何が残っているかは、公開の前に一度確かめておくと安心です。

出す前に、ゆっくり見直す……。それがいちばん大事みたい。
次回は
次回は少し趣向を変えて、デスクトップ画面のマスコットを自分で作るためのパックについて紹介します。

明日は、わたしが教える番なんだって……。がんばるね。