diff --git a/.changeset/fix-ssr-assets-manifest-middleware.md b/.changeset/fix-ssr-assets-manifest-middleware.md new file mode 100644 index 000000000000..b1693e9b4f16 --- /dev/null +++ b/.changeset/fix-ssr-assets-manifest-middleware.md @@ -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. diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 932bdf0dde03..7cc031504580 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -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); diff --git a/packages/integrations/node/test/fixtures/ssr-assets-middleware/package.json b/packages/integrations/node/test/fixtures/ssr-assets-middleware/package.json new file mode 100644 index 000000000000..c6a6e3fd73d7 --- /dev/null +++ b/packages/integrations/node/test/fixtures/ssr-assets-middleware/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/ssr-assets-middleware", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/node": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/ssr-assets-middleware/src/pages/[...path].astro b/packages/integrations/node/test/fixtures/ssr-assets-middleware/src/pages/[...path].astro new file mode 100644 index 000000000000..a3c4011d36e7 --- /dev/null +++ b/packages/integrations/node/test/fixtures/ssr-assets-middleware/src/pages/[...path].astro @@ -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; +--- + + +Not Found +catch-all: {path} + diff --git a/packages/integrations/node/test/fixtures/ssr-assets-middleware/src/pages/index.astro b/packages/integrations/node/test/fixtures/ssr-assets-middleware/src/pages/index.astro new file mode 100644 index 000000000000..85ab19afc3bc --- /dev/null +++ b/packages/integrations/node/test/fixtures/ssr-assets-middleware/src/pages/index.astro @@ -0,0 +1,7 @@ +--- +import '../styles/main.css'; +--- + +home +
home
+ diff --git a/packages/integrations/node/test/fixtures/ssr-assets-middleware/src/styles/main.css b/packages/integrations/node/test/fixtures/ssr-assets-middleware/src/styles/main.css new file mode 100644 index 000000000000..8963f2c5131a --- /dev/null +++ b/packages/integrations/node/test/fixtures/ssr-assets-middleware/src/styles/main.css @@ -0,0 +1,3 @@ +.container { + color: red; +} diff --git a/packages/integrations/node/test/node-middleware.test.js b/packages/integrations/node/test/node-middleware.test.js index 2004a1a088d8..e1603e34b93e 100644 --- a/packages/integrations/node/test/node-middleware.test.js +++ b/packages/integrations/node/test/node-middleware.test.js @@ -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 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}`); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8cb78df5a47a..42ddb57a3c51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6078,6 +6078,15 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/node/test/fixtures/ssr-assets-middleware: + dependencies: + '@astrojs/node': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/node/test/fixtures/static-headers: dependencies: '@astrojs/node':