source.unsplash.com が消えた:検死報告と代替手段のすべて

「既存の使用は引き続き動作する」と共に2021年に非推奨化され、2024年6月に停止され、それでも今日新しいコードに書き込まれ続けている。冷静な検死報告——そして3つの代替手段と、キー不要のランダム性を取り戻すプロキシを紹介します。

シェア
画面が割れたスマートフォンが淡い色の木製の台の上に置かれている白黒写真。破損したディスプレイにUnsplashのロゴが表示されている。
写真提供:Unsplash

あるストックフォトサービスがサブドメインを停止し、その2年後もなお、ドキュメントサイト、ログイン画面、コース教材、そして新たに生成されたコードを壊し続けている。本稿は1本のURLに関する事後検証だ — それが何をしていたか、何がそれを殺したか、そして代わりに何を置くべきか。加えて、この話の中でも奇妙な部分にも目を向ける。すなわち、我々のコードを書く機械たちが、それが消えたことにまだ気づいていないという事実だ。

HTTPレスポンス、逐語引用した2件のchangelogエントリ、APIの制限、壊れたプロジェクトのイシュートラッカー、そしてGitHub・npm・Stack Overflowからのカウント — これらはすべて一次資料に基づいており、それぞれの手法は脚注に記載している。修正方法だけ知りたい場合は移行テーブルへ。

今日、まだリクエストすると何が返るか

コマンド1つで、キー不要、どのマシンからでも再現可能だ。

terminal
curl -I https://source.unsplash.com/random

HTTP/2 503
cache-control: no-cache, no-store
content-type: text/html; charset=utf-8
server: Heroku
via: 2.0 heroku-router
# body: herokucdn.com/error-pages/application-error.html を指すiframe

これはDNS障害ではない。source.unsplash.comは今も名前解決される — herokudns.comホストへのCNAMEになっている — つまりリクエストには応答があるが、アプリケーションが応答しているわけではない。この点は見た目以上に重要だ。HTMLボディ付きの高速な503を受け取ったブラウザは、壊れた画像プレースホルダーを描画する。そしてresponse.okを読み取るコードや、書いた覚えのないonerrorハンドラは、テストしたことのない失敗パスに入る。

URLパターン かつて返していたもの 現在
source.unsplash.com/random任意サイズのランダムな写真503
source.unsplash.com/random/1600x900指定サイズにトリミングされたランダムな写真503
source.unsplash.com/1600x900/?apple,desk検索語に一致するランダムな写真503
source.unsplash.com/featured/1600x900?natureランダムなfeatured写真503
source.unsplash.com/collection/190727/800x600コレクションからのランダムな写真503
source.unsplash.com/user/scottwebb/1600x900特定の写真家によるランダムな写真503
source.unsplash.com/dailyその日の写真503

curl -o /dev/null -w "%{http_code}"で個別に確認した。検索機能は告知どおり最初に段階的に停止された。現在はアプリケーション全体が停止しているため、その区別はもはや意味を持たない。

「非推奨」から「停止」まで3年

両方の告知は今も unsplash.com/documentation/changelog の1か所で読める。全文を引用する。文言そのものがこの話のすべてだからだ。

2021年11月25日 — 「Unsplash Sourceの非推奨化」
「Unsplash Sourceは非推奨となります。既存の利用は引き続き動作しますが、新規プロジェクトでは完全版のUnsplash APIを使用してください。」

2024年6月11日 — 「Unsplash Sourceのサンセット」
「Unsplash Sourceは2021年の非推奨化以降、公式にはサポートされていませんでした。最終的なサンセットの一環として、まず検索機能を無効化することで段階的縮小を開始し、今後数週間のうちにアプリケーション全体を停止します。Sourceの既存利用 — 特に本番レベルのもの — は、できるだけ早く完全版のUnsplash APIに移行してください。」

順に読めば、失敗の構造は明らかだ。2021年の告知には約束が含まれていた(既存の利用は引き続き動作する)が、日付はなかった。2021年にそれを読んだ開発者には、動いているコードをそのままにしておく十分な理由があった。2022年に参加した開発者はそもそも読んでいない。2024年の告知は、誰もブックマークしていなかったページで、3年後に「今後数週間」と告げた。

