Add support for the object notation in redirects

This commit is contained in:
Matthew Phillips 2023-05-23 14:46:00 -04:00
parent b8ef648f8c
commit 4adedbaafe
10 changed files with 91 additions and 29 deletions

View file

@ -1582,6 +1582,8 @@ export interface AstroAdapter {
type Body = string;
export type ValidRedirectStatus = 300 | 301 | 302 | 303 | 304 | 307 | 308;
// Shared types between `Astro` global and API context object
interface AstroSharedContext<Props extends Record<string, any> = Record<string, any>> {
/**
@ -1611,7 +1613,7 @@ interface AstroSharedContext<Props extends Record<string, any> = Record<string,
/**
* Redirect to another page (**SSR Only**).
*/
redirect(path: string, status?: 301 | 302 | 303 | 307 | 308): Response;
redirect(path: string, status?: ValidRedirectStatus): Response;
/**
* Object accessed via Astro middleware
@ -1834,6 +1836,11 @@ export interface RoutePart {
spread: boolean;
}
type RedirectConfig = string | {
status: ValidRedirectStatus;
destination: string;
}
export interface RouteData {
route: string;
component: string;
@ -1846,7 +1853,7 @@ export interface RouteData {
segments: RoutePart[][];
type: RouteType;
prerender: boolean;
redirect?: string;
redirect?: RedirectConfig;
redirectRoute?: RouteData;
}

View file

@ -518,14 +518,16 @@ async function generatePath(
throw err;
}
switch(response.status) {
case 301:
case 302: {
switch(true) {
case (response.status >= 300 && response.status < 400): {
const location = getRedirectLocationOrThrow(response.headers);
body = `<!doctype html>
<title>Redirecting to: ${location}</title>
<meta http-equiv="refresh" content="0;url=${location}" />`;
pageData.route.redirect = location;
// A dynamic redirect, set the location so that integrations know about it.
if(pageData.route.type !== 'redirect') {
pageData.route.redirect = location;
}
break;
}
default: {

View file

@ -1,4 +1,4 @@
import type { RouteData, RedirectRouteData, Params } from '../../@types/astro';
import type { RouteData, RedirectRouteData, Params, ValidRedirectStatus } from '../../@types/astro';
export function routeIsRedirect(route: RouteData | undefined): route is RedirectRouteData {
return route?.type === 'redirect';
@ -8,5 +8,20 @@ export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): s
const routeData = redirectRoute.redirectRoute;
const route = redirectRoute.redirect;
return routeData?.generate(data) || routeData?.pathname || route || '/';
if(typeof routeData !== 'undefined') {
return routeData?.generate(data) || routeData?.pathname || '/';
} else if(typeof route === 'string') {
return route;
} else if(typeof route === 'undefined') {
return '/';
}
return route.destination;
}
export function redirectRouteStatus(redirectRoute: RouteData): ValidRedirectStatus {
const routeData = redirectRoute.redirectRoute;
if(typeof routeData?.redirect === 'object') {
return routeData.redirect.status;
}
return 301;
}

View file

@ -1,2 +1,2 @@
export { getRedirectLocationOrThrow } from './validate.js';
export { routeIsRedirect, redirectRouteGenerate } from './helpers.js';
export { routeIsRedirect, redirectRouteGenerate, redirectRouteStatus } from './helpers.js';

View file

@ -8,7 +8,7 @@ import type { RenderContext } from './context.js';
import type { Environment } from './environment.js';
import { createResult } from './result.js';
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.js';
import { routeIsRedirect, redirectRouteGenerate } from '../redirects/index.js';
import { routeIsRedirect, redirectRouteGenerate, redirectRouteStatus } from '../redirects/index.js';
interface GetParamsAndPropsOptions {
mod: ComponentInstance;
@ -114,9 +114,9 @@ export type RenderPage = {
export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) {
if(routeIsRedirect(renderContext.route)) {
return new Response(null, {
status: 301,
status: redirectRouteStatus(renderContext.route),
headers: {
location: redirectRouteGenerate(renderContext.route!, renderContext.params)
location: redirectRouteGenerate(renderContext.route, renderContext.params)
}
});
}

View file

@ -48,7 +48,11 @@ describe('Astro.redirect', () => {
redirects: {
'/one': '/',
'/two': '/',
'/blog/[...slug]': '/articles/[...slug]'
'/blog/[...slug]': '/articles/[...slug]',
'/three': {
status: 302,
destination: '/'
}
}
});
await fixture.build();
@ -68,6 +72,10 @@ describe('Astro.redirect', () => {
html = await fixture.readFile('/two/index.html');
expect(html).to.include('http-equiv="refresh');
expect(html).to.include('url=/');
html = await fixture.readFile('/three/index.html');
expect(html).to.include('http-equiv="refresh');
expect(html).to.include('url=/');
});
it('Generates page for dynamic routes', async () => {

View file

@ -1,4 +1,4 @@
import type { AstroConfig, RouteData } from 'astro';
import type { AstroConfig, RouteData, ValidRedirectStatus } from 'astro';
import fs from 'fs';
export type RedirectDefinition = {
@ -6,9 +6,16 @@ export type RedirectDefinition = {
input: string;
target: string;
weight: 0 | 1;
status: 200 | 404 | 301;
status: 200 | 404 | ValidRedirectStatus;
};
function getRedirectStatus(route: RouteData): ValidRedirectStatus {
if(typeof route.redirect === 'object') {
return route.redirect.status;
}
return 301;
}
export async function createRedirects(
config: AstroConfig,
routes: RouteData[],
@ -27,8 +34,8 @@ export async function createRedirects(
definitions.push({
dynamic: false,
input: route.pathname,
target: route.redirect,
status: 301,
target: typeof route.redirect === 'object' ? route.redirect.destination : route.redirect,
status: getRedirectStatus(route),
weight: 1
});
continue;

View file

@ -15,6 +15,10 @@ describe('SSG - Redirects', () => {
integrations: [testIntegration()],
redirects: {
'/other': '/',
'/two': {
status: 302,
destination: '/'
},
'/blog/[...slug]': '/team/articles/[...slug]'
}
});
@ -26,6 +30,7 @@ describe('SSG - Redirects', () => {
let parts = redirects.split(/\s+/);
expect(parts).to.deep.equal([
'/blog/*', '/team/articles/*/index.html', '301',
'/two', '/', '302',
'/other', '/', '301',
'/nope', '/', '301',
'/team/articles/*', '/team/articles/*/index.html', '200'

View file

@ -1,4 +1,7 @@
import type { AstroConfig, RouteData, RoutePart } from 'astro';
import nodePath from 'node:path';
const pathJoin = nodePath.posix.join;
// https://vercel.com/docs/project-configuration#legacy/routes
interface VercelRoute {
@ -62,26 +65,33 @@ function getRedirectLocation(route: RouteData, config: AstroConfig): string {
if(route.redirectRoute) {
const pattern = getReplacePattern(route.redirectRoute.segments);
const path = (config.trailingSlash === 'always' ? appendTrailingSlash(pattern) : pattern);
return config.base + path;
return pathJoin(config.base, path);
} else if(typeof route.redirect === 'object') {
return pathJoin(config.base, route.redirect.destination);
} else {
return config.base + route.redirect;
return pathJoin(config.base, route.redirect || '');
}
}
function getRedirectStatus(route: RouteData): number {
if(typeof route.redirect === 'object') {
return route.redirect.status;
}
return 301;
}
export function getRedirects(routes: RouteData[], config: AstroConfig): VercelRoute[] {
let redirects: VercelRoute[] = [];
for(const route of routes) {
if(route.type === 'redirect') {
if(true || route.pathname) {
redirects.push({
src: config.base + getMatchPattern(route.segments),
headers: { Location: getRedirectLocation(route, config) },
status: 301
})
} else {
console.error(`Dynamic routes not yet supported`);
}
redirects.push({
src: config.base + getMatchPattern(route.segments),
headers: { Location: getRedirectLocation(route, config) },
status: getRedirectStatus(route)
});
} else if (route.type === 'page') {
if (config.trailingSlash === 'always') {
redirects.push({

View file

@ -12,6 +12,10 @@ describe('Redirects', () => {
redirects: {
'/one': '/',
'/two': '/',
'/three': {
status: 302,
destination: '/'
},
'/blog/[...slug]': '/team/articles/[...slug]',
}
});
@ -32,9 +36,13 @@ describe('Redirects', () => {
expect(oneRoute.headers.Location).to.equal('/');
expect(oneRoute.status).to.equal(301);
const twoRoute = config.routes.find(r => r.src === '/\\/one');
const twoRoute = config.routes.find(r => r.src === '/\\/two');
expect(twoRoute.headers.Location).to.equal('/');
expect(twoRoute.status).to.equal(301);
const threeRoute = config.routes.find(r => r.src === '/\\/three');
expect(threeRoute.headers.Location).to.equal('/');
expect(threeRoute.status).to.equal(302);
});
it('defines dynamic routes', async () => {