source.unsplash.com a disparu : le post-mortem et toutes les façons de le remplacer

Dépréciée en 2021 avec la promesse que « les usages existants continueront de fonctionner », coupée en juin 2024, et pourtant encore écrite dans du code neuf aujourd'hui. Le post-mortem mesuré — et les trois solutions de remplacement, dont le proxy qui redonne l'aléatoire sans clé API.

Partager
Photographie en noir et blanc d'un smartphone à l'écran fissuré posé sur une surface en bois clair, avec le logo Unsplash visible sur l'écran endommagé.
Photo via Unsplash

Un service de photos stock a désactivé un sous-domaine, et deux ans plus tard, celui-ci continue de casser des sites de documentation, des écrans de connexion, des exercices de cours et du code fraîchement généré. Voici l'autopsie d'une URL — ce qu'elle faisait, ce qui l'a tuée, et ce qu'il faut mettre à sa place — accompagnée d'un regard mesuré sur la partie la plus étrange de l'histoire : les machines qui écrivent notre code n'ont pas remarqué sa disparition.

Les réponses HTTP, les deux entrées de changelog citées mot pour mot, les limites de l'API, les gestionnaires de tickets des projets qui ont cassé, ainsi que les décomptes issus de GitHub, npm et Stack Overflow proviennent tous de sources primaires, avec la méthode pour chacune dans les notes de bas de page. Si vous voulez uniquement la solution, allez directement à la table de migration.

Ce que vous obtenez aujourd'hui, si vous le demandez encore

Une seule commande, sans clé, reproductible depuis n'importe quelle machine :

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
# corps : un iframe pointant vers herokucdn.com/error-pages/application-error.html

Rien ici n'est un échec DNS. source.unsplash.com se résout toujours — c'est un CNAME vers un hôte herokudns.com — donc la requête reçoit une réponse, simplement pas d'une application. Ce détail compte plus qu'il n'y paraît : un navigateur qui reçoit rapidement un 503 avec un corps HTML affiche un espace réservé d'image cassée, et tout code qui lit response.ok ou un gestionnaire onerror que vous n'avez jamais écrit emprunte le chemin d'échec que vous n'avez jamais testé.

Motif d'URL Ce qu'il renvoyait autrefois Aujourd'hui
source.unsplash.com/randomUne photo aléatoire, toute taille503
source.unsplash.com/random/1600x900Une photo aléatoire, recadrée à la taille voulue503
source.unsplash.com/1600x900/?apple,deskUne photo aléatoire correspondant à des termes de recherche503
source.unsplash.com/featured/1600x900?natureUne photo aléatoire mise en avant503
source.unsplash.com/collection/190727/800x600Une photo aléatoire issue d'une collection503
source.unsplash.com/user/scottwebb/1600x900Une photo aléatoire d'un photographe donné503
source.unsplash.com/dailyLa photo du jour503

Vérifié individuellement avec curl -o /dev/null -w "%{http_code}". La fonction de recherche avait été arrêtée en premier, comme annoncé ; aujourd'hui, l'application entière est désactivée, donc la distinction n'a plus lieu d'être.

Trois ans entre « déprécié » et « éteint »

Les deux annonces restent lisibles, au même endroit, sur unsplash.com/documentation/changelog. Citées intégralement, car la formulation est toute l'histoire :

25 novembre 2021 — « Unsplash Source being deprecated »
« Unsplash Source is being deprecated. Existing uses will continue to work, however for new projects use the full Unsplash API. »

11 juin 2024 — « Unsplash Source sunset »
« Unsplash Source has been officially unsupported since its deprecation in 2021. As part of the final sunsetting, we will first wind down by disabling the search feature, and in the coming weeks turn off the application entirely. Existing uses of Source — particularly production-level ones — should migrate as soon as possible to the full Unsplash API. »

Lues dans l'ordre, la source de l'échec devient évidente. L'avis de 2021 contenait une promesse (existing uses will continue to work) et aucune date. Un développeur qui l'a lu en 2021 avait toute raison de laisser son code fonctionnel tel quel ; un développeur arrivé en 2022 ne l'a jamais lu du tout. L'avis de 2024 a donné « les prochaines semaines », trois ans plus tard, sur une page que personne n'avait mise de côté.

Unsplash publie bien une politique de dépréciation, et elle est raisonnable — la documentation indique que pour les champs et points d'accès publiquement documentés, les changements sont annoncés sur le changelog avec au moins 3 semaines de préavis, et les points d'accès renvoient un en-tête Warning pendant la période de dépréciation. Le même paragraphe contient la phrase qui explique pourquoi rien de tout cela ne protégeait Source : « For any non-publicly documented fields or endpoints, we may make changes to these with no warning. » Source n'a jamais été un point d'accès de l'API documentée. Il se situait en dehors de la politique qui l'aurait couvert.

  • La leçon pratique n'est pas « Unsplash a été négligent ». C'est qu'une URL que l'on peut utiliser sans lire aucune documentation est une URL dont on n'a pas non plus lu la politique de dépréciation.
  • La casse a précédé l'annonce. Un ticket Drupal déposé le 28 novembre 2022 signale déjà « I get always a Heroku application error », dix-huit mois avant l'entrée de retrait. C'est ainsi que ces services meurent : lentement, puis dans une annonce que vous ne verrez jamais.

L'annonce est plus difficile à trouver que la panne

La dépréciation de 2021 a été publiée sur changelog.unsplash.com, et c'est l'URL vers laquelle pointe tout rapport de bug contemporain — y compris celui de Drupal ci-dessus. Trois mesures :

  1. Le point d'accès HTTPS est cassé. openssl s_client -connect changelog.unsplash.com:443 renvoie tlsv1 alert internal error — la poignée de main échoue avant même la présentation d'un certificat. Chaque lien de l'ère 2021, qui était en https://, est donc mort dans un navigateur.
  2. En HTTP simple, il redirige, mais sans utilité. Suivre http://changelog.unsplash.com/deprecations/2021/11/25/source-deprecation.html aboutit, deux sauts plus tard, à un 400 sur unsplash.com/@documentation/changelog/deprecations/2021/11/25/source-deprecation/html — le chemin a été absorbé par la route de nom d'utilisateur du site.
  3. L'archive présente un trou exactement là où se trouve le retrait. La dernière capture réussie de l'ancien changelog par la Wayback Machine date du 24 mars 2024 ; sa première capture du nouveau date du 23 août 2024. Le retrait a été annoncé le 11 juin 2024 — à l'intérieur de cet écart de cinq mois.

Rien de tout cela n'est une conspiration ; c'est une migration de CMS ordinaire. Mais la conséquence est bien réelle, et c'est la raison pour laquelle cet article cite les deux entrées intégralement : la trace primaire d'une dépréciation devrait survivre à la chose qu'elle déprécie, et ici, elle a bien failli ne pas le faire.

Ce qui a vraiment cassé

Pas des projets annexes. Les défaillances ci-dessous sont des entrées publiques de gestionnaires de tickets ; les titres, dates et états proviennent des API GitHub et drupal.org.

Projet Ticket Ouvert le Ce qu'il dit
MUI (Material UI) #42736 24 juin 2024 « [docs] Random Unsplash photo URL is no longer functional » — le modèle officiel Sign-in side livrait une image morte. Fermé trois jours plus tard.
Nextcloud #115 17 jan. 2023 « Migrate to Unsplash API » — l'application d'arrière-plan reposait sur des URI Source. Resté ouvert dix-huit mois, fermé le 16 juillet 2024.
sindresorhus/Actions #248 28 mai 2024 « Get Unsplash Image: 503 Error » — une action iOS/macOS Shortcuts, cassée deux semaines avant l'annonce du retrait.
Drupal — Gin Login #3324054 28 nov. 2022 « Unsplash has deprecated source.unsplash.com — this delays reCAPTCHA from loading, preventing users from logging in. »

Relisez cette dernière ligne, car c'est celle qu'il vaut la peine d'intérioriser. Une image décorative à côté d'un formulaire de connexion — l'élément le plus manifestement non critique de la page — a dégénéré en panne d'authentification, parce qu'une requête tierce lente se trouvait sur le chemin du CAPTCHA dont le formulaire de connexion avait besoin. Personne ne l'a conçu ainsi. C'est né de l'ordre dans lequel un navigateur charge les éléments.

