静态资源 404:3 个原因 + 修复路径

/images/foo.png 404——路径 / base / 上传步骤。

部署后页面打开正常,但图片是裂图、CSS 没加载、字体掉光——浏览器 Network 面板里一片 404。本地 npm run dev 一切正常,让人怀疑是平台问题,但九成情况是因为文件根本没被 build 复制到 dist/、或者你写的路径和实际部署的 base 不一致、或者 Vite/Astro 的资源处理把哈希加到了文件名上而你硬编码了原名。

本文按命中率列出 5 类典型场景,每类都给一条能在 dist/ 或 DevTools 里直接验证的判断方式。

常见原因

按命中率从高到低:

1. 资源放错位置,没进 build 产物

只有放在 public/ 下的文件会被原样复制到 dist/(路径不变)。放在 src/assets/ 的资源必须通过 import 引用,让 Vite 处理;直接写 /src/assets/foo.png 在浏览器里 404。

写法结果
文件在 public/images/foo.png,HTML 写 /images/foo.pngOK
文件在 src/assets/foo.png,HTML 写 /src/assets/foo.png404
文件在 src/assets/foo.png,组件里 import foo from './foo.png'OK,Vite 会输出到 dist/_astro/foo.<hash>.png

如何判断

npm run build
find dist -name "foo.png" -o -name "foo.*.png"

找不到就是没进产物。

2. base 路径偏移

astro.config.mjs 里设了 base: '/blog',所有资源实际部署到 /blog/... 下。如果你模板里硬编码 /images/foo.png,浏览器请求的就是 https://yourdomain.com/images/foo.png(404)而不是 https://yourdomain.com/blog/images/foo.png

如何判断

grep -n "base:" astro.config.mjs

如果有 base,用 import.meta.env.BASE_URL 拼接路径,或用 Astro 的 <Image> / <a href={Astro.url}> 而不是硬编码。

3. 大小写不一致

/Images/Foo.PNG/images/foo.png 在 macOS 本地(大小写不敏感)是同一个文件,在 Linux 部署服务器(大小写敏感)是两个文件。本地永远好,生产永远 404。

如何判断

ls public/images/ | grep -i foo

文件实际叫什么,HTML 里就必须写一字不差,包括大小写和扩展名。

4. Vite/Astro 加了 content hash,你引用的还是原名

Vite 默认把 src/ 里 import 的资源加哈希,输出成 foo.a3f7b9.png。如果你在 .mdx / .astro 里硬编码 /src/assets/foo.png/foo.png,找不到。

如何判断:DevTools → Network → 看 404 那个请求的 URL,再 ls dist/_astro/ 看实际文件名带没带哈希。

5. CDN 还没同步 / 旧版本缓存

刚部署完,CDN 边缘节点还在拉新文件。或者上一次部署的 HTML 缓存了,引用了旧的 hash 文件名,但 dist/ 里那个 hash 文件已经被新 build 覆盖。

如何判断

curl -I "https://yourdomain.com/images/foo.png"
curl -I "https://yourdomain.com/images/foo.png?cb=$(date +%s)"

带 buster 是 200、不带是 404 → CDN 缓存;两个都 404 → 文件确实没传上去。

最短修复路径

按收益从高到低,前 3 步通常能解决 80% 的问题。

Step 1:本地 build 后确认文件在 dist 里

rm -rf dist/
npm run build
find dist -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.svg" -o -name "*.css" -o -name "*.woff*" \) | head -20

把 DevTools 里 404 那个 URL 的路径片段(如 images/foo.png)拿来:

find dist -path "*images/foo*"
  • 找不到 → 文件没进产物,检查 public/ 位置或换成 import
  • 找得到但名字带 hash → 你引用的是原名,需要让框架处理引用

Step 2:用框架推荐的资源引用方式

Astro 推荐:

---
import { Image } from 'astro:assets';
import foo from '../assets/foo.png';
---
<Image src={foo} alt="foo" />

这样 Vite 会保证 build 后引用和文件名一致(带 hash 也对得上)。

如果资源不能用 import(比如 MDX 里的图),放进 public/

public/images/foo.png   →  HTML 里写 /images/foo.png

注意 public/ 里的文件不会被处理——不压缩、不加 hash、不优化。换文件名时浏览器可能缓存旧版本,需要手动加 cache buster。

Step 3:处理 base 路径

如果有 base: '/blog',所有硬编码路径都要前缀。Astro 推荐:

<img src={`${import.meta.env.BASE_URL}images/foo.png`} alt="" />

或者:

<img src={new URL('images/foo.png', Astro.url).pathname} alt="" />

不要写 src="/images/foo.png"——只在 base: '/' 时正确。

Step 4:清 CDN,按文件清

# 验证源站
curl -I "https://yourdomain.com/images/foo.png?cb=$(date +%s)"
# 看 CDN 状态
curl -I "https://yourdomain.com/images/foo.png"

如果带 buster 200、不带 404,清这个 URL 的 CDN:

  • Cloudflare:Purge Custom URLs,粘贴完整 URL
  • Vercel:vercel --prod --force 重部署
  • Netlify:Deploys → Trigger deploy → Clear cache and deploy site

Step 5:CI 加资源 404 冒烟测试

部署后跑:

#!/usr/bin/env bash
set -e
BASE="https://yourdomain.com"
# 关键资源列表
ASSETS=(
  "/favicon.ico"
  "/images/og-default.png"
  "/fonts/inter.woff2"
)
for a in "${ASSETS[@]}"; do
  code=$(curl -s -o /dev/null -w "%{http_code}" "$BASE$a")
  [[ "$code" == "200" ]] || { echo "FAIL $a$code"; exit 1; }
done
echo "All key assets OK"

任何缺失资源在部署后 30 秒内就会被发现。

预防建议

  • 资源全部小写 + kebab-case,杜绝大小写敏感问题
  • 组件内引用资源用 import,让框架管 hash;不要硬编码 /src/... 路径
  • 如果用了 base,所有路径用 import.meta.env.BASE_URL 拼接
  • 部署后 CI 跑关键资源 200 检查,favicon / og 图 / 主字体最少要在内
  • 大文件资源放 CDN 单独 host,避免 build 体积失控

相关阅读

标签: #部署 / 托管 #排查 #排查