Add support for the object notation in redirects
This commit is contained in:
parent
b8ef648f8c
commit
4adedbaafe
10 changed files with 91 additions and 29 deletions
|
@ -1582,6 +1582,8 @@ export interface AstroAdapter {
|
||||||
|
|
||||||
type Body = string;
|
type Body = string;
|
||||||
|
|
||||||
|
export type ValidRedirectStatus = 300 | 301 | 302 | 303 | 304 | 307 | 308;
|
||||||
|
|
||||||
// Shared types between `Astro` global and API context object
|
// Shared types between `Astro` global and API context object
|
||||||
interface AstroSharedContext<Props extends Record<string, any> = Record<string, any>> {
|
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 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
|
* Object accessed via Astro middleware
|
||||||
|
@ -1834,6 +1836,11 @@ export interface RoutePart {
|
||||||
spread: boolean;
|
spread: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RedirectConfig = string | {
|
||||||
|
status: ValidRedirectStatus;
|
||||||
|
destination: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RouteData {
|
export interface RouteData {
|
||||||
route: string;
|
route: string;
|
||||||
component: string;
|
component: string;
|
||||||
|
@ -1846,7 +1853,7 @@ export interface RouteData {
|
||||||
segments: RoutePart[][];
|
segments: RoutePart[][];
|
||||||
type: RouteType;
|
type: RouteType;
|
||||||
prerender: boolean;
|
prerender: boolean;
|
||||||
redirect?: string;
|
redirect?: RedirectConfig;
|
||||||
redirectRoute?: RouteData;
|
redirectRoute?: RouteData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -518,14 +518,16 @@ async function generatePath(
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(response.status) {
|
switch(true) {
|
||||||
case 301:
|
case (response.status >= 300 && response.status < 400): {
|
||||||
case 302: {
|
|
||||||
const location = getRedirectLocationOrThrow(response.headers);
|
const location = getRedirectLocationOrThrow(response.headers);
|
||||||
body = `<!doctype html>
|
body = `<!doctype html>
|
||||||
<title>Redirecting to: ${location}</title>
|
<title>Redirecting to: ${location}</title>
|
||||||
<meta http-equiv="refresh" content="0;url=${location}" />`;
|
<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;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|
|
@ -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 {
|
export function routeIsRedirect(route: RouteData | undefined): route is RedirectRouteData {
|
||||||
return route?.type === 'redirect';
|
return route?.type === 'redirect';
|
||||||
|
@ -8,5 +8,20 @@ export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): s
|
||||||
const routeData = redirectRoute.redirectRoute;
|
const routeData = redirectRoute.redirectRoute;
|
||||||
const route = redirectRoute.redirect;
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
export { getRedirectLocationOrThrow } from './validate.js';
|
export { getRedirectLocationOrThrow } from './validate.js';
|
||||||
export { routeIsRedirect, redirectRouteGenerate } from './helpers.js';
|
export { routeIsRedirect, redirectRouteGenerate, redirectRouteStatus } from './helpers.js';
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type { RenderContext } from './context.js';
|
||||||
import type { Environment } from './environment.js';
|
import type { Environment } from './environment.js';
|
||||||
import { createResult } from './result.js';
|
import { createResult } from './result.js';
|
||||||
import { callGetStaticPaths, findPathItemByKey, RouteCache } from './route-cache.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 {
|
interface GetParamsAndPropsOptions {
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance;
|
||||||
|
@ -114,9 +114,9 @@ export type RenderPage = {
|
||||||
export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) {
|
export async function renderPage({ mod, renderContext, env, apiContext }: RenderPage) {
|
||||||
if(routeIsRedirect(renderContext.route)) {
|
if(routeIsRedirect(renderContext.route)) {
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 301,
|
status: redirectRouteStatus(renderContext.route),
|
||||||
headers: {
|
headers: {
|
||||||
location: redirectRouteGenerate(renderContext.route!, renderContext.params)
|
location: redirectRouteGenerate(renderContext.route, renderContext.params)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,11 @@ describe('Astro.redirect', () => {
|
||||||
redirects: {
|
redirects: {
|
||||||
'/one': '/',
|
'/one': '/',
|
||||||
'/two': '/',
|
'/two': '/',
|
||||||
'/blog/[...slug]': '/articles/[...slug]'
|
'/blog/[...slug]': '/articles/[...slug]',
|
||||||
|
'/three': {
|
||||||
|
status: 302,
|
||||||
|
destination: '/'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await fixture.build();
|
await fixture.build();
|
||||||
|
@ -68,6 +72,10 @@ describe('Astro.redirect', () => {
|
||||||
html = await fixture.readFile('/two/index.html');
|
html = await fixture.readFile('/two/index.html');
|
||||||
expect(html).to.include('http-equiv="refresh');
|
expect(html).to.include('http-equiv="refresh');
|
||||||
expect(html).to.include('url=/');
|
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 () => {
|
it('Generates page for dynamic routes', async () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AstroConfig, RouteData } from 'astro';
|
import type { AstroConfig, RouteData, ValidRedirectStatus } from 'astro';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
export type RedirectDefinition = {
|
export type RedirectDefinition = {
|
||||||
|
@ -6,9 +6,16 @@ export type RedirectDefinition = {
|
||||||
input: string;
|
input: string;
|
||||||
target: string;
|
target: string;
|
||||||
weight: 0 | 1;
|
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(
|
export async function createRedirects(
|
||||||
config: AstroConfig,
|
config: AstroConfig,
|
||||||
routes: RouteData[],
|
routes: RouteData[],
|
||||||
|
@ -27,8 +34,8 @@ export async function createRedirects(
|
||||||
definitions.push({
|
definitions.push({
|
||||||
dynamic: false,
|
dynamic: false,
|
||||||
input: route.pathname,
|
input: route.pathname,
|
||||||
target: route.redirect,
|
target: typeof route.redirect === 'object' ? route.redirect.destination : route.redirect,
|
||||||
status: 301,
|
status: getRedirectStatus(route),
|
||||||
weight: 1
|
weight: 1
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -15,6 +15,10 @@ describe('SSG - Redirects', () => {
|
||||||
integrations: [testIntegration()],
|
integrations: [testIntegration()],
|
||||||
redirects: {
|
redirects: {
|
||||||
'/other': '/',
|
'/other': '/',
|
||||||
|
'/two': {
|
||||||
|
status: 302,
|
||||||
|
destination: '/'
|
||||||
|
},
|
||||||
'/blog/[...slug]': '/team/articles/[...slug]'
|
'/blog/[...slug]': '/team/articles/[...slug]'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -26,6 +30,7 @@ describe('SSG - Redirects', () => {
|
||||||
let parts = redirects.split(/\s+/);
|
let parts = redirects.split(/\s+/);
|
||||||
expect(parts).to.deep.equal([
|
expect(parts).to.deep.equal([
|
||||||
'/blog/*', '/team/articles/*/index.html', '301',
|
'/blog/*', '/team/articles/*/index.html', '301',
|
||||||
|
'/two', '/', '302',
|
||||||
'/other', '/', '301',
|
'/other', '/', '301',
|
||||||
'/nope', '/', '301',
|
'/nope', '/', '301',
|
||||||
'/team/articles/*', '/team/articles/*/index.html', '200'
|
'/team/articles/*', '/team/articles/*/index.html', '200'
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import type { AstroConfig, RouteData, RoutePart } from 'astro';
|
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
|
// https://vercel.com/docs/project-configuration#legacy/routes
|
||||||
interface VercelRoute {
|
interface VercelRoute {
|
||||||
|
@ -62,26 +65,33 @@ function getRedirectLocation(route: RouteData, config: AstroConfig): string {
|
||||||
if(route.redirectRoute) {
|
if(route.redirectRoute) {
|
||||||
const pattern = getReplacePattern(route.redirectRoute.segments);
|
const pattern = getReplacePattern(route.redirectRoute.segments);
|
||||||
const path = (config.trailingSlash === 'always' ? appendTrailingSlash(pattern) : pattern);
|
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 {
|
} 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[] {
|
export function getRedirects(routes: RouteData[], config: AstroConfig): VercelRoute[] {
|
||||||
let redirects: VercelRoute[] = [];
|
let redirects: VercelRoute[] = [];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
for(const route of routes) {
|
for(const route of routes) {
|
||||||
if(route.type === 'redirect') {
|
if(route.type === 'redirect') {
|
||||||
if(true || route.pathname) {
|
redirects.push({
|
||||||
redirects.push({
|
src: config.base + getMatchPattern(route.segments),
|
||||||
src: config.base + getMatchPattern(route.segments),
|
headers: { Location: getRedirectLocation(route, config) },
|
||||||
headers: { Location: getRedirectLocation(route, config) },
|
status: getRedirectStatus(route)
|
||||||
status: 301
|
});
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.error(`Dynamic routes not yet supported`);
|
|
||||||
}
|
|
||||||
} else if (route.type === 'page') {
|
} else if (route.type === 'page') {
|
||||||
if (config.trailingSlash === 'always') {
|
if (config.trailingSlash === 'always') {
|
||||||
redirects.push({
|
redirects.push({
|
||||||
|
|
|
@ -12,6 +12,10 @@ describe('Redirects', () => {
|
||||||
redirects: {
|
redirects: {
|
||||||
'/one': '/',
|
'/one': '/',
|
||||||
'/two': '/',
|
'/two': '/',
|
||||||
|
'/three': {
|
||||||
|
status: 302,
|
||||||
|
destination: '/'
|
||||||
|
},
|
||||||
'/blog/[...slug]': '/team/articles/[...slug]',
|
'/blog/[...slug]': '/team/articles/[...slug]',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -32,9 +36,13 @@ describe('Redirects', () => {
|
||||||
expect(oneRoute.headers.Location).to.equal('/');
|
expect(oneRoute.headers.Location).to.equal('/');
|
||||||
expect(oneRoute.status).to.equal(301);
|
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.headers.Location).to.equal('/');
|
||||||
expect(twoRoute.status).to.equal(301);
|
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 () => {
|
it('defines dynamic routes', async () => {
|
||||||
|
|
Loading…
Reference in a new issue