摘要

在将我的新 Astro 博客部署到 GitHub Pages 后,我遇到了一个棘手的问题:网站内容可以正常显示,但所有 CSS 样式都无法加载,浏览器控制台里充满了 404 错误。本文记录了我从最初的错误猜想到最终定位并解决问题的完整过程,希望能帮助遇到类似问题的开发者们。

一、问题浮现:消失的 CSS

万事俱备,只欠东风。我用最新的 Astro 框架搭建了个人博客,本地开发一切正常,npm run build 构建也顺利完成。当我兴致勃勃地将 dist 目录下的产物推送到 GitHub Pages 后,现实给了我一记重拳:

网站首页可以访问,但页面是”裸奔”的——没有任何样式。

打开浏览器开发者工具,ConsoleNetwork 面板清晰地揭示了问题所在:所有的 CSS 文件请求都返回了 404 Not Found

这些 CSS 文件的路径看起来像这样:

https://geyuxu.com/_astro/_slug_.BvCO7WHQ.css

问题很明确:服务器无法找到 Astro 构建出来的 CSS 文件。但为什么呢?

二、漫漫排查路:错误的假设

我的排查过程遵循了典型的从易到难、从普遍到特殊的思路,但也因此走了一些弯路。

假设 1:Jekyll 捣乱?

GitHub Pages 默认使用 Jekyll 来构建网站。Jekyll 有一个众所周知的约定:它会忽略所有以下划线 (_) 开头的目录和文件,因为它们被认为是特殊的,比如 _posts_includes 等。

Astro 构建产物中的 CSS 目录恰好是 _astro。这看起来就是罪魁祸首!

解决方案尝试:在仓库的根目录添加一个空的 .nojekyll 文件。这个文件的作用是告诉 GitHub Pages:“请不要用 Jekyll 处理我的网站,我这是一个纯静态站点。”

结果:清理缓存,重新部署,问题依旧。_astro 目录下的 CSS 文件仍然是 404。

这个结果让我很困惑。.nojekyll 应该已经禁用了 Jekyll,为什么以下划线开头的资源还是无法访问?

假设 2:目录访问问题?

我开始怀疑是不是 _astro 目录本身因为某些服务器配置而无法被直接访问。或许它缺少一个 index.html 文件?

解决方案尝试:在 dist/_astro/ 目录下手动创建一个空的 index.html 文件,然后重新部署。

结果:毫无悬念,失败了。这本就是一个不切实际的猜测,因为我们是请求具体的文件,而不是访问目录。

三、柳暗花明:定位真正原因

在排除了最常见的 Jekyll 问题后,我开始重新审视那个神秘的 404。.nojekyll 文件确实禁用了 Jekyll 的构建过程,但它并不能覆盖 GitHub Pages 服务器的所有规则。

经过一番搜索和验证,我终于发现了问题的关键:

GitHub Pages (或其底层的 Web 服务器) 有一条更深层的规则:它会阻止所有对以下划线 (_) 开头的文件的直接访问,这似乎是一个安全或约定策略,独立于 Jekyll 的行为。

Astro 生成的 CSS 文件名,例如 _slug_.BvCO7WHQ.css,恰好也以下划线开头!这才是 .nojekyll 文件无效的根本原因。我们不仅要处理 _astro 目录,还要处理文件名本身。

四、终极解决方案:自定义 Astro 构建产物命名

既然无法改变 GitHub Pages 的规则,那我们就只能改变我们自己的产物。我们需要让 Astro 在构建时,不要生成以下划线开头的文件名。

幸运的是,Astro 底层使用 Vite 作为其构建工具,并向我们开放了 Vite 的配置接口。我们可以通过修改 astro.config.mjs 文件来定制构建行为。

配置方案

具体的配置项是 vite.build.rollupOptions.output.assetFileNames。它可以让我们完全控制资源文件(如 CSS、图片等)的输出路径和名称。

打开项目根目录下的 astro.config.mjs 文件,添加 vite 配置项:

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://geyuxu.com',
  // ... 其他配置
  vite: {
    build: {
      rollupOptions: {
        output: {
          // 修改资产文件命名规则,避免下划线开头
          assetFileNames: (assetInfo) => {
            const info = assetInfo.name.split('.');
            const ext = info[info.length - 1];
            const name = info.slice(0, -1).join('.');
            // 如果文件名以下划线开头,替换为 'assets-'
            const finalName = name.startsWith('_') ? name.replace(/^_/, 'assets-') : name;
            return `_astro/${finalName}.[hash].${ext}`;
          }
        }
      }
    }
  },
  // ... 其他配置如 markdown 等
});

