From 404 to 200: Solving Missing CSS on Astro with GitHub Pages
The paths for these CSS files looked like this:
https://geyuxu.com/_astro/_slug_.BvCO7WHQ.css
The server couldn't find the CSS files Astro had generated. The question was why.
Investigation: Two Dead Ends
Hypothesis 1: Jekyll
GitHub Pages uses Jekyll by default. Jekyll ignores all files and directories beginning with an underscore--_posts, _includes, and so on. Astro's build output places CSS under _astro, which looked like the obvious cause.
Fix attempted: I added an empty .nojekyll file to the repository root. This tells GitHub Pages to skip Jekyll and serve the directory as plain static content.
Result: The CSS files under _astro still returned 404.
The .nojekyll file should have disabled Jekyll entirely. Something else was blocking access.
Hypothesis 2: Directory Access
Perhaps _astro was inaccessible because it lacked an index.html.
Fix attempted: I created an empty index.html inside dist/_astro/ and redeployed.
Result: No effect. We were requesting specific files, not browsing a directory.
The Real Cause
Disabling Jekyll was necessary but not sufficient. After further research, I found a more fundamental constraint:
GitHub Pages blocks direct web access to any file whose name starts with an underscore. This rule appears to operate at the server level, independently of Jekyll.
Astro's generated CSS filenames--such as _slug_.BvCO7WHQ.css--also start with an underscore. The _astro directory name was one problem; the filenames themselves were another. .nojekyll addressed neither.
Fixing the Build Output
Since the platform rule can't be changed, the solution is to configure Astro to stop generating underscore-prefixed filenames.
Astro uses Vite internally and exposes Vite's configuration API through astro.config.mjs. The relevant option is vite.build.rollupOptions.output.assetFileNames, which controls the output path and naming pattern for assets such as CSS and images.
Configuration
Open astro.config.mjs in your project root and add the vite configuration object:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://geyuxu.com',
// ... other configs
vite: {
build: {
rollupOptions: {
output: {
// Modify asset file naming rules to avoid underscore prefixes
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
const ext = info[info.length - 1];
const name = info.slice(0, -1).join('.');
// If filename starts with underscore, replace with 'assets-'
const finalName = name.startsWith('_') ? name.replace(/^_/, 'assets-') : name;
return `_astro/${finalName}.[hash].${ext}`;
}
}
}
}
},
// ... other configs like markdown, etc.
});
assetFileNamesaccepts a function called once per asset.assetInfo.nameholds Vite's suggested filename, e.g._slug_.BvCO7WHQ.css.- If the name starts with
_,replace(/^_/, 'assets-')rewrites it toassets-slug_.BvCO7WHQ.css.
Simpler Alternative
If renaming is the only goal, a static pattern avoids the conditional entirely:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
// ...
vite: {
build: {
rollupOptions: {
output: {
// A simpler pattern for all assets
assetFileNames: 'assets/[name].[hash][extname]',
// JS entry files
entryFileNames: "assets/entry.[hash].js",
// JS code-splitted chunks
chunkFileNames: "assets/chunk.[hash].js",
}
}
}
}
});
This places all assets in an assets/ directory using Rollup's [name] and [hash] placeholders, which naturally avoids the leading underscore.
Verification
After rebuilding and redeploying:
npm run build
npm run deploy
CSS now loaded from paths like assets-slug_.BvCO7WHQ.css with HTTP 200. Confirm with:
curl -I https://geyuxu.com/_astro/assets-slug_.BvCO7WHQ.css
Response:
HTTP/2 200
content-type: text/css; charset=utf-8
Lessons
Platform constraints run deeper than surface tools. .nojekyll disables the Jekyll build step; it does not override the server's file-serving rules. Understanding what each layer of a platform actually controls matters more than reaching for the standard fix.
Fix at the source. Configuring the build to produce compliant output is cleaner than patching the deployment environment. Vite's assetFileNames option gave full control without touching any platform configuration.
Underscores carry implicit meaning. In Jekyll, in Node.js conventions, and apparently in GitHub Pages' serving rules, a leading underscore marks a file as special or private. When deploying to opinionated platforms, naming choices in the build tool have real consequences.