From a053492fbf13da7704a842df2b5ac9976221327b Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Fri, 26 Sep 2025 20:59:27 +1000 Subject: [PATCH 1/4] feat(node): handle redirects dynamically in static mode --- .changeset/weak-eagles-give.md | 6 ++ .../src/core/build/plugins/plugin-manifest.ts | 2 +- packages/integrations/node/src/index.ts | 3 + packages/integrations/node/src/serve-app.ts | 4 +- .../test/fixtures/redirects/astro.config.mjs | 20 ++++++ .../node/test/fixtures/redirects/package.json | 9 +++ .../src/pages/content/[...path].astro | 9 +++ .../redirects/src/pages/new-page-301.astro | 3 + .../redirects/src/pages/new-page-302.astro | 3 + .../redirects/src/pages/new-page.astro | 3 + .../redirects/src/pages/pages/[slug].astro | 9 +++ .../integrations/node/test/redirects.test.js | 70 +++++++++++++++++++ 12 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 .changeset/weak-eagles-give.md create mode 100644 packages/integrations/node/test/fixtures/redirects/astro.config.mjs create mode 100644 packages/integrations/node/test/fixtures/redirects/package.json create mode 100644 packages/integrations/node/test/fixtures/redirects/src/pages/content/[...path].astro create mode 100644 packages/integrations/node/test/fixtures/redirects/src/pages/new-page-301.astro create mode 100644 packages/integrations/node/test/fixtures/redirects/src/pages/new-page-302.astro create mode 100644 packages/integrations/node/test/fixtures/redirects/src/pages/new-page.astro create mode 100644 packages/integrations/node/test/fixtures/redirects/src/pages/pages/[slug].astro create mode 100644 packages/integrations/node/test/redirects.test.js diff --git a/.changeset/weak-eagles-give.md b/.changeset/weak-eagles-give.md new file mode 100644 index 000000000000..ea90960f1d7e --- /dev/null +++ b/.changeset/weak-eagles-give.md @@ -0,0 +1,6 @@ +--- +'@astrojs/node': minor +'astro': patch +--- + +Handle configured redirects dynamically in static mode diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 86d5aefd1702..c4edb8f776ca 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -250,7 +250,7 @@ async function buildManifest( const pageData = internals.pagesByKeys.get(makePageDataKey(route.route, route.component)); if (!pageData) continue; - if (route.prerender && !needsStaticHeaders) { + if (route.prerender && route.type !== 'redirect' && !needsStaticHeaders) { continue; } const scripts: SerializedRouteInfo['scripts'] = []; diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts index 2a99e383bf6f..cd9f8175fd08 100644 --- a/packages/integrations/node/src/index.ts +++ b/packages/integrations/node/src/index.ts @@ -72,6 +72,9 @@ export default function createIntegration(userOptions: UserOptions): AstroIntegr } updateConfig({ + build: { + redirects: false, + }, image: { endpoint: { route: config.image.endpoint.route ?? '_image', diff --git a/packages/integrations/node/src/serve-app.ts b/packages/integrations/node/src/serve-app.ts index 09473e50cc75..77f12954ce4c 100644 --- a/packages/integrations/node/src/serve-app.ts +++ b/packages/integrations/node/src/serve-app.ts @@ -45,7 +45,9 @@ export function createAppHandler(app: NodeApp, options: Options): RequestHandler return; } - const routeData = app.match(request); + // Redirects are considered prerendered routes in static mode, but we want to + // handle them dynamically, so prerendered routes are included here. + const routeData = app.match(request, true); if (routeData) { const response = await als.run(request.url, () => app.render(request, { diff --git a/packages/integrations/node/test/fixtures/redirects/astro.config.mjs b/packages/integrations/node/test/fixtures/redirects/astro.config.mjs new file mode 100644 index 000000000000..7f72f9fe45c1 --- /dev/null +++ b/packages/integrations/node/test/fixtures/redirects/astro.config.mjs @@ -0,0 +1,20 @@ +import { defineConfig } from 'astro/config'; +import nodejs from '@astrojs/node'; + +export default defineConfig({ + adapter: nodejs({ mode: 'standalone' }), + redirects: { + '/old-page': '/new-page', + '/old-page-301': { + status: 301, + destination: '/new-page-301', + }, + '/old-page-302': { + status: 302, + destination: '/new-page-302', + }, + '/dynamic/[slug]': '/pages/[slug]', + '/spread/[...path]': '/content/[...path]', + '/external': 'https://example.com', + }, +}); diff --git a/packages/integrations/node/test/fixtures/redirects/package.json b/packages/integrations/node/test/fixtures/redirects/package.json new file mode 100644 index 000000000000..57923f6bb6ec --- /dev/null +++ b/packages/integrations/node/test/fixtures/redirects/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/redirects", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*", + "@astrojs/node": "workspace:*" + } +} diff --git a/packages/integrations/node/test/fixtures/redirects/src/pages/content/[...path].astro b/packages/integrations/node/test/fixtures/redirects/src/pages/content/[...path].astro new file mode 100644 index 000000000000..8ac107f17654 --- /dev/null +++ b/packages/integrations/node/test/fixtures/redirects/src/pages/content/[...path].astro @@ -0,0 +1,9 @@ +--- +// Spread route for testing spread redirects + +export function getStaticPaths() { + return [ + { params: { path: 'some/nested/path' } }, + ]; +} +--- diff --git a/packages/integrations/node/test/fixtures/redirects/src/pages/new-page-301.astro b/packages/integrations/node/test/fixtures/redirects/src/pages/new-page-301.astro new file mode 100644 index 000000000000..9d861e3a126d --- /dev/null +++ b/packages/integrations/node/test/fixtures/redirects/src/pages/new-page-301.astro @@ -0,0 +1,3 @@ +--- +// This is the destination page for 301 redirects +--- diff --git a/packages/integrations/node/test/fixtures/redirects/src/pages/new-page-302.astro b/packages/integrations/node/test/fixtures/redirects/src/pages/new-page-302.astro new file mode 100644 index 000000000000..248d5cb15f2b --- /dev/null +++ b/packages/integrations/node/test/fixtures/redirects/src/pages/new-page-302.astro @@ -0,0 +1,3 @@ +--- +// This is the destination page for 302 redirects +--- diff --git a/packages/integrations/node/test/fixtures/redirects/src/pages/new-page.astro b/packages/integrations/node/test/fixtures/redirects/src/pages/new-page.astro new file mode 100644 index 000000000000..76cafd980594 --- /dev/null +++ b/packages/integrations/node/test/fixtures/redirects/src/pages/new-page.astro @@ -0,0 +1,3 @@ +--- +// This is the destination page for redirects +--- diff --git a/packages/integrations/node/test/fixtures/redirects/src/pages/pages/[slug].astro b/packages/integrations/node/test/fixtures/redirects/src/pages/pages/[slug].astro new file mode 100644 index 000000000000..43c0d0e20f76 --- /dev/null +++ b/packages/integrations/node/test/fixtures/redirects/src/pages/pages/[slug].astro @@ -0,0 +1,9 @@ +--- +// Dynamic page for testing dynamic redirects + +export function getStaticPaths() { + return [ + { params: { slug: 'test-slug' } }, + ]; +} +--- diff --git a/packages/integrations/node/test/redirects.test.js b/packages/integrations/node/test/redirects.test.js new file mode 100644 index 000000000000..b6d6e5de72c0 --- /dev/null +++ b/packages/integrations/node/test/redirects.test.js @@ -0,0 +1,70 @@ +import assert from 'node:assert/strict'; +import { after, before, describe, it } from 'node:test'; +import nodejs from '../dist/index.js'; +import { loadFixture, waitServerListen } from './test-utils.js'; + +describe('Redirects', () => { + /** @type {import('./test-utils').Fixture} */ + let fixture; + let server; + + before(async () => { + fixture = await loadFixture({ + root: './fixtures/redirects/', + adapter: nodejs({ mode: 'standalone' }), + }); + await fixture.build(); + const { startServer } = await fixture.loadAdapterEntryModule(); + const res = startServer(); + server = res.server; + await waitServerListen(server.server); + }); + + after(async () => { + await server.stop(); + await fixture.clean(); + }); + + function fetchEndpoint(url, options = {}) { + return fetch( + `http://${server.host}:${server.port}/${url}`, + { ...options, redirect: 'manual' }, + ); + } + + it('should redirect with default 301 status for simple redirects', async () => { + const response = await fetchEndpoint('old-page'); + assert.equal(response.status, 301); + assert.equal(response.headers.get('location'), '/new-page'); + }); + + it('should redirect with custom 301 status', async () => { + const response = await fetchEndpoint('old-page-301'); + assert.equal(response.status, 301); + assert.equal(response.headers.get('location'), '/new-page-301'); + }); + + it('should redirect with custom 302 status', async () => { + const response = await fetchEndpoint('old-page-302'); + assert.equal(response.status, 302); + assert.equal(response.headers.get('location'), '/new-page-302'); + }); + + it('should handle dynamic redirects with parameters', async () => { + const response = await fetchEndpoint('dynamic/test-slug'); + assert.equal(response.status, 301); + assert.equal(response.headers.get('location'), '/pages/test-slug'); + }); + + it('should handle spread redirects with parameters', async () => { + const response = await fetchEndpoint('spread/some/nested/path'); + assert.equal(response.status, 301); + assert.equal(response.headers.get('location'), '/content/some/nested/path'); + }); + + it('should redirect to external URL', async () => { + const response = await fetchEndpoint('external'); + assert.equal(response.status, 301); + assert.equal(response.headers.get('location'), 'https://example.com/'); + }); +}); From ec1a198a0c2229eca3e9f6fd3caa4b9a3fa40a20 Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Fri, 26 Sep 2025 21:15:45 +1000 Subject: [PATCH 2/4] update lockfile --- pnpm-lock.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb39998aeece..1f41b815382f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5703,6 +5703,15 @@ importers: specifier: workspace:* version: link:../../../../../astro + packages/integrations/node/test/fixtures/redirects: + dependencies: + '@astrojs/node': + specifier: workspace:* + version: link:../../.. + astro: + specifier: workspace:* + version: link:../../../../../astro + packages/integrations/node/test/fixtures/sessions: dependencies: '@astrojs/node': From dbce2ccbfc6abb07f7a04831c399f0e689547177 Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Sat, 4 Oct 2025 08:06:20 +1000 Subject: [PATCH 3/4] update changeset --- .changeset/weak-eagles-give.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.changeset/weak-eagles-give.md b/.changeset/weak-eagles-give.md index ea90960f1d7e..2c7e455dd287 100644 --- a/.changeset/weak-eagles-give.md +++ b/.changeset/weak-eagles-give.md @@ -3,4 +3,10 @@ 'astro': patch --- -Handle configured redirects dynamically in static mode +Updates redirect handling to be consistent across `static` and `server` output, aligning with the behavior of other adapters. + +Previously, the Node.js adapter used default HTML files with meta refresh tags when in `static` output. This often resulted in an extra flash of the page on redirect, while also not applying the proper status code for redirections. It's also likely less friendly to search engines. + +This update ensures that configured redirects are always handled as HTTP redirects regardless of output mode, and the default HTML files for the redirects are no longer generated in `static` output. It makes the Node.js adapter more consistent with the other official adapters. + +No change to your project is required to take advantage of this new adapter functionality. It is not expected to cause any breaking changes. However, in an unlikely event that you previously rely on the generated redirecting HTML files for some reason, you may need to handle the redirects differently now. Otherwise you should just notice smoother redirects, with more accurate HTTP status codes, and may potentially see some SEO gains. From cf0d82a6b6d517f92c6544201e3023ffbc779dad Mon Sep 17 00:00:00 2001 From: Xidorn Quan Date: Sun, 12 Oct 2025 06:34:48 +1100 Subject: [PATCH 4/4] Update .changeset/weak-eagles-give.md Co-authored-by: Sarah Rainsberger <5098874+sarah11918@users.noreply.github.com> --- .changeset/weak-eagles-give.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/weak-eagles-give.md b/.changeset/weak-eagles-give.md index 2c7e455dd287..58e102e7acd3 100644 --- a/.changeset/weak-eagles-give.md +++ b/.changeset/weak-eagles-give.md @@ -9,4 +9,4 @@ Previously, the Node.js adapter used default HTML files with meta refresh tags w This update ensures that configured redirects are always handled as HTTP redirects regardless of output mode, and the default HTML files for the redirects are no longer generated in `static` output. It makes the Node.js adapter more consistent with the other official adapters. -No change to your project is required to take advantage of this new adapter functionality. It is not expected to cause any breaking changes. However, in an unlikely event that you previously rely on the generated redirecting HTML files for some reason, you may need to handle the redirects differently now. Otherwise you should just notice smoother redirects, with more accurate HTTP status codes, and may potentially see some SEO gains. +No change to your project is required to take advantage of this new adapter functionality. It is not expected to cause any breaking changes. However, if you relied on the previous redirecting behavior, you may need to handle your redirects differently now. Otherwise you should notice smoother redirects, with more accurate HTTP status codes, and may potentially see some SEO gains.