Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-ssr-assets-manifest-middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes CSS, fonts, and other assets failing to load when using `@astrojs/node` in middleware mode with a catch-all route. Previously these assets were incorrectly matched by the catch-all instead of being served as static files.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification! Maybe we can tweak the changeset to describe that and not suggest that those assets will definitely load (if a user doesn’t add their own static file middleware)?

Suggested change
Fixes CSS, fonts, and other assets failing to load when using `@astrojs/node` in middleware mode with a catch-all route. Previously these assets were incorrectly matched by the catch-all instead of being served as static files.
Fixes catch-all routes incorrectly intercepting requests for static assets when using the `@astrojs/node` adapter in middleware mode.

Otherwise all looks good to me!

11 changes: 11 additions & 0 deletions packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@ async function createManifest(
internals.staticFiles.add(file);
}

// Also include SSR-emitted assets (CSS, fonts, images) tracked in ssrAssetsPerEnvironment.
// These assets are moved to the client directory by ssrMoveAssets() later in the pipeline,
// so they haven't landed on disk yet when we glob above. Without this, adapters in middleware
// mode won't recognize them as static files and will match them against catch-all routes instead.
// See: https://github.com/withastro/astro/issues/16039
for (const [, ssrAssets] of internals.ssrAssetsPerEnvironment) {
for (const asset of ssrAssets) {
internals.staticFiles.add(asset);
}
}

const staticFiles = internals.staticFiles;
const encodedKey = await encodeKey(await buildOpts.key);
const manifest = await buildManifest(buildOpts, internals, Array.from(staticFiles), encodedKey);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@test/ssr-assets-middleware",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/node": "workspace:*",
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
// Catch-all route — simulates a custom 404 handler.
// This is intentional: if SSR-emitted assets are missing from manifest.assets,
// asset requests like /_astro/*.css will match this catch-all instead of being
// served as static files. See https://github.com/withastro/astro/issues/16039
const { path } = Astro.params;
---
<!DOCTYPE html>
<html lang="en">
<head><title>Not Found</title></head>
<body>catch-all: {path}</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
import '../styles/main.css';
---
<html lang="en">
<head><title>home</title></head>
<body><div>home</div></body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.container {
color: red;
}
64 changes: 64 additions & 0 deletions packages/integrations/node/test/node-middleware.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,67 @@ describe('behavior from middleware, middleware with fastify', () => {
assert.equal(body.text().includes('bar'), true);
});
});

// Regression test for https://github.com/withastro/astro/issues/16039
// SSR-emitted assets (CSS, fonts, images) must appear in manifest.assets so that
// the Node adapter in middleware mode can identify them as static files and NOT
// match them against catch-all routes.
describe('middleware with fastify and catch-all route: SSR assets in manifest', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let server;

before(async () => {
fixture = await loadFixture({
root: './fixtures/ssr-assets-middleware/',
output: 'server',
adapter: nodejs({ mode: 'middleware' }),
vite: {
build: {
// Prevent CSS/SVG from being inlined so they appear as separate
// files in dist/client/_astro/ and are tracked in ssrAssetsPerEnvironment.
assetsInlineLimit: 0,
},
},
});
await fixture.build();
const { handler } = await fixture.loadAdapterEntryModule();
const app = Fastify({ logger: false });
await app
.register(fastifyStatic, {
root: fileURLToPath(
new URL('./fixtures/ssr-assets-middleware/dist/client', import.meta.url),
),
})
.register(fastifyMiddie);
app.use(handler);

await app.listen({ port: 8890 });

server = app;
});

after(async () => {
server.close();
await fixture.clean();
});

it('should serve SSR-emitted CSS assets directly, not the catch-all page', async () => {
// First get the index page to find the CSS asset URL
const indexRes = await fetch('http://localhost:8890/');
assert.equal(indexRes.status, 200);
const html = await indexRes.text();
const $ = cheerio.load(html);

// Find the CSS link tag injected by Astro
const cssHref = $('link[rel="stylesheet"]').attr('href');
assert.ok(cssHref, 'Expected a CSS <link> tag in the page');
assert.match(cssHref, /\/_astro\/.*\.css$/);

// Request the CSS asset — it must be served as CSS, not as the catch-all HTML page
const cssRes = await fetch(`http://localhost:8890${cssHref}`);
assert.equal(cssRes.status, 200);
const contentType = cssRes.headers.get('content-type');
assert.ok(contentType?.includes('text/css'), `Expected text/css, got: ${contentType}`);
});
});
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading