Support prerender in Netlify redirects (#5904)
* Support prerender in Netlify redirects * Updated sorting algorithm * Update packages/integrations/netlify/src/shared.ts Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com> Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
This commit is contained in:
parent
9e57268f13
commit
f5adbd6b55
12 changed files with 240 additions and 17 deletions
5
.changeset/real-rules-occur.md
Normal file
5
.changeset/real-rules-occur.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/netlify': patch
|
||||
---
|
||||
|
||||
Support prerender in \_redirects
|
|
@ -163,7 +163,7 @@ export function netlifyEdgeFunctions({ dist }: NetlifyEdgeFunctionsOptions = {})
|
|||
'astro:build:done': async ({ routes, dir }) => {
|
||||
await bundleServerEntry(_buildConfig, _vite);
|
||||
await createEdgeManifest(routes, entryFile, _config.root);
|
||||
await createRedirects(routes, dir, entryFile, true);
|
||||
await createRedirects(_config, routes, dir, entryFile, true);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -48,7 +48,7 @@ function netlifyFunctions({
|
|||
}
|
||||
},
|
||||
'astro:build:done': async ({ routes, dir }) => {
|
||||
await createRedirects(routes, dir, entryFile, false);
|
||||
await createRedirects(_config, routes, dir, entryFile, false);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,7 +1,16 @@
|
|||
import type { RouteData } from 'astro';
|
||||
import type { AstroConfig, RouteData } from 'astro';
|
||||
import fs from 'fs';
|
||||
|
||||
type RedirectDefinition = {
|
||||
dynamic: boolean;
|
||||
input: string;
|
||||
target: string;
|
||||
weight: 0 | 1;
|
||||
status: 200 | 404;
|
||||
};
|
||||
|
||||
export async function createRedirects(
|
||||
config: AstroConfig,
|
||||
routes: RouteData[],
|
||||
dir: URL,
|
||||
entryFile: string,
|
||||
|
@ -10,37 +19,116 @@ export async function createRedirects(
|
|||
const _redirectsURL = new URL('./_redirects', dir);
|
||||
const kind = edge ? 'edge-functions' : 'functions';
|
||||
|
||||
// Create the redirects file that is used for routing.
|
||||
let _redirects = '';
|
||||
const definitions: RedirectDefinition[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
if (route.pathname) {
|
||||
if (route.distURL) {
|
||||
_redirects += `
|
||||
${route.pathname} /${route.distURL.toString().replace(dir.toString(), '')} 200`;
|
||||
definitions.push({
|
||||
dynamic: false,
|
||||
input: route.pathname,
|
||||
target: prependForwardSlash(route.distURL.toString().replace(dir.toString(), '')),
|
||||
status: 200,
|
||||
weight: 1
|
||||
});
|
||||
} else {
|
||||
_redirects += `
|
||||
${route.pathname} /.netlify/${kind}/${entryFile} 200`;
|
||||
definitions.push({
|
||||
dynamic: false,
|
||||
input: route.pathname,
|
||||
target: `/.netlify/${kind}/${entryFile}`,
|
||||
status: 200,
|
||||
weight: 1,
|
||||
});
|
||||
|
||||
if (route.route === '/404') {
|
||||
_redirects += `
|
||||
/* /.netlify/${kind}/${entryFile} 404`;
|
||||
definitions.push({
|
||||
dynamic: true,
|
||||
input: '/*',
|
||||
target: `/.netlify/${kind}/${entryFile}`,
|
||||
status: 404,
|
||||
weight: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const pattern =
|
||||
'/' + route.segments.map(([part]) => (part.dynamic ? '*' : part.content)).join('/');
|
||||
'/' + 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 (route.distURL) {
|
||||
_redirects += `
|
||||
${pattern} /${route.distURL.toString().replace(dir.toString(), '')} 200`;
|
||||
const target = `${pattern}` + (config.build.format === 'directory' ? '/index.html' : '.html');
|
||||
definitions.push({
|
||||
dynamic: true,
|
||||
input: pattern,
|
||||
target,
|
||||
status: 200,
|
||||
weight: 1
|
||||
});
|
||||
} else {
|
||||
_redirects += `
|
||||
${pattern} /.netlify/${kind}/${entryFile} 200`;
|
||||
definitions.push({
|
||||
dynamic: true,
|
||||
input: pattern,
|
||||
target: `/.netlify/${kind}/${entryFile}`,
|
||||
status: 200,
|
||||
weight: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _redirects = prettify(definitions);
|
||||
|
||||
// Always use appendFile() because the redirects file could already exist,
|
||||
// e.g. due to a `/public/_redirects` file that got copied to the output dir.
|
||||
// If the file does not exist yet, appendFile() automatically creates it.
|
||||
await fs.promises.appendFile(_redirectsURL, _redirects, 'utf-8');
|
||||
}
|
||||
|
||||
function prettify(definitions: RedirectDefinition[]) {
|
||||
let minInputLength = 0, minTargetLength = 0;
|
||||
definitions.sort((a, b) => {
|
||||
// Find the longest input, so we can format things nicely
|
||||
if(a.input.length > minInputLength) {
|
||||
minInputLength = a.input.length;
|
||||
}
|
||||
if(b.input.length > minInputLength) {
|
||||
minInputLength = b.input.length;
|
||||
}
|
||||
|
||||
// Same for the target
|
||||
if(a.target.length > minTargetLength) {
|
||||
minTargetLength = a.target.length;
|
||||
}
|
||||
if(b.target.length > minTargetLength) {
|
||||
minTargetLength = b.target.length;
|
||||
}
|
||||
|
||||
// Sort dynamic routes on top
|
||||
return b.weight - a.weight;
|
||||
});
|
||||
|
||||
let _redirects = '';
|
||||
// Loop over the definitions
|
||||
definitions.forEach((defn, i) => {
|
||||
// Figure out the number of spaces to add. We want at least 4 spaces
|
||||
// after the input. This ensure that all targets line up together.
|
||||
let inputSpaces = (minInputLength - defn.input.length) + 4;
|
||||
let targetSpaces = (minTargetLength - defn.target.length) + 4;
|
||||
_redirects += (i === 0 ? '' : '\n') + defn.input + ' '.repeat(inputSpaces) + defn.target + ' '.repeat(Math.abs(targetSpaces)) + defn.status;
|
||||
});
|
||||
return _redirects;
|
||||
}
|
||||
|
||||
function prependForwardSlash(str: string) {
|
||||
return str[0] === '/' ? str : '/' + str;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,13 @@ describe('Dynamic pages', () => {
|
|||
|
||||
it('Dynamic pages are included in the redirects file', async () => {
|
||||
const redir = await fixture.readFile('/_redirects');
|
||||
expect(redir).to.match(/\/products\/\*/);
|
||||
expect(redir).to.match(/\/products\/:id/);
|
||||
});
|
||||
|
||||
it('Prerendered routes are also included using placeholder syntax', async () => {
|
||||
const redir = await fixture.readFile('/_redirects');
|
||||
expect(redir).to.include('/pets/:cat /pets/:cat/index.html 200');
|
||||
expect(redir).to.include('/pets/:dog /pets/:dog/index.html 200');
|
||||
expect(redir).to.include('/pets /.netlify/functions/entry 200');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
export const prerender = true
|
||||
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{
|
||||
params: {cat: 'cat1'},
|
||||
props: {cat: 'cat1'}
|
||||
},
|
||||
{
|
||||
params: {cat: 'cat2'},
|
||||
props: {cat: 'cat2'}
|
||||
},
|
||||
{
|
||||
params: {cat: 'cat3'},
|
||||
props: {cat: 'cat3'}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const { cat } = Astro.props;
|
||||
|
||||
---
|
||||
|
||||
<div>Good cat, {cat}!</div>
|
||||
|
||||
<a href="/">back</a>
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
export const prerender = true
|
||||
|
||||
export function getStaticPaths() {
|
||||
return [
|
||||
{
|
||||
params: {dog: 'dog1'},
|
||||
props: {dog: 'dog1'}
|
||||
},
|
||||
{
|
||||
params: {dog: 'dog2'},
|
||||
props: {dog: 'dog2'}
|
||||
},
|
||||
{
|
||||
params: {dog: 'dog3'},
|
||||
props: {dog: 'dog3'}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const { dog } = Astro.props;
|
||||
|
||||
---
|
||||
|
||||
<div>Good dog, {dog}!</div>
|
||||
|
||||
<a href="/">back</a>
|
|
@ -0,0 +1,12 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Astro</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>testing</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>testing</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
export const prerender = true;
|
||||
---
|
||||
<html>
|
||||
<head>
|
||||
<title>Testing</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>testing</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,30 @@
|
|||
import { expect } from 'chai';
|
||||
import netlifyAdapter from '../../dist/index.js';
|
||||
import { loadFixture, testIntegration } from './test-utils.js';
|
||||
|
||||
describe('Mixed Prerendering with SSR', () => {
|
||||
/** @type {import('./test-utils').Fixture} */
|
||||
let fixture;
|
||||
|
||||
before(async () => {
|
||||
fixture = await loadFixture({
|
||||
root: new URL('./fixtures/prerender/', import.meta.url).toString(),
|
||||
output: 'server',
|
||||
adapter: netlifyAdapter({
|
||||
dist: new URL('./fixtures/prerender/dist/', import.meta.url),
|
||||
}),
|
||||
site: `http://example.com`,
|
||||
integrations: [testIntegration()],
|
||||
});
|
||||
await fixture.build();
|
||||
});
|
||||
it('Wildcard 404 is sorted last', async () => {
|
||||
const redir = await fixture.readFile('/_redirects');
|
||||
const baseRouteIndex = redir.indexOf('/ /.netlify/functions/entry 200');
|
||||
const oneRouteIndex = redir.indexOf('/one /one/index.html 200');
|
||||
const fourOhFourWildCardIndex = redir.indexOf('/* /.netlify/functions/entry 404');
|
||||
|
||||
expect(fourOhFourWildCardIndex).to.be.greaterThan(baseRouteIndex);
|
||||
expect(fourOhFourWildCardIndex).to.be.greaterThan(oneRouteIndex);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue