source.unsplash.com 已彻底失效:事后剖析与所有替代方案

2021年宣布弃用时承诺“现有用途将继续可用”,2024年6月被彻底关闭,如今仍在被写入新代码。这篇冷静的事后分析——以及三种替代方案,包括一个能找回无需密钥随机性的代理方案。

一张黑白照片,一部屏幕破裂的智能手机放在浅色木质表面上,损坏的屏幕上显示着 Unsplash 的标志。
图片来自 Unsplash

一家图库服务下线了一个子域名,两年后,它仍在破坏文档站点、登录页面、课程练习,以及刚刚生成出来的代码。这是一篇关于某个 URL 的事后剖析 —— 它曾经做了什么、是什么让它停摆、以及该用什么来替代它 —— 并冷静审视这个故事中更奇特的一面:写我们代码的机器,根本没有注意到它已经消失了。

文中的 HTTP 响应、逐字引用的两条 changelog 记录、API 限制、那些出问题项目的 issue 追踪记录,以及来自 GitHub、npm 和 Stack Overflow 的统计数字,全部来自一手资料,每一处的核实方法都写在文末注释中。如果你只想直接看解决方案,请跳到迁移对照表。

如果你现在仍在请求它,会得到什么

一条命令,无需密钥,任何机器都能复现:

终端
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
# 响应体:一个指向 herokucdn.com/error-pages/application-error.html 的 iframe

这里的问题都不是 DNS 解析失败。source.unsplash.com 仍然可以解析 —— 它是一个 CNAME,指向某个 herokudns.com 主机 —— 所以请求会得到响应,只是响应者不是应用程序本身。这个细节比听起来更重要:一个浏览器如果很快收到一个带有 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一张随机的精选照片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}" 逐一核实。搜索功能是按公告先行下线的;而如今整个应用都已关闭,这个区别已经不再存在。

从“已弃用”到“下线”,用了三年

两条公告如今仍可在同一处读到: unsplash.com/documentation/changelog。 逐字引用,因为这些措辞本身就是整个故事:

2021 年 11 月 25 日 ——“Unsplash Source being deprecated”(Unsplash Source 已被弃用)
“Unsplash Source 正在被弃用。现有用法将继续正常运作,但新项目请使用完整的 Unsplash API。”

2024 年 6 月 11 日 ——“Unsplash Source sunset”(Unsplash Source 下线)
“自 2021 年被弃用以来,Unsplash Source 就已正式不再获得支持。作为最终下线的一部分,我们将首先关闭搜索功能,并在未来几周内彻底关闭该应用。现有使用 Source 的项目 —— 尤其是生产级项目 —— 应尽快迁移到完整的 Unsplash API。”

按顺序阅读,失败的原因就很明显了。2021 年的通知包含一个承诺(现有用法将继续正常运作),却没有给出具体日期。一位在 2021 年读到它的开发者,完全有理由不去动那些能正常运作的代码;而一位在 2022 年才加入项目的开发者,则根本没有读过这则通知。2024 年的通知给出了“未来几周”这个期限,但那已是三年之后,而且发布在一个没人收藏过的页面上。

Unsplash 确实发布了一份弃用政策,而且是一份合理的政策 —— 文档中写明,对于公开文档中记载的字段和接口,任何变更都会在 changelog 中提前至少 3 周公告,并且在弃用期间,相关接口会返回一个 Warning 响应头。而同一段落里的另一句话,恰恰解释了为什么这些保护都没能覆盖到 Source:“对于任何未被公开记载的字段或接口,我们可能不经任何警告就进行变更。” Source 从来都不是那份文档化 API 中的一个接口。它一直处在本应保护它的那份政策之外。

  • 这里的实际教训并不是“Unsplash 太粗心”。 而是:一个你无需阅读任何文档就能使用的 URL,同样意味着你也从未读过它的弃用政策。
  • 故障早于公告出现。 一个于 2022 年 11 月 28 日提交的 Drupal issue 就已经报告“我总是遇到 Heroku 应用程序错误”,比下线公告早了十八个月。这类服务往往就是这样死去的:先是缓慢地出问题,然后才有一则你从未看到过的公告。

这则公告比故障本身还难找到

