Implement support for dynamic routes in redirects (#7173)
* Implement support for dynamic routes in redirects * Remove the .only * No need to special-case redirects in static build
This commit is contained in:
parent
4857c7d317
commit
25d7d208ba
13 changed files with 151 additions and 55 deletions
|
@ -1841,6 +1841,7 @@ export interface RouteData {
|
||||||
type: RouteType;
|
type: RouteType;
|
||||||
prerender: boolean;
|
prerender: boolean;
|
||||||
redirect?: string;
|
redirect?: string;
|
||||||
|
redirectRoute?: RouteData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RedirectRouteData = RouteData & {
|
export type RedirectRouteData = RouteData & {
|
||||||
|
|
|
@ -35,8 +35,8 @@ import { debug, info } from '../logger/core.js';
|
||||||
import { callMiddleware } from '../middleware/callMiddleware.js';
|
import { callMiddleware } from '../middleware/callMiddleware.js';
|
||||||
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
|
import { createEnvironment, createRenderContext, renderPage } from '../render/index.js';
|
||||||
import { callGetStaticPaths } from '../render/route-cache.js';
|
import { callGetStaticPaths } from '../render/route-cache.js';
|
||||||
import { getRedirectLocationOrThrow } from '../redirects/index.js';
|
import { getRedirectLocationOrThrow, routeIsRedirect } from '../redirects/index.js';
|
||||||
import {
|
import {
|
||||||
createAssetLink,
|
createAssetLink,
|
||||||
createModuleScriptsSet,
|
createModuleScriptsSet,
|
||||||
createStylesheetElementSet,
|
createStylesheetElementSet,
|
||||||
|
@ -173,15 +173,18 @@ async function generatePage(
|
||||||
let pageModulePromise = ssrEntry.pageMap?.get(pageData.component);
|
let pageModulePromise = ssrEntry.pageMap?.get(pageData.component);
|
||||||
const middleware = ssrEntry.middleware;
|
const middleware = ssrEntry.middleware;
|
||||||
|
|
||||||
if (!pageModulePromise) {
|
if (!pageModulePromise && routeIsRedirect(pageData.route)) {
|
||||||
if(pageData.route.type === 'redirect') {
|
if(pageData.route.redirectRoute) {
|
||||||
pageModulePromise = () => Promise.resolve({ 'default': Function.prototype }) as any;
|
pageModulePromise = ssrEntry.pageMap?.get(pageData.route.redirectRoute!.component);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
pageModulePromise = { default: () => {} } as any;
|
||||||
`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!pageModulePromise) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to find the module for ${pageData.component}. This is unexpected and likely a bug in Astro, please report.`
|
||||||
|
);
|
||||||
|
}
|
||||||
const pageModule = await pageModulePromise();
|
const pageModule = await pageModulePromise();
|
||||||
if (shouldSkipDraft(pageModule, opts.settings)) {
|
if (shouldSkipDraft(pageModule, opts.settings)) {
|
||||||
info(opts.logging, null, `${magenta('⚠️')} Skipping draft ${pageData.route.component}`);
|
info(opts.logging, null, `${magenta('⚠️')} Skipping draft ${pageData.route.component}`);
|
||||||
|
@ -519,7 +522,9 @@ async function generatePath(
|
||||||
case 301:
|
case 301:
|
||||||
case 302: {
|
case 302: {
|
||||||
const location = getRedirectLocationOrThrow(response.headers);
|
const location = getRedirectLocationOrThrow(response.headers);
|
||||||
body = `<!doctype html><meta http-equiv="refresh" content="0;url=${location}" />`;
|
body = `<!doctype html>
|
||||||
|
<title>Redirecting to: ${location}</title>
|
||||||
|
<meta http-equiv="refresh" content="0;url=${location}" />`;
|
||||||
pageData.route.redirect = location;
|
pageData.route.redirect = location;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,12 @@
|
||||||
import type { RouteData, RedirectRouteData } from '../../@types/astro';
|
import type { RouteData, RedirectRouteData, Params } 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';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function redirectRouteGenerate(redirectRoute: RouteData, data: Params): string {
|
||||||
|
const routeData = redirectRoute.redirectRoute;
|
||||||
|
const route = redirectRoute.redirect;
|
||||||
|
|
||||||
|
return routeData?.generate(data) || routeData?.pathname || route || '/';
|
||||||
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
export { getRedirectLocationOrThrow } from './validate.js';
|
export { getRedirectLocationOrThrow } from './validate.js';
|
||||||
export { routeIsRedirect } from './helpers.js';
|
export { routeIsRedirect, redirectRouteGenerate } 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 } from '../redirects/index.js';
|
import { routeIsRedirect, redirectRouteGenerate } from '../redirects/index.js';
|
||||||
|
|
||||||
interface GetParamsAndPropsOptions {
|
interface GetParamsAndPropsOptions {
|
||||||
mod: ComponentInstance;
|
mod: ComponentInstance;
|
||||||
|
@ -116,7 +116,7 @@ export async function renderPage({ mod, renderContext, env, apiContext }: Render
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 301,
|
status: 301,
|
||||||
headers: {
|
headers: {
|
||||||
location: renderContext.route!.redirect
|
location: redirectRouteGenerate(renderContext.route!, renderContext.params)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -444,6 +444,7 @@ export function createRouteManifest(
|
||||||
.map(([{ dynamic, content }]) => (dynamic ? `[${content}]` : content))
|
.map(([{ dynamic, content }]) => (dynamic ? `[${content}]` : content))
|
||||||
.join('/')}`.toLowerCase();
|
.join('/')}`.toLowerCase();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
routes.unshift({
|
routes.unshift({
|
||||||
type: 'redirect',
|
type: 'redirect',
|
||||||
|
@ -451,11 +452,12 @@ export function createRouteManifest(
|
||||||
pattern,
|
pattern,
|
||||||
segments,
|
segments,
|
||||||
params,
|
params,
|
||||||
component: '',
|
component: from,
|
||||||
generate,
|
generate,
|
||||||
pathname: pathname || void 0,
|
pathname: pathname || void 0,
|
||||||
prerender: false,
|
prerender: false,
|
||||||
redirect: to
|
redirect: to,
|
||||||
|
redirectRoute: routes.find(r => r.route === to)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
25
packages/astro/test/fixtures/ssr-redirect/src/pages/articles/[...slug].astro
vendored
Normal file
25
packages/astro/test/fixtures/ssr-redirect/src/pages/articles/[...slug].astro
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const posts = [
|
||||||
|
{ slug: 'one', data: {draft: false, title: 'One'} },
|
||||||
|
{ slug: 'two', data: {draft: false, title: 'Two'} }
|
||||||
|
];
|
||||||
|
return posts.map((post) => {
|
||||||
|
return {
|
||||||
|
params: { slug: post.slug },
|
||||||
|
props: { draft: post.data.draft, title: post.data.title },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const { slug } = Astro.params;
|
||||||
|
const { title } = Astro.props;
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{ title }</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{ title }</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
10
packages/astro/test/fixtures/ssr-redirect/src/pages/index.astro
vendored
Normal file
10
packages/astro/test/fixtures/ssr-redirect/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Testing</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Testing</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -45,14 +45,39 @@ describe('Astro.redirect', () => {
|
||||||
fixture = await loadFixture({
|
fixture = await loadFixture({
|
||||||
root: './fixtures/ssr-redirect/',
|
root: './fixtures/ssr-redirect/',
|
||||||
output: 'static',
|
output: 'static',
|
||||||
|
redirects: {
|
||||||
|
'/one': '/',
|
||||||
|
'/two': '/',
|
||||||
|
'/blog/[...slug]': '/articles/[...slug]'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
await fixture.build();
|
await fixture.build();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Includes the meta refresh tag.', async () => {
|
it('Includes the meta refresh tag in Astro.redirect pages', async () => {
|
||||||
const html = await fixture.readFile('/secret/index.html');
|
const html = await fixture.readFile('/secret/index.html');
|
||||||
expect(html).to.include('http-equiv="refresh');
|
expect(html).to.include('http-equiv="refresh');
|
||||||
expect(html).to.include('url=/login');
|
expect(html).to.include('url=/login');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Includes the meta refresh tag in `redirect` config pages', async () => {
|
||||||
|
let html = await fixture.readFile('/one/index.html');
|
||||||
|
expect(html).to.include('http-equiv="refresh');
|
||||||
|
expect(html).to.include('url=/');
|
||||||
|
|
||||||
|
html = await fixture.readFile('/two/index.html');
|
||||||
|
expect(html).to.include('http-equiv="refresh');
|
||||||
|
expect(html).to.include('url=/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Generates page for dynamic routes', async () => {
|
||||||
|
let html = await fixture.readFile('/blog/one/index.html');
|
||||||
|
expect(html).to.include('http-equiv="refresh');
|
||||||
|
expect(html).to.include('url=/articles/one');
|
||||||
|
|
||||||
|
html = await fixture.readFile('/blog/two/index.html');
|
||||||
|
expect(html).to.include('http-equiv="refresh');
|
||||||
|
expect(html).to.include('url=/articles/two');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -65,46 +65,18 @@ export async function createRedirects(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const pattern =
|
const pattern = generateDynamicPattern(route);
|
||||||
'/' +
|
|
||||||
route.segments
|
|
||||||
.map(([part]) => {
|
|
||||||
//(part.dynamic ? '*' : part.content)
|
|
||||||
if (part.dynamic) {
|
|
||||||
if (part.spread) {
|
|
||||||
return '*';
|
|
||||||
} else {
|
|
||||||
return ':' + part.content;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return part.content;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.join('/');
|
|
||||||
|
|
||||||
//if(kind === 'static') {
|
if (route.distURL) {
|
||||||
if(route.redirect) {
|
const targetRoute = route.redirectRoute ?? route;
|
||||||
definitions.push({
|
const targetPattern = generateDynamicPattern(targetRoute);
|
||||||
dynamic: true,
|
|
||||||
input: pattern,
|
|
||||||
target: route.redirect,
|
|
||||||
status: 301,
|
|
||||||
weight: 1
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(kind === 'static') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
else if (route.distURL) {
|
|
||||||
const target =
|
const target =
|
||||||
`${pattern}` + (config.build.format === 'directory' ? '/index.html' : '.html');
|
`${targetPattern}` + (config.build.format === 'directory' ? '/index.html' : '.html');
|
||||||
definitions.push({
|
definitions.push({
|
||||||
dynamic: true,
|
dynamic: true,
|
||||||
input: pattern,
|
input: pattern,
|
||||||
target,
|
target,
|
||||||
status: 200,
|
status: route.type === 'redirect' ? 301 : 200,
|
||||||
weight: 1,
|
weight: 1,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -127,6 +99,26 @@ export async function createRedirects(
|
||||||
await fs.promises.appendFile(_redirectsURL, _redirects, 'utf-8');
|
await fs.promises.appendFile(_redirectsURL, _redirects, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateDynamicPattern(route: RouteData) {
|
||||||
|
const pattern =
|
||||||
|
'/' +
|
||||||
|
route.segments
|
||||||
|
.map(([part]) => {
|
||||||
|
//(part.dynamic ? '*' : part.content)
|
||||||
|
if (part.dynamic) {
|
||||||
|
if (part.spread) {
|
||||||
|
return '*';
|
||||||
|
} else {
|
||||||
|
return ':' + part.content;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return part.content;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join('/');
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
|
||||||
function prettify(definitions: RedirectDefinition[]) {
|
function prettify(definitions: RedirectDefinition[]) {
|
||||||
let minInputLength = 4,
|
let minInputLength = 4,
|
||||||
minTargetLength = 4;
|
minTargetLength = 4;
|
||||||
|
|
|
@ -33,7 +33,10 @@ describe('SSG - Redirects', () => {
|
||||||
|
|
||||||
// This uses the dynamic Astro.redirect, so we don't know that it's a redirect
|
// This uses the dynamic Astro.redirect, so we don't know that it's a redirect
|
||||||
// until runtime. This is correct!
|
// until runtime. This is correct!
|
||||||
'/nope', '/.netlify/functions/entry', '200'
|
'/nope', '/.netlify/functions/entry', '200',
|
||||||
|
|
||||||
|
// A real route
|
||||||
|
'/team/articles/*', '/.netlify/functions/entry', '200',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const posts = [
|
||||||
|
{ slug: 'one', data: {draft: false, title: 'One'} },
|
||||||
|
{ slug: 'two', data: {draft: false, title: 'Two'} }
|
||||||
|
];
|
||||||
|
return posts.map((post) => {
|
||||||
|
return {
|
||||||
|
params: { slug: post.slug },
|
||||||
|
props: { draft: post.data.draft, title: post.data.title },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
const { slug } = Astro.params;
|
||||||
|
const { title } = Astro.props;
|
||||||
|
---
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{ title }</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{ title }</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,8 +1,6 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { load as cheerioLoad } from 'cheerio';
|
|
||||||
import { loadFixture, testIntegration } from './test-utils.js';
|
import { loadFixture, testIntegration } from './test-utils.js';
|
||||||
import { netlifyStatic } from '../../dist/index.js';
|
import { netlifyStatic } from '../../dist/index.js';
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
describe('SSG - Redirects', () => {
|
describe('SSG - Redirects', () => {
|
||||||
/** @type {import('../../../astro/test/test-utils').Fixture} */
|
/** @type {import('../../../astro/test/test-utils').Fixture} */
|
||||||
|
@ -16,7 +14,8 @@ describe('SSG - Redirects', () => {
|
||||||
site: `http://example.com`,
|
site: `http://example.com`,
|
||||||
integrations: [testIntegration()],
|
integrations: [testIntegration()],
|
||||||
redirects: {
|
redirects: {
|
||||||
'/other': '/'
|
'/other': '/',
|
||||||
|
'/blog/[...slug]': '/team/articles/[...slug]'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await fixture.build();
|
await fixture.build();
|
||||||
|
@ -26,8 +25,10 @@ describe('SSG - Redirects', () => {
|
||||||
let redirects = await fixture.readFile('/_redirects');
|
let redirects = await fixture.readFile('/_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',
|
||||||
'/other', '/', '301',
|
'/other', '/', '301',
|
||||||
'/nope', '/', '301'
|
'/nope', '/', '301',
|
||||||
|
'/team/articles/*', '/team/articles/*/index.html', '200'
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue