To test every route for hydration errors, start with automatic discovery. hydration-proof finds pages from your framework and build, then adds any dynamic values, sitemap entries or crawled links you configure. The report shows where each route came from. A dynamic route without a real value is skipped with a note.
Where routes come from
| Source | Meaning |
|---|---|
config | routes.paths, routes.dynamic or --route |
discovered | Found by the framework adapter, for example in app/ or pages/ |
manifest | Example values of a dynamic route, read from the Next.js build output (including i18n locales) |
sitemap | Listed in a sitemap |
crawl | Linked from a tested page |
not-found | The not-found probe |
include, exclude and --grep apply to routes from every source.
Discovery for each framework
Discovery is on by default when you do not set paths. Your
adapter decides where to look:
| Adapter | Routes |
|---|---|
next | app/ and pages/, plus the pages the build pre-rendered. Route groups, parallel routes and private folders are handled; API routes are skipped by the default exclude |
react-router | react-router routes --json |
remix | remix routes --json |
astro | src/pages |
vite, node, none | From the config: list them in routes.paths |
List routes yourself
Use paths for redirects, rewrites, pages behind a flag or a framework that
cannot discover routes. Setting paths turns automatic discovery off unless
you also set discover: true. An entry is a path or an object with options:
import { defineConfig } from "hydration-proof";
export default defineConfig({
routes: {
paths: [
"/",
"/pricing?plan=team",
// A page that is meant to answer 404
{ path: "/404-page", expectStatus: [404], ready: { selector: "#app" } },
// A page that is meant to redirect
{ path: "/account", expectRedirect: "/login" },
// Add scenarios: ["admin"] after defining an admin scenario
],
},
});expectStatuslists HTTP statuses that are not errors for the route.expectRedirectis the path the route should end on. Without it, a page that ends on another path is reported as HP9010.readyoverrides thereadyoptions for this route, for example a page that never goes quiet.scenarioslimits the route to some scenarios.
| Option | Type | Default | Description |
|---|---|---|---|
| path* | string | — | Path, e.g. /pricing or /products/42?tab=reviews. |
| expectRedirect | string | — | The final URL (path) the route must redirect to. Other redirects are reported. |
| pattern | string | the path without query | Route pattern used for grouping and fingerprints. |
| expectStatus | number[] | — | HTTP statuses that are not errors for this route (e.g. [404]). |
| scenarios | string[] | — | Only test in these scenarios. |
| ready | ReadyConfig | — | Per-route readiness overrides. |
| navigateFrom | string | checks.navigation.from | Page the navigation check starts from for this route. |
Dynamic routes
A route like /products/[id] needs a value before it can be tested. Add real
test data in dynamic if your build does not provide it:
import { defineConfig } from "hydration-proof";
export default defineConfig({
routes: {
dynamic: {
"/products/[id]": ["your-stable-test-product-id"],
// Several parameters: separate the values with "/";
// a catch-all takes the rest
"/[lang]/docs/[...slug]": ["en/getting-started/install"],
},
},
});Replace the sample product ID and slug with values that exist in your app.
The route syntax works across frameworks: [id], [...slug] (catch-all) and
[[lang]] (optional). React Router's :id, * and :lang?, and Astro's
[id] and [...slug], are converted, so routes.dynamic looks the same
everywhere.
For Next.js, the build may already supply values from generateStaticParams
or getStaticPaths. The tool uses up to manifestExamples values per route
(three by default). You only need routes.dynamic for routes the build
cannot supply. A skipped route is listed at the end of the run.
Query-string variants
When a page renders differently for a query string, test each variant:
import { defineConfig } from "hydration-proof";
export default defineConfig({
routes: {
query: { "/search": ["?q=shoes", "?q=&page=2"] },
},
});Routes from the sitemap
Set sitemap: true or pass --sitemap to add URLs from your sitemap. The
tool checks Sitemap: entries in robots.txt and /sitemap.xml. You can
also set a sitemap URL or path. Absolute production URLs are tested against
the local app, not the production server.
Crawl links from tested pages
Set crawl: true or pass --crawl to follow links on tested pages. By
default, the tool follows same-origin links two levels deep, up to 50 routes.
Use { depth, limit } to change that. It does not crawl outside your app.
| Option | Type | Default | Description |
|---|---|---|---|
| depth | number | 2 | Link depth from the start routes. |
| limit | number | 50 | Maximum number of crawled routes. |
The not-found page
For Next.js, notFound is on: hydration-proof also loads
/hydration-proof-not-found and checks that your not-found page hydrates. That
URL must answer with a 404. Set notFound: false to skip it.
Include and exclude routes with globs
include and exclude take globs, matched against the route's path:
*matches one path segment.**matches any number of segments, and/blog/**also matches/blog.excludedefaults to["/api/**"]. Setting it replaces that default, so keep/api/**in your list.
import { defineConfig } from "hydration-proof";
export default defineConfig({
routes: {
include: ["/", "/blog/**", "/products/*"],
exclude: ["/api/**", "/admin/**"],
},
});Scenarios have their own include and exclude, for pages only one identity
can open.
Choose routes on the command line
# Only these routes; discovery, the sitemap and the not-found probe are off
npx hydration-proof test --route / --route /pricing
# Only routes whose path matches a regular expression
npx hydration-proof test --grep "^/blog"
# Also the sitemap, and links found on tested pages
npx hydration-proof test --sitemap --crawl
# Only the routes the files changed since main can affect
npx hydration-proof test --changed main--grep applies to routes from every source. --changed reads an import graph
of your sources; see CI for how it picks routes.
The route cache
Discovered routes are cached in .hydration-proof/cache/routes.json. A new
build or a changed route folder refreshes the cache. Sitemap and crawled
routes are read again on every run. Use --no-cache if a route change was
missed.
If a new page is missing, run with --no-cache, or set cache: false in the
config to always discover again:
npx hydration-proof test --no-cacheHow to test every route for hydration errors in a Next.js app
This config, from the package's examples, is what most Next.js apps end up with after the first run: discovery, one real value per dynamic segment, and the routes that are not pages.
import { defineConfig } from "hydration-proof";
export default defineConfig({
routes: {
discover: true,
// Routes discovery cannot see: redirects, rewrites, anything behind a flag
paths: ["/pricing?plan=team"],
// One real value per dynamic segment, or the route is skipped
dynamic: {
"/blog/[slug]": ["hello-world"],
"/products/[id]": ["your-stable-test-product-id"],
},
exclude: ["/api/**", "/admin/**"],
},
});All route options
| Option | Type | Default | Description |
|---|---|---|---|
| paths | (string | RouteEntry)[] | ["/"] when discovery is off | Routes to test. Strings are paths. |
| dynamic | Record<string, string[]> | — | Example values for dynamic segments, e.g. { "/products/[id]": ["1", "42"] }. |
| include | string[] | — | Glob patterns (*, **) a route must match. |
| exclude | string[] | — | Glob patterns of routes to skip. |
| discover | boolean | true when paths is empty | Find routes from the framework (Next.js app/ and pages/, plus build manifests). |
| query | Record<string, string[]> | — | Query-string variants per route pattern, e.g. { "/search": ["?q=shoes", "?q=&page=2"] }. |
| sitemap | boolean | string | — | Read routes from the sitemap: true for /sitemap.xml (and robots.txt), or a sitemap URL/path. |
| crawl | boolean | CrawlConfig | — | Follow same-origin links found on tested pages. |
| notFound | boolean | true for Next.js | Also test a URL that does not exist, to check the not-found page hydrates. |
| manifestExamples | number | 3 | Most example values taken per dynamic route from build manifests. |