代码解释

  • assetFileNames 接受一个函数,该函数为每个资源文件调用
  • assetInfo.name 包含了 Vite 建议的原始文件名,例如 _slug_.BvCO7WHQ.css
  • 我们检查 assetInfo.name 是否以 _ 开头
  • 如果是,我们使用 replace(/^_/, 'assets-') 将开头的下划线替换为 assets-
  • 最终生成的文件名变成了 assets-slug_.BvCO7WHQ.css

验证结果

在应用了以上配置后,重新运行 npm run build,然后将 dist 目录部署到 GitHub Pages:

npm run build
npm run deploy

结果:成功!CSS 文件现在以 assets-slug_.BvCO7WHQ.css 这样的路径加载,HTTP 状态码从 404 变成了 200。网站样式完美呈现。

我们可以通过以下命令验证:

curl -I https://geyuxu.com/_astro/assets-slug_.BvCO7WHQ.css

返回:

HTTP/2 200 
content-type: text/css; charset=utf-8

五、总结与反思

这次调试经历虽然曲折,但收获颇丰:

关键经验

  1. 深入理解平台限制:不能想当然地认为 .nojekyll 能解决所有问题。需要了解托管平台(GitHub Pages)自身的底层规则,而不仅仅是其表面工具(Jekyll)的规则。

  2. 从根源解决问题:与其尝试用各种”补丁”去适应部署环境,不如直接控制构建过程,生成符合环境要求的产物。这是一种更干净、更可维护的解决方案。

  3. 善用工具的配置能力:现代前端框架(如 Astro)和构建工具(如 Vite)通常都提供了强大的配置选项。深入阅读文档、了解这些配置项,是解决复杂问题的金钥匙。

  4. 下划线是特殊字符:在 Web 开发中,以下划线开头的文件/目录通常有特殊含义(如 Jekyll, Node.js 的私有模块等)。在命名和部署时要特别留意,避免与平台规则冲突。

Debug 技巧

  • 系统性排查:从最常见的原因开始,逐步深入
  • 验证假设:每个解决方案都要实际测试,不能想当然
  • 关注细节:文件名、路径、HTTP 状态码等细节往往包含关键信息
  • 查阅文档:当常见解决方案失效时,深入研究工具的配置选项

适用场景

这个解决方案适用于所有遇到类似问题的 Astro + GitHub Pages 部署场景,也可以扩展到其他使用 Vite 构建工具的框架(如 Vue、React 等)在 GitHub Pages 上的部署问题。

希望我的经验能为你节省一些调试时间。Happy coding!

Ge Yuxu • AI & Engineering

脱敏说明:本文所有出现的表名、字段名、接口地址、变量名、IP地址及示例数据等均非真实,仅用于阐述技术思路与实现步骤,示例代码亦非公司真实代码。示例方案亦非公司真实完整方案,仅为本人记忆总结,用于技术学习探讨。
    • 文中所示任何标识符并不对应实际生产环境中的名称或编号。
    • 示例 SQL、脚本、代码及数据等均为演示用途,不含真实业务数据,也不具备直接运行或复现的完整上下文。
    • 读者若需在实际项目中参考本文方案,请结合自身业务场景及数据安全规范,使用符合内部命名和权限控制的配置。

Data Desensitization Notice: All table names, field names, API endpoints, variable names, IP addresses, and sample data appearing in this article are fictitious and intended solely to illustrate technical concepts and implementation steps. The sample code is not actual company code. The proposed solutions are not complete or actual company solutions but are summarized from the author's memory for technical learning and discussion.
    • Any identifiers shown in the text do not correspond to names or numbers in any actual production environment.
    • Sample SQL, scripts, code, and data are for demonstration purposes only, do not contain real business data, and lack the full context required for direct execution or reproduction.
    • Readers who wish to reference the solutions in this article for actual projects should adapt them to their own business scenarios and data security standards, using configurations that comply with internal naming and access control policies.

版权声明:本文版权归原作者所有,未经作者事先书面许可,任何单位或个人不得以任何方式复制、转载、摘编或用于商业用途。
    • 若需非商业性引用或转载本文内容,请务必注明出处并保持内容完整。
    • 对因商业使用、篡改或不当引用本文内容所产生的法律纠纷,作者保留追究法律责任的权利。

Copyright Notice: The copyright of this article belongs to the original author. Without prior written permission from the author, no entity or individual may copy, reproduce, excerpt, or use it for commercial purposes in any way.
    • For non-commercial citation or reproduction of this content, attribution must be given, and the integrity of the content must be maintained.
    • The author reserves the right to pursue legal action against any legal disputes arising from the commercial use, alteration, or improper citation of this article's content.

Copyright © 1989–Present Ge Yuxu. All Rights Reserved.