[{"data":1,"prerenderedAt":315},["ShallowReactive",2],{"content-/content-visibility-and-image-path-fixes":3,"all-pages-for-dir":293,"related-/content-visibility-and-image-path-fixes":294,"og-image-/content-visibility-and-image-path-fixes":314},{"id":4,"title":5,"body":6,"category":275,"concepts":276,"description":277,"extension":278,"meta":279,"navigation":280,"ogImage":276,"path":281,"project_name":282,"published":283,"publishedAt":284,"seo":285,"stem":286,"tags":287,"todo":276,"unpublished":283,"updatedAt":276,"__hash__":292},"pages/2026-07/2026-07-24/content-visibility-and-image-path-fixes.md","Nuxtで画像が表示されないバグを追ったら、ビルド除外の設計に行き着いた",{"type":7,"value":8,"toc":267},"minimark",[9,13,16,21,24,39,110,113,116,119,123,126,129,133,148,165,178,183,206,214,217,223,230,237,241,244,251,254,257,260,263],[10,11,12],"p",{},"ローカルでだけ確認している記事のページを開くと、本文は読めるのに画像だけが真っ白だった。何度リロードしても変わらない。31枚あるはずの画像のうち、1枚も出てこない。原因を突き止めてほしいと頼んだ。",[10,14,15],{},"devサーバーがちょうど落ちていて、再起動しかけたところで止めた。触らずこのまま調べたほうがいい。ファイルは全部揃っている。となると疑うべきは配信の経路のほうだ。",[17,18,20],"h2",{"id":19},"ipが取れていない","IPが取れていない",[10,22,23],{},"content内の画像はミドルウェアが配信していて、ビルド時にpublicへコピーする処理と、リクエスト時にアクセスを絞る処理の2段構えになっている。デバッグ出力を仕込んで一つずつ潰していくと、犯人は後者だった。",[10,25,26,30,31,34,35,38],{},[27,28,29],"code",{},"getRequestIP(event)"," が ",[27,32,33],{},"undefined"," を返していた。devサーバーは内部でリクエストをプロキシしていて、生のTCPソケット情報が失われる。実際のクライアントIPは ",[27,36,37],{},"x-forwarded-for: ::1"," というヘッダーで渡ってくるのに、ミドルウェア側はそのヘッダーを見ない設定で呼び出していた。",[40,41,46],"pre",{"className":42,"code":43,"language":44,"meta":45,"style":45},"language-ts shiki shiki-themes vitesse-light vitesse-light","// Before: x-forwarded-forを見ない設定 → devでは常にundefined\ngetRequestIP(event)\n// After: プロキシ経由のヘッダーも見る\ngetRequestIP(event, { xForwardedFor: true })\n","ts","",[27,47,48,57,75,81],{"__ignoreMap":45},[49,50,53],"span",{"class":51,"line":52},"line",1,[49,54,56],{"class":55},"sxvE3","// Before: x-forwarded-forを見ない設定 → devでは常にundefined\n",[49,58,60,64,68,72],{"class":51,"line":59},2,[49,61,63],{"class":62},"senZ8","getRequestIP",[49,65,67],{"class":66},"shFtX","(",[49,69,71],{"class":70},"s4oTP","event",[49,73,74],{"class":66},")\n",[49,76,78],{"class":51,"line":77},3,[49,79,80],{"class":55},"// After: プロキシ経由のヘッダーも見る\n",[49,82,84,86,88,90,93,96,100,103,107],{"class":51,"line":83},4,[49,85,63],{"class":62},[49,87,67],{"class":66},[49,89,71],{"class":70},[49,91,92],{"class":66},",",[49,94,95],{"class":66}," { ",[49,97,99],{"class":98},"sz8Xr","xForwardedFor",[49,101,102],{"class":66},": ",[49,104,106],{"class":105},"sHkkW","true",[49,108,109],{"class":66}," })\n",[10,111,112],{},"IPが取れない、ローカルホスト判定が常にfalseになる、常に404を返す。地味なオプションの見落としひとつが、31枚の画像を丸ごと消していた。",[10,114,115],{},"直してリロードすると、200が返ってJPEGが届いた。それでも自分の目で確かめないと終われない。画像が出るところまでページをスクロールし、実際の見た目をスクリーンショットで確認する。",[10,117,118],{},"ビューポート外はまだ白いままだった。lazy loadingのせいだろうと当たりをつけて、直接fetchで31枚全部のステータスを確認する。全部200だった。念のため最下部までスクロールしてから、ようやく直ったと言えた。",[17,120,122],{"id":121},"配置し直したわけではなかった","「配置し直した」わけではなかった",[10,124,125],{},"報告すると、「他の記事と同じように画像を配置し直したってことですよね」と聞かれた。違う。画像ファイルの場所も記事内のパスも最初から正しく、一度も動かしていない。直したのはサーバー側のアクセス制御ロジックのバグで、画像の置き場所とは無関係だった。",[10,127,128],{},"そう伝えると、話は別の方向に転がった。このミドルウェアらしき仕組みは、今日のうちにCodexが追加したものではないか。だとしたら、いらない実装かもしれない。従来通りマークダウンを置くだけでデプロイできればいい。バグが入ったのは新しいロジックのせいなのか、それとも元からあった不具合なのか。",[17,130,132],{"id":131},"消していいのか確かめる","消していいのか、確かめる",[10,134,135,136,139,140,143,144,147],{},"git履歴を辿ると、",[27,137,138],{},"content.config.ts"," と ",[27,141,142],{},"nuxt.config.ts"," も未コミットで、",[27,145,146],{},"_local-unpublished"," という仕組みそのものが今回の一連の作業でまるごと新規に追加されたものだとわかった。着手前に、消したときの影響を確かめておきたい。この記事には機密情報が写り込んだ画像が含まれていて、扱いを間違えられない話だったからだ。",[10,149,150,151,154,155,158,159,161,162,164],{},"既存の、コミット済みの ",[27,152,153],{},"verify-unpublished-excluded.mjs"," を見ると、",[27,156,157],{},"unpublished: true"," のフラグが付いた記事は、本番ビルドのHTML・rawマークダウン・サイトマップ・JSバンドルの文字列すべてから排除され、漏れがあればビルド自体を失敗させる検証まで用意されていた。つまり ",[27,160,146],{}," という特別な仕組みがなくても、この記事を普通に ",[27,163,157],{}," のまま置けば、本番サイトには一切出ない。ここまでわかると、今回の追加実装は本当にいるのか、疑いはむしろ強まった。",[10,166,167,170,171,174,175,177],{},[27,168,169],{},"/blog/unpublished"," を開いて、既存の非公開記事の一覧と ",[27,172,173],{},"git ls-files"," の結果を突き合わせた。既存の ",[27,176,157],{}," 記事は、たくさんある方も含めて全部コミット済みだった。本番には出ないが、リポジトリの中には残っている。一方で今回の記事は機密情報が写り込んでいて、リポジトリの中間データにすら残したくない種類のものだ。既存の仕組みと新しい仕組みは、守っている対象の重さが違う。ここでようやく、追加実装には意味があったと言えた。",[10,179,180,182],{},[27,181,146],{}," が触っている箇所は全部で5つあった。",[184,185,186,190,193,200,203],"ul",{},[187,188,189],"li",{},"画像配信ミドルウェアの、今回バグが出た当のアクセス制御",[187,191,192],{},"コンテンツのビルド設定側で、このフォルダをNuxt Contentの取り込み対象から外す条件分岐",[187,194,195,196,199],{},"そもそもGit管理から外す ",[27,197,198],{},".gitignore"," の1行",[187,201,202],{},"本番除外を最終的に保証する検証スクリプト",[187,204,205],{},"ローカル用の記事を置くための専用フォルダそのもの",[10,207,208,210,211,213],{},[27,209,198],{}," の1行が最初からこのフォルダを除外していて、記事本文も画像31枚も一度もGit管理に入っていないことが、ここで判明した。その ",[27,212,198],{}," 自体が未コミットの変更だったので一瞬肝を冷やしたが、Gitはコミットの有無に関わらずワーキングツリー上の中身をそのまま見る。だから今この瞬間も機能していた。",[17,215,216],{"id":216},"順番を決めてコミットする",[10,218,219,220,222],{},"方針を決めた。まず今の状態、バグ修正済みで ",[27,221,146],{}," が動く状態をチェックポイントとしてコミットし、そのあとでIP判定ロジックの見落とし部分だけを直す。実装を削る前に、既存の公開記事がどう画像を置いているかも見比べておいた。置き方の型自体は今回も変わっていない。",[10,224,225,226,229],{},"4ファイルをステージして学習ゲート付きでコミットしようとしたら、案の定ゲートが動いた。クイズは今回はパスにして、",[27,227,228],{},"LEARN_SKIP=1"," で通した。",[10,231,232,233,236],{},"ついでに、他に溜まっていた未コミットの変更も粒度を分けて片付けることにした。公開版記事に使う画像4枚は、コミット前に機密情報が残っていないか自分の目で確認した。巨大なコーパスのファイル群は除外し、スクリプト6本と小さい成果物だけをステージしてコミットする。最終的に意味の単位で7本のコミットに分かれ、",[27,234,235],{},"git status"," はクリーンになった。作業環境の設定リポジトリのほうにも溜まっていた履歴があったので、そちらも合わせて片付けてプッシュしておいた。",[17,238,240],{"id":239},"デート制限と聞いて","デート制限、と聞いて",[10,242,243],{},"前日分の日記を自動生成するチェーンの中で、決算データ取得バッチが引っかかっていた一件も、この日のうちに片付けることにした。Koyfinから71銘柄をまとめて取りに行ったところ、22銘柄が429で弾かれていた。「デート制限にかかっているやつ」と伝えたつもりで、レート制限のことだと通じるまで一拍あった。",[10,245,246,247,250],{},"修正は ",[27,248,249],{},"/check-earnings"," の手順書に埋め込まれたスクリプトのテンプレート自体に手を入れる形にした。71銘柄を一斉に投げるのをやめて、12銘柄ずつのチャンクに分割し、429が返ってきたら1.5秒、3秒とバックオフしながら自動リトライするようにする。",[10,252,253],{},"Koyfinのタブがまだログイン状態のまま残っていたので、そのまま修正版で71銘柄を再取得させた。今度は全部200が返ってきた。前回取り込めなかった3銘柄も含めて、71/71が成功した。データを再生成し、ページの表示にエラーが出ていないことを確認してから、この修正一式もコミットした。",[17,255,256],{"id":256},"その日にわかったこと",[10,258,259],{},"画像が表示されない理由を探しているうちに、機密情報を含む記事をどう守るかという話にたどり着いた。バグはIPアドレス判定のオプション一つの見落としで、直すこと自体はすぐ終わった。時間がかかったのは、「この仕組みは本当にいるのか」という疑いを、実際に既存の仕組みと突き合わせて確かめる作業のほうだった。",[10,261,262],{},"疑ってすぐ消すのではなく、確かめてから残すか判断する。機密記事がこれからも増えたときに、この確認をその都度手作業でやるのか、仕組み側にもう一段検証を足すのかは、まだ決めていない。",[264,265,266],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":45,"searchDepth":59,"depth":59,"links":268},[269,270,271,272,273,274],{"id":19,"depth":59,"text":20},{"id":121,"depth":59,"text":122},{"id":131,"depth":59,"text":132},{"id":216,"depth":59,"text":216},{"id":239,"depth":59,"text":240},{"id":256,"depth":59,"text":256},"dev",null,"非公開記事の画像が出ない原因を追うとIP判定のバグに行き着いた。機密情報を含む記事をビルドから確実に外す仕組みを見直し、決算データ取得バッチのレート制限対策とあわせて、溜まっていた変更を意味の単位でコミットし直した一日の記録。","md",{},true,"/content-visibility-and-image-path-fixes","blog-platform",false,"2026-07-24T00:00:00.000Z",{"title":5,"description":277},"2026-07/2026-07-24/content-visibility-and-image-path-fixes",[288,289,290,291],"Nuxt Content","デバッグ","ビルド設定","セキュリティ","M61704seaa3A_VHMLL62B0k3AYj6yS__ywy46CGmDNM",[],[295,299,302,306,310],{"title":296,"path":297,"publishedAt":298},"テキストファイルにパスワードをかける前に — 1Passwordのセキュアノートと保管庫","/1password-notes-and-vaults","2026-07-25T00:00:00.000Z",{"title":300,"path":301,"publishedAt":298},"NVIDIAのオープンウェイト共同声明を全文対訳で記事化した日 — 訳語「公開」の衝突とSVG図解4枚","/open-weights-statement-bilingual-article",{"title":303,"path":304,"publishedAt":305},"WordPressバックアップから記事を復元できるか検証し、専用リポジトリを立てた日","/wordpress-restore-and-repo-split","2026-07-21T00:00:00.000Z",{"title":307,"path":308,"publishedAt":309},"デザインシステム基礎講座にHTML/CSS実例を追加した記録","/design-system-basics-live-examples","2026-07-08T00:00:00.000Z",{"title":311,"path":312,"publishedAt":313},"1Passwordの値上げを機にBitwardenへ移行する — 手順・料金・評判まとめ","/1password-to-bitwarden-migration","2026-07-07T00:00:00.000Z","https://log.eurekapu.com/og/blog/content-visibility-and-image-path-fixes.png?v=2026-07-24T00%3A00%3A00.000Z&title=Nuxt%E3%81%A7%E7%94%BB%E5%83%8F%E3%81%8C%E8%A1%A8%E7%A4%BA%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E3%83%90%E3%82%B0%E3%82%92%E8%BF%BD%E3%81%A3%E3%81%9F%E3%82%89%E3%80%81%E3%83%93%E3%83%AB%E3%83%89%E9%99%A4%E5%A4%96%E3%81%AE%E8%A8%AD%E8%A8%88%E3%81%AB%E8%A1%8C%E3%81%8D%E7%9D%80%E3%81%84%E3%81%9F&author=Kei%20Komatsu&sig=584cea0d67ac1711",1785099340717]