Votre assistant de codage n'a pas reçu le mémo

Voici la partie qui transforme une panne de 2024 en problème de 2026. source.unsplash.com a été documenté, commenté sur des blogs, enseigné et copié pendant environ huit ans. Tout ce texte se trouve dans les données d'entraînement des modèles qui écrivent maintenant notre code de démarrage — et le texte n'expire pas. Trois décomptes :

MesureValeur au 30 août 2026Comment elle a été obtenue
Fichiers contenant source.unsplash.com 3 344 API de recherche de code GitHub, q=source.unsplash.com (uniquement le code public indexé — un plancher, pas un total)
Dépôts, dans un échantillon de 100 fichiers, créés après le retrait 12 sur 77 Même requête, 100 résultats, dédupliqués en 77 dépôts, created_at comparé au 11 juin 2024
…et dépôts de cet échantillon ayant reçu un push dans les 12 derniers mois 20 sur 77 Dépôts vivants, pas des archives — dont elastic/kibana, dont le fichier de démo contient encore imageUrl: 'https://source.unsplash.com/64x64/?dingo'
Téléchargements mensuels de unsplash-source-es6 23 API du registre npm — un wrapper pour un service mort, publié pour la dernière fois en 2022, encore installé
Publications Stack Overflow le mentionnant 1 459 API Stack Exchange, total /search/excerpts

