同一篇稿子发到 6 个平台,为什么只有这里的图片全裂了?
一稿多发最尴尬的瞬间:文章发出去 10 分钟,读者在评论区问「你的图呢?」
排查之后你会发现,问题往往不在你的 Markdown,而在图床和 Referer 校验。
本文把「多平台发文时图片为什么会裂」这件事讲清楚,并给出一套可以落地的处理方案。
现象:同一份源文件,三种结果
把同一份 Markdown 分发到不同平台,图片的表现通常分三类:
| 表现 | 典型原因 |
|---|---|
| 图片正常 | 平台站外图片不做来源校验,或自己的图床可以被外链 |
| 图片自动转存后正常 | 平台抓取外链图,上传到自己的图床后再替换链接 |
| 图片全裂 | 源图床对 Referer 做了校验,站外请求一律 403 |
第三类最容易被误判成「Markdown 写错了」。实际上链接是完全正确的,只是服务端拒绝了这次请求。
根因:Referer 校验
很多图床(尤其是云厂商对象存储的默认域名)会做防盗链:
http
GET /direct/xxxx.png
Referer: 掘金站内地址
→ 403 Forbidden
而同一个地址,如果 Referer 是它自己的域名,就返回 200。
也就是说:图片能不能显示,取决于「谁在请求它」。
这也解释了一个很常见的困惑------「我本地预览明明是好的」:本地文件或者同域引用根本不会带上触发校验的 Referer。
一条命令验证
排查第一步,先确认是不是来源校验:
bash
# 不带 Referer
curl -s -o /dev/null -w "%{http_code}\n" $URL
# 带上目标平台的 Referer
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Referer: 掘金站内地址" \
"某 CDN 的直链地址"
两个结果不一致(200 / 403),基本可以定位就是防盗链。
陷阱:懒加载会让「自动检测」给出假阴性
很多人用「图片加载失败」来判断裂图,写法大致是:
js
const bad = [...document.images].filter(img => img.naturalWidth === 0);
这个判断在懒加载 面前是失真的:还没滚动到的图片,naturalWidth 也是 0,但它并没有裂。
更可靠的做法是看 src 的最终形态:
js
const imgs = [...document.querySelectorAll('.article-content img')];
const hosts = imgs.map(i => {
try { return new URL(i.currentSrc || i.src).host; }
catch { return 'INVALID'; }
});
console.table(hosts);
判断标准变成两条:
src是否指向该平台自己的图床域名(转存成功);- 是否仍是原始外链域名(可能被拒)。
对站点自己的域名,再补一次 HTTP 探测,就能得到确定结论,而不是靠 naturalWidth 猜。
解决方案:从「一份稿子」到「每平台一份」
方案一:发布前批量转存(推荐)
思路很简单:目标平台自己的图床,才是唯一不会 403 的来源。
流程:
解析 Markdown 中的图片链接
→ 下载到本地临时目录
→ 上传到目标平台图床
→ 用返回的新链接替换原文
→ 再执行发布
用 Node 实现核心两步并不复杂:
js
// 1) 下载源图,保留原始扩展名
async function download(url, dir) {
const res = await fetch(url);
if (!res.ok) throw new Error(`download ${res.status}: ${url}`);
const ext = (new URL(url).pathname.match(/\.(png|jpe?g|gif|webp)$/i) || ['.png'])[0];
const file = path.join(dir, crypto.randomUUID() + ext);
await fs.writeFile(file, Buffer.from(await res.arrayBuffer()));
return file;
}
// 2) 串行转存,避免把平台图床打限流
for (const link of links) {
const local = await download(link, tmpDir);
const uploaded = await uploadToPlatform(local); // 各平台接口/交互不同
md = md.replaceAll(link, uploaded);
}
两个实践要点:
- 串行,不要并发。上传接口大多有限流,并发只会换来一批 429;
- 保留扩展名 。不少平台的图床按后缀判断
Content-Type,丢了后缀会变成下载而不是显示。
方案二:为每个平台维护一个「图床版本」
如果平台数量固定、发文频率高,更省事的做法是:一份内容,多份链接版本。
bash
jjmd/article.md # 通用版(用平台 A 的图床)
c51md/article.md # 51CTO 版(必须用站内图床)
用脚本维护一次映射表,之后每次发文只替换图片链接前缀即可。代价是要多维护几份文件,好处是发布环节零风险、零等待。
把「图片可用性」做成发布前的断言
最后一件事,也是最能省时间的:把检查前置到发布之前。
js
function assertImagesResolvable(md, allowedHosts) {
const links = [...md.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g)].map(m => m[1]);
const bad = links.filter(l => {
try { return !allowedHosts.includes(new URL(l).host); }
catch { return true; }
});
if (bad.length) {
throw new Error(`以下图片不在允许的图床内,发布前必须转存:\n${bad.join('\n')}`);
}
}
允许域名列表由各平台决定:站内图床 + 明确支持外链的域名,其余一律拦下。
这样做的价值不是「多检查一步」,而是把发布后才发现的问题 提前到发布前必然发现------后者是可以自动修复的,前者只能重发。
小结
- 图片裂不裂,取决于图床的 Referer 校验,跟 Markdown 写法无关;
- 用
curl对比带/不带Referer的返回码,一秒定性; - 别用
naturalWidth判断裂图,懒加载会骗你; - 想彻底解决,就把图片转存到目标平台自己的图床,并串行执行;
- 最后,把图床域名白名单做成发布前断言。
一稿多发真正的工作量,从来不在写,而在适配。