Skip to content

feat: add mpa mode to file-system routing - #197

Merged
uhyo merged 3 commits into
masterfrom
feat/fs-routes-mpa-route-display
Oct 22, 2026
Merged

uhyo merged 3 commits into
masterfrom
feat/fs-routes-mpa-route-display

Conversation

@uhyo

@uhyo uhyo commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Summary

File-system routing currently always produces an SPA: pages render with FUNSTACK Router's <Router>, and link clicks are intercepted for soft client-side navigation. Some users prefer a classic multi-page site without soft navigation. This PR adds a required mode field to fsRoutes with two values:

  • "spa" — the existing behavior.
  • "mpa" — pages render with <RouteDisplay> from @funstack/router 1.6, which never intercepts navigations, so every link loads the destination's own HTML.
funstackStatic({
  fsRoutes: {
    dir: "./src/pages",
    root: "./src/root.tsx",
    adapter: "@funstack/static/fs-routes/next-adapter",
    mode: "mpa", // or "spa"
  },
});

mode is required like the other fsRoutes fields, so the choice is explicit in every config; the plugin fails with a clear error when it is missing. File-system routing is experimental, so existing configs adding the field is within the feature's stability contract. The lower-level createFsRoutesEntries keeps a "spa" default, mirroring how adapter is optional there.

How MPA mode works

  • FsRoutesApp renders <RouteDisplay routes ssr={{ path }}> instead of <Router>.
  • Each page's route definitions cover only its own route chain (nested root-first). Since the document never outlives its URL, sibling routes can never match and are left out.
  • Server Components on the chain are inlined directly with their params and route object; no FsRouteSlot is involved. Client Components are still passed as component types so the router renders them with the matched params.
  • Chunk registration is skipped entirely: nothing is fetched on navigation, so the build emits exactly one RSC payload per page. (The e2e fixture emits 11 payload files in MPA mode versus 29 for the equivalent SPA fixture.)
  • Route ids are identical to SPA mode, so route objects stay portable between the two.

FsRoutesMode is exported from @funstack/static/fs-routes.

Tests

  • Unit: 7 new runtime tests (RouteDisplay usage, chain-only definitions, inlined Server Components, Client Component pass-through, no chunks, id parity with SPA mode) and 3 new plugin tests (mode emission for both values, missing mode, invalid mode).
  • E2E: new fixture-fs-routing-mpa fixture with build and dev-server specs (ports 4181/4182). Navigation tests assert a full document replacement by planting a window marker before each click and checking it is gone afterwards; shell client state is also checked to reset. Both the new MPA projects and the existing SPA projects pass in build and dev mode.

Docs and example

  • New "SPA or MPA" section in the File-System Routing guide, with a note at the top of the SPA-only "Params on Client-Side Navigation" section. All fsRoutes snippets now include mode.
  • mode documented in the funstackStatic() API reference.
  • New example-fs-routing-mpa package showing full page loads on links, useSearchParams's setter performing a full navigation under <RouteDisplay>, and a dynamic Server Component page in MPA mode.

Notes for review

  • The remote also has an older feat/fs-routes-mpa-mode branch taking a different approach (JavaScript-free pages via SSR/RSC entry changes). This PR is independent of it.

🤖 Generated with Claude Code

https://claude.ai/code/session_014ERdZEFjZXcbrh8pQttyc4

Add an optional `mode` option to `fsRoutes` (and `createFsRoutesEntries`).
The default `"spa"` keeps the current behavior: pages render with FUNSTACK
Router's `<Router>`, links are followed by soft client-side navigation, and
Server Component output is pre-rendered as RSC chunks fetched on navigation.

The new `"mpa"` mode renders pages with `<RouteDisplay>` (FUNSTACK Router
1.6), which never intercepts navigations, so every link loads the
destination's own HTML. Each page's route definitions cover only its own
route chain with Server Component output inlined directly, and no
navigation chunks are registered or emitted.

Includes unit tests, an e2e fixture with build and dev-server specs, and
documentation for the new option.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ERdZEFjZXcbrh8pQttyc4
Add `example-fs-routing-mpa`, a sibling of `example-fs-routing` configured
with `fsRoutes.mode: "mpa"`. It shows that every link loads a new document
(client-side state in the shell resets), that `useSearchParams`'s setter
performs a full page navigation under `<RouteDisplay>`, and that a dynamic
Server Component page needs no route object to show the right params.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ERdZEFjZXcbrh8pQttyc4
Make `mode` a required field of `fsRoutes`, like `dir`, `root` and
`adapter`, so the choice between soft client-side navigation ("spa") and
full page loads ("mpa") is explicit in every config. The plugin fails with a
clear error when it is missing. `createFsRoutesEntries` keeps its "spa"
default, mirroring how `adapter` is optional there.

File-system routing is experimental, so this is not a semver-breaking
change for the package.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ERdZEFjZXcbrh8pQttyc4
@uhyo
uhyo merged commit 7175800 into master Oct 22, 2026
2 checks passed
@uhyo
uhyo deleted the feat/fs-routes-mpa-route-display branch October 22, 2026 14:34
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.

2 participants