2021 年的弃用公告发布在 changelog.unsplash.com 上,而这也是当时所有相关 bug 报告所链接到的 URL —— 包括上文提到的那个 Drupal issue。三项实测结果:

  1. HTTPS 端点已经损坏。 openssl s_client -connect changelog.unsplash.com:443 返回 tlsv1 alert internal error —— 握手在任何证书被出示之前就已失败。因此,所有 2021 年那个使用 https:// 的链接,在浏览器中都是失效的。
  2. 通过普通 HTTP 会跳转,但跳转结果并无用处。 跟随 http://changelog.unsplash.com/deprecations/2021/11/25/source-deprecation.html, 经过两次跳转后,最终落在 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 日 —— 正好落在这五个月的空窗期内。

这一切都算不上什么阴谋,只是一次普通的 CMS 迁移。但由此造成的后果是真实的,这也是本文要逐字引用两条公告全文的原因:一条弃用公告的原始记录本应比它所弃用的东西存在得更久,而在这里,它几乎没能做到。

究竟破坏了什么

受影响的不是什么业余小项目。以下故障都记录在公开的 issue 追踪系统中;标题、日期和状态均来自 GitHub 和 drupal.org 的 API。

项目 Issue 提出时间 内容摘要
MUI (Material UI) #42736 2024 年 6 月 24 日 “[docs] Random Unsplash photo URL is no longer functional”(随机 Unsplash 图片 URL 已失效)—— 官方的 Sign-in side 模板发布时带有一张失效的图片。三天后关闭。
Nextcloud #115 2023 年 1 月 17 日 “Migrate to Unsplash API”(迁移到 Unsplash API)—— 该背景图应用是基于 Source URI 构建的。开放了十八个月,于 2024 年 7 月 16 日关闭。
sindresorhus/Actions #248 2024 年 5 月 28 日 “Get Unsplash Image: 503 Error”—— 一个 iOS/macOS 快捷指令操作,在下线公告发布两周前就已经出问题。
Drupal — Gin Login #3324054 2022 年 11 月 28 日 “Unsplash has deprecated source.unsplash.com —— 这会延迟 reCAPTCHA 的加载,导致用户无法登录。”

再读一遍最后一行,因为它才是最值得记在心里的一条。登录表单旁边的一张装饰性图片 —— 页面上最明显无关紧要的资源 —— 却退化成了一次身份验证故障,因为一个缓慢的第三方请求挡在了登录表单所需的 CAPTCHA 前面。没有人是故意这样设计的。它是浏览器加载资源的先后顺序自然导致的结果。

你的编程助手没收到通知

这才是让一次 2024 年的故障演变成 2026 年问题的关键所在。source.unsplash.com 被写进文档、写进博客、写进课程教材、被人反复复制,前后大约持续了八年。所有这些文本,都存在于如今为我们编写起始代码的那些模型的训练数据里 —— 而文本是不会过期的。三组统计数字:

统计项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 个月内仍有提交记录的仓库数 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 总数

