Hydration Proof

Search documentation

Find a page or section

Discovered, listed, from the sitemap or crawled.

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

SourceMeaning
configroutes.paths, routes.dynamic or --route
discoveredFound by the framework adapter, for example in app/ or pages/
manifestExample values of a dynamic route, read from the Next.js build output (including i18n locales)
sitemapListed in a sitemap
crawlLinked from a tested page
not-foundThe 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:

AdapterRoutes
nextapp/ 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-routerreact-router routes --json
remixremix routes --json
astrosrc/pages
vite, node, noneFrom 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:

hydration-proof.config.ts
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
    ],
  },
});
  • expectStatus lists HTTP statuses that are not errors for the route.
  • expectRedirect is the path the route should end on. Without it, a page that ends on another path is reported as HP9010.
  • ready overrides the ready options for this route, for example a page that never goes quiet.
  • scenarios limits the route to some scenarios.
OptionTypeDefaultDescription
path*string—Path, e.g. /pricing or /products/42?tab=reviews.
expectRedirectstring—The final URL (path) the route must redirect to. Other redirects are reported.
patternstringthe path without queryRoute pattern used for grouping and fingerprints.
expectStatusnumber[]—HTTP statuses that are not errors for this route (e.g. [404]).
scenariosstring[]—Only test in these scenarios.
readyReadyConfig—Per-route readiness overrides.
navigateFromstringchecks.navigation.fromPage 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:

hydration-proof.config.ts
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:

hydration-proof.config.ts
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.

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.

OptionTypeDefaultDescription
depthnumber2Link depth from the start routes.
limitnumber50Maximum 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.
  • exclude defaults to ["/api/**"]. Setting it replaces that default, so keep /api/** in your list.
hydration-proof.config.ts
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-cache

How 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.

hydration-proof.config.ts
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

OptionTypeDefaultDescription
paths(string | RouteEntry)[]["/"] when discovery is offRoutes to test. Strings are paths.
dynamicRecord<string, string[]>—Example values for dynamic segments, e.g. { "/products/[id]": ["1", "42"] }.
includestring[]—Glob patterns (*, **) a route must match.
excludestring[]—Glob patterns of routes to skip.
discoverbooleantrue when paths is emptyFind routes from the framework (Next.js app/ and pages/, plus build manifests).
queryRecord<string, string[]>—Query-string variants per route pattern, e.g. { "/search": ["?q=shoes", "?q=&page=2"] }.
sitemapboolean | string—Read routes from the sitemap: true for /sitemap.xml (and robots.txt), or a sitemap URL/path.
crawlboolean | CrawlConfig—Follow same-origin links found on tested pages.
notFoundbooleantrue for Next.jsAlso test a URL that does not exist, to check the not-found page hydrates.
manifestExamplesnumber3Most example values taken per dynamic route from build manifests.