Skip to content
Draft
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
49 changes: 43 additions & 6 deletions packages/astro/src/core/build/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,16 +545,53 @@ async function generatePath(
if (routeIsRedirect(route) && !config.build.redirects) {
return undefined;
}

const locationSite = getRedirectLocationOrThrow(response.headers);
const siteURL = config.site;
const location = siteURL ? new URL(locationSite, siteURL) : locationSite;
const fromPath = new URL(request.url).pathname;
body = redirectTemplate({
status: response.status,
absoluteLocation: location,
relativeLocation: locationSite,
from: fromPath,
});

const threeXXRoute = matchRoute('/3xx', options.routesList);

if (threeXXRoute) {
const threeXXRenderContext = await RenderContext.create({
pipeline,
pathname: pathname,
request,
routeData: threeXXRoute,
clientAddress: undefined,
});

// Set props for 3xx page
threeXXRenderContext.props = {
status: response.status,
location: locationSite,
from: fromPath,
};

// Render the 3xx page
const redirectResponse = await threeXXRenderContext.render(mod);
let html = await redirectResponse.text();

const delay = response.status === 302 ? 2 : 0;
html = html.replace(
/<head[^>]*>/i,
`$&
<meta http-equiv="refresh" content="${delay};url=${locationSite}">
<meta name="robots" content="noindex">
<link rel="canonical" href="${location}">`,
);

body = html;
} else {
body = redirectTemplate({
status: response.status,
absoluteLocation: location,
relativeLocation: locationSite,
from: fromPath,
});
}

if (config.compressHTML === true) {
body = body.replaceAll('\n', '');
}
Expand Down
5 changes: 5 additions & 0 deletions packages/astro/src/core/routing/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function matchAllRoutes(pathname: string, manifest: RoutesList): RouteDat

const ROUTE404_RE = /^\/404\/?$/;
const ROUTE500_RE = /^\/500\/?$/;
const ROUTE3XX_RE = /^\/3xx\/?$/;

export function isRoute404(route: string) {
return ROUTE404_RE.test(route);
Expand All @@ -29,6 +30,10 @@ export function isRoute500(route: string) {
return ROUTE500_RE.test(route);
}

export function isRoute3xx(route: string) {
return ROUTE3XX_RE.test(route);
}

/**
* Determines if the given route matches a 404 or 500 error page.
*
Expand Down
72 changes: 67 additions & 5 deletions packages/astro/src/vite-plugin-astro-server/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { getProps } from '../core/render/index.js';
import { createRequest } from '../core/request.js';
import { redirectTemplate } from '../core/routing/3xx.js';
import { matchAllRoutes } from '../core/routing/index.js';
import { isRoute404, isRoute500 } from '../core/routing/match.js';
import { isRoute3xx, isRoute404, isRoute500 } from '../core/routing/match.js';
import { PERSIST_SYMBOL } from '../core/session.js';
import { getSortedPreloadedMatches } from '../prerender/routing.js';
import type { ComponentInstance, RoutesList } from '../types/astro.js';
Expand Down Expand Up @@ -50,6 +50,10 @@ function getCustom500Route(manifestData: RoutesList): RouteData | undefined {
return manifestData.routes.find((r) => isRoute500(r.route));
}

function getCustom3xxRoute(manifestData: RoutesList): RouteData | undefined {
return manifestData.routes.find((r) => isRoute3xx(r.route));
}

export async function matchRoute(
pathname: string,
routesList: RoutesList,
Expand Down Expand Up @@ -124,6 +128,20 @@ export async function matchRoute(
};
}

const custom3xx = getCustom3xxRoute(routesList);
if (custom3xx) {
const filePath = new URL(`./${custom3xx.component}`, config.root);
const preloadedComponent = await pipeline.preload(custom3xx, filePath);

return {
route: custom3xx,
filePath,
resolvedPathname: pathname,
preloadedComponent,
mod: preloadedComponent,
};
}

return undefined;
}

Expand Down Expand Up @@ -292,10 +310,54 @@ export async function handleRoute({
//
// By default, we should give priority to the status code passed, although it's possible that
// the `Response` emitted by the user is a redirect. If so, then return the returned response.
if (response.status < 400 && response.status >= 300) {
if (
response.status >= 300 &&
response.status < 400 &&

const isStatus3xx = response.status < 400 && response.status >= 300;

if (isStatus3xx) {
const threeXXRoute = await matchRoute('/3xx', routesList, pipeline);

if (threeXXRoute) {
const location = response.headers.get('location')!;

renderContext = await RenderContext.create({
locals,
pipeline,
pathname,
middleware,
request,
routeData: threeXXRoute.route,
clientAddress: incomingRequest.socket.remoteAddress,
});

renderContext.props = {
status: response.status,
location,
from: pathname,
};

const redirectResponse = await renderContext.render(threeXXRoute.preloadedComponent);
const headers = Object.fromEntries(redirectResponse.headers.entries());

const html = await redirectResponse.text();

const delay = response.status === 302 ? 2 : 0;

const injectedHtml = html.replace(
/<head[^>]*>/i,
`$&
<meta http-equiv="refresh" content="${delay};url=${location}">
<meta name="robots" content="noindex">
<link rel="canonical" href="${location}">`,
);

response = new Response(injectedHtml, {
status: response.status,
headers: {
...headers,
'content-type': 'text/html',
},
});
} else if (
routeIsRedirect(route) &&
!config.build.redirects &&
pipeline.settings.buildOutput === 'static'
Expand Down
51 changes: 51 additions & 0 deletions packages/astro/test/custom-3xx.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';

describe('Custom 3xx page', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
let $;
let devServer;

before(async () => {
fixture = await loadFixture({
root: './fixtures/custom-3xx/',
site: 'http://example.com/',
});
devServer = await fixture.startDevServer();
});

after(async () => {
await devServer.stop();
});

it('shows custom 3xx page on redirect', async () => {
const response = await fixture.fetch('/redirect-page');
assert.equal(response.status, 302);

const html = await response.text();
$ = cheerio.load(html);
assert.equal($('h1').text(), 'Custom Redirect Page');
assert.ok(
$('p.destination').text().includes('/destination'),
'Location should contain /destination',
);
assert.equal($('p.status').text(), '302');
});

it('shows custom 3xx page on config-defined temporary redirect', async () => {
const response = await fixture.fetch('/temp-redirect');
assert.equal(response.status, 307);

const html = await response.text();
$ = cheerio.load(html);
assert.equal($('h1').text(), 'Custom Redirect Page');
assert.ok(
$('p.destination').text().includes('/destination'),
'Location should contain /destination',
);
assert.equal($('p.status').text(), '307');
});
});
11 changes: 11 additions & 0 deletions packages/astro/test/fixtures/custom-3xx/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from "astro/config";

export default defineConfig({
redirects: {
"/temp-redirect": {
status: 307,
destination: "/destination",
},
},
});

8 changes: 8 additions & 0 deletions packages/astro/test/fixtures/custom-3xx/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/custom-3xx",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}
13 changes: 13 additions & 0 deletions packages/astro/test/fixtures/custom-3xx/src/pages/3xx.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
const { status, location } = Astro.props;
---
<html>
<head>
<title>Custom Redirect</title>
</head>
<body>
<h1>Custom Redirect Page</h1>
<p class="destination">{location}</p>
<p class="status">{status}</p>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
// This is the page we're redirecting to
---
<html>
<head>
<title>Destination Page</title>
</head>
<body>
<h1>Successfully Redirected!</h1>
</body>
</html>
10 changes: 10 additions & 0 deletions packages/astro/test/fixtures/custom-3xx/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
---
<html>
<head>
<title>Custom 3xx test</title>
</head>
<body>
<h1>Custom 3xx test</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
const url = new URL('/destination', Astro.url);
return Astro.redirect(url);
---
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
const url = new URL('/destination', Astro.url);
return Astro.redirect(url);
---
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

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

Loading