Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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/ssg-crashed-worker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/router': patch
---

fix: a crashed ssg worker no longer stalls the build until it times out
5 changes: 5 additions & 0 deletions .changeset/ssg-fail-5xx-routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/router': patch
---

fix: ssg no longer silently skips routes that render a 5xx error page
5 changes: 5 additions & 0 deletions .changeset/ssg-retries-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/router': minor
---

feat: add the ssg `retries` option to re-render a failed route before the build fails
5 changes: 5 additions & 0 deletions e2e/qwik-e2e/apps/qwikrouter-ssg-retries/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"description": "Qwik Router SSG retries fixture",
"private": true,
"type": "module"
}
13 changes: 13 additions & 0 deletions e2e/qwik-e2e/apps/qwikrouter-ssg-retries/src/entry.ssr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createRenderer } from '@qwik.dev/router';
import Root from './root';

export default createRenderer((opts) => ({
jsx: <Root />,
options: {
...opts,
containerAttributes: {
lang: 'en',
...opts.containerAttributes,
},
},
}));
18 changes: 18 additions & 0 deletions e2e/qwik-e2e/apps/qwikrouter-ssg-retries/src/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { component$ } from '@qwik.dev/core';
import { DocumentHeadTags, RouterOutlet, useQwikRouter } from '@qwik.dev/router';

export default component$(() => {
useQwikRouter();

return (
<>
<head>
<meta charset="utf-8" />
<DocumentHeadTags />
</head>
<body>
<RouterOutlet />
</body>
</>
);
});
17 changes: 17 additions & 0 deletions e2e/qwik-e2e/apps/qwikrouter-ssg-retries/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { component$ } from '@qwik.dev/core';
import { routeLoader$ } from '@qwik.dev/router';

export const useFlakyData = routeLoader$(async (requestEvent) => {
const { existsSync, writeFileSync } = await import('node:fs');
const marker = requestEvent.env.get('SSG_RETRIES_MARKER')!;
if (!existsSync(marker)) {
writeFileSync(marker, 'failed once');
throw requestEvent.error(503, 'Service unavailable');
}
return 'rendered after a retry';
});