Unsplashは非推奨化ポリシーを公開しており、それ自体は妥当な内容だ — ドキュメントには、公式にドキュメント化されたフィールドおよびエンドポイントについては、少なくとも3週間前にchangelogで変更を告知し、非推奨期間中はエンドポイントがWarningヘッダーを返す、と記載されている。同じ段落には、なぜそのポリシーがSourceを守らなかったのかを説明する一文もある。「公式にドキュメント化されていないフィールドやエンドポイントについては、予告なく変更を行う場合があります。」Sourceはドキュメント化されたAPIのエンドポイントだったことは一度もない。それをカバーするはずのポリシーの外側にあったのだ。

  • ここから得るべき教訓は「Unsplashが不注意だった」ではない。 ドキュメントを読まずに使えるURLは、その非推奨化ポリシーも読んでいないURLだ、ということだ。
  • 破損は告知に先行していた。 2022年11月28日に提出されたあるDrupalのイシューは、サンセット告知の18か月前の時点ですでに「常にHerokuのアプリケーションエラーが出る」と報告している。こうしたサービスはこのように死んでいく。徐々に、そして誰も見ない告知の中で。

告知自体が障害よりも見つけにくい

2021年の非推奨化告知はchangelog.unsplash.com上に公開され、そのURLこそが当時のバグ報告 — 上記のDrupalのものを含む — すべてがリンクしている場所だ。3つの測定結果を示す。

  1. HTTPSエンドポイントが壊れている。 openssl s_client -connect changelog.unsplash.com:443はtlsv1 alert internal errorを返す — 証明書が提示される前にハンドシェイクが失敗する。したがって、https://だった2021年当時のリンクはすべて、ブラウザ上では死んでいる。
  2. プレーンHTTP経由ではリダイレクトされるが、役には立たない。 http://changelog.unsplash.com/deprecations/2021/11/25/source-deprecation.htmlを辿ると、2ホップ後に unsplash.com/@documentation/changelog/deprecations/2021/11/25/source-deprecation/htmlで400に行き着く — パスがサイトのユーザー名ルートに飲み込まれてしまっている。
  3. アーカイブにはサンセットの時期にちょうど穴が空いている。 Wayback Machineが保持する旧changelogの最後の成功キャプチャは2024年3月24日で、新changelogの最初のキャプチャは2024年8月23日だ。サンセットの告知は2024年6月11日 — この5か月間の空白の中にある。

これは陰謀ではなく、ごく普通のCMS移行の結果だ。しかし影響は現実のもので、本稿が両方のエントリを全文引用している理由もここにある。非推奨化の一次記録は、非推奨にした対象そのものより長く生き残るべきものだが、ここではもう少しで生き残れないところだった。

実際に何が壊れたか

サイドプロジェクトの話ではない。以下の障害は公開のイシュートラッカーのエントリであり、タイトル・日付・状態はGitHubとdrupal.orgのAPIから取得したものだ。

プロジェクト イシュー 作成日 内容
MUI(Material UI) #42736 2024年6月24日 「[docs] Random Unsplash photo URL is no longer functional」— 公式のSign-in sideテンプレートが死んだ画像を出荷していた。3日後にクローズ。
Nextcloud #115 2023年1月17日 「Migrate to Unsplash API」— バックグラウンドアプリがSourceのURIを前提に構築されていた。18か月オープンのまま、2024年7月16日にクローズ。
sindresorhus/Actions #248 2024年5月28日 「Get Unsplash Image: 503 Error」— iOS/macOS用Shortcutsアクションが、サンセット告知の2週間前にすでに壊れていた。
Drupal — Gin Login #3324054 2022年11月28日 「Unsplashはsource.unsplash.comを非推奨化した — これによりreCAPTCHAの読み込みが遅延し、ユーザーがログインできなくなる。」

最後の行をもう一度読んでほしい。心に留めておく価値がある内容だからだ。ログインフォームの横にある装飾用の画像 — ページ内で最も明らかに重要度が低いアセット — が、認証障害へと発展した。遅い外部リクエストが、ログインフォームに必要なCAPTCHAの前に居座ってしまったからだ。誰もそういうつもりで書いたわけではない。それはブラウザが物を読み込む順序から自然に生まれた結果だった。

あなたのコーディングアシスタントはメモを見ていない

