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