export default component$(() => {
const data = useFlakyData();
return <p>{data.value}</p>;
});
80 changes: 80 additions & 0 deletions e2e/qwik-e2e/tests/qwikrouter/ssg-retries.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { expect, test } from '@playwright/test';
import { qwikVite } from '@qwik.dev/core/optimizer';
import { ssgAdapter } from '@qwik.dev/router/adapters/ssg/vite';
import { qwikRouter } from '@qwik.dev/router/vite';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { build, createBuilder, type InlineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const repoRoot = resolve(__dirname, '../../../../');
const appDir = resolve(repoRoot, 'e2e/qwik-e2e/apps/qwikrouter-ssg-retries');
const distDir = resolve(appDir, 'dist');
const serverDir = resolve(appDir, 'server');

test.describe('router ssg retries', () => {
// Both tests build into the same `distDir`/`serverDir`, so they must not run in parallel.
test.describe.configure({ mode: 'serial', timeout: 120_000 });

test.skip(({ browserName }) => browserName !== 'chromium', 'Runs once in Chromium e2e.');

test('fails the build when the route fails and retries are off', async () => {
await expect(buildFixtureApp({ retries: 0 })).rejects.toThrow(
'At least one path failed to render'
);
});

test('re-renders a route that failed once when retries are enabled', async () => {
await buildFixtureApp({ retries: 1 });

const html = await readFile(resolve(distDir, 'index.html'), 'utf-8');
expect(html).toContain('rendered after a retry');
});
});

async function buildFixtureApp({ retries }: { retries: number }) {
await rm(distDir, { recursive: true, force: true });
await rm(serverDir, { recursive: true, force: true });
const markerDir = await mkdtemp(join(tmpdir(), 'qwik-router-ssg-retries-'));
process.env.SSG_RETRIES_MARKER = join(markerDir, 'failed-once');

const getConfig = (extra?: InlineConfig): InlineConfig => ({
root: appDir,
mode: 'production',
configFile: false,
clearScreen: false,
logLevel: 'error',
...extra,
});

try {
await build(
getConfig({
plugins: [
qwikRouter(),
tsconfigPaths({ root: '.' }),
qwikVite({ tsOptimizer: true, client: { outDir: distDir } }),
],
})
);

const builder = await createBuilder(
getConfig({
build: { ssr: true, outDir: serverDir },
plugins: [
qwikRouter(),
tsconfigPaths({ root: '.' }),
qwikVite({ tsOptimizer: true }),
ssgAdapter({ origin: 'https://retries.qwik.dev', include: ['/*'], retries }),
],
})
);
await builder.buildApp();
} finally {
delete process.env.SSG_RETRIES_MARKER;
await rm(markerDir, { recursive: true, force: true });
}
}
4 changes: 2 additions & 2 deletions packages/docs/src/routes/api/qwik-router-ssg/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
}
],
"kind": "Interface",
"content": "```typescript\nexport interface SsgRenderOptions extends RenderOptions \n```\n**Extends:** RenderOptions\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nemitData?\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Set to `false` if the generated per-loader data files should not be written to disk. Defaults to `true`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nemitHtml?\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Set to `false` if the generated static HTML files should not be written to disk. Setting to `false` is useful if the SSG should only write the per-loader data files to disk. Defaults to `true`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nexclude?\n\n\n</td><td>\n\n\n</td><td>\n\nstring\\[\\]\n\n\n</td><td>\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should not be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. `exclude` always takes priority over `include`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\ninclude?\n\n\n</td><td>\n\n\n</td><td>\n\nstring\\[\\]\n\n\n</td><td>\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. If not provided, all routes will be static generated. `exclude` always takes priority over `include`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nlog?\n\n\n</td><td>\n\n\n</td><td>\n\n'debug' \\| 'quiet'\n\n\n</td><td>\n\n_(Optional)_ Log level. `'quiet'` suppresses per-page output, `'debug'` enables verbose logging.\n\n\n</td></tr>\n<tr><td>\n\nmaxTasksPerWorker?\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n_(Optional)_ Maximum number of tasks to be running at one time per worker. Defaults to `20`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nmaxWorkers?\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n_(Optional)_ Maximum number of workers to use while generating the static pages. Defaults to the number of CPUs available.\n\n\n</td></tr>\n<tr><td>\n\norigin\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\nThe URL `origin`<!-- -->, which is a combination of the scheme (protocol) and hostname (domain). For example, `https://qwik.dev` has the protocol `https://` and domain `qwik.dev`<!-- -->. However, the `origin` does not include a `pathname`<!-- -->.\n\nThe `origin` is used to provide a full URL during Static Site Generation (SSG), and to simulate a complete URL rather than just the `pathname`<!-- -->. For example, in order to render a correct canonical tag URL or URLs within the `sitemap.xml`<!-- -->, the `origin` must be provided too.\n\nIf the site also starts with a pathname other than `/`<!-- -->, please use the `basePathname` option in the Qwik Router config options.\n\n\n</td></tr>\n<tr><td>\n\noutDir\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\nFile system directory where the static files should be written.\n\n\n</td></tr>\n<tr><td>\n\nsitemapOutFile?\n\n\n</td><td>\n\n\n</td><td>\n\nstring \\| null\n\n\n</td><td>\n\n_(Optional)_ File system path to write the `sitemap.xml` to. Defaults to `sitemap.xml` and written to the root of the `outDir`<!-- -->. Setting to `null` will prevent the sitemap from being created.\n\n\n</td></tr>\n</tbody></table>",
"content": "```typescript\nexport interface SsgRenderOptions extends RenderOptions \n```\n**Extends:** RenderOptions\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nemitData?\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Set to `false` if the generated per-loader data files should not be written to disk. Defaults to `true`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nemitHtml?\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n_(Optional)_ Set to `false` if the generated static HTML files should not be written to disk. Setting to `false` is useful if the SSG should only write the per-loader data files to disk. Defaults to `true`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nexclude?\n\n\n</td><td>\n\n\n</td><td>\n\nstring\\[\\]\n\n\n</td><td>\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should not be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. `exclude` always takes priority over `include`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\ninclude?\n\n\n</td><td>\n\n\n</td><td>\n\nstring\\[\\]\n\n\n</td><td>\n\n_(Optional)_ Defines file system routes relative to the source `routes` directory that should be static generated. Accepts wildcard behavior. This should not include the \"base\" pathname. If not provided, all routes will be static generated. `exclude` always takes priority over `include`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nlog?\n\n\n</td><td>\n\n\n</td><td>\n\n'debug' \\| 'quiet'\n\n\n</td><td>\n\n_(Optional)_ Log level. `'quiet'` suppresses per-page output, `'debug'` enables verbose logging.\n\n\n</td></tr>\n<tr><td>\n\nmaxTasksPerWorker?\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n_(Optional)_ Maximum number of tasks to be running at one time per worker. Defaults to `20`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\nmaxWorkers?\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n_(Optional)_ Maximum number of workers to use while generating the static pages. Defaults to the number of CPUs available.\n\n\n</td></tr>\n<tr><td>\n\norigin\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\nThe URL `origin`<!-- -->, which is a combination of the scheme (protocol) and hostname (domain). For example, `https://qwik.dev` has the protocol `https://` and domain `qwik.dev`<!-- -->. However, the `origin` does not include a `pathname`<!-- -->.\n\nThe `origin` is used to provide a full URL during Static Site Generation (SSG), and to simulate a complete URL rather than just the `pathname`<!-- -->. For example, in order to render a correct canonical tag URL or URLs within the `sitemap.xml`<!-- -->, the `origin` must be provided too.\n\nIf the site also starts with a pathname other than `/`<!-- -->, please use the `basePathname` option in the Qwik Router config options.\n\n\n</td></tr>\n<tr><td>\n\noutDir\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\nFile system directory where the static files should be written.\n\n\n</td></tr>\n<tr><td>\n\nretries?\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n_(Optional)_ Number of times to retry a route whose render failed or timed out, or an `onStaticGenerate` call that threw, before recording the error. Each retry waits 500ms, doubling up to 2s, plus up to 30% at random. Defaults to `0` (the first failure is final).\n\n\n</td></tr>\n<tr><td>\n\nsitemapOutFile?\n\n\n</td><td>\n\n\n</td><td>\n\nstring \\| null\n\n\n</td><td>\n\n_(Optional)_ File system path to write the `sitemap.xml` to. Defaults to `sitemap.xml` and written to the root of the `outDir`<!-- -->. Setting to `null` will prevent the sitemap from being created.\n\n\n</td></tr>\n</tbody></table>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/ssg/types.ts",
"mdFile": "router.ssgrenderoptions.md"
},
Expand Down Expand Up @@ -96,7 +96,7 @@
}
],
"kind": "Interface",
"content": "```typescript\nexport interface SsgResult \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nduration\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nerrors\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nrendered\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nstaticPaths\n\n\n</td><td>\n\n\n</td><td>\n\nstring\\[\\]\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
"content": "```typescript\nexport interface SsgResult \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nduration\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nerrors\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nrendered\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nretried\n\n\n</td><td>\n\n\n</td><td>\n\nnumber\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nstaticPaths\n\n\n</td><td>\n\n\n</td><td>\n\nstring\\[\\]\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik-router/src/ssg/types.ts",
"mdFile": "router.staticgenerateresult.md"
}
Expand Down
28 changes: 28 additions & 0 deletions packages/docs/src/routes/api/qwik-router-ssg/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,21 @@ File system directory where the static files should be written.
</td></tr>
<tr><td>

