Skip to content

fix: client data provider falls back to HTML extraction for SSG hosts#41

Merged
jonlaing merged 2 commits into
mainfrom
fix/static-data-provider-fallback
Jul 24, 2026
Merged

fix: client data provider falls back to HTML extraction for SSG hosts#41
jonlaing merged 2 commits into
mainfrom
fix/static-data-provider-fallback

Conversation

@jonlaing

Copy link
Copy Markdown
Owner

Summary

Fix client-side navigation on SSG (static-file-hosted) sites — clicking a <Link> updated the URL but blanked the <Outlet>, with no error in the console.

Root cause

makeClientLayer's data provider was doing:

```ts
const response = yield* Effect.tryPromise(() => fetch(`${path}?${qs.toString()}`));
const json = yield* Effect.tryPromise(() => response.json());
return json as unknown as RouteDataService;
```

That works against the SSR HTTP handler (which returns JSON for `?_data=1` requests). It silently breaks on any static host — static file servers ignore the query string and return the target page's HTML shell. `response.json()` throws on that HTML, `.pipe(Effect.orDie)` converts the failure into a defect and kills the fiber. The Outlet's data effect dies before rendering; the URL has already updated because `pushPath` runs before the fetch.

No error surfaces because the fiber's defect is swallowed by the ambient runtime.

Fix

The provider now inspects the response's `Content-Type` header:

  • `application/json` — parse as before (SSR server case).
  • Anything else — treat as an SSG HTML shell. Extract the `window.EFFEX_DATA` blob that `generateDocument` embeds in every SSG'd page and `JSON.parse` it.

The escapes `serializeForHtml` emits (`\u003c` / `\u003e` / `\u0026`) are native JSON escapes, so the extract → `JSON.parse` round-trip is lossless — including payloads that contain `</script>` or ampersands.

Callers

No config change required. Projects using `Platform.makeClientLayer(router)` just start navigating correctly after upgrading.

Test plan

  • 5 unit tests in `Platform.test.ts` covering the extract:
    • Round-trip with `generateDocument`'s real output
    • Payloads containing `<script>`, `</script>`, ampersands
    • No-data-present case → `undefined`
    • Malformed-JSON case → `undefined`
    • Sanity check that `serializeForHtml` produces the expected `\uXXXX` escapes
  • Full suite: 733 pass / 2 skipped
  • `npx tsc --noEmit` clean
  • Manual: verify on the portfolio site that link clicks now render the target page

Note

Two knobs I considered but didn't touch:

  1. `.pipe(Effect.orDie)` on the provider effect. This is why the failure was silent. Kept as-is because with the HTML fallback the effect no longer fails in the common case; a genuine network failure still deserves to die.
  2. Emit static `?_data=1` JSON files during `buildStaticSite`. Would avoid the HTML round-trip and shave payload size. Bigger change and portable-across-hosts is the more important property right now.

🤖 Generated with Claude Code

jonlaing and others added 2 commits July 23, 2026 23:04
makeClientLayer's data provider was unconditionally parsing ?_data=1 responses as JSON. That works against the SSR HTTP handler but silently breaks on any static-file host: static servers ignore the query string and return the target page's HTML shell, response.json() throws, .pipe(Effect.orDie) kills the fiber, Outlet goes blank with no console error.

Now the provider checks Content-Type. JSON responses parse as before; anything else is treated as HTML, and the embedded window.__EFFEX_DATA__ blob (which every SSG'd page has via generateDocument) is extracted via regex and JSON.parsed. serializeForHtml's <>& escapes are already JSON-native (\uXXXX), so the round-trip is lossless.

Callers don't need to change anything — SSG projects using makeClientLayer just start navigating correctly.

Test coverage: unit-tests the extraction against generateDocument's real output, including payloads containing </script>, ampersands, and angle brackets, plus the no-data and malformed-json cases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Effect.orDie was making genuinely-failed fetches (network errors, malformed responses, etc.) invisible in the console — the exact reason the SSG navigation bug went undiagnosed. Log the error with route context before Effect.orDie so devtools + error trackers pick it up.

Also warn (not error) when the HTML fallback succeeds but no __EFFEX_DATA__ blob is present in the response. Non-fatal — the route may legitimately have no loader data — but a broken build/deploy should be visible instead of quietly serving undefined data.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploying effex-api with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2aeb30a
Status: ✅  Deploy successful!
Preview URL: https://0b185a65.effex-api.pages.dev
Branch Preview URL: https://fix-static-data-provider-fal.effex-api.pages.dev

View logs

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 24, 2026

Copy link
Copy Markdown

Deploying effex with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2aeb30a
Status: ✅  Deploy successful!
Preview URL: https://b6a77d5a.effex.pages.dev
Branch Preview URL: https://fix-static-data-provider-fal.effex.pages.dev

View logs

@jonlaing
jonlaing merged commit f5f0603 into main Jul 24, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant