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 }) => {
|
'astro:build:done': async ({ routes, dir }) => {
|
||||||
await bundleServerEntry(_buildConfig, _vite);
|
await bundleServerEntry(_buildConfig, _vite);
|
||||||
await createEdgeManifest(routes, entryFile, _config.root);
|
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 }) => {
|
'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';
|
import fs from 'fs';
|
||||||
|
|
||||||
|
type RedirectDefinition = {
|
||||||
|
dynamic: boolean;
|
||||||
|
input: string;
|
||||||
|
target: string;
|
||||||
|
weight: 0 | 1;
|
||||||
|
status: 200 | 404;
|
||||||
|
};
|
||||||
|
|
||||||
export async function createRedirects(
|
export async function createRedirects(
|
||||||
|
config: AstroConfig,
|
||||||
routes: RouteData[],
|
routes: RouteData[],
|
||||||
dir: URL,
|
dir: URL,
|
||||||
entryFile: string,
|
entryFile: string,
|
||||||
|
@ -10,37 +19,116 @@ export async function createRedirects(
|
||||||
const _redirectsURL = new URL('./_redirects', dir);
|
const _redirectsURL = new URL('./_redirects', dir);
|
||||||
const kind = edge ? 'edge-functions' : 'functions';
|
const kind = edge ? 'edge-functions' : 'functions';
|
||||||
|
|
||||||
// Create the redirects file that is used for routing.
|
const definitions: RedirectDefinition[] = [];
|
||||||
let _redirects = '';
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
if (route.pathname) {
|
if (route.pathname) {
|
||||||
if (route.distURL) {
|
if (route.distURL) {
|
||||||
_redirects += `
|
definitions.push({
|
||||||
${route.pathname} /${route.distURL.toString().replace(dir.toString(), '')} 200`;
|
dynamic: false,
|
||||||
|
input: route.pathname,
|
||||||
|
target: prependForwardSlash(route.distURL.toString().replace(dir.toString(), '')),
|
||||||
|
status: 200,
|
||||||
|
weight: 1
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
_redirects += `
|
definitions.push({
|
||||||
${route.pathname} /.netlify/${kind}/${entryFile} 200`;
|
dynamic: false,
|
||||||
|
input: route.pathname,
|
||||||
|
target: `/.netlify/${kind}/${entryFile}`,
|
||||||
|
status: 200,
|
||||||
|
weight: 1,
|
||||||
|
});
|
||||||
|
|
||||||
if (route.route === '/404') {
|
if (route.route === '/404') {
|
||||||
_redirects += `
|
definitions.push({
|
||||||
/* /.netlify/${kind}/${entryFile} 404`;
|
dynamic: true,
|
||||||
|
input: '/*',
|
||||||
|
target: `/.netlify/${kind}/${entryFile}`,
|
||||||
|
status: 404,
|
||||||
|
weight: 0
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const pattern =
|
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) {
|
if (route.distURL) {
|
||||||
_redirects += `
|
const target = `${pattern}` + (config.build.format === 'directory' ? '/index.html' : '.html');
|
||||||
${pattern} /${route.distURL.toString().replace(dir.toString(), '')} 200`;
|
definitions.push({
|
||||||
|
dynamic: true,
|
||||||
|
input: pattern,
|
||||||
|
target,
|
||||||
|
status: 200,
|
||||||
|
weight: 1
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
_redirects += `
|
definitions.push({
|
||||||
${pattern} /.netlify/${kind}/${entryFile} 200`;
|
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,
|
// 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.
|
// 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.
|
// If the file does not exist yet, appendFile() automatically creates it.
|
||||||
await fs.promises.appendFile(_redirectsURL, _redirects, 'utf-8');
|
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 () => {
|
it('Dynamic pages are included in the redirects file', async () => {
|
||||||
const redir = await fixture.readFile('/_redirects');
|
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…
Add table
Reference in a new issue