retries?

</td><td>

</td><td>

number

</td><td>

_(Optional)_ Number of times to retry a route whose render failed or timed out, or an `onStaticGenerate` call that threw, before recording the error. Each retry waits 500ms, doubling up to 2s, plus up to 30% at random. Defaults to `0` (the first failure is final).

</td></tr>
<tr><td>

sitemapOutFile?

</td><td>
Expand Down Expand Up @@ -512,6 +527,19 @@ number
</td></tr>
<tr><td>

retried

</td><td>

</td><td>

number

</td><td>

</td></tr>
<tr><td>

staticPaths

</td><td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ created_at: '2023-03-20T23:45:13Z'
---

import PackageManagerTabs from '~/components/package-manager-tabs/index.tsx';
import { Note } from '~/components/note/note';

# Static Site Generation (SSG) Overview

Expand Down Expand Up @@ -123,6 +124,54 @@ SSG is designed to be run at build time, but it can also be re-run at any time a

To re-run SSG, you can run the `server/run-ssg.js` script. This script will re-run the SSG process and update the static HTML files in the `dist` folder. (or whatever `outDir` you have configured in your SSG config).

## Failed renders

SSG fails the build when a route fails to render, so a broken page never ships as static HTML. A render fails when:

- the route throws an error that it doesn't handle,
- a [`<Catch>`](../../../labs/catch/index.mdx) catches an error during the render,
- the route renders a 5xx error page, for example when a `routeLoader$` throws `requestEvent.error(503, 'Service unavailable')`,
- the render takes longer than 30 seconds.

