snappy-prerender 2.6.0 → 2.8.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 +100 -0
- package/README.md +281 -101
- package/assets/snappy-logo.png +0 -0
- package/docs/limitations.md +94 -0
- package/docs/react-snap-migration.md +45 -0
- package/docs/report.md +135 -0
- package/package.json +4 -1
- package/src/cli-init.js +205 -0
- package/src/cli-options.js +258 -0
- package/src/cli.js +128 -309
- package/src/core/artifacts.js +2 -0
- package/src/core/audit/index.js +114 -0
- package/src/core/audit/links.js +74 -0
- package/src/core/audit/metadata.js +197 -0
- package/src/core/audit/sitemap.js +39 -0
- package/src/core/clock.js +60 -0
- package/src/core/config-validation.js +557 -0
- package/src/core/config-warnings.js +150 -0
- package/src/core/config.js +189 -136
- package/src/core/crawl.js +362 -0
- package/src/core/csp.js +138 -0
- package/src/core/destination.js +23 -7
- package/src/core/frameworks.js +94 -0
- package/src/core/hydration-diff.js +93 -0
- package/src/core/hydration-policy.js +38 -0
- package/src/core/init.js +143 -30
- package/src/core/inline-css.js +52 -9
- package/src/core/locale-context.js +63 -0
- package/src/core/locales.js +385 -0
- package/src/core/normalize.js +37 -6
- package/src/core/page-content.js +57 -7
- package/src/core/page-meta.js +122 -0
- package/src/core/preload-links.js +133 -0
- package/src/core/preload-manifest.js +6 -2
- package/src/core/render.js +126 -35
- package/src/core/report.js +116 -0
- package/src/core/resources.js +5 -1
- package/src/core/route-config.js +17 -0
- package/src/core/route-sources.js +220 -0
- package/src/core/run.js +156 -131
- package/src/core/seo-locale.js +234 -0
- package/src/core/settle.js +2 -4
- package/src/core/sitemap.js +90 -14
- package/src/core/text-separators.js +11 -7
- package/src/core/verify.js +164 -71
- package/src/core/write.js +314 -52
- package/src/head/index.js +104 -6
- package/src/head/manager.js +304 -48
- package/src/head/tags.js +349 -30
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/snappy-logo.png" width="150" alt="snappy-prerender logo">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# snappy-prerender
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
Render a single-page app to static HTML at build time. Point the tool at the build output; it renders each route in Chrome, writes one HTML file per route, and leaves the app unchanged. The output includes page content, titles, meta tags, canonical links and Open Graph tags.
|
|
8
|
+
|
|
9
|
+
Version 2.8 adds deterministic clock and randomness controls, locale and hreflang generation, cookie/localStorage SEO locale seeding, and a client-side head layer that replaces React Helmet or React Helmet Async for prerendered apps.
|
|
4
10
|
|
|
5
11
|
[](https://www.npmjs.com/package/snappy-prerender)
|
|
6
12
|
[](https://www.npmjs.com/package/snappy-prerender)
|
|
@@ -9,15 +15,15 @@
|
|
|
9
15
|
|
|
10
16
|
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
17
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
18
|
+
- No SSR entry, framework migration or app changes. If the app builds to a directory, it can be prerendered.
|
|
19
|
+
- React 16.8, 17, 18 and 19, Vite 6/7/8, or any static build.
|
|
20
|
+
- Based on the react-snap approach, reimplemented for current toolchains with Playwright and a hydration check.
|
|
15
21
|
|
|
16
22
|
## Who this is for
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
Use it for client-rendered apps that need crawler-visible HTML: Create React App, Vite + React, other SPAs, CMS-backed sites and client-routed marketing pages.
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
Do not use it for apps that already render HTML on the server, such as Next.js, Remix or Astro.
|
|
21
27
|
|
|
22
28
|
## Quick start
|
|
23
29
|
|
|
@@ -81,41 +87,41 @@ if (!report.ok) {
|
|
|
81
87
|
}
|
|
82
88
|
```
|
|
83
89
|
|
|
84
|
-
##
|
|
90
|
+
## Based on react-snap
|
|
85
91
|
|
|
86
|
-
react-snap
|
|
92
|
+
[react-snap](https://github.com/stereobooster/react-snap) showed that a browser and a build step can produce crawler-readable HTML without a server renderer. It has been unmaintained since 2019.
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
snappy-prerender reimplements that approach; no react-snap code is reused. It uses Playwright, resolves an installed Chrome or Edge, supports Vite, ships a head layer, and verifies hydration.
|
|
89
95
|
|
|
90
|
-
If you are moving over,
|
|
96
|
+
If you are moving over, [Migrating from react-snap](docs/react-snap-migration.md) maps the options.
|
|
91
97
|
|
|
92
98
|
## What you get
|
|
93
99
|
|
|
94
100
|
| | |
|
|
95
101
|
| --- | --- |
|
|
96
|
-
| **Static HTML per route** | `route/index.html` (or `route.html` with `flatOutput`), plus
|
|
97
|
-
| **Head and metadata** |
|
|
98
|
-
| **Hydration report** |
|
|
99
|
-
| **
|
|
100
|
-
| **Output optimisation** | Critical CSS, HTML/CSS minification, preconnect hints, image preload, a preload manifest, script and style
|
|
101
|
-
| **CSS-in-JS support** | CSSOM-only styles
|
|
102
|
-
| **Ajax state replay** | Captured JSON and `window.snapSaveState()` are injected before the first script
|
|
103
|
-
| **Guided setup** | `snappy-prerender init`
|
|
104
|
-
| **Any static build** | Base paths, `serveCmd
|
|
102
|
+
| **Static HTML per route** | `route/index.html` (or `route.html` with `flatOutput`), plus `404.html`. Identical files are left untouched. |
|
|
103
|
+
| **Head and metadata** | Per-route title, description, robots, canonical, Open Graph and Twitter tags, updated on client-side navigation. |
|
|
104
|
+
| **Hydration report** | Each route is reloaded with the real bundle and reported as `hydrated`, `re-rendered`, `undetected` or `unknown`, with mismatch messages. |
|
|
105
|
+
| **Ready contract** | `window.__prerenderReady`, a selector, or network quiescence. |
|
|
106
|
+
| **Output optimisation** | Critical CSS, HTML/CSS minification, preconnect hints, image preload, a preload manifest, script and style removal. |
|
|
107
|
+
| **CSS-in-JS support** | CSSOM-only styles and constructable stylesheets are written back into the HTML. |
|
|
108
|
+
| **Ajax state replay** | Captured JSON and `window.snapSaveState()` are injected before the first script. |
|
|
109
|
+
| **Guided setup** | `snappy-prerender init` writes a `snappy.config.js` from the project settings and a few answers. |
|
|
110
|
+
| **Any static build** | Base paths, `serveCmd`, screenshots, dry runs and bounded concurrency. |
|
|
105
111
|
|
|
106
112
|
## How it works
|
|
107
113
|
|
|
108
|
-
1. Serves the built output on an ephemeral loopback port
|
|
109
|
-
2. Crawls same-origin links from the seed routes
|
|
110
|
-
3. Waits for the
|
|
111
|
-
4. Freezes CSS animations, optionally blocks third-party requests, records
|
|
112
|
-
5. Serialises the DOM
|
|
113
|
-
6. Writes `route/index.html`
|
|
114
|
-
7. Reloads
|
|
114
|
+
1. Serves the built output on an ephemeral loopback port, or copies it to `destination`.
|
|
115
|
+
2. Crawls same-origin links from the seed routes in a fresh browser context per page.
|
|
116
|
+
3. Waits for the ready contract, quiet network, fonts and two animation frames.
|
|
117
|
+
4. Freezes CSS animations, optionally blocks third-party requests, records resources and reconciles head tags.
|
|
118
|
+
5. Serialises the DOM and post-processes it: removes freeze styles and `data-prerender-remove` elements, de-duplicates head styles, drops dead blob stylesheets, adds link hints, optionally inlines CSS, removes or marks scripts, optionally minifies, and records metadata issues.
|
|
119
|
+
6. Writes `route/index.html` or a screenshot, leaving identical files untouched.
|
|
120
|
+
7. Reloads each written file with the client bundle and reports hydration errors and boot mode.
|
|
115
121
|
|
|
116
122
|
## Head and metadata
|
|
117
123
|
|
|
118
|
-
Per-route titles, descriptions, canonical links and Open Graph/Twitter tags come from
|
|
124
|
+
Per-route titles, descriptions, canonical links and Open Graph/Twitter tags come from the package's `Head` component. It updates the document head on every render, including client-side navigation, and the prerenderer captures the result.
|
|
119
125
|
|
|
120
126
|
```jsx
|
|
121
127
|
import { Head } from 'snappy-prerender/head';
|
|
@@ -127,11 +133,18 @@ import { Head } from 'snappy-prerender/head';
|
|
|
127
133
|
type="article"
|
|
128
134
|
locale="en_GB"
|
|
129
135
|
robots="index,follow"
|
|
136
|
+
lang="en"
|
|
137
|
+
dir="ltr"
|
|
138
|
+
structuredData={{ '@context': 'https://schema.org', '@type': 'WebPage', name: 'Awards' }}
|
|
139
|
+
alternates={[
|
|
140
|
+
{ hreflang: 'en', href: '/awards' },
|
|
141
|
+
{ hreflang: 'x-default', href: '/awards' },
|
|
142
|
+
]}
|
|
130
143
|
extra={[{ name: 'keywords', content: 'polymer' }, { rel: 'alternate', href: '/feed.xml' }]}
|
|
131
144
|
/>
|
|
132
145
|
```
|
|
133
146
|
|
|
134
|
-
Project-wide values come from the `metadata` option. The prerenderer injects them into every page, so the component
|
|
147
|
+
Project-wide values come from the `metadata` option. The prerenderer injects them into every page, so the component builds absolute URLs from the public site instead of the build server:
|
|
135
148
|
|
|
136
149
|
```js
|
|
137
150
|
snappy({
|
|
@@ -139,52 +152,223 @@ snappy({
|
|
|
139
152
|
siteUrl: 'https://example.com',
|
|
140
153
|
siteName: 'Example',
|
|
141
154
|
titleTemplate: '%s | Example',
|
|
155
|
+
defaultTitle: 'Example',
|
|
142
156
|
defaultImage: '/share.png',
|
|
143
157
|
trailingSlash: 'never', // 'preserve' | 'always' | 'never'
|
|
144
158
|
},
|
|
145
159
|
});
|
|
146
160
|
```
|
|
147
161
|
|
|
162
|
+
### Replacing React Helmet or React Helmet Async
|
|
163
|
+
|
|
164
|
+
For a client-rendered app, `Head` covers the tag and attribute surface React Helmet and React Helmet Async expose on the client. An app does not need either package:
|
|
165
|
+
|
|
166
|
+
```jsx
|
|
167
|
+
<Head
|
|
168
|
+
title="Awards"
|
|
169
|
+
titleAttributes={{ itemProp: 'name' }}
|
|
170
|
+
htmlAttributes={{ className: 'theme-dark', 'data-theme': 'dark' }}
|
|
171
|
+
bodyAttributes={{ className: 'root', 'data-scroll': 'locked' }}
|
|
172
|
+
images={['/share/awards.png', '/share/awards-square.png']}
|
|
173
|
+
alternateLocales={['en_GB', 'fr_FR']}
|
|
174
|
+
tags={[
|
|
175
|
+
{ tag: 'base', attrs: { href: '/' } },
|
|
176
|
+
{ tag: 'script', attrs: { src: '/analytics.js', async: true } },
|
|
177
|
+
{ tag: 'script', attrs: { type: 'application/json' }, text: '{"feature":true}' },
|
|
178
|
+
{ tag: 'style', attrs: { media: 'screen' }, text: 'body { color: #111 }' },
|
|
179
|
+
{ tag: 'noscript', html: '<img src="/pixel.png" alt="">' },
|
|
180
|
+
]}
|
|
181
|
+
onChangeClientState={(state, added, removed) => {}}
|
|
182
|
+
/>
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`Head` does not accept JSX children; use `tags`, `extra` or the props below for every value.
|
|
186
|
+
|
|
187
|
+
`tags` accepts `script`, `style`, `noscript` and `base`:
|
|
188
|
+
|
|
189
|
+
- `attrs` takes arbitrary attributes. `true` emits an empty attribute; `false`, `null` and `undefined` omit it. `className`, `htmlFor`, `httpEquiv` and the other React spellings map to their DOM names.
|
|
190
|
+
- `text` sets `textContent` for `script`, `style` and `noscript`. A closing-tag sequence inside script or style text is escaped, so the value cannot terminate its own element.
|
|
191
|
+
- `html` sets trusted `innerHTML` on `noscript` only. Never pass untrusted input.
|
|
192
|
+
- `key` gives a tag an explicit identity. Without one, external scripts key by `src` and inline tags key per mounted `Head`, so several scripts or styles survive together.
|
|
193
|
+
- `base` is a singleton; the last mounted `Head` that sets one wins.
|
|
194
|
+
|
|
195
|
+
`htmlAttributes`, `bodyAttributes` and `titleAttributes` write arbitrary attributes on the document element, body and generated title. Values the document already had are restored when no Head wants them, including after client-side navigation and after a prerendered page hydrates. `lang` and `dir` remain shortcuts; an entry in `htmlAttributes` wins when both are supplied.
|
|
196
|
+
|
|
197
|
+
`defaultTitle` fills the document title only when no mounted Head supplied a real `title`. It is not run through `titleTemplate`. `images` emits one `og:image` per URL and uses the first for `twitter:image`. `alternateLocales` emits one `og:locale:alternate` per value. Both keep every value through the merge.
|
|
198
|
+
|
|
199
|
+
`extra` stays the escape hatch for additional `meta` and `link` tags. Set `duplicates: true` on repeated same-name metas (for example `article:tag`) so each value keeps its own tag; an explicit `key` still works.
|
|
200
|
+
|
|
201
|
+
`onChangeClientState(state, added, removed)` runs after a real DOM change, not on every render. `state` groups the merged entries into `title`, `meta`, `link`, `script`, `style`, `noscript`, `base`, `htmlAttributes`, `bodyAttributes` and `titleAttributes`. `added` and `removed` are entry descriptors with `kind`, `key` and their value.
|
|
202
|
+
|
|
203
|
+
React 19 hoists native `<title>`, `<meta>` and `<link>` elements. `Head` adopts an existing tag with the same identity and updates it instead of adding a second one. Use one owner per tag: either React 19 native metadata or `Head`, not both for the same tag. The run still warns when a page ships more than one `<title>`.
|
|
204
|
+
|
|
205
|
+
The `snappy-prerender/head` entry is plain JavaScript with JSDoc types for `HeadProps`, `HeadTag`, `HeadAttributes`, `HeadChange` and the imperative API. It ships no TypeScript files.
|
|
206
|
+
|
|
207
|
+
This is a client-side head manager. It deliberately has no `HelmetProvider`, `helmetContext`, `HelmetData`, `renderStatic()`, streaming or server rendering API; the prerenderer renders the real app in a browser and captures the live head.
|
|
208
|
+
|
|
148
209
|
What it emits, per route: `title` (with the template applied unless the title already carries its suffix), `meta[name=description]`, `meta[name=robots]`, `link[rel=canonical]`, `og:title`, `og:description`, `og:type`, `og:url`, `og:image`, `og:site_name`, `og:locale`, and `twitter:card` / `twitter:title` / `twitter:description` / `twitter:image`. Switch a family off with `openGraph: false` or `twitter: false`, and skip the canonical with `canonical: false`.
|
|
149
210
|
|
|
150
|
-
Rules
|
|
151
|
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
154
|
-
-
|
|
155
|
-
- When no Head wants a tag
|
|
156
|
-
- The component renders nothing, so it
|
|
157
|
-
- The title template
|
|
158
|
-
-
|
|
159
|
-
- `
|
|
160
|
-
-
|
|
211
|
+
Rules:
|
|
212
|
+
- `metadata` values are defaults; a `Head` prop wins. When several Heads render, the last one wins per tag.
|
|
213
|
+
- Tags are matched by the attribute a crawler reads, so existing tags are updated instead of duplicated. Tags that may legitimately repeat keep a value-specific identity, so multiple images, alternate locales, scripts and styles survive.
|
|
214
|
+
- `defaultTitle` is used only when no mounted Head supplied a title, and it is never run through `titleTemplate`.
|
|
215
|
+
- Generated tags carry `data-snappy-head-created` beside `data-snappy-head`, so a later page load can tell them from tags the app wrote.
|
|
216
|
+
- When no Head wants a tag, a generated one is removed and an app-written one is restored to its previous value. That original is recorded in the page during prerendering, together with the original html, body and title attributes.
|
|
217
|
+
- The component renders nothing, so it does not affect hydration of page content.
|
|
218
|
+
- The title template is skipped when the title already ends with its fixed part, or is the site name, so a hand-built title is not suffixed twice.
|
|
219
|
+
- `structuredData` emits `<script type="application/ld+json">` for one object or an array, keyed per mounted `Head`. Values are escaped; add `@context` when needed.
|
|
220
|
+
- `lang` and `dir` are written on `<html>`. Later Heads win, and the original values return when no Head wants them. `htmlAttributes` covers every other document attribute, and `bodyAttributes` / `titleAttributes` do the same for body and title.
|
|
221
|
+
- `alternateLocales` emits `og:locale:alternate` for each listed locale.
|
|
222
|
+
- `alternates` emits `<link rel="alternate" hreflang="...">` with absolute URLs using the same `siteUrl`/`base`/`trailingSlash` policy as canonical links. The same alternates feed sitemap `xhtml:link` entries.
|
|
223
|
+
- `og:url`, `og:image` and canonical need absolute URLs. `metadata.siteUrl` must be an origin; put a sub-path deployment in `base`. Without `siteUrl`, build-server URLs are removed by default and the run warns. `trailingSlash` applies to page URLs only. Canonical, social and sitemap URLs share the page-path policy: directory output keeps its slash, flat output stays extensionless, and `trailingSlash` wins.
|
|
224
|
+
- `metadata.base` overrides the base path for absolute URLs and defaults to `base`.
|
|
225
|
+
- On React 19, React also hoists `<title>`, `<meta>` and `<link>` from JSX. `Head` adopts a matching hoisted `<meta>` or `<link>` and updates it in place. A hoisted `<title>` that already has the Head text is left untouched so React can remove it on navigation; older template titles are removed so the written page has one document title. `titleAttributes` apply only when `Head` owns the title. Use one metadata system; the run warns when a page still ships more than one `<title>`.
|
|
161
226
|
- Every metadata value has a CLI flag: `--metadata-site-url`, `--metadata-site-name`, `--metadata-title-template`, `--metadata-default-image`, `--metadata-trailing-slash`.
|
|
162
|
-
- `react` is an optional peer dependency
|
|
227
|
+
- `react` is an optional peer dependency for this entry point. Non-React code can call `setHead(id, props)` and `clearHead(id)`.
|
|
228
|
+
|
|
229
|
+
## Audit, reports and route sources
|
|
230
|
+
|
|
231
|
+
The audit runs by default and reports:
|
|
232
|
+
|
|
233
|
+
- route errors, truncation, empty or duplicate output, broken internal links;
|
|
234
|
+
- duplicate or missing metadata, canonical mismatches, heading counts, sitemap disagreement and orphan pages;
|
|
235
|
+
- CSP violations reported by the browser.
|
|
236
|
+
|
|
237
|
+
Findings are returned in `report.audit` and logged. They do not fail the run unless `failOnAuditError` is true. `auditIgnoreLinks` excludes dynamic links, and `--no-audit` skips the audit. See [docs/report.md](docs/report.md) for the rule list and JSON schema.
|
|
238
|
+
|
|
239
|
+
`reportPath` (or `--report`) writes the report as versioned JSON. URLs are reduced to origin and path, and the storage-state path is redacted.
|
|
240
|
+
|
|
241
|
+
Routes can come from `include` and crawling, from a text/JSON `routesFile`, or from a local or remote sitemap via `routesFromSitemap`. Relative source paths resolve against the config file. Root-relative sitemap locations have `base` stripped, and a remote sitemap's off-origin locations are ignored. If either source is supplied and `include` is not set, the root is not seeded implicitly. With `maxRoutes` or `routePriority`, crawling runs in deterministic breadth-first waves sorted by priority then path. `truncated.droppedRoutes` lists routes that did not fit.
|
|
242
|
+
|
|
243
|
+
`routeOverrides` applies `timeout`, `waitFor`, `readySelector`, `readyFlag`, `crawl`, `storageState` and `hydration` to matching routes; later entries win per key. `transformHtml` runs once per route after minification and before preload/CSP injection. It may be async and must return a string or nothing.
|
|
244
|
+
|
|
245
|
+
`preloadLinks` injects `<link rel="modulepreload">` for runtime chunks, font preloads with their response content type, and the LCP image. It complements `preloadManifest`: the manifest is for hosts that send Early Hints, while the injected links work on any static host. Script hints are skipped when `removeScriptTags` is on.
|
|
246
|
+
|
|
247
|
+
`csp: 'hash'` adds a `<meta http-equiv="Content-Security-Policy">` with SHA-256 hashes for inline scripts and styles. It implies `externalScripts: true` and no CSS swap handler, like `strict`. The policy allows `'self'` only, so third-party scripts or styles need their own policy; the audit warns when the browser reports a blocked request. An existing CSP meta is kept and reported. Inline `style=""` attributes and event handlers cannot be hashed; the audit warns about the attributes.
|
|
163
248
|
|
|
164
249
|
## Hydration verification
|
|
165
250
|
|
|
166
|
-
After writing the output,
|
|
251
|
+
After writing the output, each route is loaded again in a fresh browser context with the client bundle. By default the results are reported but do not fail the build; the static HTML is already written. Set `failOnHydrationError: true` (or pass `--fail-on-hydration-error`) to make hydration a build gate.
|
|
252
|
+
|
|
253
|
+
**Hydration errors.** Mismatch messages for React, Vue, Svelte and Preact are reported with the route, framework and message. React text mismatches and minified production codes (`#418`, `#423`, `#425`) are recognised; Preact uses the React-compatible messages. A mismatch means the client re-renders the page instead of adopting the static markup. The crawler-visible HTML is unchanged.
|
|
167
254
|
|
|
168
|
-
|
|
255
|
+
The framework is detected from DOM markers only (`__reactContainer`, `__vue_app__`/`__vue__`, `__k`, `data-svelte-h`); nothing is imported or executed. Set `framework` when a custom root defeats detection. When a route has hydration errors or re-rendered, `firstMismatch` reports the first difference between the written HTML and the live container as `{ path, kind, expected, actual }`, bounded to 5,000 nodes and depth 20.
|
|
169
256
|
|
|
170
|
-
|
|
257
|
+
A probe installed before the app's scripts run records the first prerendered node and checks whether it survived:
|
|
171
258
|
|
|
172
259
|
| Mode | Meaning |
|
|
173
260
|
| --- | --- |
|
|
174
|
-
| `hydrated` | The app
|
|
261
|
+
| `hydrated` | The app kept the prerendered markup. |
|
|
175
262
|
| `re-rendered` | The app discarded it and rendered from scratch. |
|
|
176
|
-
| `undetected` | No
|
|
177
|
-
| `unknown` | The prerendered node was
|
|
263
|
+
| `undetected` | No framework root was found on the container. |
|
|
264
|
+
| `unknown` | The prerendered node was not seen, for example a container outside the usual ids. |
|
|
265
|
+
|
|
266
|
+
A verification navigation that returns HTTP 4xx/5xx, or a container with no content, is reported as failed with its `status` or `empty` flag. `undetected` and `unknown` are reported as counts; `failOnUndetected: true` fails the build for them. A verification pass that cannot run is listed in `verification.errors`; `failOnVerificationError: true` fails separately from hydration errors.
|
|
267
|
+
|
|
268
|
+
`re-rendered` usually means the app calls `createRoot` instead of `hydrateRoot`. Since `createRoot` does not emit a hydration error, the mode report is the only signal. It is reported per route as `verification.routes[].mode` and summarised in `verification.modes`. Set `failOnRerender: true` to fail the build.
|
|
269
|
+
|
|
270
|
+
**Page errors.** Uncaught browser errors are reported as warnings, and console errors at debug level. Errors thrown during rendering are counted in the summary and reported per route; `failOnPageError: true` makes them fail the build. By default the static HTML is still written.
|
|
271
|
+
|
|
272
|
+
**React 16 and 17.** Hydration and boot modes are judged the same way, and their dev mismatch warnings are recognised. Their production builds report nothing for a mismatch, so the boot mode (`hydrated` versus `re-rendered`) is the signal.
|
|
273
|
+
|
|
274
|
+
**Rebuilds.** `verifyChangedOnly: true` skips verification for routes whose HTML file did not change. The report shows how many routes were skipped.
|
|
275
|
+
|
|
276
|
+
## Deterministic time
|
|
277
|
+
|
|
278
|
+
Copyright lines and dated content often differ between prerendering and the verification reload. Fix the clock for both passes:
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
snappy({
|
|
282
|
+
freezeTime: true, // SOURCE_DATE_EPOCH, or the run start
|
|
283
|
+
timezone: 'UTC',
|
|
284
|
+
randomSeed: 42, // optional; deterministic Math.random for demo content
|
|
285
|
+
});
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`freezeTime` also accepts an ISO string or epoch milliseconds, which is the reproducible-build option:
|
|
289
|
+
|
|
290
|
+
```js
|
|
291
|
+
freezeTime: '2026-05-04T12:00:00Z'
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Playwright's fixed clock is installed before navigation and keeps real timers running, so settling and scheduled work are unaffected. The page can read `window.__SNAPPY_NOW__` (epoch ms) or `window.__SNAPPY_NOW_ISO__`. `SOURCE_DATE_EPOCH` is honoured when `freezeTime` is `true`. A fixed clock is a build and verification control only: production still uses real time. For a live clock, render it client-only or use a hydration allow-list. `performance.now()` is not frozen because schedulers rely on it.
|
|
295
|
+
|
|
296
|
+
## Hydration policies
|
|
297
|
+
|
|
298
|
+
If a route is intentionally live or random, allow its re-render explicitly instead of suppressing verification globally:
|
|
178
299
|
|
|
179
|
-
|
|
300
|
+
```js
|
|
301
|
+
snappy({
|
|
302
|
+
hydration: {
|
|
303
|
+
allowRerender: ['/status', '/live/*'],
|
|
304
|
+
skip: ['/admin/*'],
|
|
305
|
+
},
|
|
306
|
+
routeOverrides: [
|
|
307
|
+
{ match: '/live/private', hydration: 'strict' },
|
|
308
|
+
],
|
|
309
|
+
});
|
|
310
|
+
```
|
|
180
311
|
|
|
181
|
-
`
|
|
312
|
+
`allow-rerender` keeps the mode in the report but moves its hydration errors to `ignoredHydrationErrors` and does not fail the run. `skip` omits the route from verification and records it in `verification.skippedRoutes`. A matching `routeOverrides[].hydration` wins over the global lists; `skip` wins if both global lists match. The CLI flags are `--allow-rerender` and `--skip-hydration`.
|
|
182
313
|
|
|
183
|
-
|
|
314
|
+
## Locales and hreflang
|
|
184
315
|
|
|
185
|
-
|
|
316
|
+
`locales` adds config-driven language metadata. Existing manual `Head` props still win; generated tags only fill missing values.
|
|
186
317
|
|
|
187
|
-
|
|
318
|
+
```js
|
|
319
|
+
snappy({
|
|
320
|
+
locales: {
|
|
321
|
+
mode: 'path',
|
|
322
|
+
default: 'en',
|
|
323
|
+
xDefault: 'en',
|
|
324
|
+
hreflang: true,
|
|
325
|
+
list: [
|
|
326
|
+
{ code: 'en', prefix: '', htmlLang: 'en', ogLocale: 'en_US' },
|
|
327
|
+
{
|
|
328
|
+
code: 'fr',
|
|
329
|
+
prefix: '/fr',
|
|
330
|
+
htmlLang: 'fr',
|
|
331
|
+
ogLocale: 'fr_FR',
|
|
332
|
+
routes: { '/about': '/a-propos' },
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
In `path` mode, the default locale is crawled once and every locale variant is rendered from the same base routes. `routes` maps translated slugs; a value is the path without the locale prefix. In `single` mode, `active` selects the locale rendered in this build and `list[].url` supplies the other deployments for hreflang. A locale entry can carry `locale` (Playwright/navigator), `timezone`, `storageState`, `cookies` and `localStorage`.
|
|
340
|
+
|
|
341
|
+
The package sets `<html lang>`/`dir` when missing, adds canonical URLs, `og:locale`, `og:locale:alternate`, self/alternate/`x-default` hreflang links, and feeds the same alternates into the sitemap. `Head` also accepts `alternateLocales={['en_US', 'de_DE']}` for app-managed Open Graph tags. The audit checks invalid and duplicate hreflang codes, missing self links, missing `x-default`, reciprocal links inside the same build, and an `<html lang>` that disagrees with the active locale.
|
|
342
|
+
|
|
343
|
+
Path mode requires one public origin and an app that routes the prefix or uses the translated `routes` map. Hreflang needs distinct crawlable URLs; a cookie-only locale does not create them. Use single mode for separate locale domains.
|
|
344
|
+
|
|
345
|
+
## Cookie/localStorage SEO locale
|
|
346
|
+
|
|
347
|
+
For an app whose locale lives in a cookie or `localStorage`, `seoLocale` selects the SEO language before the app boots:
|
|
348
|
+
|
|
349
|
+
```js
|
|
350
|
+
snappy({
|
|
351
|
+
seoLocale: {
|
|
352
|
+
code: 'fr',
|
|
353
|
+
source: 'both', // 'localStorage' | 'cookie' | 'both'
|
|
354
|
+
localStorageKey: 'locale',
|
|
355
|
+
cookie: { name: 'locale', path: '/', sameSite: 'Lax', secure: false },
|
|
356
|
+
seedRender: true,
|
|
357
|
+
seedVerification: true,
|
|
358
|
+
inject: true,
|
|
359
|
+
overwrite: true,
|
|
360
|
+
forceHtmlLang: true,
|
|
361
|
+
navigator: false,
|
|
362
|
+
routes: ['/*'],
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`seedRender` and `seedVerification` put the value into the prerender and verification browsers. `inject` writes a small bootstrap before the app bundle so real visitors get the same locale the static HTML was rendered in. `overwrite: false` only sets the value when missing, preserving a visitor's existing choice. `forceHtmlLang` overrides an app-owned `<html lang>`; `navigator` also emulates `navigator.language`/`Accept-Language`. In path mode the value comes from each locale's code; in single mode it comes from `locales.active` or `seoLocale.code`.
|
|
368
|
+
|
|
369
|
+
Under `csp: 'hash'` the bootstrap is hashed with the other inline scripts. Under `csp: 'strict'` or `externalScripts` it is written as a same-origin script. Combining `inject: true` with `removeScriptTags: true` is rejected because the bootstrap would be stripped.
|
|
370
|
+
|
|
371
|
+
This control does not create new crawlable URLs by itself. In single mode it produces one locale per output; for multiple crawlable locales use path-prefixed locales or separate builds/deployments. With `inject: false` the HTML is localized but a visitor with different storage may see another locale after hydration.
|
|
188
372
|
|
|
189
373
|
## Ready contract
|
|
190
374
|
|
|
@@ -228,7 +412,7 @@ snappy({ inlineCss: true }) // alias for 'inline'
|
|
|
228
412
|
npm install --save-dev beasties
|
|
229
413
|
```
|
|
230
414
|
|
|
231
|
-
It inlines the critical CSS and converts the remaining stylesheets to non-blocking loads using `media="print"` plus an inline `onload` handler, with a `<noscript>` fallback.
|
|
415
|
+
It inlines the critical CSS and converts the remaining stylesheets to non-blocking loads using `media="print"` plus an inline `onload` handler, with a `<noscript>` fallback. The inline handler is inline JavaScript, so a strict `script-src` without `unsafe-inline` leaves the deferred stylesheets unapplied. Use `'inline'` or a nonce-aware setup in that case.
|
|
232
416
|
|
|
233
417
|
### Minification
|
|
234
418
|
|
|
@@ -236,7 +420,7 @@ It inlines the critical CSS and converts the remaining stylesheets to non-blocki
|
|
|
236
420
|
snappy({ minifyHtml: true, minifyCss: true });
|
|
237
421
|
```
|
|
238
422
|
|
|
239
|
-
`minifyHtml` uses html-minifier-terser with react-snap's defaults (whitespace collapsing, boolean attribute collapsing, attribute sorting). **Whitespace collapsing changes text nodes, which React
|
|
423
|
+
`minifyHtml` uses html-minifier-terser with react-snap's defaults (whitespace collapsing, boolean attribute collapsing, attribute sorting). **Whitespace collapsing changes text nodes, which React detects during hydration.** Use `{ collapseWhitespace: false }` if the verifier reports mismatches. `minifyCss` is passed to clean-css and also minifies CSS inlined into the HTML.
|
|
240
424
|
|
|
241
425
|
### Link hints
|
|
242
426
|
|
|
@@ -244,11 +428,11 @@ snappy({ minifyHtml: true, minifyCss: true });
|
|
|
244
428
|
snappy({ preconnectThirdParty: true, preloadImages: true, preloadManifest: true });
|
|
245
429
|
```
|
|
246
430
|
|
|
247
|
-
- `preconnectThirdParty` (default `true`) records every third-party origin the page tried to reach
|
|
431
|
+
- `preconnectThirdParty` (default `true`) records every third-party origin the page tried to reach, including blocked ones, and adds `<link rel="preconnect">` for each.
|
|
248
432
|
- `preloadImages` adds `<link rel="preload" as="image">` for same-origin images the page loaded.
|
|
249
|
-
- `preloadManifest` writes `preload-manifest.json
|
|
433
|
+
- `preloadManifest` writes `preload-manifest.json`: per route, a `Link` header value for the scripts and stylesheets the final HTML still references. Entries respect `ignoreForPreload` (default `['service-worker.js']`), inline CSS, tag removal and the base path. Serve them as Early Hints or `Link` headers.
|
|
250
434
|
|
|
251
|
-
react-snap called this `http2PushManifest`. Browsers removed HTTP/2 push (Chrome
|
|
435
|
+
react-snap called this `http2PushManifest`. Browsers removed HTTP/2 push (Chrome 106), so the manifest is a header list rather than a push list.
|
|
252
436
|
|
|
253
437
|
### Sitemap
|
|
254
438
|
|
|
@@ -256,7 +440,7 @@ react-snap called this `http2PushManifest`. Browsers removed HTTP/2 push (Chrome
|
|
|
256
440
|
snappy({ sitemap: true, metadata: { siteUrl: 'https://example.com' } });
|
|
257
441
|
```
|
|
258
442
|
|
|
259
|
-
|
|
443
|
+
The sitemap is generated from the written routes using the same page-path policy as canonical URLs. `noindex` pages and the not-found route are omitted. Above 50,000 URLs it is split behind a sitemap index. Entries have no `<lastmod>`; a build date would claim every page changed on every build.
|
|
260
444
|
|
|
261
445
|
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
446
|
|
|
@@ -271,7 +455,7 @@ snappy({
|
|
|
271
455
|
});
|
|
272
456
|
```
|
|
273
457
|
|
|
274
|
-
The persisted metadata element is
|
|
458
|
+
The persisted metadata element is kept when `removeScriptTags` is on; other scripts are removed.
|
|
275
459
|
|
|
276
460
|
### Removing volatile markup
|
|
277
461
|
|
|
@@ -281,9 +465,9 @@ Mark any element with `data-prerender-remove` and it is dropped from the output:
|
|
|
281
465
|
<div data-prerender-remove>{ads.render()}</div>
|
|
282
466
|
```
|
|
283
467
|
|
|
284
|
-
Use it for regions that only
|
|
468
|
+
Use it for regions that only exist after the client takes over, such as ads or chat widgets. Remove markup that React does not render on its first client pass. Removing markup React expects causes the hydration mismatch the verifier reports.
|
|
285
469
|
|
|
286
|
-
Some libraries stamp values that change on every
|
|
470
|
+
Some libraries stamp values that change on every load. Ant Design's `rc-menu`, for example, 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 removed from the output:
|
|
287
471
|
|
|
288
472
|
```js
|
|
289
473
|
snappy({ removeAttributes: ['data-menu-id', 'aria-controls'] });
|
|
@@ -314,21 +498,21 @@ Two things the dial cannot remove, because they are what prerendered HTML is:
|
|
|
314
498
|
|
|
315
499
|
## Third-party requests
|
|
316
500
|
|
|
317
|
-
Third-party requests are
|
|
501
|
+
Third-party requests are allowed by default. Most React sites load content from a CMS, headless API or separate service origin; blocking those during prerendering produces pages without data. The default matches react-snap.
|
|
318
502
|
|
|
319
503
|
```js
|
|
320
|
-
snappy({ blockThirdParty: false }); // default: allow
|
|
321
|
-
snappy({ blockThirdParty: true }); // abort
|
|
504
|
+
snappy({ blockThirdParty: false }); // default: allow all
|
|
505
|
+
snappy({ blockThirdParty: true }); // abort third-party requests
|
|
322
506
|
snappy({ blockThirdParty: true, allowedHosts: ['cms.example.com'] }); // block all but the CMS
|
|
323
507
|
```
|
|
324
508
|
|
|
325
|
-
|
|
509
|
+
Blocking makes rendering deterministic and keeps analytics, ads and chat widgets from executing during a build. `allowedHosts` keeps a CMS API reachable while blocking everything else.
|
|
326
510
|
|
|
327
|
-
Preconnect hints are generated for
|
|
511
|
+
Preconnect hints are generated for every third-party origin the page contacts, including allowlisted hosts while blocking. Set `preconnectThirdParty: false` to disable them. The CLI equivalent for blocking is `--block-third-party`.
|
|
328
512
|
|
|
329
513
|
## CSS-in-JS
|
|
330
514
|
|
|
331
|
-
Emotion, styled-components, vanilla-extract, stitches
|
|
515
|
+
Emotion, styled-components, vanilla-extract, stitches and JSS work when their styles end up as text inside a `<style>` tag. Three cases need extra handling:
|
|
332
516
|
|
|
333
517
|
**CSSOM-only styles.** In production, emotion and styled-components v5 switch to `CSSStyleSheet.insertRule` ("speedy" mode), which puts rules in the CSS object model and leaves the style element empty in the DOM. Serialising the page would drop every one of those rules. `captureRuntimeStyles` (default `true`) copies them back into the element text, and also folds document-level constructable stylesheets (`document.adoptedStyleSheets`) into a `<style>` tag. Set it to `false` to leave stylesheets untouched.
|
|
334
518
|
|
|
@@ -363,7 +547,7 @@ if (cached) return cached;
|
|
|
363
547
|
return fetch('/api/items?page=2').then((response) => response.json());
|
|
364
548
|
```
|
|
365
549
|
|
|
366
|
-
`window.snapStore` is keyed by the request path
|
|
550
|
+
`window.snapStore` is keyed by the request path, including its query string. Responses larger than `maxCachedBytes` (default 5 MiB) are skipped so one large endpoint cannot bloat every page.
|
|
367
551
|
|
|
368
552
|
**App state.** Define `window.snapSaveState` and whatever it returns is injected as globals before the first script:
|
|
369
553
|
|
|
@@ -438,7 +622,18 @@ The function is awaited once per run, and whatever it returns must be a plain op
|
|
|
438
622
|
| `exclude` | `[]` | Routes to skip, strings or RegExp |
|
|
439
623
|
| `crawl` | `true` | Follow same-origin links from seeds |
|
|
440
624
|
| `maxDepth` | `null` | Crawl depth limit, `null` is unlimited, `0` renders seeds only |
|
|
441
|
-
| `maxRoutes` | `null` | Stop after this many routes; hitting the cap marks the run incomplete |
|
|
625
|
+
| `maxRoutes` | `null` | Stop after this many routes; hitting the cap marks the run incomplete and lists the routes that did not fit in `truncated.droppedRoutes` |
|
|
626
|
+
| `routePriority` | `[]` | Patterns ranked first when crawling, strings or RegExp; setting it selects deterministic BFS |
|
|
627
|
+
| `routesFile` | `null` | Text or JSON route seeds, one route per line or `{ "routes": [] }`; a relative path resolves against the config file |
|
|
628
|
+
| `routesFromSitemap` | `null` | Local path or http(s) URL whose `<loc>` entries seed routes; sitemap indexes are followed one level, the public base is stripped from root-relative locs, and off-origin locs are ignored. Absolute locs in a local sitemap need `metadata.siteUrl` or a remote sitemap origin |
|
|
629
|
+
| `routeOverrides` | `[]` | Per-route `{ match, timeout, waitFor, readySelector, readyFlag, crawl, storageState, hydration }`; later entries win |
|
|
630
|
+
| `freezeTime` | `false` | `true`, an ISO string or epoch ms; fixes `Date`/`Date.now()` in render and verification. `true` uses `SOURCE_DATE_EPOCH` or the run start |
|
|
631
|
+
| `timezone` | `null` | IANA timezone applied to render and verification contexts |
|
|
632
|
+
| `randomSeed` | `null` | Deterministic `Math.random()` during render and verification; never for security |
|
|
633
|
+
| `hydration` | `{ allowRerender: [], skip: [] }` | Route patterns whose re-render is allowed or whose verification is skipped |
|
|
634
|
+
| `locales` | `null` | Locale matrix for path or single mode, automatic hreflang, canonical, `og:locale` and sitemap alternates |
|
|
635
|
+
| `seoLocale` | `null` | Optional cookie/localStorage locale seed and output bootstrap for SEO builds |
|
|
636
|
+
| `transformHtml` | `null` | Function `(html, { route, file, stats, config })` run before preload/CSP injection; may be async |
|
|
442
637
|
| `concurrency` | derived | Parallel pages, derived from CPU count, capped at 8 |
|
|
443
638
|
| `timeout` | `30000` | Navigation, ready and network-settle timeout in ms; each wait gets this budget |
|
|
444
639
|
| `quietPeriod` | `500` | Network must be idle this long before capture |
|
|
@@ -462,7 +657,7 @@ The function is awaited once per run, and whatever it returns must be a plain op
|
|
|
462
657
|
| `scrollStepDelay` | `100` | Delay between scroll steps when `scrollToBottom` is on |
|
|
463
658
|
| `captureRuntimeStyles` | `true` | Fold CSSOM-only styles and constructable stylesheets into the output |
|
|
464
659
|
| `captureFormState` | `true` | Sync `checked` and `selected` into the output markup |
|
|
465
|
-
| `csp` | `'off'` | `'off'` leaves the injected pieces inline; `'strict'` makes the output work under `script-src 'self'` by moving state to a same-origin file and dropping the CSS swap handlers |
|
|
660
|
+
| `csp` | `'off'` | `'off'` leaves the injected pieces inline; `'strict'` makes the output work under `script-src 'self'` by moving state to a same-origin file and dropping the CSS swap handlers; `'hash'` adds a meta policy with SHA-256 hashes for inline scripts and styles |
|
|
466
661
|
| `inlineCss` | `false` | `'inline'`, `'critical'` (needs beasties) or `true` for `'inline'` |
|
|
467
662
|
| `criticalCssPreload` | `'media'` | Swap used for CSS deferred by `inlineCss: 'critical'`; `false` emits no swap handlers, for a strict script-src |
|
|
468
663
|
| `minifyHtml` | `false` | Minify output HTML, `true` for defaults or an html-minifier-terser options object |
|
|
@@ -470,8 +665,10 @@ The function is awaited once per run, and whatever it returns must be a plain op
|
|
|
470
665
|
| `preconnectThirdParty` | `true` | Add preconnect hints for every third-party origin the page contacts |
|
|
471
666
|
| `preloadImages` | `false` | Add preload hints for same-origin images |
|
|
472
667
|
| `preloadManifest` | `false` | Write `preload-manifest.json` with Link header hints, derived from the final HTML |
|
|
473
|
-
| `
|
|
668
|
+
| `preloadLinks` | `false` | Inject module, font and LCP image preload hints for static hosts; script hints are skipped when `removeScriptTags` is on |
|
|
669
|
+
| `ignoreForPreload` | `['service-worker.js']` | File names excluded from the manifest and injected hints |
|
|
474
670
|
| `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` |
|
|
671
|
+
| `imageSitemap` | `false` | List same-origin page images in the sitemap |
|
|
475
672
|
| `cacheAjaxRequests` | `false` | Expose captured JSON responses as `window.snapStore` |
|
|
476
673
|
| `maxCachedBytes` | `5242880` | Largest JSON response cached per request, measured on the serialised body |
|
|
477
674
|
| `removeBlobs` | `true` | Drop stylesheet links pointing at dead `blob:` URLs |
|
|
@@ -482,13 +679,22 @@ The function is awaited once per run, and whatever it returns must be a plain op
|
|
|
482
679
|
| `removeStyleTags` | `false` | Strip every style tag from the output; cannot be combined with `inlineCss` `'inline'` |
|
|
483
680
|
| `removeScriptTags` | `false` | Strip every script tag from the output |
|
|
484
681
|
| `asyncScriptTags` | `false` | Mark external scripts async |
|
|
485
|
-
| `externalScripts` | `false` | Write injected state scripts as content-hashed same-origin files instead of inline; set by `csp: 'strict'` |
|
|
682
|
+
| `externalScripts` | `false` | Write injected state scripts as content-hashed same-origin files instead of inline; set by `csp: 'strict'` or `'hash'` |
|
|
486
683
|
| `flatOutput` | `false` | Write `about.html` instead of `about/index.html` |
|
|
487
|
-
| `notFoundRoute` | `/404` | Route emitted as `404.html` with a robots noindex tag; a notice is logged when it is never prerendered |
|
|
684
|
+
| `notFoundRoute` | `/404` | Route emitted as `404.html` with a robots noindex tag, even when the server answers HTTP 404; a notice is logged when it is never prerendered. A page can also declare `<meta name="prerender-status-code" content="404">` |
|
|
685
|
+
| `audit` | `true` | Check the written site for route health, broken links, metadata, sitemap and CSP issues |
|
|
686
|
+
| `failOnAuditError` | `false` | Fail when the audit finds an error |
|
|
687
|
+
| `auditIgnoreLinks` | `[]` | Link patterns excluded from the audit, strings or RegExp |
|
|
688
|
+
| `reportPath` | `null` | Write the versioned JSON run report to this file; URLs and the storage-state path are redacted |
|
|
488
689
|
| `saveAs` | `html` | `html`, `png` or `jpeg`; identical screenshots are left untouched |
|
|
489
690
|
| `metadata` | `null` | Defaults for the head component: `siteUrl` (origin only), `siteName`, `titleTemplate`, `defaultImage`, `trailingSlash`, `base` |
|
|
490
691
|
| `verify` | `true` | Run the hydration verification pass |
|
|
692
|
+
| `framework` | `auto` | `auto`, `react`, `vue`, `svelte` or `preact`; detects the mounted framework from DOM markers |
|
|
693
|
+
| `retries` | `0` | Retry a retryable render failure this many times |
|
|
694
|
+
| `retryDelay` | `500` | Wait between render attempts, in ms |
|
|
695
|
+
| `traceOnError` | `false` | Write a Playwright trace for the final failed attempt under `snappy-traces/` |
|
|
491
696
|
| `verifyChangedOnly` | `false` | Skip verification for routes whose HTML file did not change since the last run |
|
|
697
|
+
| `reuseVerificationContext` | `false` | Reuse one browser context per verification worker; routes share storage, so use only for stateless apps |
|
|
492
698
|
| `failOnHydrationError` | `false` | Fail the run on hydration errors; off by default, where they are reported instead |
|
|
493
699
|
| `failOnRerender` | `false` | Fail the run when a route re-renders instead of hydrating |
|
|
494
700
|
| `failOnPageError` | `false` | Fail the run when a page throws while rendering; off by default, where page errors are reported |
|
|
@@ -522,41 +728,15 @@ if (!report.ok) {
|
|
|
522
728
|
}
|
|
523
729
|
```
|
|
524
730
|
|
|
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
|
|
731
|
+
The report exposes `routes`, `files` (with per-route `status` and `noindex`), `routeDetails` (the per-route records used by the JSON report), `errors`, `pageErrors` (uncaught browser errors seen while rendering), `consoleErrors`, `failedRequests`, `emptyPages`, `duplicatePages`, `duplicateTitles` (routes shipping more than one title), `verification` (nullable; per-route `mode`, `framework`, `firstMismatch`, `status` and `empty` plus `modes` and `frameworks` summaries), `audit` (nullable; `errors`, `warnings`, `linksChecked`), `preloadManifest`, `sitemap`, `pruned`, `truncated`, `notFoundRoutes`, `cspSkipped`, `traces`, and `ok`. See [docs/report.md](docs/report.md) for the JSON schema.
|
|
526
732
|
|
|
527
733
|
`snappy-prerender` exports `DEFAULTS`, `prerender` and `resolveConfig`; the default export is the Vite plugin.
|
|
528
734
|
|
|
529
|
-
##
|
|
530
|
-
|
|
531
|
-
Kept: crawling, include, concurrency, viewport, waitFor, executable path, external server, third-party skipping (now on by default), `userAgent`, `inlineCss`, `minifyHtml`, `minifyCss`, `cacheAjaxRequests`, `snapSaveState`, `preconnectThirdParty`, `preloadImages`, `removeBlobs`, `removeStyleTags`, `removeScriptTags`, `asyncScriptTags`, `destination`, `saveAs` (html/png/jpeg).
|
|
735
|
+
## Further reading
|
|
532
736
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
Dropped: `sourceMaps` (declared but never referenced in react-snap's code), `fixWebpackChunksIssue` and `fixInsertRule` (webpack/Chrome-era workarounds), `port` (ephemeral loopback port instead), `puppeteer.cache`, and the preload polyfill (modulepreload is universally supported).
|
|
536
|
-
|
|
537
|
-
## Limitations
|
|
538
|
-
|
|
539
|
-
- **Build-time data is a snapshot.** Anything fetched during rendering is frozen into the HTML until the next build. Use `cacheAjaxRequests` and `snapSaveState` so the client replays the same data.
|
|
540
|
-
- **Hydration mismatches are your app's to fix.** The verifier tells you exactly which route and which React error. Common causes: `Date.now()`, `Math.random()`, `localStorage` reads during render, viewport-dependent markup, and CSS-in-JS that re-injects styles on hydration.
|
|
541
|
-
- **Markup that depends on load state cannot hydrate.** A component that swaps a placeholder for real content once an image or widget loads renders one thing during prerendering (where the network has settled) and another on the client's first render. The verifier reports those routes as `re-rendered`. Render deterministic markup instead — a plain `<img loading="lazy">` rather than a JS placeholder swap — or accept the re-render.
|
|
542
|
-
- **Authenticated routes need `storageState`.** Never prerender personal data into static HTML without deciding to.
|
|
543
|
-
- **Runtime-only routes** (heavy API dependence, real-time data) are better served by `serveCmd` or left client-rendered.
|
|
544
|
-
- Route query strings are ignored; routes are deduplicated without them.
|
|
545
|
-
- Routes whose decoded path contains dot segments or backslashes are skipped, so encoded traversal cannot create stray directories.
|
|
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.
|
|
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>`.
|
|
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.
|
|
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.
|
|
737
|
+
[Limitations](docs/limitations.md), [report schema and audit rules](docs/report.md), [migrating from react-snap](docs/react-snap-migration.md).
|
|
557
738
|
|
|
558
739
|
## Requirements
|
|
559
|
-
|
|
560
740
|
- Node.js >= 22.12
|
|
561
741
|
- Chrome or Edge installed (detected automatically), or allow the one-time Chrome for Testing download fallback
|
|
562
742
|
- `beasties` only if you use `inlineCss: 'critical'` (optional peer dependency)
|