Hydration Proof

Search documentation

Find a page or section

Discovered, listed, from the sitemap or crawled.

To test every route for hydration errors, hydration-proof combines the routes it discovers from your framework and build output with the paths, dynamic values, sitemap entries and crawled links you configure. Every page in the report records where its route came from. A dynamic route needs example values, or it 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 when you do not list paths. The adapter decides where routes come from:

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

paths adds routes discovery cannot know about: redirects, rewrites, pages behind a flag, or every route of a framework without discovery. An entry is a path string, with an optional query, or an object with more 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" },
      // Only in one scenario
      { path: "/admin", scenarios: ["admin"] },
    ],
  },
});
  • 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*stringPath, e.g. /pricing or /products/42?tab=reviews.
expectRedirectstringThe 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.
readyReadyConfigPer-route readiness overrides.
navigateFromstringchecks.navigation.fromPage the navigation check starts from for this route.

Dynamic routes

A route like /products/[id] has nothing to render until it has a value. Give it example values in dynamic:

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  routes: {
    dynamic: {
      "/products/[id]": ["1", "42"],
      // Several parameters: separate the values with "/";
      // a catch-all takes the rest
      "/[lang]/docs/[...slug]": ["en/getting-started/install"],
    },
  },
});

One syntax works for every framework: [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, pages the build pre-rendered with generateStaticParams or getStaticPaths are tested without any config: up to manifestExamples values (3 by default) are taken per dynamic route from the build output. Dynamic routes that end up with no values are skipped, and the run lists them in a note.

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

sitemap: true (or --sitemap) also tests the routes in your sitemap. It reads the Sitemap: lines of robots.txt and /sitemap.xml; you can also give a sitemap URL or path. Sitemap indexes are followed, and absolute URLs are moved to the tested app, so a sitemap that names https://example.com still works locally.

crawl: true (or --crawl) also tests same-origin links found on the tested pages, to a depth of 2 and at most 50 routes. Pass { depth, limit } to change that. Crawled links are grouped under the most specific known route pattern, and crawling never leaves the app's origin.

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. The cache key covers the Next.js build id and the modification times of the route folders, so a new build or a changed route folder invalidates it. Sitemap and crawled routes are never cached.

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]": ["1", "42"],
    },
    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 | stringRead routes from the sitemap: true for /sitemap.xml (and robots.txt), or a sitemap URL/path.
crawlboolean | CrawlConfigFollow 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.