source.unsplash.com 已彻底失效:事后剖析与所有替代方案
2021年宣布弃用时承诺“现有用途将继续可用”,2024年6月被彻底关闭,如今仍在被写入新代码。这篇冷静的事后分析——以及三种替代方案,包括一个能找回无需密钥随机性的代理方案。
一家图库服务下线了一个子域名,两年后,它仍在破坏文档站点、登录页面、课程练习,以及刚刚生成出来的代码。这是一篇关于某个 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。三项实测结果:
- HTTPS 端点已经损坏。
openssl s_client -connect changelog.unsplash.com:443返回tlsv1 alert internal error—— 握手在任何证书被出示之前就已失败。因此,所有 2021 年那个使用https://的链接,在浏览器中都是失效的。 - 通过普通 HTTP 会跳转,但跳转结果并无用处。 跟随
http://changelog.unsplash.com/deprecations/2021/11/25/source-deprecation.html, 经过两次跳转后,最终落在unsplash.com/@documentation/changelog/deprecations/2021/11/25/source-deprecation/html的一个400错误上 —— 这个路径被网站的用户名路由给吞掉了。 - 存档记录中,恰好在下线发生的时间段留下了一个空洞。 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,页面就不再依赖任何随机机制:
<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 也是如此。
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。
// 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 ← 这才是你要找的目标
然后,一次性决定好:外部图片可以让你付出多大的代价。以下四条规则,无论下一次是谁引发下线事故,都能扛得住:
- 在构建时获取,而不是在请求时获取。 一张在构建过程中就被解析的图片,会在 CI 中、在开发者面前失败,而不是在凌晨 3 点、在用户面前失败。
- 绝不让一个装饰性资源阻塞关键路径。 不要在登录表单上方预加载任何外部资源;为每一个第三方
<img>都设置onerror回退方案,以及明确的width/height,这样一次失败付出的代价只是一个空白方框,而不是布局抖动或脚本卡死。 - 把这项检查加入 CI。 上面第 3 步,运行在你的构建产物上,能把“最终有人发现了问题”变成一次红色的构建失败。这是唯一能防止问题重演的一步。
- 像对待其他依赖一样,为外部依赖设定预算。 写清楚你的页面允许依赖哪些主机,以及每一个主机宕机时会发生什么。一个你无需注册就能使用的 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
条帖子)。代码搜索仅覆盖已被索引的公开仓库,因此每一个数字都只是下限。
一手来源: Unsplash API changelog · Unsplash API 文档 · Unsplash 署名规范 · Unsplash 状态页 · MUI #42736 · Nextcloud #115 · sindresorhus/Actions #248 · Drupal Gin Login #3324054 · Lorem Picsum · Openverse · Pexafy API 与 MCP 文档。
常见问题
source.unsplash.com 是暂时故障,还是已经永久关闭?
/random、/1600x900/?query、/collection/…、/daily)现在都会返回 HTTP 503,并显示 Heroku 通用的 Application Error(应用程序错误)页面。该域名仍可解析,因此故障表现为图片加载失败,而非网络错误。source.unsplash.com/random 的直接替代方案是什么?
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 网址?
我还能在不使用 API 密钥的情况下获取随机的 Unsplash 照片吗?
/photos/random 之后,该端点需要 Client-ID。你有两条无需密钥的途径:一是自行搭建代理,密钥保留在服务器端,公开网址看起来与旧版类似;二是使用第三方占位图服务,例如 Lorem Picsum(picsum.photos/800/600),它提供真实照片且无需密钥,但完全无法指定主题。Unsplash API 允许下载图片并自行托管吗?
ixid 参数、为摄影师和 Unsplash 署名,并在用户下载文件时触发该照片的下载端点。将文件镜像到你自己的 CDN,是唯一你无权自行做的优化。为什么 2021 年的弃用通知没能保护现有用户?
Warning 请求头——仅适用于公开记录在案的字段和端点,而同一段落也明确指出,任何未被记录的内容都可能在没有预警的情况下发生变更。Source 从未被列为 API 的正式文档化端点,因此它不在本应保护它的政策范围之内。我该如何找出项目中所有已失效的 source.unsplash.com 网址?
grep -rn "source.unsplash.com" .——因为这类网址在示例代码中存活时间最长。查询数据库,因为它们常隐藏在 CMS 文章正文中(WHERE body LIKE '%source.unsplash.com%')。然后爬取你的构建输出:提取所有图片网址并逐一请求,列出所有非 200 状态码的结果。将这最后一步加入 CI 流程,这样失效的第三方资源就会导致构建失败,而不是让页面本身出问题。