La preuve la plus directe ne se trouve pas dans du code applicatif — elle se trouve dans des prompts. Le premier résultat pour cette recherche est une bibliothèque de prompts système GPT contenant la ligne « please use unsplash API( https://source.unsplash.com/1280x720/?<PUT YOUR QUERY HERE>) ». Cette instruction est encore copiée dans de nouveaux assistants aujourd'hui. Le modèle ne vérifie pas l'URL ; on lui a dit de l'utiliser, et chaque exemple qu'il a vu allait dans ce sens.

Ainsi, l'échec du code généré a deux causes indépendantes, et corriger l'une ne corrige pas l'autre : des données d'entraînement obsolètes, et des instructions obsolètes écrites par des humains par-dessus. Dans les deux cas, le symptôme est la même famille d'images qui ne se chargent jamais :

les motifs d'images mortes à rechercher
source.unsplash.com/random/1200x800   # 503 depuis mi-2024 — ne revient jamais
images.unsplash.com/photo-…           # vrai CDN, mais les ID mémorisés peuvent ne plus exister
via.placeholder.com/400               # rectangle gris, livré en production
placehold.co/800x600                  # rectangle gris, volontairement
picsum.photos/800/600                 # une vraie photo, sans rapport avec votre page
/placeholder.png                      # un fichier qui n'a jamais été ajouté au dépôt

Une note sur la deuxième ligne de cette liste : en écrivant cet article, via.placeholder.com n'a pas non plus réussi à établir de poignée de main TLS depuis notre réseau de test, et a répondu 403 en HTTP simple. Vérifiez-le depuis votre propre réseau avant de lui faire confiance — le repli vers lequel se tournent ces outils peut avoir sa propre histoire de panne.

Seul le premier motif est cassé. Les autres sont pires, d'une manière plus subtile : ils se chargent, la mise en page paraît terminée, et personne ne remarque que la page est illustrée par quelque chose qui ne correspond à rien de particulier. Et rien de tout cela n'est propre aux images — c'est la forme générale du problème. La vision du web d'un modèle est un instantané, et les points d'accès, les options de CLI, les noms de paquets et les offres gratuites continuent de bouger après la fermeture de l'obturateur.

La table de migration

Il existe exactement trois destinations, et la manière honnête de les présenter est de préciser ce à quoi on renonce. Choisissez d'abord la colonne, puis lisez votre ligne.

Ancienne URL Source A. URL CDN fixepas de clé · pas d'aléa B. API Unsplashclé · appel côté serveur C. Votre propre proxyclé cachée · aléa retrouvé
/random images.unsplash.com/photo-… — une photo que vous avez choisie GET /photos/random /?w=1600
/random/1600x900 …?w=1600&h=900&fit=crop /photos/random + paramètres Imgix sur l'URL retournée /?w=1600&h=900&fit=crop
/1600x900/?apple,desk Aucun équivalent — choisissez une photo à la main /photos/random?query=apple,desk /?query=apple,desk&w=1600
/featured/1600x900?nature Aucun équivalent /photos/random?query=nature « featured » n'a pas de successeur /?query=nature&w=1600
/collection/67920491/1600x900 Aucun équivalent /photos/random?collections=67920491 /?collections=67920491&w=1600
/user/scottwebb/1600x900 Aucun équivalent /photos/random?username=scottwebb /?username=scottwebb&w=1600
/daily Fixez une photo, faites-la tourner à chaque build Aucun équivalent — mettez vous-même en cache une photo aléatoire pour 24 h Idem, avec le cache dans le proxy

L'option A est celle que la plupart des gens veulent réellement. Si l'image était décorative — une bannière, un panneau latéral de connexion, un fond de carte — vous n'aviez jamais besoin d'une photo différente à chaque requête. Choisissez-en une, conservez l'URL CDN, et la page cesse de dépendre de quoi que ce soit d'aléatoire :

une URL Unsplash fixe et redimensionnable — sans clé, sans appel API
<img src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1600&h=900&fit=crop&auto=format"
     width="1600" height="900" alt="…">
# Paramètres officiellement pris en charge : w, h, crop, fit, fm, auto=format, q, dpr.
# Conservez tout paramètre ixid fourni par l'API — c'est lui qui déclenche le comptage de la vue.

L'option B est la voie officielle, et elle déplace l'appel côté serveur, car un Client-ID dans du JavaScript front-end est un identifiant publié. Notez les deux règles qui piègent les gens : collections/topics ne peuvent pas être combinés avec query dans la même requête, et count (max 30) change la forme de la réponse, qui devient un tableau même quand elle vaut 1.

le remplacement officiel de /random
curl "https://api.unsplash.com/photos/random?query=nature&orientation=landscape" \
  -H "Authorization: Client-ID YOUR_ACCESS_KEY" \
  -H "Accept-Version: v1"

# → JSON. L'image se trouve dans .urls.regular / .urls.raw (ajoutez w/h/fit vous-même).
# → X-Ratelimit-Limit: 1000   X-Ratelimit-Remaining: 999

Option C : reconstruire Source, en une quarantaine de lignes

Si ce que vous avez perdu était véritablement le comportement — une URL sans clé qui renvoie une photo différente à chaque fois, utilisable directement dans une balise <img>, dans un champ de CMS, ou dans un site statique où il n'y a pas de serveur — alors il vous faut faire tourner ce point d'accès vous-même. C'est un petit worker devant l'API, et les trois éléments qui lui permettent de survivre au contact de la production sont le cache, la vérification du référent, et la transmission du reste de la chaîne de requête au CDN.

worker.js — un point d'accès sans clé, façon Source, au-dessus de /photos/random
// Cloudflare Workers. Ailleurs (Deno Deploy, Val Town…) la forme est la même,
// mais ouvrez un cache nommé avec caches.open() plutôt que caches.default.
// UNSPLASH_KEY reste côté serveur. Les appelants ne la voient jamais.
const ALLOWED = ["example.com", "www.example.com"];   // vos domaines uniquement
const API_PARAMS = ["query", "collections", "topics", "username", "orientation"];
const TTL = 60;                                       // secondes — protège le quota horaire

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

export default {
  async fetch(req, env, ctx) {
    // 0. GET uniquement : l'API Cache refuse de stocker autre chose, et un point
    //    d'accès image n'a de toute façon aucun autre verbe à traiter.
    if (req.method !== "GET")
      return new Response("Method not allowed", { status: 405 });
    const url = new URL(req.url);

    // 1. Seules vos propres pages peuvent l'intégrer — un point d'accès public
    //    de photo aléatoire sur Internet, c'est le quota de quelqu'un d'autre à brûler.
    const ref = req.headers.get("referer");       // absent chez pas mal de clients légitimes
    if (ref && !ALLOWED.includes(host(ref)))
      return new Response("Forbidden", { status: 403 });

    // 2. Cache par combinaison de paramètres, pour qu'une page avec 12 images
    //    coûte un appel API par minute au lieu de douze par rendu.
    const cache = caches.default;
    const hit = await cache.match(req);
    if (hit) return hit;

    // 3. Demander une photo aléatoire à l'API officielle.
    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",
    }});
    // un 403 ici signale en général le quota horaire, pas une mauvaise clé — augmentez le TTL, ne paniquez pas
    if (!r.ok) return new Response("Upstream " + r.status, { status: 502 });
    const photo = await r.json();

    // 4. Reconstruire l'URL de l'image : garder ixid, ajouter les paramètres de taille de l'appelant.
    const img = new URL(photo.urls.raw);          // .raw porte déjà 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,
      // le crédit voyage avec la redirection ; les valeurs d'en-tête doivent être en ASCII, d'où l'encodage
      "X-Photo-Credit": encodeURIComponent(photo.user.name + " on Unsplash"),
      "X-Photo-Link": photo.links.html,
    }});
    ctx.waitUntil(cache.put(req, res.clone()));
    return res;
  },
};

