Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
12 changes: 12 additions & 0 deletions .changeset/weak-eagles-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@astrojs/node': minor
'astro': patch
---

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.
2 changes: 1 addition & 1 deletion packages/astro/src/core/build/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'] = [];
Expand Down
3 changes: 3 additions & 0 deletions packages/integrations/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export default function createIntegration(userOptions: UserOptions): AstroIntegr
}

updateConfig({
build: {
redirects: false,
},
image: {
endpoint: {
route: config.image.endpoint.route ?? '_image',
Expand Down
4 changes: 3 additions & 1 deletion packages/integrations/node/src/serve-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "@test/redirects",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*",
"@astrojs/node": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
// Spread route for testing spread redirects

export function getStaticPaths() {
return [
{ params: { path: 'some/nested/path' } },
];
}
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
// This is the destination page for 301 redirects
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
// This is the destination page for 302 redirects
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
// This is the destination page for redirects
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
// Dynamic page for testing dynamic redirects

export function getStaticPaths() {
return [
{ params: { slug: 'test-slug' } },
];
}
---
70 changes: 70 additions & 0 deletions packages/integrations/node/test/redirects.test.js
Original file line number Diff line number Diff line change
@@ -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/');
});
});
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