◉ANTIGRAVITY LABEN
記事一覧/Tips & 活用術
✦ Tips & 活用術/2026-10-19上級

URL を書き換えたら、あとから読み込む画像だけ 404 になりました。replaceState と相対パスの確かめ方

一覧から個別表示へ切り替えるとき history.replaceState で URL を書き換えたところ、まだ読み込んでいない画像だけが 404 になりました。loading=lazy も同じ挙動だった点、確認用の最小ページとスクリプト、直し方三種の比較までを書き残します。

history API遅延読み込み相対パスPlaywright4個人開発112Antigravity389

✦ プレミアム記事

作品の一覧から個別の表示へ切り替えるとき、ページを丸ごと読み込み直さずに URL だけを書き換える実装を入れました。アドレスバーには個別の URL が出て、戻るボタンも期待どおりに動きます。

ところが、スクロールしてあとから出てくるはずの画像だけが、いくつか 404 になりました。最初に見えていた画像は問題なく出ているので、確認も一度は「問題なし」と流しかけたほどです。

個人開発の小さなサイトでも起こりうる話ですので、原因が「相対パスの基準が、書き換えた URL に移っていた」ことだった経緯と、手元で再現した結果、直し方三種の違いを書き残します。

症状は「まだ読み込んでいない画像」にだけ出ました

壊れた画像には共通点がありました。ページを開いた時点で画面内にあった画像は表示され、スクロールして初めて近づく画像だけが出ない、という並びです。

ブラウザの開発者ツールで失敗した要求を見ると、URL が本来の場所と違っておりました。画像の置き場所が一階層ぶん深くなり、そこには当然ファイルがありません。

この時点で、キャッシュでも CDN でもなく、「どの URL を基準に相対パスを解決したか」が動いたのだと見当がつきました。以前、同一 URL で差し替えた資産が古いまま残る件では キャッシュを疑う順番 を整理しましたが、今回は取得先の URL そのものが違います。切り分けの入口はそこでした。

手元で再現する最小ページ

実サイトの条件を持ち込まず、必要な要素だけに削った確認ページを作りました。先頭に画面内の画像を一枚、空白を挟んで、あとから読み込む画像を二枚置きます。

一枚は HTML 標準の loading="lazy"、もう一枚は IntersectionObserver で data-src を src に入れ替える自前方式です。どちらも、読み込みの直前に history.replaceState で URL を深い階層へ書き換えます。

<!doctype html>
<html><head><meta charset="utf-8"><title>replaceState 再現</title></head><body>
<!-- 画面内の画像: 書き換え前に読み込みが始まる -->
<img id="eager" src="img/a.jpg" width="100" height="100">
<div style="height:3000px">spacer</div>
<!-- あとから読み込む画像 2 種 -->
<img id="native" loading="lazy" src="img/b.jpg" width="100" height="100">
<img id="js" data-src="img/c.jpg" width="100" height="100">
<script>
// 一覧 /gallery/ から個別 /gallery/item/12/ へ、ページを読み直さず切り替える想定
history.replaceState(null, '', '/gallery/item/12/');
const io = new IntersectionObserver(es => es.forEach(e => {
  if (!e.isIntersecting) return;
  const img = e.target;
  if (img.dataset.src) img.src = img.dataset.src; // ← ここで相対パスが解決される
  io.unobserve(img);
}));
document.querySelectorAll('img').forEach(i => io.observe(i));
window.scrollTo(0, document.body.scrollHeight);
</script></body></html>

このページを /gallery/fix.html として配信し、Playwright の Chromium で開いて、実際にどの URL へ要求が飛んだかを数えます。見るのは見た目ではなく、応答の URL とステータスコードと naturalWidth です。

# run.py — 応答 URL とステータス、各画像の最終的な読み込み状態を表示する
import threading, http.server, socketserver, functools
from playwright.sync_api import sync_playwright
 
Handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory="site")
srv = socketserver.TCPServer(("127.0.0.1", 8811), Handler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
 
with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    seen = []
    page.on("response", lambda r: seen.append((r.url.split("8811")[1], r.status)))
    page.goto("http://127.0.0.1:8811/gallery/fix.html")
    page.wait_for_timeout(1500)  # 遅延読み込みが動き終わるのを待つ
    print("url      =", page.url.split("8811")[1])
    print("responses=", [s for s in seen if "img" in s[0]])
    print("imgs     =", page.evaluate(
        "[...document.images].map(i => [i.id, i.currentSrc.split(location.host)[1], i.naturalWidth])"))
    browser.close()

何を解決するためのコードかというと、「見た目では判断せず、要求先の URL と naturalWidth の組で壊れ方を特定する」ためです。naturalWidth が 0 のまま残る画像は、読み込み自体が失敗しております。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦URL を書き換える実装を入れる前に、あとから読み込まれる画像が壊れないかを、数分で動かせる確認ページで先に調べられるようになります
✦画像が一部だけ 404 になったとき、「相対パスの基準が動いた」ことを応答の URL から見抜き、キャッシュや CDN を疑って回り道をする時間を省けるようになります
✦絶対パスへ固定する方法と base 要素を使う方法の副作用を比べて、自分のページに合う直し方をエージェントへの指示文に書けるようになります
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Antigravity Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

✦ Tips & 活用術2026-10-15
画像が壊れて見えたのは、確かめ方のせいでした。iframe 検証を疑う順番と、実ページの currentSrc
別タブの iframe に実ページを差し込んで画像を点検し、「読み込まれていない」と誤診した失敗の記録です。非表示の枠では描画幅 0 のまま naturalWidth が 0 になり、遅延読み込みは未開始のまま残ります。手元で再現した数値と、実ページで currentSrc と描画幅を見る検査スクリプト、置換を正規表現で機械的に行ったときの落とし穴まで書き残します。
✦ Tips & 活用術2026-10-17
検索の管理画面に「ページにリダイレクトがあります」と出たとき、直すものと放っておくものを分けた基準
Search Console の「ページにリダイレクトがあります」を、自分のサイトで仕分けた記録です。http→https・www・末尾スラッシュ・言語の振り分けは想定内、チェーン・ループ・転送先のエラーや noindex は要対応という線引きと、転送を一つずつ辿って分類する Python スクリプト、素の要求では見えない言語依存の転送を二通りの要求で見つける方法まで書き残します。
✦ Tips & 活用術2026-04-27
Antigravity で CHANGELOG.md を git ログから半自動で書く — 個人開発でも意思決定の履歴を残す
半年前の自分の判断が思い出せない — 個人開発でよくある問題です。Antigravity に git log を読ませて CHANGELOG.md のドラフトを作る半自動ワークフローを、実際のスクリプト付きで紹介します。