Migrer une URL devient alors une opération de rechercher-remplacer, ce qui est exactement ce qui rend cette option rentable en vingt minutes :

remplacement un-pour-un
- https://source.unsplash.com/collection/67920491/1600x900
+ https://img.example.com/?collections=67920491&w=1600&h=900&fit=crop

Deux remarques de conception, qui ont chacune coûté une mauvaise après-midi à quelqu'un avant d'être consignées. La redirection (302), plutôt que le relais des octets, vous dispense de payer la bande passante et continue de faire compter la vue sur le CDN d'Unsplash, ce que demandent les directives. Et la vérification du Referer est volontairement permissive quand l'en-tête est absent — de nombreux clients légitimes le suppriment — tout en bloquant bien le cas évident où votre point d'accès deviendrait l'API image gratuite de quelqu'un d'autre.

Les règles que vous héritez dès que vous utilisez l'API

Source n'avait pas de règles parce qu'il n'y avait pas de compte. L'API en a cinq qui changent la façon dont vous architecturez la chose, toutes tirées de la documentation actuelle :

  • Les limites de débit sont par heure, et faibles au départ. 50 requêtes/heure en mode démo ; 1 000/heure une fois votre application approuvée pour la production. Seuls les appels vers api.unsplash.com comptent — les requêtes d'image vers images.unsplash.com ne comptent pas. Lisez X-Ratelimit-Remaining sur chaque réponse.
  • Le hotlinking est obligatoire, pas simplement autorisé. Unsplash exige que les URL d'images renvoyées par l'API soient intégrées directement, afin que les vues de la photo puissent être attribuées au photographe. Copier le fichier vers votre propre CDN est la seule optimisation que vous n'avez pas le droit de faire.
  • Conservez le paramètre ixid. Redimensionner et recadrer l'URL retournée est attendu ; en retirer le paramètre qui identifie votre application ne l'est pas.
  • L'attribution et le suivi des téléchargements font partie du contrat — le photographe et Unsplash sont crédités, et un « téléchargement » est signalé via le point d'accès de téléchargement de la photo lorsqu'un utilisateur récupère le fichier, ce qui est un événement que vous devez déclencher vous-même.
  • Les produits distribués nécessitent un enregistrement dynamique des clients. Si vous livrez un plugin, un thème ou un CMS auto-hébergé, une seule clé partagée constitue à la fois une violation des règles et un point de défaillance unique ; l'API dispose d'un mécanisme d'enregistrement prévu exactement pour ce cas.

C'est le moment d'être honnête sur le périmètre : si vous mettez de toute façon en place une API et une clé, le choix de quelle API image utiliser devient soudain ouvert, et il vaut la peine d'y consacrer cinq minutes avant d'écrire le client. Nous avons comparé les offres gratuites — quotas, règles, comportement de recherche et formes de réponse — dans la comparaison des API photo stock gratuites.

Si vous vouliez seulement un espace réservé, dites-le

