diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 35c96a7a99d1..43c347d32721 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -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( + /]*>/i, + `$& + + + `, + ); + + body = html; + } else { + body = redirectTemplate({ + status: response.status, + absoluteLocation: location, + relativeLocation: locationSite, + from: fromPath, + }); + } + if (config.compressHTML === true) { body = body.replaceAll('\n', ''); } diff --git a/packages/astro/src/core/routing/match.ts b/packages/astro/src/core/routing/match.ts index caa3ae743119..55cf30d2bd2b 100644 --- a/packages/astro/src/core/routing/match.ts +++ b/packages/astro/src/core/routing/match.ts @@ -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); @@ -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. * diff --git a/packages/astro/src/vite-plugin-astro-server/route.ts b/packages/astro/src/vite-plugin-astro-server/route.ts index 8c52ed4dc218..e1698858509d 100644 --- a/packages/astro/src/vite-plugin-astro-server/route.ts +++ b/packages/astro/src/vite-plugin-astro-server/route.ts @@ -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'; @@ -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, @@ -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; } @@ -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( + /]*>/i, + `$& + + + `, + ); + + response = new Response(injectedHtml, { + status: response.status, + headers: { + ...headers, + 'content-type': 'text/html', + }, + }); + } else if ( routeIsRedirect(route) && !config.build.redirects && pipeline.settings.buildOutput === 'static' diff --git a/packages/astro/test/custom-3xx.test.js b/packages/astro/test/custom-3xx.test.js new file mode 100644 index 000000000000..f21cf4e51237 --- /dev/null +++ b/packages/astro/test/custom-3xx.test.js @@ -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'); + }); +}); diff --git a/packages/astro/test/fixtures/custom-3xx/astro.config.mjs b/packages/astro/test/fixtures/custom-3xx/astro.config.mjs new file mode 100644 index 000000000000..f61b2b3842de --- /dev/null +++ b/packages/astro/test/fixtures/custom-3xx/astro.config.mjs @@ -0,0 +1,11 @@ +import { defineConfig } from "astro/config"; + +export default defineConfig({ + redirects: { + "/temp-redirect": { + status: 307, + destination: "/destination", + }, + }, +}); + diff --git a/packages/astro/test/fixtures/custom-3xx/package.json b/packages/astro/test/fixtures/custom-3xx/package.json new file mode 100644 index 000000000000..35d2f06d62fe --- /dev/null +++ b/packages/astro/test/fixtures/custom-3xx/package.json @@ -0,0 +1,8 @@ +{ + "name": "@test/custom-3xx", + "version": "0.0.0", + "private": true, + "dependencies": { + "astro": "workspace:*" + } +} diff --git a/packages/astro/test/fixtures/custom-3xx/src/pages/3xx.astro b/packages/astro/test/fixtures/custom-3xx/src/pages/3xx.astro new file mode 100644 index 000000000000..d774be4ccc8d --- /dev/null +++ b/packages/astro/test/fixtures/custom-3xx/src/pages/3xx.astro @@ -0,0 +1,13 @@ +--- +const { status, location } = Astro.props; +--- + + + Custom Redirect + + +

Custom Redirect Page

+

{location}

+

{status}

+ + \ No newline at end of file diff --git a/packages/astro/test/fixtures/custom-3xx/src/pages/destination.astro b/packages/astro/test/fixtures/custom-3xx/src/pages/destination.astro new file mode 100644 index 000000000000..580ea4db1165 --- /dev/null +++ b/packages/astro/test/fixtures/custom-3xx/src/pages/destination.astro @@ -0,0 +1,11 @@ +--- +// This is the page we're redirecting to +--- + + + Destination Page + + +

Successfully Redirected!

+ + \ No newline at end of file diff --git a/packages/astro/test/fixtures/custom-3xx/src/pages/index.astro b/packages/astro/test/fixtures/custom-3xx/src/pages/index.astro new file mode 100644 index 000000000000..5a46f0557810 --- /dev/null +++ b/packages/astro/test/fixtures/custom-3xx/src/pages/index.astro @@ -0,0 +1,10 @@ +--- +--- + + + Custom 3xx test + + +

Custom 3xx test

+ + \ No newline at end of file diff --git a/packages/astro/test/fixtures/custom-3xx/src/pages/redirect-page.astro b/packages/astro/test/fixtures/custom-3xx/src/pages/redirect-page.astro new file mode 100644 index 000000000000..df70a5dc584d --- /dev/null +++ b/packages/astro/test/fixtures/custom-3xx/src/pages/redirect-page.astro @@ -0,0 +1,4 @@ +--- +const url = new URL('/destination', Astro.url); +return Astro.redirect(url); +--- diff --git a/packages/astro/test/fixtures/custom-3xx/src/pages/temp-redirect.astro b/packages/astro/test/fixtures/custom-3xx/src/pages/temp-redirect.astro new file mode 100644 index 000000000000..171adce42f27 --- /dev/null +++ b/packages/astro/test/fixtures/custom-3xx/src/pages/temp-redirect.astro @@ -0,0 +1,4 @@ +--- +const url = new URL('/destination', Astro.url); +return Astro.redirect(url); +--- \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39f63aeb5338..cfcf3fd3fa32 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2897,6 +2897,12 @@ importers: specifier: workspace:* version: link:../../.. + packages/astro/test/fixtures/custom-3xx: + dependencies: + astro: + specifier: workspace:* + version: link:../../.. + packages/astro/test/fixtures/custom-404-html: dependencies: astro: