snappy-prerender 2.6.0-rc.2 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +140 -0
- package/README.md +96 -19
- package/package.json +11 -6
- package/src/cli.js +119 -12
- package/src/core/artifacts.js +110 -0
- package/src/core/blocking.js +53 -1
- package/src/core/browser.js +109 -39
- package/src/core/build-origin.js +19 -0
- package/src/core/config.js +143 -3
- package/src/core/css-url.js +34 -0
- package/src/core/destination.js +28 -3
- package/src/core/init.js +44 -1
- package/src/core/inline-css.js +34 -13
- package/src/core/metadata-origin.js +11 -2
- package/src/core/normalize.js +71 -1
- package/src/core/output.js +32 -9
- package/src/core/page-content.js +13 -0
- package/src/core/preload-manifest.js +61 -7
- package/src/core/prune.js +65 -0
- package/src/core/render.js +64 -10
- package/src/core/resources.js +1 -0
- package/src/core/routes.js +9 -0
- package/src/core/run.js +191 -241
- package/src/core/server.js +30 -4
- package/src/core/sitemap.js +94 -0
- package/src/core/url-policy.js +32 -0
- package/src/core/verify.js +18 -4
- package/src/core/write.js +278 -0
- package/src/head/index.js +1 -1
- package/src/head/tags.js +19 -26
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
|
|
6
|
+
uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Earlier history predates
|
|
7
|
+
the release tags; see the Git history for those changes.
|
|
8
|
+
|
|
9
|
+
## [2.6.0] - 2026-10-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `cleanDestination` empties the destination before copying the build output, so assets
|
|
14
|
+
removed from a build do not linger.
|
|
15
|
+
- `pruneStaleRoutes` removes pages recorded by the previous run's `.snappy-manifest.json`
|
|
16
|
+
when they are no longer produced, without touching assets or user files.
|
|
17
|
+
- New build gates: `failOnEmptyPage` (on by default), `failOnConsoleError`,
|
|
18
|
+
`failOnFailedRequest`, `failOnDuplicateContent`, `failOnUndetected`,
|
|
19
|
+
`failOnVerificationError` and the optional `failOnBuildOriginMetadata`.
|
|
20
|
+
- `browserDownloadVersion` pins the Chrome for Testing build id; a cached build is now
|
|
21
|
+
used without contacting the version feed, so offline rebuilds work.
|
|
22
|
+
- `stripBuildOriginMetadata` removes canonical and social tags that point at the
|
|
23
|
+
prerender server instead of shipping loopback URLs.
|
|
24
|
+
- Reports now expose `consoleErrors`, `failedRequests`, `emptyPages`, `duplicatePages`
|
|
25
|
+
and `pruned`; page files carry `noindex`, and verification entries carry `status` and
|
|
26
|
+
`empty`.
|
|
27
|
+
- CLI flags for the new gates and for `quietPeriod`, `scrollStepDelay`,
|
|
28
|
+
`serveCmdTimeout`, `shutdownTimeout`, `maxCachedBytes`, `viewport`, `readyFlag`,
|
|
29
|
+
`readySelector`, `headless`, `cleanDestination` and `metadata.base`.
|
|
30
|
+
- A Node 22/24 test workflow and a dedicated integration suite for the audit fixes.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **Breaking:** the minimum supported Node version is now 22.12, matching
|
|
35
|
+
`@puppeteer/browsers`.
|
|
36
|
+
- **Breaking:** an explicit `browser` choice no longer falls back to another engine.
|
|
37
|
+
`chromium` uses a Playwright-managed Chromium and asks you to install it when missing;
|
|
38
|
+
`auto` is the only choice that walks machine, Playwright cache, cached Chrome for
|
|
39
|
+
Testing, then download.
|
|
40
|
+
- **Breaking:** `include` is a true output allowlist. A wildcard-only list still seeds the
|
|
41
|
+
root for crawling, but the seed itself is not written. Passing `include: ['/']` is
|
|
42
|
+
treated as the default and leaves crawling enabled.
|
|
43
|
+
- **Breaking:** canonical, Open Graph and sitemap URLs share one page-path policy:
|
|
44
|
+
directory output keeps its trailing slash, flat output stays extensionless, and an
|
|
45
|
+
explicit `trailingSlash` wins.
|
|
46
|
+
- **Breaking:** sitemap entries no longer carry `lastmod` (a build date claimed every page
|
|
47
|
+
changed), noindex pages are excluded, and sitemaps above 50,000 URLs are split behind a
|
|
48
|
+
sitemap index.
|
|
49
|
+
- **Breaking:** `metadata.siteUrl` must be an origin without a path; sub-path deployments
|
|
50
|
+
belong in `base`. A `serveCmd` URL with a path is rejected for the same reason.
|
|
51
|
+
- **Breaking:** `removeStyleTags` combined with `inlineCss: 'inline'` or `true` is
|
|
52
|
+
rejected instead of silently removing the CSS that was just inlined.
|
|
53
|
+
- Empty output now fails a route by default; 404 pages are emitted with a robots
|
|
54
|
+
`noindex` tag.
|
|
55
|
+
- Third-party blocking now covers WebSockets and accepts hostnames with ports, wildcard
|
|
56
|
+
suffixes and leading-dot suffixes. Service workers are blocked during rendering and
|
|
57
|
+
verification.
|
|
58
|
+
- Verification uses a real browser user agent and the same reduced-motion context as the
|
|
59
|
+
render pass; mode counts are always logged and can fail the build with
|
|
60
|
+
`failOnUndetected`.
|
|
61
|
+
- Inlined stylesheets rebase relative `url()` values against their source stylesheet;
|
|
62
|
+
sheets using `@import` stay linked to preserve the cascade.
|
|
63
|
+
- Preload manifests are derived from the final HTML, so inline CSS, tag surgery and failed
|
|
64
|
+
writes no longer leave stale hints. The not-found route is omitted.
|
|
65
|
+
- Screenshots use the same unchanged-file check as HTML and dry runs report the image file
|
|
66
|
+
instead of an HTML one.
|
|
67
|
+
- `init` reads Vite's resolved `outDir` and `base`, and Create React App's `homepage`, so
|
|
68
|
+
the wizard no longer guesses `dist` and `/` for custom projects. During a prerelease it
|
|
69
|
+
prints the `@next` install tag.
|
|
70
|
+
- Unknown config keys and options that have no effect in the current combination now
|
|
71
|
+
produce warnings instead of being ignored silently.
|
|
72
|
+
- Rendered output is split across smaller modules; `run.js` is back under the 600-line
|
|
73
|
+
limit.
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
|
|
77
|
+
- `setHead` called an undefined helper and threw `ReferenceError`.
|
|
78
|
+
- HTTP 4xx/5xx navigations, empty pages and zero-route runs could be written and reported
|
|
79
|
+
as successful; they now fail with route-level errors.
|
|
80
|
+
- The static server served `index.html` for dead extensionless links and failed dotted
|
|
81
|
+
routes; HTML navigations now fall back deliberately and duplicate output is detected.
|
|
82
|
+
- Canonical and `og:url` doubled a non-root `base` path.
|
|
83
|
+
- `verifyChangedOnly` could skip a changed page when a shared state script was unchanged.
|
|
84
|
+
- `asyncScriptTags` marked the injected state script `async`, allowing it to run after the
|
|
85
|
+
application bundle.
|
|
86
|
+
- `removeAttributes` with a global or sticky RegExp skipped every other match.
|
|
87
|
+
- Output collisions that differ only by case are detected on case-insensitive
|
|
88
|
+
filesystems.
|
|
89
|
+
- A destination inside the source directory recursed until the path length limit.
|
|
90
|
+
- Inlined CSS lost relative font and image URLs.
|
|
91
|
+
- Build-server metadata shipped loopback canonical and social URLs.
|
|
92
|
+
- Preload manifests described resources the final page no longer loaded.
|
|
93
|
+
- Sitemap and canonical URLs disagreed on trailing slashes and flat output.
|
|
94
|
+
- Allowed-host matching ignored ports and wildcards, and WebSocket requests bypassed
|
|
95
|
+
blocking.
|
|
96
|
+
- Browser downloads needed the network even when a cached browser was available.
|
|
97
|
+
- The CLI advertised `--preconnect-third-party` without defining it, and several other
|
|
98
|
+
documented flags had no CLI equivalent.
|
|
99
|
+
|
|
100
|
+
## [2.6.0-rc.3] - 2026-10-03
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
|
|
104
|
+
- Computed config files: `snappy.config.js` may export an async function that returns
|
|
105
|
+
options, so routes can come from a CMS, sitemap or file listing.
|
|
106
|
+
- Sitemap generation and the `sitemap` option.
|
|
107
|
+
- Stable output: unchanged pages, state scripts and artifacts are left untouched.
|
|
108
|
+
- React 16.8 and 17 fixtures and integration coverage.
|
|
109
|
+
|
|
110
|
+
### Changed
|
|
111
|
+
|
|
112
|
+
- Documentation links to the npm package and notes the release-candidate channel.
|
|
113
|
+
|
|
114
|
+
## [2.6.0-rc.2] - 2026-09-26
|
|
115
|
+
|
|
116
|
+
### Added
|
|
117
|
+
|
|
118
|
+
- `snappy-prerender init` setup wizard, with flags for non-interactive use.
|
|
119
|
+
- CSP-friendly output: `csp: 'off' | 'strict'`, `externalScripts` and configurable
|
|
120
|
+
`criticalCssPreload`.
|
|
121
|
+
|
|
122
|
+
## [2.6.0-rc.1] - 2026-09-26
|
|
123
|
+
|
|
124
|
+
### Added
|
|
125
|
+
|
|
126
|
+
- The `snappy-prerender/head` entry point: `Head`, `setHead`, `clearHead`, metadata
|
|
127
|
+
defaults, canonical, Open Graph and Twitter tags, and client-side reconciliation.
|
|
128
|
+
- Head-layer audit fixes for tag ownership, restoration and repeated runs.
|
|
129
|
+
|
|
130
|
+
## [2.5.0] - 2026-09-25
|
|
131
|
+
|
|
132
|
+
### Added
|
|
133
|
+
|
|
134
|
+
- react-snap feature parity for v2: `inlineCss`, `minifyHtml`, `minifyCss`,
|
|
135
|
+
`preloadImages`, `cacheAjaxRequests`, `snapSaveState`, `preloadManifest`,
|
|
136
|
+
`removeBlobs`, `removeStyleTags`, `removeScriptTags`, `asyncScriptTags`,
|
|
137
|
+
`destination`, `saveAs`, `browserArgs` and `ignoreHTTPSErrors`.
|
|
138
|
+
- Hydration verification that reports `hydrated`, `re-rendered`, `undetected` and
|
|
139
|
+
`unknown`, plus per-route React mismatch messages.
|
|
140
|
+
- Third-party requests allowed by default, with opt-in blocking and host allowlists.
|
package/README.md
CHANGED
|
@@ -3,11 +3,14 @@
|
|
|
3
3
|
**Prerender your single-page app without rewriting it.** Point it at your build output, and every route is rendered in a real browser and written to disk as static HTML — content, titles, meta tags, canonical links and Open Graph tags included. Search engines and social crawlers get the full page; your users get the same app they had before.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/snappy-prerender)
|
|
6
|
+
[](https://www.npmjs.com/package/snappy-prerender)
|
|
6
7
|
[](https://www.npmjs.com/package/snappy-prerender)
|
|
7
8
|
[](https://github.com/sayakmukherjee080/snappy-prerender/blob/main/LICENSE)
|
|
8
9
|
|
|
10
|
+
Package page: [npmjs.com/package/snappy-prerender](https://www.npmjs.com/package/snappy-prerender). Install with `npm install --save-dev snappy-prerender`. Release notes are in [CHANGELOG.md](CHANGELOG.md).
|
|
11
|
+
|
|
9
12
|
- **No SSR entry, no framework migration, no app changes.** If it builds to a `dist` directory, it can be prerendered.
|
|
10
|
-
- **React 18 and
|
|
13
|
+
- **React 16.8, 17, 18 and 19, Vite 6/7/8, or any static build.**
|
|
11
14
|
- **Inspired by [react-snap](https://github.com/stereobooster/react-snap)** and rebuilt from scratch for current toolchains: a browser-based prerenderer with a hydration report attached, not another SSR framework.
|
|
12
15
|
|
|
13
16
|
## Who this is for
|
|
@@ -60,6 +63,7 @@ npx snappy-prerender dist --include /,/pricing,/blog/* --exclude /blog/drafts/*
|
|
|
60
63
|
npx snappy-prerender dist --block-third-party --allowed-hosts cms.example.com
|
|
61
64
|
npx snappy-prerender dist --metadata-site-url https://example.com --metadata-title-template '%s | Example'
|
|
62
65
|
npx snappy-prerender dist --inline-css critical --minify-html --preload-manifest
|
|
66
|
+
npx snappy-prerender dist --sitemap --verify-changed-only
|
|
63
67
|
npx snappy-prerender dist --csp strict --inline-css critical
|
|
64
68
|
```
|
|
65
69
|
|
|
@@ -70,9 +74,10 @@ import { prerender } from 'snappy-prerender/core';
|
|
|
70
74
|
|
|
71
75
|
const report = await prerender({ sourceDir: 'dist', include: ['/'] });
|
|
72
76
|
if (!report.ok) {
|
|
73
|
-
for (const failure of report.verification
|
|
77
|
+
for (const failure of report.verification?.routes.filter((entry) => !entry.ok) ?? []) {
|
|
74
78
|
console.error(failure.route, failure.hydrationErrors);
|
|
75
79
|
}
|
|
80
|
+
for (const failure of report.errors) console.error(failure.route, failure.message);
|
|
76
81
|
}
|
|
77
82
|
```
|
|
78
83
|
|
|
@@ -150,7 +155,7 @@ Rules worth knowing:
|
|
|
150
155
|
- When no Head wants a tag any more, a generated one is removed and one the app wrote is restored to the value it had before the head layer touched it. That original is recorded into the page during prerendering, so a route change brings the template's fallback metadata back.
|
|
151
156
|
- The component renders nothing, so it cannot affect hydration of the page content.
|
|
152
157
|
- The title template applies unless the title already ends with the template's fixed part, or is the site name itself, so a title composed by hand is never suffixed twice.
|
|
153
|
-
- Absolute URLs are required for `og:url`, `og:image` and canonical.
|
|
158
|
+
- Absolute URLs are required for `og:url`, `og:image` and canonical. `metadata.siteUrl` must be an origin (no path); put a sub-path deployment in `base`. Without `siteUrl`, URLs that fall back to the build server are removed from the output by default (`stripBuildOriginMetadata: true`) and the run warns. The `trailingSlash` convention applies to page URLs only; asset URLs are left alone. Canonical, social and sitemap URLs share one page-path policy: directory output keeps its slash, flat output stays extensionless, and an explicit `trailingSlash` wins.
|
|
154
159
|
- `metadata.base` overrides the base path used for absolute URLs and defaults to the `base` option.
|
|
155
160
|
- On React 19, React hoists `<title>` and `<meta>` rendered in JSX and leaves an existing template tag alone, so a page can end up with two titles. Use React's native metadata or this component, not both; the run warns when the output carries more than one `<title>`.
|
|
156
161
|
- Every metadata value has a CLI flag: `--metadata-site-url`, `--metadata-site-name`, `--metadata-title-template`, `--metadata-default-image`, `--metadata-trailing-slash`.
|
|
@@ -171,10 +176,16 @@ After writing the output, every route is loaded again with the real client bundl
|
|
|
171
176
|
| `undetected` | No React root was found on the container, so the mode could not be judged. |
|
|
172
177
|
| `unknown` | The prerendered node was never seen, for example a container outside the usual ids. |
|
|
173
178
|
|
|
179
|
+
A route whose verification navigation returns HTTP 4xx/5xx, or whose container renders no content, is reported as failed with its `status` or `empty` flag. `undetected` and `unknown` are reported as mode counts; set `failOnUndetected: true` to make any such route fail the build. A verification pass that cannot run is listed in `verification.errors`; set `failOnVerificationError: true` to fail on those separately from hydration errors.
|
|
180
|
+
|
|
174
181
|
`re-rendered` matters because an app calling `createRoot` instead of `hydrateRoot` throws the prerendered DOM away on boot — and because `createRoot` cannot raise a hydration error, nothing else would ever tell you. It is reported per route as `verification.routes[].mode` and summarised in `verification.modes`, with a warning in the log. Set `failOnRerender: true` to make it fail the build as well.
|
|
175
182
|
|
|
176
183
|
**Page errors.** Errors the browser logs while verifying are reported too: uncaught exceptions as warnings, console errors at debug level. Errors thrown during rendering itself are counted in the summary and reported per route, and `failOnPageError: true` turns them into a build failure; by default an app that throws still ships what it managed to render, because the static output is what crawlers read.
|
|
177
184
|
|
|
185
|
+
**React 16 and 17.** Hydration and boot modes are judged the same way, and their dev mismatch warnings ("Text content did not match", "Did not expect server HTML to contain") are recognised. Their production builds report nothing for a mismatch at all — no warning, no minified code — so on 16 and 17 the boot-mode report (`hydrated` versus `re-rendered`) is the signal that something drifted.
|
|
186
|
+
|
|
187
|
+
**Rebuilds.** On a large site most routes are unchanged between builds, and reloading them proves nothing new. `verifyChangedOnly: true` skips the verification pass for routes whose HTML file was left untouched, which roughly halves rebuild time; the report notes how many routes were skipped, and the first run after a change still verifies everything that moved.
|
|
188
|
+
|
|
178
189
|
## Ready contract
|
|
179
190
|
|
|
180
191
|
Most apps work with no configuration because the network-quiet heuristic is enough. Apps that fetch data after load can opt into a stronger signal:
|
|
@@ -235,10 +246,20 @@ snappy({ preconnectThirdParty: true, preloadImages: true, preloadManifest: true
|
|
|
235
246
|
|
|
236
247
|
- `preconnectThirdParty` (default `true`) records every third-party origin the page tried to reach — including ones blocked by `blockThirdParty` — and adds `<link rel="preconnect">` for each.
|
|
237
248
|
- `preloadImages` adds `<link rel="preload" as="image">` for same-origin images the page loaded.
|
|
238
|
-
- `preloadManifest` writes `preload-manifest.json` into the output directory: for each route, a `Link` header value listing the scripts and stylesheets
|
|
249
|
+
- `preloadManifest` writes `preload-manifest.json` into the output directory: for each route, a `Link` header value listing the scripts and stylesheets the final HTML still references, filtered by `ignoreForPreload` (default `['service-worker.js']`). Inline CSS and tag surgery are already reflected, the not-found route is omitted, and hints and route keys carry the base path, so serve these as Early Hints or `Link` response headers as they are.
|
|
239
250
|
|
|
240
251
|
react-snap called this `http2PushManifest`. Browsers removed HTTP/2 push (Chrome in v106), so the manifest is now a header list rather than a push list.
|
|
241
252
|
|
|
253
|
+
### Sitemap
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
snappy({ sitemap: true, metadata: { siteUrl: 'https://example.com' } });
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`snappy` already knows every route it rendered, so it can write the sitemap for you. `sitemap.xml` is generated from the routes that were written — using the same page path policy as canonical URLs, with the base path applied, `noindex` pages and the not-found route left out — and split behind a sitemap index when it exceeds 50,000 URLs. Entries carry no `<lastmod>`, because a build date would claim every page changed on every build.
|
|
260
|
+
|
|
261
|
+
When the output directory already has a `robots.txt`, a `Sitemap:` line is appended to it if it does not point anywhere yet; an existing directive is left alone. Without a `robots.txt`, the run logs the exact line to add instead of creating a file you did not ask for.
|
|
262
|
+
|
|
242
263
|
### Tag surgery
|
|
243
264
|
|
|
244
265
|
```js
|
|
@@ -252,6 +273,24 @@ snappy({
|
|
|
252
273
|
|
|
253
274
|
The persisted metadata element is exempt from `removeScriptTags`; everything else is removed as asked.
|
|
254
275
|
|
|
276
|
+
### Removing volatile markup
|
|
277
|
+
|
|
278
|
+
Mark any element with `data-prerender-remove` and it is dropped from the output:
|
|
279
|
+
|
|
280
|
+
```jsx
|
|
281
|
+
<div data-prerender-remove>{ads.render()}</div>
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Use it for regions that only make sense after the client takes over — ads, chat widgets, anything injected after mount. The rule of thumb: remove markup that React will *not* render on its first client pass. Removing something React does expect causes the very hydration mismatch it was meant to avoid, which is exactly what the verifier would then report.
|
|
285
|
+
|
|
286
|
+
Some libraries stamp values that change on every page load — antd's `rc-menu` writes `data-menu-id="rc-menu-uuid-…"` — which makes every rebuild rewrite every file and leaves `verifyChangedOnly` nothing to skip. List those attribute names (or RegExps) and they are dropped from the output:
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
snappy({ removeAttributes: ['data-menu-id', 'aria-controls'] });
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
As with removed markup, only strip attributes React does not render as props; if you guess wrong, the hydration verifier tells you which route.
|
|
293
|
+
|
|
255
294
|
### Strict CSP
|
|
256
295
|
|
|
257
296
|
One dial decides how the injected pieces are delivered. `csp: 'off'` (default) leaves them inline, which is cheapest to load. `csp: 'strict'` makes the whole output work under `script-src 'self'` with no `unsafe-inline`:
|
|
@@ -370,20 +409,38 @@ export default {
|
|
|
370
409
|
};
|
|
371
410
|
```
|
|
372
411
|
|
|
412
|
+
The config can also be an async function that returns the options. That is how routes get discovered from a CMS, a sitemap or a directory listing, without a separate prebuild script:
|
|
413
|
+
|
|
414
|
+
```js
|
|
415
|
+
// snappy.config.js
|
|
416
|
+
export default async function () {
|
|
417
|
+
const response = await fetch('https://cms.example.com/api/posts?fields=slug');
|
|
418
|
+
const posts = await response.json();
|
|
419
|
+
return {
|
|
420
|
+
sourceDir: 'dist',
|
|
421
|
+
include: ['/', '/blog', ...posts.map((post) => `/blog/${post.slug}`)],
|
|
422
|
+
metadata: { siteUrl: 'https://example.com', siteName: 'Example' },
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
The function is awaited once per run, and whatever it returns must be a plain options object. If it throws, the CLI reports the reason and stops before rendering anything.
|
|
428
|
+
|
|
373
429
|
## Options
|
|
374
430
|
|
|
375
431
|
| Option | Default | Description |
|
|
376
432
|
| --- | --- | --- |
|
|
377
433
|
| `sourceDir` | `dist` | Built output directory |
|
|
378
434
|
| `destination` | `null` | Copy the output here and write results here instead of in place |
|
|
435
|
+
| `cleanDestination` | `false` | Empty the destination before copying, so removed assets do not linger |
|
|
379
436
|
| `base` | `/` | Public base path |
|
|
380
|
-
| `include` | `['/']` | Seed routes and allowlist. Wildcards (`/blog/*`) filter discovered routes, only concrete paths are seeded
|
|
437
|
+
| `include` | `['/']` | Seed routes and output allowlist. Wildcards (`/blog/*`) filter discovered routes, only concrete paths are seeded, and a seed that does not match the list is used for crawling but not written. Passing `['/']` is treated as the default: crawl everything linked from the root |
|
|
381
438
|
| `exclude` | `[]` | Routes to skip, strings or RegExp |
|
|
382
439
|
| `crawl` | `true` | Follow same-origin links from seeds |
|
|
383
440
|
| `maxDepth` | `null` | Crawl depth limit, `null` is unlimited, `0` renders seeds only |
|
|
384
441
|
| `maxRoutes` | `null` | Stop after this many routes; hitting the cap marks the run incomplete |
|
|
385
442
|
| `concurrency` | derived | Parallel pages, derived from CPU count, capped at 8 |
|
|
386
|
-
| `timeout` | `30000` |
|
|
443
|
+
| `timeout` | `30000` | Navigation, ready and network-settle timeout in ms; each wait gets this budget |
|
|
387
444
|
| `quietPeriod` | `500` | Network must be idle this long before capture |
|
|
388
445
|
| `readyFlag` | `__prerenderReady` | Window flag awaited when defined |
|
|
389
446
|
| `readySelector` | `null` | Selector awaited when set |
|
|
@@ -394,6 +451,7 @@ export default {
|
|
|
394
451
|
| `browser` | `auto` | `auto`, `chrome`, `msedge`, `chromium`, or a path to an executable |
|
|
395
452
|
| `browserDownload` | `true` | Allow the Chrome for Testing fallback download |
|
|
396
453
|
| `browserDownloadHash` | `null` | SHA-256 digest the fallback download must match, verified before use |
|
|
454
|
+
| `browserDownloadVersion` | `null` | Chrome for Testing build id to use for the cache and download instead of resolving `stable` |
|
|
397
455
|
| `browserArgs` | `[]` | Extra browser launch arguments |
|
|
398
456
|
| `headless` | `true` | Run the browser headless |
|
|
399
457
|
| `ignoreHTTPSErrors` | `false` | Ignore TLS errors while rendering |
|
|
@@ -411,28 +469,40 @@ export default {
|
|
|
411
469
|
| `minifyCss` | `false` | Minify CSS with clean-css, also applied to inlined CSS |
|
|
412
470
|
| `preconnectThirdParty` | `true` | Add preconnect hints for every third-party origin the page contacts |
|
|
413
471
|
| `preloadImages` | `false` | Add preload hints for same-origin images |
|
|
414
|
-
| `preloadManifest` | `false` | Write `preload-manifest.json` with Link header hints |
|
|
472
|
+
| `preloadManifest` | `false` | Write `preload-manifest.json` with Link header hints, derived from the final HTML |
|
|
415
473
|
| `ignoreForPreload` | `['service-worker.js']` | File names excluded from the manifest |
|
|
474
|
+
| `sitemap` | `false` | Write `sitemap.xml` (or a sitemap index above 50,000 URLs) from the routes that do not declare noindex, and point an existing `robots.txt` at it; needs `metadata.siteUrl` |
|
|
416
475
|
| `cacheAjaxRequests` | `false` | Expose captured JSON responses as `window.snapStore` |
|
|
417
|
-
| `maxCachedBytes` | `5242880` | Largest JSON response cached per
|
|
476
|
+
| `maxCachedBytes` | `5242880` | Largest JSON response cached per request, measured on the serialised body |
|
|
418
477
|
| `removeBlobs` | `true` | Drop stylesheet links pointing at dead `blob:` URLs |
|
|
419
|
-
| `
|
|
478
|
+
| `removeAttributes` | `[]` | Attribute names or RegExps to drop from the output, for libraries that stamp per-load ids |
|
|
479
|
+
| `stripBuildOriginMetadata` | `true` | Remove canonical and social tags whose URL points at the prerender server |
|
|
480
|
+
| `failOnBuildOriginMetadata` | `false` | Fail when metadata still points at the prerender server after stripping |
|
|
481
|
+
| `pruneStaleRoutes` | `false` | Delete pages listed by the previous run's manifest that this run no longer writes |
|
|
482
|
+
| `removeStyleTags` | `false` | Strip every style tag from the output; cannot be combined with `inlineCss` `'inline'` |
|
|
420
483
|
| `removeScriptTags` | `false` | Strip every script tag from the output |
|
|
421
484
|
| `asyncScriptTags` | `false` | Mark external scripts async |
|
|
422
485
|
| `externalScripts` | `false` | Write injected state scripts as content-hashed same-origin files instead of inline; set by `csp: 'strict'` |
|
|
423
486
|
| `flatOutput` | `false` | Write `about.html` instead of `about/index.html` |
|
|
424
|
-
| `notFoundRoute` | `/404` | Route emitted as `404.html
|
|
425
|
-
| `saveAs` | `html` | `html`, `png` or `jpeg
|
|
426
|
-
| `metadata` | `null` | Defaults for the head component: `siteUrl
|
|
487
|
+
| `notFoundRoute` | `/404` | Route emitted as `404.html` with a robots noindex tag; a notice is logged when it is never prerendered |
|
|
488
|
+
| `saveAs` | `html` | `html`, `png` or `jpeg`; identical screenshots are left untouched |
|
|
489
|
+
| `metadata` | `null` | Defaults for the head component: `siteUrl` (origin only), `siteName`, `titleTemplate`, `defaultImage`, `trailingSlash`, `base` |
|
|
427
490
|
| `verify` | `true` | Run the hydration verification pass |
|
|
491
|
+
| `verifyChangedOnly` | `false` | Skip verification for routes whose HTML file did not change since the last run |
|
|
428
492
|
| `failOnHydrationError` | `false` | Fail the run on hydration errors; off by default, where they are reported instead |
|
|
429
493
|
| `failOnRerender` | `false` | Fail the run when a route re-renders instead of hydrating |
|
|
430
494
|
| `failOnPageError` | `false` | Fail the run when a page throws while rendering; off by default, where page errors are reported |
|
|
495
|
+
| `failOnEmptyPage` | `true` | Fail when a route renders no app content |
|
|
496
|
+
| `failOnConsoleError` | `false` | Fail when the browser logs a console error while rendering |
|
|
497
|
+
| `failOnFailedRequest` | `false` | Fail when a same-origin request fails while rendering |
|
|
498
|
+
| `failOnDuplicateContent` | `false` | Fail when two routes produce byte-identical HTML |
|
|
499
|
+
| `failOnUndetected` | `false` | Fail when verification cannot classify any route as hydrated or re-rendered |
|
|
500
|
+
| `failOnVerificationError` | `false` | Fail when the verification pass cannot run on a route |
|
|
431
501
|
| `failOnError` | `true` | Fail the run on route render errors |
|
|
432
502
|
| `dryRun` | `false` | Render and report without writing |
|
|
433
503
|
| `logLevel` | `info` | `silent`, `error`, `warn`, `info`, `debug` |
|
|
434
504
|
| `serveCmd` | `null` | Command that starts the app server instead of static serving |
|
|
435
|
-
| `url` | `null` | App
|
|
505
|
+
| `url` | `null` | App origin, required with `serveCmd`; put a sub-path in `base` instead |
|
|
436
506
|
| `serveCmdTimeout` | `30000` | How long to wait for `serveCmd` to answer |
|
|
437
507
|
| `shutdownTimeout` | `5000` | Grace period before force-killing `serveCmd` |
|
|
438
508
|
|
|
@@ -445,13 +515,14 @@ import { prerender } from 'snappy-prerender/core';
|
|
|
445
515
|
|
|
446
516
|
const report = await prerender({ sourceDir: 'dist', include: ['/'] });
|
|
447
517
|
if (!report.ok) {
|
|
448
|
-
for (const failure of report.verification
|
|
518
|
+
for (const failure of report.verification?.routes.filter((entry) => !entry.ok) ?? []) {
|
|
449
519
|
console.error(failure.route, failure.hydrationErrors);
|
|
450
520
|
}
|
|
521
|
+
for (const failure of report.errors) console.error(failure.route, failure.message);
|
|
451
522
|
}
|
|
452
523
|
```
|
|
453
524
|
|
|
454
|
-
The report exposes `routes`, `files` (with per-route `status`), `errors`, `pageErrors` (uncaught browser errors seen while rendering), `duplicateTitles` (routes shipping more than one title), `verification` (per-route `mode` plus a `modes` summary), `preloadManifest`, `truncated`, and `ok`.
|
|
525
|
+
The report exposes `routes`, `files` (with per-route `status` and `noindex`), `errors`, `pageErrors` (uncaught browser errors seen while rendering), `consoleErrors`, `failedRequests`, `emptyPages`, `duplicatePages`, `duplicateTitles` (routes shipping more than one title), `verification` (nullable; per-route `mode`, `status` and `empty` plus a `modes` summary), `preloadManifest`, `sitemap`, `pruned`, `truncated`, and `ok`.
|
|
455
526
|
|
|
456
527
|
`snappy-prerender` exports `DEFAULTS`, `prerender` and `resolveConfig`; the default export is the Vite plugin.
|
|
457
528
|
|
|
@@ -475,16 +546,22 @@ Dropped: `sourceMaps` (declared but never referenced in react-snap's code), `fix
|
|
|
475
546
|
- Crawling is unbounded unless `maxRoutes` is set; hitting the cap marks the run as failed so incomplete output is never shipped silently. Which routes are cut is concurrency-dependent on a large site — narrow `include`/`exclude` instead when you need a deterministic set.
|
|
476
547
|
- **React 19 native metadata.** React hoists `<title>` and `<meta>` rendered in JSX and leaves an existing template title in place, so using both systems produces two titles in the output. Use one of them; the run warns when a page ships more than one `<title>`.
|
|
477
548
|
- If two routes would write the same file — such as `/` and `/index` with `flatOutput` — neither is written and the run fails with a collision error.
|
|
478
|
-
- Screenshots are viewport-width full-page captures, so they reflect the configured `viewport`, not a device matrix.
|
|
479
|
-
- Output files from routes that no longer exist are left in place
|
|
549
|
+
- Screenshots are viewport-width full-page captures, so they reflect the configured `viewport`, not a device matrix. Identical screenshots are left untouched.
|
|
550
|
+
- **Output files from routes that no longer exist** are left in place unless `pruneStaleRoutes` is enabled, which deletes only files listed in the previous run's `.snappy-manifest.json`.
|
|
551
|
+
- **Dead extensionless links** are indistinguishable from SPA routes to the static server, so a missing page can render the shell or the app's catch-all 404. Duplicate output is reported; set `failOnDuplicateContent` to enforce it, and use an app-level 404 marker where possible.
|
|
552
|
+
- **Metadata pointing at the build server** is removed by default (`stripBuildOriginMetadata: true`) and reported. Set `metadata.siteUrl` so canonical and social URLs survive.
|
|
553
|
+
- **`inlineCss`** rebases relative font and image URLs against each stylesheet. A sheet that uses `@import` is left as a link because concatenating it would change the cascade.
|
|
554
|
+
- **`verifyChangedOnly` compares the generated HTML only.** If a bundle keeps the same filename while its contents change, that route's HTML is unchanged and its verification is skipped; content-hashed asset names (Vite and CRA defaults) make the HTML change as well.
|
|
555
|
+
- **Destination directories are merged unless `cleanDestination` is on.** That option empties the destination first; `pruneStaleRoutes` separately removes pages the run itself generated.
|
|
556
|
+
- **`removeStyleTags`** cannot be combined with `inlineCss: 'inline'` or `true`; the config is rejected instead of silently shipping pages without CSS.
|
|
480
557
|
|
|
481
558
|
## Requirements
|
|
482
559
|
|
|
483
|
-
- Node.js >=
|
|
560
|
+
- Node.js >= 22.12
|
|
484
561
|
- Chrome or Edge installed (detected automatically), or allow the one-time Chrome for Testing download fallback
|
|
485
562
|
- `beasties` only if you use `inlineCss: 'critical'` (optional peer dependency)
|
|
486
563
|
|
|
487
|
-
Browser resolution order: installed Chrome, installed Edge, a Playwright-managed browser, then a Chrome for Testing download from Google's version-pinned HTTPS bucket into `SNAPPY_BROWSER_CACHE_DIR` (defaults to the platform cache directory).
|
|
564
|
+
Browser resolution order: installed Chrome, installed Edge, a Playwright-managed browser, a Chrome for Testing build already in the tool cache, then a Chrome for Testing download from Google's version-pinned HTTPS bucket into `SNAPPY_BROWSER_CACHE_DIR` (defaults to the platform cache directory). An explicit `browser` choice never falls back to another engine. Set `browserDownloadVersion` to pin the build id and `browserDownloadHash` to pin the archive digest; without a hash the download is trusted on the strength of HTTPS and the pinned URL. Set `browserDownload: false` to forbid downloads entirely.
|
|
488
565
|
|
|
489
566
|
## Contributing
|
|
490
567
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "snappy-prerender",
|
|
3
|
-
"version": "2.6.0
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Drop-in prerendering for existing single-page apps. Renders your built app in a real browser, writes static HTML per route, reports React hydration mismatches, and optimises the output with critical CSS, link hints and cached AJAX state.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"playwright"
|
|
27
27
|
],
|
|
28
28
|
"engines": {
|
|
29
|
-
"node": ">=
|
|
29
|
+
"node": ">=22.12.0"
|
|
30
30
|
},
|
|
31
31
|
"main": "./src/index.js",
|
|
32
32
|
"exports": {
|
|
@@ -36,14 +36,17 @@
|
|
|
36
36
|
"./package.json": "./package.json"
|
|
37
37
|
},
|
|
38
38
|
"bin": {
|
|
39
|
-
"snappy-prerender": "
|
|
39
|
+
"snappy-prerender": "src/cli.js"
|
|
40
40
|
},
|
|
41
41
|
"files": [
|
|
42
42
|
"src",
|
|
43
|
+
"CHANGELOG.md",
|
|
43
44
|
"LICENSE",
|
|
44
45
|
"README.md"
|
|
45
46
|
],
|
|
46
|
-
"sideEffects":
|
|
47
|
+
"sideEffects": [
|
|
48
|
+
"./src/head/manager.js"
|
|
49
|
+
],
|
|
47
50
|
"scripts": {
|
|
48
51
|
"test": "node scripts/run-tests.mjs tests/unit",
|
|
49
52
|
"test:integration": "node scripts/run-tests.mjs tests/integration",
|
|
@@ -65,14 +68,16 @@
|
|
|
65
68
|
"beasties": "0.4.1",
|
|
66
69
|
"react": "19.2.0",
|
|
67
70
|
"react-dom": "19.2.0",
|
|
68
|
-
"vite": "7.3.
|
|
71
|
+
"vite": "7.3.7"
|
|
69
72
|
},
|
|
70
73
|
"workspaces": [
|
|
74
|
+
"tests/fixtures/react16-app",
|
|
75
|
+
"tests/fixtures/react17-app",
|
|
71
76
|
"tests/fixtures/react18-app"
|
|
72
77
|
],
|
|
73
78
|
"peerDependencies": {
|
|
74
79
|
"beasties": ">=0.4.1",
|
|
75
|
-
"react": ">=
|
|
80
|
+
"react": ">=16.8.0",
|
|
76
81
|
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
77
82
|
},
|
|
78
83
|
"peerDependenciesMeta": {
|