Une large part de l'usage de Source n'a jamais eu de rapport avec Unsplash. Il s'agissait de « mettre quelque chose qui ressemble à une image ici pendant que je construis la mise en page ». Pour cela, des services sans clé existent toujours et sont la bonne réponse :

ServiceClé ?Ce que vous obtenezOù ça s'arrête
Lorem Picsum Non De vraies photographies : picsum.photos/800/600, une fixe avec /id/237/… ou /seed/xxx/…, plus ?grayscale et ?blur=1..10. Son point d'accès /v2/list crédite la page Unsplash et l'auteur de chaque photo. Aucun ciblage de sujet. La photo n'aura aucun rapport avec votre page.
placehold.co Non Des rectangles étiquetés, à n'importe quelle taille — un vrai remplisseur de maquette, honnête. C'est une boîte grise, et elle ressemble à une boîte grise sur une capture d'écran partagée avec un client.
Openverse Non Un catalogue sous licence ouverte avec une API publique, géré par WordPress.org. Correspondance par mots-clés, et les licences varient selon l'élément — il faut les lire.

La distinction qui compte : un espace réservé est temporaire par définition. Si l'image survit jusqu'en production, ce n'est plus un espace réservé — c'est une illustration que personne n'a choisie, et le lecteur le remarque.

Une seule clé au lieu de trois

Voici la partie de la migration que personne ne planifie : ceux qui quittent Source atterrissent rarement sur une seule API. Une page a besoin d'une bannière, de deux images de section et de quelque chose pour une grille de cartes, et la réponse honnête est généralement Unsplash plus Pexels plus Pixabay — trois inscriptions, trois schémas d'authentification, trois formes de JSON, trois modèles de pagination et trois jeux de règles d'attribution, le tout pour remplir les mêmes balises <img>. Ce travail d'intégration est la véritable facture de la disparition d'une URL sans clé, et elle arrive des semaines après la panne qui l'a causée.

Ramener tout cela à une seule intégration, c'est exactement pourquoi nous avons construit Pexafy : une seule clé sur 9 bibliothèques à licence gratuite dans un schéma unique, avec une recherche sémantique au niveau de la phrase — de sorte qu'une description complète comme « un écran de téléphone fissuré sur un bureau en bois, vu du dessus » retourne des résultats classés au lieu de rien. Deux limites, énoncées clairement, car tout cet article porte sur le fait de ne pas être surpris deux fois : cela nécessite une clé, donc cela ne restaure pas ce qu'était Source — cela appartient à la même catégorie que l'API Unsplash officielle mentionnée plus haut ; et cela couvre de la photographie sous licence gratuite, pas de l'imagerie éditoriale ou de marque.

L'élément véritablement nouveau vise les assistants évoqués plus haut : un serveur MCP à mcp.pexafy.com/mcp permet à un modèle qui réciterait autrement une URL d'image de mémoire de chercher dans un vrai catalogue et de retourner une photo qui existe réellement, avec sa mention de crédit attachée. C'est une meilleure réponse aux URL mortes écrites par des machines que n'importe quelle règle de lint, et le raisonnement est détaillé dans l'infrastructure de recherche d'images pour les agents IA.

Auditez votre parc en dix minutes

Quelle que soit votre destination de migration, faites d'abord cette partie — on ne peut pas corriger des URL qu'on n'a pas trouvées. Source n'est que l'exemple du jour ; les trois mêmes étapes s'appliquent à toute ressource externe que vous intégrez.

trouver chaque référence morte, puis les tenir à l'écart
# 1. Tout dans le dépôt, y compris la documentation, les tests, les fixtures et les README.
grep -rn --binary-files=without-match \
  -e "source.unsplash.com" -e "via.placeholder.com" -e "/placeholder.png" .

# 2. Tout ce que contient la base de données — c'est là que ces liens se cachent le plus longtemps, dans le corps des CMS.
psql -c "SELECT id FROM posts WHERE body LIKE '%source.unsplash.com%'"

# 3. Tout ce que le site construit demande réellement : parcourez-le et listez les échecs.
#    Faites correspondre l'attribut, pas une extension de fichier — les URL d'images se terminent rarement en .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   ← ce que vous cherchez

