Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support prerender in Netlify redirects #5904

Merged
merged 3 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
111 changes: 97 additions & 14 deletions packages/integrations/netlify/src/shared.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import type { RouteData } from 'astro';
import type { AstroConfig, RouteData } from 'astro';
import fs from 'fs';

type RedirectDefinition = {
dynamic: boolean;
input: string;
target: string;
status: 200 | 404;
};

export async function createRedirects(
config: AstroConfig,
routes: RouteData[],
dir: URL,
entryFile: string,
Expand All @@ -10,37 +18,112 @@ 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: route.distURL.toString().replace(dir.toString(), ''),
status: 200
});
} else {
_redirects += `
${route.pathname} /.netlify/${kind}/${entryFile} 200`;
definitions.push({
dynamic: false,
input: route.pathname,
target: `/.netlify/${kind}/${entryFile}`,
status: 200
});

if (route.route === '/404') {
_redirects += `
/* /.netlify/${kind}/${entryFile} 404`;
definitions.push({
dynamic: true,
input: '/*',
target: `/.netlify/${kind}/${entryFile}`,
status: 404
});
}
}
} 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
});
} else {
_redirects += `
${pattern} /.netlify/${kind}/${entryFile} 200`;
definitions.push({
dynamic: true,
input: pattern,
target: `/.netlify/${kind}/${entryFile}`,
status: 200
});
}
}
}

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;
} else if(b.input.length > minInputLength) {
minInputLength = b.input.length;
}

// Same for the target
if(a.target.length > minTargetLength) {
minTargetLength = a.target.length;
} else if(b.target.length > minTargetLength) {
minTargetLength = b.target.length;
}

// Sort dynamic routes on top
if(a.dynamic === b.dynamic) {
// If both are the same, sort alphabetically
return a.input > b.input ? 1 : -1;
} else if(a.dynamic) {
return -1;
} else {
return 1;
}
});

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;
}
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');
Comment on lines +29 to +30
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will these conflict? I expect that they might and only /pets/:cat would be matched.

Since all of these are generating actual files we might need to track any files that the dynamic route has generated, then add redirects for the exact route?

expect(redir).to.include('/pets/cat1       /pets/cat1/index.html        200');
expect(redir).to.include('/pets/cat2       /pets/cat2/index.html        200');
expect(redir).to.include('/pets/cat3       /pets/cat3/index.html        200');
expect(redir).to.include('/pets/dog1       /pets/dog1/index.html        200');
expect(redir).to.include('/pets/dog2       /pets/dog2/index.html        200');
expect(redir).to.include('/pets/dog3       /pets/dog3/index.html        200');

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They do conflict in an SSR context but in SSG since they are pre-generated they are just redundant. Meaning Netlify is going to use the first one and ignore the second; but that doesn't matter because it will go to the right file anyways.

The user should probably combine these into 1 route, but we don't require that at the moment so this is a valid (if odd) way to split up different "types" of data in SSG.

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>