最直接的证据根本不在应用代码里 —— 而在提示词(prompt)里。该搜索的第一个结果,是一个包含 GPT 系统提示词的库,其中写着这样一句话:“please use unsplash API( https://source.unsplash.com/1280x720/?<PUT YOUR QUERY HERE>”(请使用 unsplash API)。这条指令至今仍在被复制进新的助手系统中。模型不会去验证这个 URL;它被告知要使用它,而它见过的每一个例子都与此一致。

所以,生成代码中的这类故障有两个独立的成因,修复其中一个并不能修复另一个:陈旧的训练数据,以及人类在此基础上写下的陈旧指令。无论哪种情况,症状都属于同一类——那些永远无法加载的图片:

值得排查的失效图片模式
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                      # 一个从未被添加到仓库中的文件

关于列表第二行的一个补充说明:在撰写本文时,via.placeholder.com 在我们的测试网络下同样无法完成 TLS 握手,通过普通 HTTP 请求则返回了 403。在信任它之前,请先从你自己的网络环境中核实一下 —— 这些工具所依赖的后备方案,可能自己也有一段故障史。

在这份清单中,只有第一项是真正损坏的。其余几项以更隐蔽的方式变得更糟:它们能加载出来,页面看起来像是完成了,却没人注意到这个页面配的图其实与内容毫无关联。而这一切也并非图片所独有 —— 它是这类问题的普遍形态。模型对互联网的认知是一份快照,而接口、命令行参数、包名和免费额度,会在快门按下之后继续变化。

迁移对照表

一共只有三个可选目的地,而诚实地呈现它们的方式,就是说清楚你放弃了什么。先选定一列,再阅读对应的每一行。

旧版 Source URL A. 固定 CDN URL无需密钥 · 无随机性 B. Unsplash API需密钥 · 服务端调用 C. 自建代理密钥隐藏 · 找回随机性
/random images.unsplash.com/photo-… —— 你自己选定的一张照片 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 固定一张照片,在你的构建流程中自行轮换 无对应方案 —— 需自行将一张随机照片缓存 24 小时 同上,但缓存放在代理中处理

选项 A 才是大多数人真正想要的。 如果这张图片本来就是装饰性的 —— 一张首屏大图、登录页侧边的配图、卡片背景图 —— 你原本就不需要每次请求都换一张不同的照片。选定一张,保留其 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 相当于公开一个凭据。有两条容易踩坑的规则值得注意: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

如果你真正失去的是那种行为本身 —— 一个无需密钥、每次返回不同照片、可以直接用在 <img> 标签、CMS 字段,或没有服务端的静态站点中的 URL —— 那你就必须自己运行这样一个接口。它只是 API 前面一个小小的 worker,而让它在生产环境中真正扛得住的三件事,是缓存、Referer 检查,以及把其余查询字符串原样透传给 CDN。

worker.js —— 基于 /photos/random 构建的、无需密钥的 Source 形态接口
// Cloudflare Workers。在别处(Deno Deploy、Val Town……)结构是一样的,
// 但需要用 caches.open() 打开一个命名缓存,而不是 caches.default。
// 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 张图,
    //    每分钟只消耗一次 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 就变成了简单的查找替换 —— 而这正是这个方案值得花二十分钟去做的原因:

一对一替换
- https://source.unsplash.com/collection/67920491/1600x900
+ https://img.example.com/?collections=67920491&w=1600&h=900&fit=crop

两点设计说明,都是有人先吃了一次苦头之后才被写下来的。用重定向(302)而不是代理转发字节数据,让你不用为带宽负责,同时也让浏览量仍能被计入 Unsplash 的 CDN 统计中,这正是使用指南所要求的做法。而 Referer 检查在该请求头缺失时特意采取宽松策略 —— 因为不少合法客户端会剥离这个头 —— 但同时仍然拦住了那种明显的滥用情形,即你的接口沦为别人免费的图片 API。

一旦使用 API,你就要继承这些规则

Source 没有规则,因为它没有账号体系。而 API 有五条规则,会改变你架构这套系统的方式,全部来自当前的官方文档:

  • 速率限制是按小时计算的,而且一开始很小。 演示模式下为每小时 50 次请求;应用被批准进入生产环境后为每小时 1,000 次。只有对 api.unsplash.com 的调用才会计入限额 —— 对 images.unsplash.com 的图片请求不计入。请在每个响应中读取 X-Ratelimit-Remaining。
  • 热链(hotlinking)是强制要求,而不仅仅是被允许。 Unsplash 要求 API 返回的图片 URL 必须被直接嵌入使用,这样照片的浏览量才能归属到摄影师名下。把文件镜像到你自己的 CDN 上,是你唯一不被允许做的一项优化。
  • 保留 ixid 参数。 对返回的 URL 进行尺寸调整和裁剪是被允许的;但去掉这个用于标识你应用身份的参数则不被允许。
  • 署名和下载追踪是协议的一部分 —— 摄影师和 Unsplash 都需要被署名,而当用户获取该文件时,需要通过该照片的下载接口上报一次“下载”事件,这个事件需要你自己触发。
  • 分发型产品需要动态客户端注册。 如果你发布的是一个插件、一个主题,或一个自托管的 CMS,使用一个共享密钥既违反政策,也是一个单点故障;针对这种情形,API 提供了专门的注册流程。

这正是应该诚实面对适用范围的时刻:如果你反正都要接入一个 API 和一个密钥,那么“选哪家图片 API”这个问题就重新变得开放起来,值得在写客户端代码之前花五分钟考虑清楚。我们在免费图库 API 对比一文中,比较了各家免费方案 —— 配额、规则、搜索行为和响应结构。

如果你只是想要一个占位图,那就直说

Source 的用量中,有很大一部分本来就与 Unsplash 无关。它只是被用来“先放点像图片的东西,方便我搭建布局”。对于这种需求,无需密钥的服务依然存在,也依然是正确答案:

服务需要密钥?能得到什么局限在哪里
Lorem Picsum 否 真实照片:picsum.photos/800/600,也可用 /id/237/… 或 /seed/xxx/… 固定住某一张,另外支持 ?grayscale 和 ?blur=1..10。其 /v2/list 接口会标注每张照片在 Unsplash 上的页面和作者信息。 完全没有主题定向功能。照片内容与你的页面毫无关联。
placehold.co 否 任意尺寸的带标签矩形 —— 诚实的线框稿占位图。 它就是一个灰色方块,在展示给客户的截图里看起来也确实就是个灰色方块。
Openverse 否 一个开放许可的图片目录,附带公开 API,由 WordPress.org 运营。 基于关键词匹配,而且每张图片的许可协议各不相同 —— 你必须逐一阅读。

这里的关键区别在于:占位图,从定义上讲就应该是临时的。如果这张图片一路留存到了生产环境,它就不再是占位图了 —— 而是一张没人真正挑选过的插图,读者是能看出来的。

用一个密钥代替三个

这是迁移过程中最少有人提前规划的一部分:离开 Source 的人,很少只会迁移到一个 API。一个页面可能需要一张首屏大图、两张章节配图,以及卡片网格里的若干张图,而诚实的答案往往是 Unsplash 加上 Pexels 加上 Pixabay —— 三次注册、三套鉴权方案、三种 JSON 结构、三种分页模型、三套署名规则,全部只是为了填满同样的一批 <img> 标签。这份集成工作,才是一个无需密钥的 URL 消失之后真正需要偿还的账单,而它往往在故障发生数周之后才会浮现。

我们打造 Pexafy,正是为了把这一切合并成一次集成:一个密钥,覆盖 9 个免费授权的图库,纳入统一的数据结构,并支持句子级别的语义搜索 —— 因此,一段完整的描述,比如“一部裂屏手机放在木桌上,俯拍视角”,能返回按相关性排序的结果,而不是一无所获。这里有两点限制,我们要说得明明白白,因为整篇文章讲的就是不要再被同一个坑绊倒两次:它需要密钥,所以不能还原 Source 曾有的那种形态 —— 它与上文提到的官方 Unsplash API 属于同一类;而且它承载的是免费授权的图库摄影作品,不是编辑类或品牌类图像。

真正称得上新颖的部分,是针对上文提到的那些助手而设计的:一个位于 mcp.pexafy.com/mcp 的 MCP 服务器,意味着一个原本只会凭记忆背出某个图片 URL 的模型,现在可以搜索一个真实存在的图库,返回一张确实存在、并附带署名信息的照片。相比任何 lint 规则,这才是对“机器写出失效 URL”这个问题更好的回应,其中的思路详见面向 AI 智能体的图片搜索基础设施一文。

用十分钟排查你自己的资产

无论你迁移到哪种方案,请先做这一步 —— 你无法修复自己都没找到的 URL。Source 只是今天的例子;同样的三个步骤适用于你嵌入的每一个外部资源。

找出每一处失效引用,然后阻止它们再次出现
# 1. 仓库中的一切,包括文档、测试、fixture 和 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   ← 这才是你要找的目标

然后,一次性决定好:外部图片可以让你付出多大的代价。以下四条规则,无论下一次是谁引发下线事故,都能扛得住:

  1. 在构建时获取,而不是在请求时获取。 一张在构建过程中就被解析的图片,会在 CI 中、在开发者面前失败,而不是在凌晨 3 点、在用户面前失败。
  2. 绝不让一个装饰性资源阻塞关键路径。 不要在登录表单上方预加载任何外部资源;为每一个第三方 <img> 都设置 onerror 回退方案,以及明确的 width/height,这样一次失败付出的代价只是一个空白方框,而不是布局抖动或脚本卡死。
  3. 把这项检查加入 CI。 上面第 3 步,运行在你的构建产物上,能把“最终有人发现了问题”变成一次红色的构建失败。这是唯一能防止问题重演的一步。
  4. 像对待其他依赖一样,为外部依赖设定预算。 写清楚你的页面允许依赖哪些主机,以及每一个主机宕机时会发生什么。一个你无需注册就能使用的 URL,依然是一种依赖 —— Source 证明的仅仅是:它是一种没有人真正拥有的依赖。

参考资料与注释

1 本文中的每一个状态码、每一个统计数字和每一句引用,都是于 2026 年 8 月 30 日从其一手来源核实获取的。HTTP 状态码通过对每种 URL 模式运行 curl 获得;全部返回 503,附带 server: Heroku 响应头, 响应体中嵌入了 herokucdn.com/error-pages/application-error.html。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 Issue 的标题、创建和关闭时间读取自 GitHub REST API(mui/material-ui#42736、nextcloud/unsplash#115、 sindresorhus/Actions#248),以及 drupal.org 的 JSON API,针对 gin_login 的 3324054 号 issue,其内容在 2022 年 11 月报告“我总是遇到 Heroku 应用程序错误”。

5 统计数字来源:GitHub 代码搜索 API(3,344 个文件;从 100 条结果中去重后得到 77 个仓库,其中 12 个创建于 2024 年 6 月 11 日之后, 20 个在此前 12 个月内仍有提交记录);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 的直接替代方案是什么?
并不存在无需密钥的直接替代品,这一点最好尽早接受。目前有三种替代方案。固定的 CDN 网址——images.unsplash.com/photo-…?w=1600&h=900&fit=crop——无需密钥,但始终返回同一张照片,而这其实正是大多数装饰性用途真正需要的效果。官方 API,即带有 Authorization: Client-ID 请求头的 GET https://api.unsplash.com/photos/random,可以恢复随机性,但必须在服务器端调用。自建的小型代理置于该端点之前,是唯一能让你重新获得可直接放入 <img> 标签、无需密钥的网址的方案。
为什么 AI 编程工具在 2026 年仍会生成 source.unsplash.com 网址?
因为这个网址被记录、教授和复制了大约八年之久,而训练语料库并不会随着服务下线而自动过期。截至2026年8月30日的统计:GitHub 代码搜索仍能返回 3,344 个包含该网址的文件,在一个100个文件的样本中,77个仓库里有12个是在服务关闭之后才创建的。人工编写的提示词库同样在重复这一指令——一个被广泛复制的 GPT 提示词至今仍要求模型“使用 unsplash API( https://source.unsplash.com/1280x720/?… )”。任何由模型生成的图片网址都应视为未经验证,并在 CI 中检查其状态码。
我还能在不使用 API 密钥的情况下获取随机的 Unsplash 照片吗?
无法直接从 Unsplash 获取——随机选择功能现在位于 /photos/random 之后,该端点需要 Client-ID。你有两条无需密钥的途径:一是自行搭建代理,密钥保留在服务器端,公开网址看起来与旧版类似;二是使用第三方占位图服务,例如 Lorem Picsum(picsum.photos/800/600),它提供真实照片且无需密钥,但完全无法指定主题。
Unsplash API 允许下载图片并自行托管吗?
不允许。与大多数 API 不同,Unsplash 要求使用外链(hotlinking):API 返回的图片网址必须直接嵌入使用,这样摄影师的照片浏览量才能被计入。这附带三项义务——在调整网址尺寸或裁剪时保留 ixid 参数、为摄影师和 Unsplash 署名,并在用户下载文件时触发该照片的下载端点。将文件镜像到你自己的 CDN,是唯一你无权自行做的优化。
为什么 2021 年的弃用通知没能保护现有用户?
因为通知内容与实际政策所涵盖的范围并不一致。2021年11月25日的更新日志条目承诺“现有用途将继续可用”,且未给出终止日期。Unsplash 公布的弃用政策——至少提前三周通知,并附带 Warning 请求头——仅适用于公开记录在案的字段和端点,而同一段落也明确指出,任何未被记录的内容都可能在没有预警的情况下发生变更。Source 从未被列为 API 的正式文档化端点,因此它不在本应保护它的政策范围之内。
我该如何找出项目中所有已失效的 source.unsplash.com 网址?
只需三步,十分钟即可完成。搜索代码仓库,包括文档、测试、fixtures 和 README 文件——运行 grep -rn "source.unsplash.com" .——因为这类网址在示例代码中存活时间最长。查询数据库,因为它们常隐藏在 CMS 文章正文中(WHERE body LIKE '%source.unsplash.com%')。然后爬取你的构建输出:提取所有图片网址并逐一请求,列出所有非 200 状态码的结果。将这最后一步加入 CI 流程,这样失效的第三方资源就会导致构建失败,而不是让页面本身出问题。

不必再苦苦寻找关键词。描述你想要的内容即可。

按含义搜索 9M+ 张免费图片 — 支持任何语言,响应不到 100 毫秒。