ここからが、2024年の障害を2026年の問題に変えている部分だ。source.unsplash.comは約8年間にわたり、ドキュメント化され、ブログで紹介され、教材として使われ、コピーされ続けてきた。そのテキストはすべて、今スターターコードを書いているモデルの学習データに含まれている — そしてテキストには期限がない。3つのカウントを示す。

測定項目2026年8月30日時点の値取得方法
source.unsplash.comを含むファイル数 3,344 GitHubコード検索API、q=source.unsplash.com(インデックス済みの公開コードのみ — これは下限値であり総数ではない)
100ファイルのサンプルのうち、サンセット後に作成されたリポジトリ 77件中12件 同じクエリ、100件の結果を77リポジトリに重複排除し、created_atを2024年6月11日と比較
…かつ、そのサンプルのうち過去12か月以内にpushされたリポジトリ 77件中20件 アーカイブではなく現役リポジトリ — elastic/kibanaを含む。そのデモファイルは今もimageUrl: 'https://source.unsplash.com/64x64/?dingo'のままだ
unsplash-source-es6の月間ダウンロード数 23 npmレジストリAPI — 停止済みサービス向けのラッパーで、最終公開は2022年だが今もインストールされている
それに言及するStack Overflowの投稿数 1,459 Stack Exchange API、/search/excerptsの合計

