Skip to content

Commit

Permalink
Support prerender in Netlify redirects (#5904)
Browse files Browse the repository at this point in the history
* Support prerender in Netlify redirects

* Updated sorting algorithm

* Update packages/integrations/netlify/src/shared.ts

Co-authored-by: Bjorn Lu <[email protected]>

Co-authored-by: Bjorn Lu <[email protected]>
  • Loading branch information
matthewp and bluwy committed Jan 26, 2023
1 parent 2dd458d commit 803cd0f
Show file tree
Hide file tree
Showing 12 changed files with 240 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/real-rules-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/netlify': patch
---

Support prerender in \_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
},
};
Expand Down
2 changes: 1 addition & 1 deletion packages/integrations/netlify/src/integration-functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
},
};
Expand Down
116 changes: 102 additions & 14 deletions packages/integrations/netlify/src/shared.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>testing</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>testing</h1>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
export const prerender = true;
---
<html>
<head>
<title>Testing</title>
</head>
<body>
<h1>testing</h1>
</body>
</html>
30 changes: 30 additions & 0 deletions packages/integrations/netlify/test/functions/prerender.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit 803cd0f

Please sign in to comment.