Décidez ensuite, une bonne fois pour toutes, du coût que les images externes ont le droit de vous imposer. Quatre règles qui survivent à la prochaine fermeture de service, quel qu'en soit le responsable :

  1. Récupérez l'image au moment du build, pas au moment de la requête. Une image résolue pendant le build échoue en intégration continue, devant un développeur, plutôt qu'à 3 h du matin devant un utilisateur.
  2. Ne laissez jamais une ressource décorative bloquer un chemin critique. Ne préchargez rien d'externe au-dessus d'un formulaire de connexion ; donnez à chaque <img> tiers un repli onerror et des width/height explicites, afin qu'un échec coûte une boîte vide, pas un décalage de mise en page ni un script bloqué.
  3. Ajoutez la vérification à l'intégration continue. L'étape 3 ci-dessus, exécutée sur votre résultat de build, transforme « quelqu'un a fini par remarquer » en un build en échec. C'est la seule étape qui empêche la récidive.
  4. Budgétisez vos dépendances externes comme n'importe quelle autre. Consignez de quels hôtes vos pages ont le droit de dépendre et ce qui se passe quand chacun est hors service. Une URL pour laquelle vous n'avez pas eu besoin de vous inscrire reste une dépendance — Source a prouvé que c'est simplement une dépendance que personne ne possède.

Références & notes de bas de page

1 Chaque code de statut, chaque décompte et chaque ligne citée dans cet article ont été tirés de leur source primaire le 30 août 2026. Les codes de statut HTTP ont été obtenus avec curl contre chaque motif d'URL ; tous ont renvoyé 503 avec server: Heroku et un corps intégrant herokucdn.com/error-pages/application-error.html. La résolution DNS a été confirmée le même jour (un CNAME vers un hôte herokudns.com).

2 Les deux entrées de changelog sont citées mot pour mot depuis unsplash.com/documentation/changelog. Le libellé de la politique de dépréciation (3 semaines de préavis, en-tête Warning, et l'exemption pour les points d'accès non publiquement documentés) provient de unsplash.com/documentation, le même jour.

3 L'échec TLS a été reproduit avec openssl s_client -connect changelog.unsplash.com:443 (tlsv1 alert internal error). La chaîne de redirection a été suivie avec curl -L. L'écart d'archive provient de l'API CDX de la Wayback : dernière capture 200 de changelog.unsplash.com le 20240324, première capture de unsplash.com/documentation/changelog le 20240823.

4 Titres de tickets, dates de création et de clôture lus depuis l'API REST de GitHub (mui/material-ui#42736, nextcloud/unsplash#115, sindresorhus/Actions#248) et depuis l'API JSON de drupal.org pour le ticket gin_login 3324054, dont le corps signale « I get always a Heroku application error » en novembre 2022.

5 Décomptes : API de recherche de code GitHub (3 344 fichiers ; un échantillon de 100 résultats dédupliqué en 77 dépôts, dont 12 ont été créés après le 11 juin 2024 et 20 avaient reçu un push dans les 12 mois précédents) ; API de téléchargements du registre npm (unsplash-source-es6, 23 téléchargements sur les 30 jours précédents) ; /search/excerpts de Stack Exchange (1 459 publications). La recherche de code ne couvre que les dépôts publics indexés, donc chaque chiffre constitue un plancher.

Foire aux questions