最も直接的な証拠は、アプリケーションコードの中にすらない — プロンプトの中にある。その検索の最上位結果は、GPTのシステムプロンプト集で、次の一文を含んでいる。「please use unsplash API( https://source.unsplash.com/1280x720/?<PUT YOUR QUERY HERE>」。この指示は今も新しいアシスタントに複製され続けている。モデルはURLを検証しない。使えと指示され、これまで見てきたすべての例がそれに同意していたのだから。

つまり、生成コードの不具合には2つの独立した原因があり、片方を直しても、もう片方は直らない。古い学習データと、その上に人間が書いた古い指示だ。どちらの場合でも、症状は同じ — 決して読み込まれない一群の画像だ。

grepしておく価値のある、死んだ画像のパターン
source.unsplash.com/random/1200x800   # 2024年半ば以降503 — もう戻ってこない
images.unsplash.com/photo-…           # 本物のCDNだが、暗記したIDが実在しない場合がある
via.placeholder.com/400               # 灰色の矩形が、そのまま本番に出荷される
placehold.co/800x600                  # 灰色の矩形。意図的なもの
picsum.photos/800/600                 # 本物の写真だが、ページの内容とは無関係
/placeholder.png                      # そもそもリポジトリに追加されなかったファイル

リストの2行目に脚注を1つ。本稿の執筆中、via.placeholder.comは我々のテストネットワークからもTLSハンドシェイクを完了できず、プレーンHTTPでは403を返した。信頼する前に自分のネットワークから確認してほしい — これらのツールが頼るフォールバックにも、それ自体の障害の履歴があり得る。

この中で本当に壊れているのは最初の1つだけだ。他のものはもっと厄介な形で悪い。読み込まれてしまい、レイアウトは完成しているように見え、そのページが実は何の脈絡もないもので彩られていることに誰も気づかない。そしてこれは画像に限った話ではなく、この問題全体の一般的な形だ。モデルが持つウェブの姿はスナップショットであり、シャッターが閉じた後もエンドポイント、CLIフラグ、パッケージ名、無料プランは動き続ける。

移行テーブル

移行先はちょうど3つあり、それぞれ何を諦めることになるかで提示するのが誠実なやり方だ。まず列を選び、次に自分の行を読んでほしい。

旧SourceのURL A. 固定CDN URLキー不要・ランダム性なし B. Unsplash APIキー必要・サーバー側呼び出し C. 自前のプロキシキーは非公開・ランダム性を回復
/random images.unsplash.com/photo-… — 自分で選んだ1枚の写真 GET /photos/random /?w=1600
/random/1600x900 …?w=1600&h=900&fit=crop /photos/random + 返されたURLへのImgixパラメータ /?w=1600&h=900&fit=crop
/1600x900/?apple,desk 相当なし — 手動で写真を選ぶ /photos/random?query=apple,desk /?query=apple,desk&w=1600
/featured/1600x900?nature 相当なし /photos/random?query=nature 「featured」に後継はない /?query=nature&w=1600
/collection/67920491/1600x900 相当なし /photos/random?collections=67920491 /?collections=67920491&w=1600
/user/scottwebb/1600x900 相当なし /photos/random?username=scottwebb /?username=scottwebb&w=1600
/daily 1枚の写真を固定し、ビルドごとに入れ替える 相当なし — ランダムな1枚を自分で24時間キャッシュする 同上。ただしキャッシュはプロキシ側に置く

ほとんどの人が本当に欲しいのはオプションAだ。 その画像が装飾的なもの — ヒーロー画像、ログイン画面のサイドパネル、カードの背景 — だったなら、そもそもリクエストごとに違う写真は必要なかったはずだ。1枚選んでCDNのURLを固定すれば、ページはランダムな何かに依存しなくなる。

固定でリサイズ可能なUnsplash URL — キー不要、API呼び出し不要
<img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1600&h=900&fit=crop&auto=format"
     width="1600" height="900" alt="…">
# 公式にサポートされているパラメータ: w, h, crop, fit, fm, auto=format, q, dpr
# APIから受け取ったixidパラメータはそのまま残す — 閲覧数の集計に使われる

オプションBは公式の経路であり、呼び出しをサーバー側に移す。フロントエンドのJavaScript内のClient-IDは、公開されている認証情報も同然だからだ。つまずきやすい2つのルールに注意してほしい。collections/topicsは同じリクエスト内でqueryと組み合わせられないこと、そしてcount(最大30)を指定すると、値が1であってもレスポンスの形が配列に変わることだ。

/randomの公式な代替
curl "https://api.unsplash.com/photos/random?query=nature&orientation=landscape" \
  -H "Authorization: Client-ID YOUR_ACCESS_KEY" \
  -H "Accept-Version: v1"

# → JSON。画像は .urls.regular / .urls.raw にある(w/h/fitは自分で付与する)
# → X-Ratelimit-Limit: 1000   X-Ratelimit-Remaining: 999

オプションC: Sourceを約40行で再構築する

もし失われたものが本当に「その挙動」 — キー不要で、毎回異なる写真を返し、<img>タグから、CMSのフィールドから、あるいはサーバーが存在しない静的サイトからそのまま使えるURL — だったのなら、そのエンドポイント自体を自分で運用するしかない。それはAPIの前段に置く小さなワーカー1つで済み、本番投入に耐えるための3つの要素は、キャッシュ、リファラーチェック、そしてクエリ文字列の残りをCDNへそのまま渡すことだ。

worker.js — /photos/randomの上に構築した、Sourceと同じ形のキー不要エンドポイント
// Cloudflare Workers向け。他の環境(Deno Deploy、Val Townなど)でも形は同じだが、
// caches.default の代わりに caches.open() で名前付きキャッシュを開くこと。
// UNSPLASH_KEYはサーバー側にとどまる。呼び出し側には一切見えない。
const ALLOWED = ["example.com", "www.example.com"];   // 自分のドメインのみ
const API_PARAMS = ["query", "collections", "topics", "username", "orientation"];
const TTL = 60;                                       // 秒 — 時間あたりのクォータを保護する

const host = (value) => { try { return new URL(value).hostname; } catch { return null; } };

export default {
  async fetch(req, env, ctx) {
    // 0. GETのみ許可: Cache APIはそれ以外の保存を受け付けないし、
    //    画像エンドポイントには他に応答すべき動詞もない。
    if (req.method !== "GET")
      return new Response("Method not allowed", { status: 405 });
    const url = new URL(req.url);

    // 1. 自分のページのみが埋め込みを許される — 公開インターネット上の
    //    ランダム画像エンドポイントは、他人がレート制限を消費する手段になる。
    const ref = req.headers.get("referer");       // 正規のクライアントでも欠けていることは多い
    if (ref && !ALLOWED.includes(host(ref)))
      return new Response("Forbidden", { status: 403 });

    // 2. パラメータの組み合わせごとにキャッシュし、画像が12枚あるページでも
    //    レンダリングごとに12回ではなく、1分ごとに1回のAPI呼び出しで済むようにする。
    const cache = caches.default;
    const hit = await cache.match(req);
    if (hit) return hit;

    // 3. 公式APIにランダムな写真をリクエストする。
    const api = new URL("https://api.unsplash.com/photos/random");
    for (const p of API_PARAMS)
      if (url.searchParams.has(p)) api.searchParams.set(p, url.searchParams.get(p));

    const r = await fetch(api, { headers: {
      Authorization: "Client-ID " + env.UNSPLASH_KEY,
      "Accept-Version": "v1",
    }});
    // ここでの403はたいていキーの不良ではなく時間あたりのクォータが原因 — 慌てずTTLを上げる
    if (!r.ok) return new Response("Upstream " + r.status, { status: 502 });
    const photo = await r.json();

    // 4. 画像URLを再構築する: ixidは維持し、呼び出し元のサイズ指定パラメータを追加する。
    const img = new URL(photo.urls.raw);          // .raw にはすでにixidが含まれている
    for (const [k, v] of url.searchParams)
      if (!API_PARAMS.includes(k)) img.searchParams.set(k, v);  // w, h, fit, qなど

    const res = new Response(null, { status: 302, headers: {
      Location: img.toString(),
      "Cache-Control": "public, max-age=" + TTL,
      // クレジット情報はリダイレクトと共に運ばれる。ヘッダー値はASCIIである必要があるためエンコードする。
      "X-Photo-Credit": encodeURIComponent(photo.user.name + " on Unsplash"),
      "X-Photo-Link": photo.links.html,
    }});
    ctx.waitUntil(cache.put(req, res.clone()));
    return res;
  },
};

URLの移行は、あとは検索置換だけで済む。この作業に20分かける価値がまさにここにある。

1対1の置き換え
- https://source.unsplash.com/collection/67920491/1600x900
+ https://img.example.com/?collections=67920491&w=1600&h=900&fit=crop

設計上のメモを2つ。どちらも、書き留められるまでに誰かが1度は苦い午後を過ごした結果だ。バイト列をプロキシせず302でリダイレクトすることで、帯域幅の負担を負わずに済み、かつ閲覧数もUnsplashのCDN側でカウントされ続ける。これはガイドラインが求めていることでもある。そしてRefererチェックは、ヘッダーが欠けている場合には意図的に寛容にしてある — 多くの正規のクライアントがそれを取り除くからだ — 一方で、自分のエンドポイントが他人の無料画像APIと化してしまう、明らかなケースだけは防いでいる。

APIを使い始めた瞬間に付いてくるルール

Sourceにはアカウントがなかったので、ルールもなかった。APIには、設計の仕方そのものを変える5つのルールがあり、いずれも現行のドキュメントに基づく。

  • レート制限は1時間単位で、最初は小さい。 デモモードでは50リクエスト/時、アプリケーションが本番用として承認された後は1,000/時になる。カウントされるのはapi.unsplash.comへの呼び出しのみで、images.unsplash.comへの画像リクエストはカウントされない。すべてのレスポンスでX-Ratelimit-Remainingを確認すること。
  • ホットリンクは許可されているだけでなく必須だ。 Unsplashは、APIが返す画像URLを直接埋め込むことを要求している。それによって写真の閲覧が写真家に帰属できるようになる。ファイルを自前のCDNにミラーリングすることは、自由に行える最適化ではない。
  • ixidパラメータは維持すること。 返されたURLのリサイズやトリミングは想定されているが、アプリケーションを識別するパラメータを取り除くことは想定されていない。
  • 帰属表示とダウンロードのトラッキングは契約の一部だ — 写真家とUnsplashにはクレジットが表示され、「ダウンロード」はユーザーがファイルを取得した際に、写真のダウンロードエンドポイント経由で報告される必要がある。これは自分で発火させなければならないイベントだ。
  • 配布型のプロダクトには動的クライアント登録が必要だ。 プラグイン、テーマ、セルフホスト型CMSを配布する場合、1つの共有キーを使い回すことはポリシー違反であると同時に単一障害点にもなる。APIにはまさにこのケースのための登録フローが用意されている。

ここで範囲について正直に述べておく。どのみちAPIとキーを組み込むのであれば、どの画像APIを選ぶかは改めて開けた問いになる。クライアントを書き始める前に5分かける価値がある。無料のものを比較 — クォータ、ルール、検索の挙動、レスポンスの形 — した内容は 無料ストックフォトAPI比較にまとめてある。

プレースホルダーが欲しかっただけなら、そう言おう

Sourceの利用のかなりの部分は、そもそもUnsplashそのものが目的ではなかった。「レイアウトを組んでいる間だけ、画像っぽい何かを置いておきたい」というものだ。それが目的なら、キー不要のサービスは今も存在し、正しい選択肢だ。

サービスキー必要?得られるもの限界
Lorem Picsum 不要 実在する写真: picsum.photos/800/600。/id/237/…や/seed/xxx/…で固定でき、?grayscaleや?blur=1..10も使える。/v2/listエンドポイントは各写真のUnsplashページと著作者をクレジットする。 被写体の指定が一切できない。写真はページの内容と無関係になる。
placehold.co 不要 任意サイズのラベル付き矩形 — 誠実なワイヤーフレーム用の穴埋め。 それは灰色の箱であり、クライアントに共有したスクリーンショットの中でもそう見える。
Openverse 不要 WordPress.orgが運営する、公開APIを備えたオープンライセンスのカタログ。 キーワード一致であり、ライセンスは項目ごとに異なるため、必ず確認する必要がある。

重要な区別はこうだ。プレースホルダーは定義上一時的なものだ。もしその画像が本番まで生き残るなら、それはもうプレースホルダーではない — 誰も選んでいない挿絵であり、読者にはそれが分かる。

3つのキーではなく1つのキーへ

ここが、誰も計画していない移行の側面だ。Sourceを離れる人々が1つのAPIに落ち着くことは滅多にない。1ページにヒーロー画像1枚、セクション画像2枚、カードグリッド用の何かが必要になれば、正直な答えは大抵UnsplashプラスPexelsプラスPixabayになる — 3つの登録、3つの認証方式、3つのJSON形式、3つのページネーションモデル、3組の帰属表示ルール。すべては同じ<img>タグを埋めるためだ。この統合作業こそが、キー不要のURLが消えたことに対する本当の請求書であり、それを引き起こした障害から数週間後に届く。

これを1つの統合にまとめるために、我々はPexafyを作った。9の無料ライセンスライブラリを1つのスキーマの下に1つのキーで統合し、文単位の意味検索を実現した — つまり「木製の机の上に置かれた画面の割れたスマートフォンを、上から撮影した」といった完全な説明文でも、何も返ってこないのではなく、ランク付けされた結果が返ってくる。制限は2つあり、それを率直に述べる。本稿全体が「二度と驚かない」ことを主題としているからだ。まず、キーが必要なので、Sourceが持っていたものを復元するわけではない — これは上述の公式Unsplash APIと同じカテゴリに属する。そして、扱っているのは無料ライセンスの写真であり、editorial(報道)素材やブランド画像ではない。

本当に新しい部分は上述のアシスタントに向けたものだ。mcp.pexafy.com/mcpにあるMCPサーバーによって、本来なら記憶から画像URLを唱えるだけだったモデルが、実在するカタログを検索し、実在する写真をクレジット情報付きで返せるようになる。これは、機械が書く死んだURLに対して、どんなlintルールよりも優れた答えだ。その理由は AIエージェント向け画像検索インフラにまとめてある。

自分の資産を10分で監査する

何に移行するにせよ、まずこの作業を先にやってほしい — 見つけていないURLは直しようがない。Sourceは今日たまたま取り上げた例に過ぎない。同じ3つのステップは、埋め込んでいるあらゆる外部アセットに当てはまる。

死んだ参照をすべて見つけ、混入させない
# 1. ドキュメント、テスト、フィクスチャ、READMEを含む、リポジトリ内のすべて。
grep -rn --binary-files=without-match \
  -e "source.unsplash.com" -e "via.placeholder.com" -e "/placeholder.png" .

# 2. データベースが保持しているすべて — こうしたものが最も長く隠れているのはCMSの本文だ。
psql -c "SELECT id FROM posts WHERE body LIKE '%source.unsplash.com%'"

# 3. ビルド済みサイトが実際にリクエストするすべて: クロールして失敗を一覧化する。
#    ファイル拡張子ではなく属性でマッチさせること — 画像URLが.jpgで終わるとは限らない。
grep -rhoE 'src="[^"]+"' dist/ \
  | cut -d'"' -f2 | grep -E '^https?://' | sort -u \
  | xargs -P8 -I{} curl -s -o /dev/null -w "%{http_code} {}\n" {} \
  | grep -v "^200"

# 503 https://source.unsplash.com/random/1200x800   ← 探しているのはこれ

そのうえで、外部画像にどこまでコストを払わせるかを一度きちんと決めておく。誰が引き起こそうと、次のシャットダウンを生き延びる4つのルールを示す。

  1. リクエスト時ではなく、ビルド時に取得する。 ビルド中に解決される画像は、深夜3時にユーザーの前で失敗するのではなく、CIの中で、開発者の目の前で失敗する。
  2. 装飾用のアセットにクリティカルパスを絶対にブロックさせない。 ログインフォームより上には外部のものを何もプリロードしない。すべてのサードパーティ<img>にonerrorのフォールバックと明示的なwidth/heightを与え、失敗のコストがレイアウトシフトやスクリプトの停止ではなく、単なる空白の箱で済むようにする。
  3. その確認をCIに組み込む。 上記のステップ3をビルド出力に対して実行すれば、「誰かがそのうち気づく」が「ビルドが赤くなる」に変わる。これが再発を防ぐ唯一のステップだ。
  4. 外部依存も他の依存と同じように予算化する。 自分のページがどのホストに依存してよいか、そしてそれぞれが停止したときに何が起きるかを書き出しておく。登録の必要がなかったURLであっても、それは依存関係であることに変わりはない — Sourceが証明したのは、それが単に誰も所有していない依存関係だった、ということだけだ。

参考文献と脚注

1 本稿中のすべてのステータスコード、カウント、引用文は、2026年8月30日に一次資料から取得したものだ。HTTPステータスコードは各URLパターンに対してcurlで取得し、すべてserver: Herokuと、herokucdn.com/error-pages/application-error.htmlを埋め込んだボディを伴う503を返した。DNS解決も同日に確認済み(herokudns.comホストへのCNAME)。

2 両方のchangelogエントリは、unsplash.com/documentation/changelogから逐語的に引用している。非推奨化ポリシーの文言(3週間前の告知、Warningヘッダー、公式にドキュメント化されていないエンドポイントの例外)は同日のunsplash.com/documentationから取得した。

3 TLS障害はopenssl s_client -connect changelog.unsplash.com:443(tlsv1 alert internal error)で再現した。リダイレクトチェーンはcurl -Lで追跡した。アーカイブの空白はWayback CDX APIから取得した。changelog.unsplash.comの最後の200キャプチャは20240324、unsplash.com/documentation/changelogの最初のキャプチャは20240823。

4 イシューのタイトル、作成日、クローズ日はGitHub REST API (mui/material-ui#42736、nextcloud/unsplash#115、 sindresorhus/Actions#248)、およびgin_loginのイシュー3324054に関するdrupal.orgのJSON APIから読み取った。その本文は2022年11月時点で「常にHerokuのアプリケーションエラーが出る」と報告している。

5 カウント: GitHubコード検索API (3,344ファイル。100件のサンプルを77リポジトリに重複排除し、そのうち12件が2024年6月11日以降に作成され、20件が直近12か月以内にpushされていた)。npmレジストリのダウンロード数API (unsplash-source-es6、直近30日間で23ダウンロード)。Stack Exchangeの/search/excerpts (1,459件の投稿)。コード検索の対象はインデックス済みの公開リポジトリのみであり、したがって各数値は下限値である。

よくある質問

source.unsplash.comはダウンしているだけですか、それとも完全に停止されたのですか?
完全に停止されました。Unsplashは2024年6月11日にサービス終了を発表し——「まず検索機能を無効化することで段階的に縮小し、今後数週間でアプリケーション全体を停止する」——これは2021年11月25日にサービスを非推奨化した後のことでした。すべてのパターン(/random、/1600x900/?query、/collection/…、/daily)はHTTP 503を返し、Herokuの汎用Application Errorページが表示されます。ホスト名自体は解決可能なため、この障害はネットワークエラーではなく、壊れた画像として表示されます。
source.unsplash.com/randomの直接的な代替手段は何ですか?
キー不要でそのまま差し替えられる方法は存在せず、これは早い段階で受け入れておくべき点です。代替手段は3つあります。固定のCDN URL——images.unsplash.com/photo-…?w=1600&h=900&fit=crop——はキーを必要としませんが、常に同じ写真を返します。これは実際には装飾目的の多くのケースで十分な要件です。公式API、GET https://api.unsplash.com/photos/randomをAuthorization: Client-IDヘッダー付きで呼び出せばランダム性を取り戻せますが、サーバーサイドから呼び出す必要があります。自分で所有する小さなプロキシをこのエンドポイントの前段に置くことだけが、そのまま<img>タグに埋め込めるキー不要のURLを取り戻せる唯一の方法です。
なぜAIコーディングツールは2026年になってもsource.unsplash.comのURLを生成し続けるのですか?
このURLはおよそ8年間ドキュメント化され、教材として使われ、コピーされ続けてきたためで、サービスが終了しても学習データが期限切れになるわけではないからです。2026年8月30日時点の計測では、GitHubのコード検索は依然として3,344件のファイルにこのURLを含むと表示しており、100件のサンプルのうち77リポジトリ中12件はサービス終了後に作成されていました。人間が書いたプロンプト集もこの指示を繰り返しており、広くコピーされているあるGPTプロンプトは今もモデルに「unsplash APIを使う( https://source.unsplash.com/1280x720/?… )」と指示しています。モデルが書いた画像URLは未検証のものとして扱い、CIでステータスコードを確認してください。
APIキーなしでUnsplashのランダムな写真を取得することはまだ可能ですか?
Unsplashから直接取得することはできません——ランダム選択は現在/photos/randomの背後にあり、Client-IDが必要です。キー不要の選択肢は2つあります。1つは自分でホストするプロキシで、キーはサーバーサイドに留まり、公開URLは以前のものに似た形になります。もう1つはLorem Picsum(picsum.photos/800/600)のようなサードパーティのプレースホルダーサービスで、実際の写真をキーなしで提供しますが、被写体を指定するターゲティング機能は一切ありません。
Unsplash APIは画像のダウンロードと自前サーバーでのホスティングを許可していますか?
いいえ。ほとんどのAPIとは異なり、Unsplashはホットリンクを要求します。つまりAPIが返す画像URLは直接埋め込まなければならず、それによって写真の閲覧数が撮影者にカウントされます。これには3つの義務が伴います——URLをリサイズやクロップする際はixidパラメータを保持すること、撮影者とUnsplashをクレジット表示すること、ユーザーがファイルを取得する際は写真のダウンロードエンドポイントを発火させることです。ファイルを自前のCDNにミラーリングすることは、あなたが自由に行える最適化ではありません。
なぜ2021年の非推奨化通知は既存ユーザーを保護しなかったのですか?
通知とポリシーが同じ対象を扱っていなかったためです。2021年11月25日の変更履歴のエントリは「既存の使用は引き続き動作する」と約束し、終了日を示していませんでした。Unsplashが公開している非推奨化ポリシー——少なくとも3週間前の通知とWarningヘッダーの付与——は公開ドキュメント化されているフィールドやエンドポイントに適用されるものであり、同じ段落には、ドキュメント化されていないものは予告なく変更される可能性があると明記されています。Sourceは一度もAPIのドキュメント化されたエンドポイントではなかったため、それを保護するはずのポリシーの対象外だったのです。
プロジェクト内のsource.unsplash.comの死んだURLをすべて見つけるにはどうすればよいですか?
3つのパスで10分です。まずリポジトリをgrepすること。ドキュメント、テスト、フィクスチャ、READMEを含めて——grep -rn "source.unsplash.com" .——これらのURLはサンプルコード内で最も長く生き残るためです。次にデータベースをクエリすること。CMSの記事本文はこれらが隠れている場所です(WHERE body LIKE '%source.unsplash.com%')。最後にビルド済み出力をクロールすること。すべての画像URLを抽出し、それぞれにリクエストを送り、200以外を返すものを一覧化します。この最後のパスをCIに追加すれば、死んだサードパーティのアセットはページではなくビルドを失敗させるようになります。

キーワード探しはもう終わり。伝えたいことをそのまま書きましょう。

無料利用可能な9M+点の画像を意味で検索。どの言語でも、100 ms未満で。