[{"data":1,"prerenderedAt":338},["ShallowReactive",2],{"content-/svg-gallery-viewer":3,"all-pages-for-dir":336,"og-image-/svg-gallery-viewer":337},{"id":4,"title":5,"body":6,"category":317,"description":318,"extension":319,"meta":320,"navigation":321,"ogImage":322,"path":323,"project_name":324,"published":325,"publishedAt":326,"seo":327,"stem":328,"tags":329,"todo":322,"unpublished":325,"updatedAt":322,"__hash__":335},"pages/2026-07/2026-07-01/svg-gallery-viewer.md","過去記事に散らばったSVG図版を一覧するギャラリービューアを作った",{"type":7,"value":8,"toc":303},"minimark",[9,13,17,21,24,28,31,54,57,60,63,94,109,113,134,152,156,163,180,189,193,196,222,246,256,259,285,288],[10,11,5],"h1",{"id":12},"過去記事に散らばったsvg図版を一覧するギャラリービューアを作った",[14,15,16],"h2",{"id":16},"なぜ作ったか",[18,19,20],"p",{},"過去に自分で作ったSVG図版を、あの記事に入れたはず、と思っても、どの日付のどの記事だったか思い出せない。フォルダを開いて日付ディレクトリを一つずつ覗いて回るうちに、探すのを諦めて似た図をもう一度書き直してしまう。同じ図が別々の記事に生えていく現象が起き始めていた。",[18,22,23],{},"タイル状に並べて視線を一往復させれば、あった、と即座に見つかるはず。過去のSVGを新しい順で一覧できる専用ページを立てることにした。",[14,25,27],{"id":26},"_2パターンのビューア","2パターンのビューア",[18,29,30],{},"1本のページに3つのビューを載せた。ツールバーのボタンでモードを切り替える。",[32,33,34,42,48],"ul",{},[35,36,37,41],"li",{},[38,39,40],"strong",{},"全画面グリッド",": PowerPointのスライド一覧に近い見た目。カード内にはSVGだけを敷き詰めて、タイトル・日付・ファイル名などのメタ情報は消した。列数は「大」「小」ボタンで増減でき、俯瞰したいときは列を増やして密度を上げ、絵柄を確認したいときは列を減らして拡大する",[35,43,44,47],{},[38,45,46],{},"4分割ビュー",": 1画面に4枚を並べる中間モード。ページャは「1 / 166」の形式で刻まれ、矢印キーで4枚単位で送れる",[35,49,50,53],{},[38,51,52],{},"1枚ずつビュー",": いわゆるスライド形式。右矢印キーでSVGを1枚ずつ進める。ページャは「1 / 663」（初期実装時点）と表示され、記事タイトルも下部に添える",[18,55,56],{},"一覧の並び順は新しい順で固定した。作ったばかりのものほど記憶に残っているので、探し始めの視線は必ず先頭から流す。",[14,58,59],{"id":59},"実装の骨格",[18,61,62],{},"新規追加したファイルはこの3本。",[32,64,65,72,82,88],{},[35,66,67,71],{},[68,69,70],"code",{},"apps/web/scripts/generate-svg-manifest.mjs"," — ディレクトリを再帰スキャンして SVG のマニフェスト JSON を吐く",[35,73,74,77,78,81],{},[68,75,76],{},"apps/web/app/data/svgManifest.json"," — 生成物。",[68,79,80],{},"{ url, dir, fileName, mtime, size }"," の配列",[35,83,84,87],{},[68,85,86],{},"apps/web/app/composables/useSvgGallery.ts"," — マニフェストとブログ記事一覧を突き合わせて、記事情報付きの表示アイテムに整形する純粋関数 + composable",[35,89,90,93],{},[68,91,92],{},"apps/web/app/pages/svg-gallery.vue"," — 3ビュー切り替えとキーボード操作を実装したページ本体",[18,95,96,97,100,101,104,105,108],{},"マニフェスト生成は ",[68,98,99],{},"package.json"," の ",[68,102,103],{},"predev"," / ",[68,106,107],{},"prebuild"," に組み込んで、dev server 起動や本番ビルドの直前に必ず走らせる。これでSVGを追加してもフックせずに反映される。",[110,111,112],"h3",{"id":112},"記事情報とのマッチング",[18,114,115,100,118,121,122,125,126,129,130,133],{},[68,116,117],{},"useSvgGallery.ts",[68,119,120],{},"joinSvgWithArticles"," が、マニフェストの各エントリを記事にひもづける。",[68,123,124],{},"/images/{slug}/foo.svg"," 形式は末尾セグメントで記事の path とマッチさせ、",[68,127,128],{},"YYYY-MM-DD"," 形式のディレクトリは ",[68,131,132],{},"publishedAt"," でマッチさせる。純粋関数として切り出したのでユニットテストで検証しやすい。",[18,135,136,139,140,143,144,147,148,151],{},[68,137,138],{},"useBlogArticles"," を2回連続で ",[68,141,142],{},"await"," すると、Nuxt の非同期コンテキストが1回目の await 後に消えて2回目の ",[68,145,146],{},"useAsyncData"," が「useNuxtApp outside of setup」で落ちる問題を踏んだ。両方の Promise を同期的に発行してから ",[68,149,150],{},"Promise.all"," で待つ形に書き直したら通った。",[14,153,155],{"id":154},"宅検takken配下を除外した","宅検（takken）配下を除外した",[18,157,158,159,162],{},"初期実装で 663 枚のカードが並んで満足していたら、たしかブログの一覧側では宅検の記事は除外していたはず、宅検はこっちの ",[68,160,161],{},"/takken/"," 側で別のビュアーを持っているはず、という指摘が入った。",[18,164,165,166,169,170,172,173,175,176,179],{},"確かめてみると 663 枚のうち ",[38,167,168],{},"529 枚が takken 配下"," だった。ブログ図版が沈むどころか、takken の山に埋もれていた形。",[68,171,138],{}," は ",[68,174,161],{}," 配下の記事をあらかじめ落としているが、ギャラリーは画像ファイル側の URL を直接見るため、記事側フィルタとは別に画像パス側で ",[68,177,178],{},"/images/takken/"," を弾く必要があった。",[18,181,182,185,186,188],{},[68,183,184],{},"EXCLUDED_URL_PREFIXES"," に ",[68,187,178],{}," を追加して、両側で二重に除外する構造に整えた。結果、663 → 134 枚まで絞られた。",[14,190,192],{"id":191},"数が少ない気がするから抽出漏れが見つかった","「数が少ない気がする」から抽出漏れが見つかった",[18,194,195],{},"134 枚に絞ったあと、明らかに数が少ない気がする、との指摘が飛んできた。感覚として、これまで書いたSVGはもっとあったはず、というもの。",[18,197,198,201,202,205,206,209,210,213,214,217,218,221],{},[68,199,200],{},"content/"," 配下を走らせてみたら 201 枚のSVGがそこにあった。マニフェスト生成スクリプトは当初 ",[68,203,204],{},"public/images/**/*.svg"," しかスキャンしていなかった。dev 環境では ",[68,207,208],{},"content/YYYY-MM/YYYY-MM-DD/*.svg"," に直に置いた図版は ",[68,211,212],{},"public/"," にコピーされず、",[68,215,216],{},"server/middleware/content-images.ts"," 経由で ",[68,219,220],{},"/YYYY-MM/YYYY-MM-DD/xxx.svg"," の URL で配信される。生成スクリプトはミドルウェア経路を認識していなかったため、記事本文に直接埋め込んだ図版がまるごと拾えていなかった。",[18,223,224,227,228,231,232,234,235,238,239,242,243,245],{},[68,225,226],{},"generate-svg-manifest.mjs"," を拡張し、",[68,229,230],{},"public/images/"," に加えて ",[68,233,200],{}," も再帰的にスキャンして、",[68,236,237],{},"YYYY-MM/YYYY-MM-DD/*.svg"," を拾って ",[68,240,241],{},"dir = \"YYYY-MM-DD\""," として登録するようにした。ミドルウェア配信の URL 形式（",[68,244,220],{},"）でそのまま指せるので、ギャラリー側は特別扱いなしに表示できる。",[18,247,248,249,252,253,255],{},"修正後、134 → ",[38,250,251],{},"335 枚"," に増えた。追加された 201 枚は ",[68,254,200],{}," 直下から掘り出した図版で、記事本文の中に SVG タグで埋め込んでいたやつが全部それ。「数が少ない気がする」という感覚が、実装の見落とし1つを引き出した瞬間だった。",[14,257,258],{"id":258},"学び",[32,260,261,264,270,282],{},[35,262,263],{},"一覧を作ると、揃った瞬間に感覚が異常を検知する。「なんか少ない」という違和感は、感覚がスキャンした結果と過去の記憶を突き合わせたシグナル。数字を拾いに行くきっかけになる",[35,265,266,267,269],{},"ブログ側と画像側で二重フィルタが必要になる構造は、",[68,268,138],{}," が記事レベルで除外している一方、画像は物理ファイルとして残っていることに起因する。片方だけ直しても表示は変わらないので、記事フィルタと画像フィルタは片方直したらもう片方も点検する",[35,271,272,273,275,276,278,279,281],{},"Nuxt の非同期コンテキストは ",[68,274,142],{}," を挟むごとに切れる可能性がある。",[68,277,146],{}," を複数回叩くときは、Promise を同期発行して ",[68,280,150],{}," で待つのを型として覚えておく",[35,283,284],{},"過去に作った成果物を一覧できるページは、ブログ本文とは別の入口として静かに効いてくる。同じ図を書き直すのを止められる",[14,286,287],{"id":287},"今後の余地",[32,289,290,297,300],{},[35,291,292,293,296],{},"記事にひもづかない孤立SVG（",[68,294,295],{},"dir"," が空、または記事がマッチしない）を別ビューでまとめて可視化する",[35,298,299],{},"ファイル名やタグでのインクリメンタル検索を上部バーに載せる",[35,301,302],{},"グリッドの列数を localStorage に保存し、次回開いたときに前回の見え方を復元する",{"title":304,"searchDepth":305,"depth":305,"links":306},"",2,[307,308,309,313,314,315,316],{"id":16,"depth":305,"text":16},{"id":26,"depth":305,"text":27},{"id":59,"depth":305,"text":59,"children":310},[311],{"id":112,"depth":312,"text":112},3,{"id":154,"depth":305,"text":155},{"id":191,"depth":305,"text":192},{"id":258,"depth":305,"text":258},{"id":287,"depth":305,"text":287},"dev","ブログ記事に埋め込んだSVG図版が増えすぎてどの記事にあるか思い出せなくなった。全画面グリッドとスライド形式の2パターンのビューアを実装し、生成スクリプトの抽出漏れも直して335枚を新しい順に俯瞰できるようにした。","md",{},true,null,"/svg-gallery-viewer","mdx-playground",false,"2026-07-01T00:00:00.000Z",{"title":5,"description":318},"2026-07/2026-07-01/svg-gallery-viewer",[330,331,332,333,334],"Nuxt","Vue","SVG","ギャラリー","composable","KMxOqr2IFK_0itBAcGNMIoZmoCREbcUyMW7b26qygXs",[],"https://log.eurekapu.com/og/blog/svg-gallery-viewer.png?v=2026-07-01T00%3A00%3A00.000Z&title=%E9%81%8E%E5%8E%BB%E8%A8%98%E4%BA%8B%E3%81%AB%E6%95%A3%E3%82%89%E3%81%B0%E3%81%A3%E3%81%9FSVG%E5%9B%B3%E7%89%88%E3%82%92%E4%B8%80%E8%A6%A7%E3%81%99%E3%82%8B%E3%82%AE%E3%83%A3%E3%83%A9%E3%83%AA%E3%83%BC%E3%83%93%E3%83%A5%E3%83%BC%E3%82%A2%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%9F&author=Kei%20Komatsu&sig=2cf3836afbcfa838",1782975568336]