source.unsplash.com est-il en panne, ou a-t-il été arrêté définitivement ?
Définitivement. Unsplash a annoncé l'arrêt le 11 juin 2024 — « nous allons d'abord réduire le service en désactivant la fonction de recherche, puis dans les semaines à venir désactiver complètement l'application » — après avoir déprécié le service le 25 novembre 2021. Chaque motif (/random, /1600x900/?query, /collection/…, /daily) renvoie désormais un HTTP 503 avec la page générique Application Error de Heroku. Le nom d'hôte se résout toujours, donc l'échec apparaît comme une image cassée plutôt qu'une erreur réseau.
Quel est le remplacement direct de source.unsplash.com/random ?
Il n'existe pas de solution de remplacement sans clé, et c'est un point qu'il vaut mieux accepter tout de suite. Trois solutions existent. Une URL CDN fixe — images.unsplash.com/photo-…?w=1600&h=900&fit=crop — ne nécessite aucune clé mais renvoie toujours la même photo, ce qui correspond en réalité à la plupart des usages décoratifs. L'API officielle, GET https://api.unsplash.com/photos/random avec un en-tête Authorization: Client-ID, restaure l'aléatoire mais doit être appelée côté serveur. Un petit proxy que vous hébergez vous-même devant ce endpoint est la seule option qui redonne une URL sans clé, utilisable directement dans une balise <img>.
Pourquoi les outils de codage IA génèrent-ils encore des URL source.unsplash.com en 2026 ?
Parce que cette URL a été documentée, enseignée et copiée pendant environ huit ans, et un corpus d'entraînement n'expire pas quand un service disparaît. Mesuré le 30 août 2026 : la recherche de code GitHub renvoie encore 3 344 fichiers la contenant, et sur un échantillon de 100 fichiers, 12 des 77 dépôts avaient été créés après l'arrêt du service. Les bibliothèques de prompts écrites par des humains répètent aussi l'instruction — un prompt GPT largement copié demande encore au modèle d'« utiliser l'API unsplash ( https://source.unsplash.com/1280x720/?… ) ». Considérez toute URL d'image générée par un modèle comme non vérifiée et contrôlez les codes de statut en CI.
Puis-je encore obtenir une photo Unsplash aléatoire sans clé API ?
Pas directement depuis Unsplash — la sélection aléatoire se trouve désormais derrière /photos/random, qui nécessite un Client-ID. Vos deux routes sans clé sont un proxy que vous hébergez vous-même, où la clé reste côté serveur et l'URL publique ressemble à l'ancienne, ou un service tiers de type placeholder comme Lorem Picsum (picsum.photos/800/600), qui sert de vraies photographies sans clé mais sans aucun ciblage de sujet.
L'API Unsplash permet-elle de télécharger et d'auto-héberger les images ?
Non. Contrairement à la plupart des API, Unsplash impose le hotlinking : les URL d'images renvoyées par l'API doivent être intégrées directement, afin que les vues de photo soient comptabilisées pour le photographe. Trois obligations en découlent — conserver le paramètre ixid lors du redimensionnement ou du recadrage de l'URL, créditer le photographe et Unsplash, et déclencher le endpoint de téléchargement de la photo lorsqu'un utilisateur récupère le fichier. Copier les fichiers sur votre propre CDN est la seule optimisation que vous n'êtes pas libre de faire.
Pourquoi l'avis de dépréciation de 2021 n'a-t-il pas protégé les utilisateurs existants ?
Parce que l'avis et la politique ne couvraient pas la même chose. L'entrée du changelog du 25 novembre 2021 promettait que « les usages existants continueront de fonctionner » sans donner de date de fin. La politique de dépréciation publiée par Unsplash — au moins trois semaines de préavis plus un en-tête Warning — s'applique aux champs et endpoints documentés publiquement, et ce même paragraphe précise que tout ce qui n'est pas documenté peut changer sans avertissement. Source n'a jamais été un endpoint documenté de l'API, il se trouvait donc hors du champ de la politique censée le protéger.
Comment retrouver toutes les URL source.unsplash.com mortes dans mon projet ?
Trois passages, dix minutes. Grepez le dépôt, y compris la documentation, les tests, les fixtures et les README — grep -rn "source.unsplash.com" . — car ces URL survivent le plus longtemps dans le code d'exemple. Interrogez la base de données, car c'est dans le corps des articles du CMS qu'elles se cachent (WHERE body LIKE '%source.unsplash.com%'). Puis parcourez votre build final : extrayez chaque URL d'image et interrogez-la, en listant tout ce qui ne renvoie pas 200. Ajoutez ce dernier passage à votre CI, pour qu'un asset tiers mort fasse échouer un build plutôt qu'une page.

Ne cherchez plus de mots-clés. Décrivez ce que vous avez en tête.

Recherchez parmi 9M+ images libres de droits par le sens — dans n'importe quelle langue, en moins de 100 ms.