過去記事に散らばったSVG図版を一覧するギャラリービューアを作った
過去記事に散らばったSVG図版を一覧するギャラリービューアを作った
なぜ作ったか
過去に自分で作ったSVG図版を、あの記事に入れたはず、と思っても、どの日付のどの記事だったか思い出せない。フォルダを開いて日付ディレクトリを一つずつ覗いて回るうちに、探すのを諦めて似た図をもう一度書き直してしまう。同じ図が別々の記事に生えていく現象が起き始めていた。
タイル状に並べて視線を一往復させれば、あった、と即座に見つかるはず。過去のSVGを新しい順で一覧できる専用ページを立てることにした。
2パターンのビューア
1本のページに3つのビューを載せた。ツールバーのボタンでモードを切り替える。
- 全画面グリッド: PowerPointのスライド一覧に近い見た目。カード内にはSVGだけを敷き詰めて、タイトル・日付・ファイル名などのメタ情報は消した。列数は「大」「小」ボタンで増減でき、俯瞰したいときは列を増やして密度を上げ、絵柄を確認したいときは列を減らして拡大する
- 4分割ビュー: 1画面に4枚を並べる中間モード。ページャは「1 / 166」の形式で刻まれ、矢印キーで4枚単位で送れる
- 1枚ずつビュー: いわゆるスライド形式。右矢印キーでSVGを1枚ずつ進める。ページャは「1 / 663」(初期実装時点)と表示され、記事タイトルも下部に添える
一覧の並び順は新しい順で固定した。作ったばかりのものほど記憶に残っているので、探し始めの視線は必ず先頭から流す。
実装の骨格
新規追加したファイルはこの3本。
apps/web/scripts/generate-svg-manifest.mjs— ディレクトリを再帰スキャンして SVG のマニフェスト JSON を吐くapps/web/app/data/svgManifest.json— 生成物。{ url, dir, fileName, mtime, size }の配列apps/web/app/composables/useSvgGallery.ts— マニフェストとブログ記事一覧を突き合わせて、記事情報付きの表示アイテムに整形する純粋関数 + composableapps/web/app/pages/svg-gallery.vue— 3ビュー切り替えとキーボード操作を実装したページ本体
マニフェスト生成は package.json の predev / prebuild に組み込んで、dev server 起動や本番ビルドの直前に必ず走らせる。これでSVGを追加してもフックせずに反映される。
記事情報とのマッチング
useSvgGallery.ts の joinSvgWithArticles が、マニフェストの各エントリを記事にひもづける。/images/{slug}/foo.svg 形式は末尾セグメントで記事の path とマッチさせ、YYYY-MM-DD 形式のディレクトリは publishedAt でマッチさせる。純粋関数として切り出したのでユニットテストで検証しやすい。
useBlogArticles を2回連続で await すると、Nuxt の非同期コンテキストが1回目の await 後に消えて2回目の useAsyncData が「useNuxtApp outside of setup」で落ちる問題を踏んだ。両方の Promise を同期的に発行してから Promise.all で待つ形に書き直したら通った。
宅検(takken)配下を除外した
初期実装で 663 枚のカードが並んで満足していたら、たしかブログの一覧側では宅検の記事は除外していたはず、宅検はこっちの /takken/ 側で別のビュアーを持っているはず、という指摘が入った。
確かめてみると 663 枚のうち 529 枚が takken 配下 だった。ブログ図版が沈むどころか、takken の山に埋もれていた形。useBlogArticles は /takken/ 配下の記事をあらかじめ落としているが、ギャラリーは画像ファイル側の URL を直接見るため、記事側フィルタとは別に画像パス側で /images/takken/ を弾く必要があった。
EXCLUDED_URL_PREFIXES に /images/takken/ を追加して、両側で二重に除外する構造に整えた。結果、663 → 134 枚まで絞られた。
「数が少ない気がする」から抽出漏れが見つかった
134 枚に絞ったあと、明らかに数が少ない気がする、との指摘が飛んできた。感覚として、これまで書いたSVGはもっとあったはず、というもの。
content/ 配下を走らせてみたら 201 枚のSVGがそこにあった。マニフェスト生成スクリプトは当初 public/images/**/*.svg しかスキャンしていなかった。dev 環境では content/YYYY-MM/YYYY-MM-DD/*.svg に直に置いた図版は public/ にコピーされず、server/middleware/content-images.ts 経由で /YYYY-MM/YYYY-MM-DD/xxx.svg の URL で配信される。生成スクリプトはミドルウェア経路を認識していなかったため、記事本文に直接埋め込んだ図版がまるごと拾えていなかった。
generate-svg-manifest.mjs を拡張し、public/images/ に加えて content/ も再帰的にスキャンして、YYYY-MM/YYYY-MM-DD/*.svg を拾って dir = "YYYY-MM-DD" として登録するようにした。ミドルウェア配信の URL 形式(/YYYY-MM/YYYY-MM-DD/xxx.svg)でそのまま指せるので、ギャラリー側は特別扱いなしに表示できる。
修正後、134 → 335 枚 に増えた。追加された 201 枚は content/ 直下から掘り出した図版で、記事本文の中に SVG タグで埋め込んでいたやつが全部それ。「数が少ない気がする」という感覚が、実装の見落とし1つを引き出した瞬間だった。
学び
- 一覧を作ると、揃った瞬間に感覚が異常を検知する。「なんか少ない」という違和感は、感覚がスキャンした結果と過去の記憶を突き合わせたシグナル。数字を拾いに行くきっかけになる
- ブログ側と画像側で二重フィルタが必要になる構造は、
useBlogArticlesが記事レベルで除外している一方、画像は物理ファイルとして残っていることに起因する。片方だけ直しても表示は変わらないので、記事フィルタと画像フィルタは片方直したらもう片方も点検する - Nuxt の非同期コンテキストは
awaitを挟むごとに切れる可能性がある。useAsyncDataを複数回叩くときは、Promise を同期発行してPromise.allで待つのを型として覚えておく - 過去に作った成果物を一覧できるページは、ブログ本文とは別の入口として静かに効いてくる。同じ図を書き直すのを止められる
今後の余地
- 記事にひもづかない孤立SVG(
dirが空、または記事がマッチしない)を別ビューでまとめて可視化する - ファイル名やタグでのインクリメンタル検索を上部バーに載せる
- グリッドの列数を localStorage に保存し、次回開いたときに前回の見え方を復元する