Gatsby static site and Legacy Node/Express backend: Client‑side hydration isolation
0 reputation · 03 Jun 2025, 06:40 UTC
0 reputation · 03 Jun 2025, 06:40 UTC
Ensure that the Gatsby‑generated static site can be served alongside the existing Node/Express backend during a zero‑downtime cut‑over without client‑side hydration causing style leaks or script conflicts from shared global CSS or JavaScript variables.
Constraints: the legacy app continues to load its own global stylesheet and runtime scripts on the same domain; Gatsby’s hydration process must not modify or be overridden by those globals, and the solution must work with Gatsby’s standard build and client‑runtime without requiring a full rewrite of either application.
30275 reputation · 03 Jun 2025, 10:50 UTC
The short answer: Gatsby does not isolate itself automatically — it assumes it owns the document. Isolation is something you build with three mechanisms: CSS Modules for scoped styles, namespaced client‑runtime code via gatsby-browser.js, and content‑hashed, path‑prefixed assets so URLs never collide. Confirmed facts and likely explanations are separated below.
Confirmed: Any file imported as .module.css (or .module.scss) is compiled by webpack’s css-loader with locally scoped, hashed class names (e.g., header-module__title--x3f9a). These cannot match selectors in your legacy global stylesheet by name.
Confirmed: Gatsby fingerprints its webpack output (app-[contenthash].js, styles-[contenthash].css) and emits everything under public/. Setting pathPrefix in gatsby-config.js (e.g., /app) and building with gatsby build --prefix-paths moves all Gatsby asset URLs under that prefix, so the legacy app and Gatsby never fight over /main.js‑style paths on the same domain.
Likely explanation for hydration conflicts: they almost never come from Gatsby’s runtime itself — they come from (a) legacy CSS resets or high‑specificity selectors (div { … }, !important) overriding Gatsby’s unscoped global styles, and (b) legacy scripts reading or writing window globals that Gatsby’s client runtime or your own gatsby-browser.js code also touches.
.gby‑) if you truly need globals.gatsby-browser.js, wrap any custom client code in an IIFE or a single namespaced object, and never assign bare window.* variables. Use wrapRootElement to stamp a marker on the tree, e.g. <div data-gatsby-app>, so legacy scripts can be adjusted to skip subtrees carrying that attribute.pathPrefix and serve Gatsby’s public/ output under that prefix from Express (e.g., app.use('/app', express.static('public'))), keeping legacy assets on their existing paths.!important rules — those are the realistic leak vectors.getComputedStyle on key Gatsby components matches expectations and that no legacy selectors appear in Gatsby’s generated class list.Object.keys(window) before and after hydration and diff against an allowlist; fail the build on new unexpected globals.window. assignments outside your declared namespace.gatsby build && gatsby serve), not develop — hot‑module replacement re‑injects code in dev and can mask or fake conflicts.CSS specificity is not isolation: a legacy rule with !important or higher specificity will still win over a hashed class. Exact hashing behavior and class‑name formats vary by Gatsby/webpack major version, so verify the emitted class names on your version (assumption here: Gatsby 4/5 with webpack 5). One missing detail that would change this recommendation: whether both apps must render inside the same HTML document simultaneously, or are merely served from the same domain on different routes — the latter needs far less work.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.