From 01cf355fbbd07b1d0361b589651168b4a96f7921 Mon Sep 17 00:00:00 2001 From: Jon Laing Date: Mon, 3 Aug 2026 17:19:50 -0400 Subject: [PATCH] docs(hydrate): update SSG hydration guide to match the fixed pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SSG page's client entry was showing the same buggy `Effect.provide(App(), navLayer)` bake-in that this PR fixes in the templates. The quick-start also incorrectly claimed SSG doesn't need `Platform.makeClientLayer` — it does, both for NavigationContext and so the stripped-loader routes can read window.__EFFEX_DATA__ / fetch `?_data=1` on client-side nav. Both pages now show `hydrate(App(), root, { layers: Platform.makeClientLayer(router) })`. Co-Authored-By: Claude Opus 4.7 --- .../06-platform/02-static-site-generation.md | 16 +++++++--------- apps/docs/content/quick-start.md | 10 +++++++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/docs/content/06-platform/02-static-site-generation.md b/apps/docs/content/06-platform/02-static-site-generation.md index cfd6e2c3..376ab452 100644 --- a/apps/docs/content/06-platform/02-static-site-generation.md +++ b/apps/docs/content/06-platform/02-static-site-generation.md @@ -153,23 +153,21 @@ The first command builds the client bundle. The second builds the SSR entry, and ## Client Hydration -Static pages are hydrated on the client just like SSR pages. The client entry provides the Navigation layer: +Static pages are hydrated on the client just like SSR pages. Pass the client layer via `options.layers` so `hydrate` builds it in its long-lived scope — baking it in via `Effect.provide(App(), layer)` would tear the layer down as soon as the element function returns, killing the popstate listener and reactive subscriptions: ```typescript // src/client.ts -import { Effect } from "effect"; import { hydrate } from "@effex/dom/hydrate"; -import { Navigation } from "@effex/router"; +import { Platform } from "@effex/platform"; import { App } from "./app.js"; import { router } from "./routes.js"; -const navLayer = Navigation.makeLayer(router); - -hydrate( - Effect.provide(App(), navLayer), - document.getElementById("root")!, -); +hydrate(App(), document.getElementById("root")!, { + layers: Platform.makeClientLayer(router), +}); ``` +`Platform.makeClientLayer` provides both `NavigationContext` and `RouteDataProvider` — the latter reads the SSG-embedded `window.__EFFEX_DATA__` on first load, then fetches `?_data=1` for subsequent navigations (the Vite plugin strips loaders from the client bundle, so this fetch is how loader data reaches the client after hydration). + After hydration, clicking a Link triggers client-side navigation — the browser doesn't reload the page. diff --git a/apps/docs/content/quick-start.md b/apps/docs/content/quick-start.md index 5c89ca6b..cc59795d 100644 --- a/apps/docs/content/quick-start.md +++ b/apps/docs/content/quick-start.md @@ -295,13 +295,17 @@ export const document = { ### Client hydration -Like SSR, the client hydrates after the static HTML loads. But since there's no server at runtime, `Platform.makeClientLayer` isn't needed — route data is embedded in the HTML: +Like SSR, the client hydrates after the static HTML loads. SSG still needs `Platform.makeClientLayer` — it provides `NavigationContext` (for `Outlet`/`Link`) and reads the SSG-embedded `window.__EFFEX_DATA__` on first load. On subsequent client-side navigations it fetches the matching HTML shell and pulls the embedded data out (the Vite plugin strips loaders from the client bundle, so this is how loader data reaches routes after hydration): ```typescript -import { hydrate } from "@effex/dom"; +import { hydrate } from "@effex/dom/hydrate"; +import { Platform } from "@effex/platform"; import { App } from "./App.js"; +import { router } from "./routes.js"; -hydrate(App(), document.getElementById("root")!); +hydrate(App(), document.getElementById("root")!, { + layers: Platform.makeClientLayer(router), +}); ``` ### Scripts