A route that redirects or responds with a 4xx status, such as a 404, is skipped instead: SSG writes no file for it, and the build goes on. The exception is your [custom 404 page](../../advanced/routing/index.mdx#custom-404-page), which SSG writes as `404.html`.

### Retrying failed renders

A data source can fail for a moment during a build, for example when an API rate-limits the prerender. Set the `retries` option to render a failed route again before SSG records the error:
Comment thread
maiieul marked this conversation as resolved.
Resolved

```ts title="adapters/ssg/vite.config.ts"
import { ssgAdapter } from '@qwik.dev/router/adapters/ssg/vite';
import { extendConfig } from '@qwik.dev/router/vite';
import baseConfig from '../../vite.config.ts';

export default extendConfig(baseConfig, () => {
return {
build: {
ssr: true,
rolldownOptions: {
input: ['@qwik-router-config'],
},
},
plugins: [
ssgAdapter({
origin: 'https://yoursite.qwik.dev',
retries: 2,
}),
],
};
});
```

With `retries: 2`, SSG renders a failed route up to 3 times. It waits 500ms before the first retry and doubles the wait for each retry after that, up to 2s, plus up to 30% at random so routes that failed together don't retry together. When a render times out, SSG restarts the worker that ran it before retrying, and the other routes that worker was rendering start again without using their retries. Each retry prints a warning with the error, even when `log` is `'quiet'`, and the build summary counts the routes that needed a retry, so a build that only passed thanks to a retry still shows the flaky routes.

`retries` also covers `onStaticGenerate`: if listing a dynamic route's params throws, SSG calls it again with the same waits before it stops the build.

`retries` defaults to `0`. Once a route has used its retries, a failed render is recorded as an error, and a render that timed out stops the build at once.

<Note>
A retry can't fix a bug. A route that fails the same way every time renders `retries + 1` times before the build fails, so keep the value small.
</Note>

## Dynamic SSG Routes

So far, we've only discussed how to generate static HTML files for a single route path. However, in most cases, you'll want to generate HTML files for multiple route paths that has dynamic params. For example, a product site may have a route path for each product, such as `/product/:id`. In this case, you'll want to generate HTML files for each product page, which would require generating HTML files for each product ID.
Expand Down
Loading
Loading