0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00

Handle getStaticPaths with prerendered pages (#5734)

* fix(#5661): ensure getStaticPaths is correctly handled for prerendered pages

* test: add prerender getStaticPaths cases

* chore: add changeset

* test: add props to test suite

* chore: update lockfile

Co-authored-by: Nate Moore <nate@astro.build>
This commit is contained in:
Nate Moore 2023-01-04 14:20:13 -05:00 committed by GitHub
parent cc4606d65e
commit 55cea0a9d8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 408 additions and 5 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix `prerender` when used with `getStaticPaths`

View file

@ -1004,6 +1004,7 @@ export type AsyncRendererComponentFn<U> = (
export interface ComponentInstance {
default: AstroComponentFactory;
css?: string[];
prerender?: boolean;
getStaticPaths?: (options: GetStaticPathsOptions) => GetStaticPathsResult;
}

View file

@ -181,7 +181,7 @@ async function getPathsForRoute(
route: pageData.route,
isValidate: false,
logging: opts.logging,
ssr: false,
ssr: opts.settings.config.output === 'server',
})
.then((_result) => {
const label = _result.staticPaths.length === 1 ? 'page' : 'pages';

View file

@ -139,6 +139,8 @@ function buildManifest(
const joinBase = (pth: string) => (bareBase ? bareBase + '/' + pth : pth);
for (const pageData of eachPrerenderedPageData(internals)) {
if (!pageData.route.pathname) continue;
const outFolder = getOutFolder(
opts.settings.config,
pageData.route.pathname!,

View file

@ -353,7 +353,7 @@ but ${plural ? 'none were.' : 'it was not.'} able to server-side render \`${comp
'`getStaticPaths()` function is required for dynamic routes. Make sure that you `export` a `getStaticPaths` function from your dynamic route.',
hint: `See https://docs.astro.build/en/core-concepts/routing/#dynamic-routes for more information on dynamic routes.
Alternatively, set \`output: "server"\` in your Astro config file to switch to a non-static server build.
Alternatively, set \`output: "server"\` in your Astro config file to switch to a non-static server build. This error can also occur if using \`export const prerender = true;\`.
See https://docs.astro.build/en/guides/server-side-rendering/ for more information on non-static rendering.`,
},
/**

View file

@ -31,7 +31,7 @@ export async function callGetStaticPaths({
}: CallGetStaticPathsOptions): Promise<RouteCacheEntry> {
validateDynamicRouteModule(mod, { ssr, logging, route });
// No static paths in SSR mode. Return an empty RouteCacheEntry.
if (ssr) {
if (ssr && !mod.prerender) {
return { staticPaths: Object.assign([], { keyed: new Map() }) };
}
// Add a check here to make TypeScript happy.

View file

@ -31,10 +31,10 @@ export function validateDynamicRouteModule(
route: RouteData;
}
) {
if (ssr && mod.getStaticPaths) {
if (ssr && mod.getStaticPaths && !mod.prerender) {
warn(logging, 'getStaticPaths', 'getStaticPaths() is ignored when "output: server" is set.');
}
if (!ssr && !mod.getStaticPaths) {
if ((!ssr || mod.prerender) && !mod.getStaticPaths) {
throw new AstroError({
...AstroErrorData.GetStaticPathsRequired,
location: { file: route.component },

View file

@ -0,0 +1,8 @@
{
"name": "@test/ssr-prerender-get-static-paths",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,22 @@
---
export function getStaticPaths({ paginate }) {
if (globalThis.isCalledOnce) {
throw new Error("Can only be called once!");
}
globalThis.isCalledOnce = true;
return [
{params: {calledTwiceTest: 'a'}},
{params: {calledTwiceTest: 'b'}},
{params: {calledTwiceTest: 'c'}},
];
}
export const prerender = true;
const { params } = Astro;
---
<html>
<head>
<title>Page {params.calledTwiceTest}</title>
</head>
<body></body>
</html>

View file

@ -0,0 +1,19 @@
---
export async function getStaticPaths() {
return [
{ params: { year: '2022', slug: 'post-1' } },
{ params: { year: 2022, slug: 'post-2' } },
{ params: { slug: 'post-2', year: '2022' } },
]
}
export const prerender = true;
const { year, slug } = Astro.params
---
<html>
<head>
<title>{year} | {slug}</title>
</head>
<body></body>
</html>

View file

@ -0,0 +1,16 @@
export const prerender = true;
export async function getStaticPaths() {
return [
{ params: { slug: 'thing1' } },
{ params: { slug: 'thing2' } }
];
}
export async function get() {
return {
body: JSON.stringify({
title: '[slug]'
}, null, 4)
};
}

View file

@ -0,0 +1,34 @@
---
export async function getStaticPaths() {
return [
{
params: { name: 'tacos' },
props: { yum: 10 },
},
{
params: { name: 'potatoes' },
props: { yum: 7 },
},
{
params: { name: 'spaghetti' },
props: { yum: 5 },
}
]
}
export const prerender = true;
const { yum } = Astro.props;
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Food</title>
</head>
<body>
<p id="url">{ Astro.url.pathname }</p>
<p id="props">{ yum }</p>
</body>
</html>

View file

@ -0,0 +1,10 @@
---
export function getStaticPaths() {
return [
[ { params: {slug: "slug1"} } ],
[ { params: {slug: "slug2"} } ],
]
}
export const prerender = true;
---

View file

@ -0,0 +1,23 @@
---
export function getStaticPaths() {
return [{
params: { pizza: 'papa-johns' },
}, {
params: { pizza: 'dominos' },
}, {
params: { pizza: 'grimaldis/new-york' },
}]
}
export const prerender = true;
const { pizza } = Astro.params
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{pizza ?? 'The landing page'}</title>
</head>
<body>
<h1>Welcome to {pizza ?? 'The landing page'}</h1>
</body>
</html>

View file

@ -0,0 +1,22 @@
---
export function getStaticPaths() {
return [{
params: { cheese: 'mozzarella', topping: 'pepperoni' },
}, {
params: { cheese: 'provolone', topping: 'sausage' },
}]
}
export const prerender = true;
const { cheese, topping } = Astro.params
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>{cheese}</title>
</head>
<body>
<h1>🍕 It's pizza time</h1>
<p>{cheese}-{topping}</p>
</body>
</html>

View file

@ -0,0 +1,30 @@
---
export async function getStaticPaths() {
return [
{
params: { page: 1 },
},
{
params: { page: 2 },
},
{
params: { page: 3 }
}
]
};
export const prerender = true;
const { page } = Astro.params
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Posts Page {page}</title>
<link rel="canonical" href={canonicalURL.href}>
</head>
<body>
<h1>Welcome to page {page}</h1>
</body>
</html>

View file

@ -0,0 +1,202 @@
import { expect } from 'chai';
import { loadFixture } from './test-utils.js';
import * as cheerio from 'cheerio';
describe('prerender getStaticPaths - build calls', () => {
before(async () => {
// reset the flag used by [...calledTwiceTest].astro between each test
globalThis.isCalledOnce = false;
const fixture = await loadFixture({
root: './fixtures/ssr-prerender-get-static-paths/',
site: 'https://mysite.dev/',
base: '/blog',
});
await fixture.build();
});
it('is only called once during build', () => {
// useless expect; if build() throws in setup then this test fails
expect(true).to.equal(true);
});
});
describe('prerender getStaticPaths - dev calls', () => {
let fixture;
let devServer;
before(async () => {
// reset the flag used by [...calledTwiceTest].astro between each test
globalThis.isCalledOnce = false;
fixture = await loadFixture({ root: './fixtures/ssr-prerender-get-static-paths/' });
devServer = await fixture.startDevServer();
});
after(async () => {
devServer.stop();
});
it('only calls prerender getStaticPaths once', async () => {
let res = await fixture.fetch('/a');
expect(res.status).to.equal(200);
res = await fixture.fetch('/b');
expect(res.status).to.equal(200);
res = await fixture.fetch('/c');
expect(res.status).to.equal(200);
});
});
describe('prerender getStaticPaths - 404 behavior', () => {
let fixture;
let devServer;
before(async () => {
// reset the flag used by [...calledTwiceTest].astro between each test
globalThis.isCalledOnce = false;
fixture = await loadFixture({ root: './fixtures/ssr-prerender-get-static-paths/' });
devServer = await fixture.startDevServer();
});
after(async () => {
devServer.stop();
});
it('resolves 200 on matching static path - named params', async () => {
const res = await fixture.fetch('/pizza/provolone-sausage');
expect(res.status).to.equal(200);
});
it('resolves 404 on pattern match without static path - named params', async () => {
const res = await fixture.fetch('/pizza/provolone-pineapple');
expect(res.status).to.equal(404);
});
it('resolves 200 on matching static path - rest params', async () => {
const res = await fixture.fetch('/pizza/grimaldis/new-york');
expect(res.status).to.equal(200);
});
it('resolves 404 on pattern match without static path - rest params', async () => {
const res = await fixture.fetch('/pizza/pizza-hut');
expect(res.status).to.equal(404);
});
});
describe('prerender getStaticPaths - route params type validation', () => {
let fixture, devServer;
before(async () => {
// reset the flag used by [...calledTwiceTest].astro between each test
globalThis.isCalledOnce = false;
fixture = await loadFixture({ root: './fixtures/ssr-prerender-get-static-paths/' });
devServer = await fixture.startDevServer();
});
after(async () => {
await devServer.stop();
});
it('resolves 200 on nested array parameters', async () => {
const res = await fixture.fetch('/nested-arrays/slug1');
expect(res.status).to.equal(200);
});
it('resolves 200 on matching static path - string params', async () => {
// route provided with { params: { year: "2022", slug: "post-2" }}
const res = await fixture.fetch('/blog/2022/post-1');
expect(res.status).to.equal(200);
});
it('resolves 200 on matching static path - numeric params', async () => {
// route provided with { params: { year: 2022, slug: "post-2" }}
const res = await fixture.fetch('/blog/2022/post-2');
expect(res.status).to.equal(200);
});
});
describe('prerender getStaticPaths - numeric route params', () => {
let fixture;
let devServer;
before(async () => {
// reset the flag used by [...calledTwiceTest].astro between each test
globalThis.isCalledOnce = false;
fixture = await loadFixture({
root: './fixtures/ssr-prerender-get-static-paths/',
site: 'https://mysite.dev/',
});
devServer = await fixture.startDevServer();
});
after(async () => {
await devServer.stop();
});
it('resolves 200 on matching static paths', async () => {
// routes params provided for pages /posts/1, /posts/2, and /posts/3
for (const page of [1, 2, 3]) {
let res = await fixture.fetch(`/posts/${page}`);
expect(res.status).to.equal(200);
const html = await res.text();
const $ = cheerio.load(html);
const canonical = $('link[rel=canonical]');
expect(canonical.attr('href')).to.equal(
`https://mysite.dev/posts/${page}`,
`doesn't trim the /${page} route param`
);
}
});
});
describe('prerender getStaticPaths - Astro.url', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
// reset the flag used by [...calledTwiceTest].astro between each test
globalThis.isCalledOnce = false;
fixture = await loadFixture({
root: './fixtures/ssr-prerender-get-static-paths/',
site: 'https://mysite.dev/',
});
await fixture.build();
});
it('Sets the current pathname', async () => {
const html = await fixture.readFile('/food/tacos/index.html');
const $ = cheerio.load(html);
expect($('#url').text()).to.equal('/food/tacos/');
});
});
describe('prerender getStaticPaths - props', () => {
/** @type {import('./test-utils').Fixture} */
let fixture;
before(async () => {
// reset the flag used by [...calledTwiceTest].astro between each test
globalThis.isCalledOnce = false;
fixture = await loadFixture({
root: './fixtures/ssr-prerender-get-static-paths/',
site: 'https://mysite.dev/',
});
await fixture.build();
});
it('Sets the current pathname', async () => {
const html = await fixture.readFile('/food/tacos/index.html');
const $ = cheerio.load(html);
expect($('#props').text()).to.equal('10');
});
});

View file

@ -1117,6 +1117,9 @@ importers:
'@astrojs/node': link:../../../../integrations/node
astro: link:../../..
packages/astro/test/benchmark/simple/dist/server:
specifiers: {}
packages/astro/test/fixtures/0-css:
specifiers:
'@astrojs/react': workspace:*
@ -2354,6 +2357,12 @@ importers:
dependencies:
astro: link:../../..
packages/astro/test/fixtures/ssr-prerender-get-static-paths:
specifiers:
astro: workspace:*
dependencies:
astro: link:../../..
packages/astro/test/fixtures/ssr-preview:
specifiers:
astro: workspace:*