diff --git a/packages/docs/package.json b/packages/docs/package.json
index 1b10df9..e4b1c3f 100644
--- a/packages/docs/package.json
+++ b/packages/docs/package.json
@@ -11,7 +11,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
- "@funstack/router": "^1.2.0",
+ "@funstack/router": "^1.4.0",
"@funstack/static": "workspace:*",
"@shikijs/rehype": "^4.4.3",
"@types/node": "catalog:",
diff --git a/packages/docs/src/pages/learn/FileSystemRouting.mdx b/packages/docs/src/pages/learn/FileSystemRouting.mdx
index 11daafb..1205af8 100644
--- a/packages/docs/src/pages/learn/FileSystemRouting.mdx
+++ b/packages/docs/src/pages/learn/FileSystemRouting.mdx
@@ -118,7 +118,7 @@ export default function BlogPost({ params }: { params: { slug: string } }) {
}
```
-This generates `blog/hello.html` and `blog/world.html`. Each page component receives the resolved `params` as a prop.
+This generates `blog/hello.html` and `blog/world.html`. Each page component receives the resolved `params` as a prop (see [Params on Client-Side Navigation](#params-on-client-side-navigation) for how `params` behaves when navigating in the browser).
A dynamic route **must** export `generateStaticParams`; the build fails otherwise. A static site can only serve pages that were enumerated at build time, so a dynamic route without it would produce no output.
@@ -132,7 +132,58 @@ export function generateStaticParams() {
export { default } from "./_page"; // _page.tsx is marked "use client"
```
-> **Note:** Because static hosting serves one pre-rendered RSC payload per page, soft client-side navigation between different values of the _same_ dynamic route reflects the params of the initially-loaded page. Loading a dynamic URL directly always renders the correct params. Static routes and layouts navigate fully on the client.
+## Params on Client-Side Navigation
+
+Loading any generated URL directly always renders the correct params. During soft client-side navigation between pages of the _same_ dynamic route (say, from `/blog/hello` to `/blog/world`), what the `params` prop holds depends on the kind of the component:
+
+- **Client Components** (pages and layouts marked `"use client"`) are rendered by FUNSTACK Router in the browser, so they receive the **live** params of the URL currently shown. They stay correct across soft navigation.
+- **Server Components** render once at build time, so their `params` prop — and their entire rendered output — currently reflects the values the page was generated with after such a navigation. This is a temporary limitation, not the intended end state: the destination page's pre-rendered RSC payload exists in the build output, and loading it on client-side navigation so that Server Component pages update too is being worked on ([#174](https://github.com/uhyo/funstack-static/issues/174)). Until then, use the route object below to read live params, or make the page body a Client Component if it must fully react to param changes.
+
+### Reading Live Params with the Route Object
+
+Every page and layout receives a `route` prop: an opaque **route object** identifying its route. In a Client Component, hand it to FUNSTACK Router's [`useRouteParams`](https://github.com/uhyo/funstack-router) hook to read the live params of the current URL. A Server Component cannot use hooks itself, but can forward the prop to a Client Component:
+
+```tsx
+// src/pages/blog/[slug]/page.tsx (a Server Component)
+import type { FsRouteComponentProps } from "@funstack/static/fs-routes";
+import { LiveSlug } from "./live-slug";
+
+export function generateStaticParams() {
+ return [{ slug: "hello" }, { slug: "world" }];
+}
+
+export default function BlogPost({
+ params,
+ route,
+}: FsRouteComponentProps<{ slug: string }>) {
+ return (
+
+ Post generated for: {params.slug}
+
+
+ );
+}
+```
+
+```tsx
+// src/pages/blog/[slug]/live-slug.tsx
+"use client";
+import { useRouteParams } from "@funstack/router";
+import type { FsRouteObject } from "@funstack/static/fs-routes";
+
+export function LiveSlug({
+ route,
+}: {
+ route: FsRouteObject<{ slug: string }>;
+}) {
+ const params = useRouteParams(route); // params of the URL currently shown
+ return
Now viewing: {params.slug}
;
+}
+```
+
+The `FsRouteComponentProps` helper types the `params` and `route` props any page or layout receives. Like the `params` prop itself, the `Params` type argument is declared by you and not verified against the route's path.
+
+The `route` prop reaches the two kinds of components differently, with the same result: FUNSTACK Static passes it to Server Components at build time, while FUNSTACK Router (v1.4.0 or later) passes it to Client Components at render time — along with the live `params` and its other [route component props](https://github.com/uhyo/funstack-router).
## Custom Conventions (Adapters)
diff --git a/packages/example-fs-routing/package.json b/packages/example-fs-routing/package.json
index d9aacb3..41c08fc 100644
--- a/packages/example-fs-routing/package.json
+++ b/packages/example-fs-routing/package.json
@@ -10,7 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
- "@funstack/router": "^1.2.0",
+ "@funstack/router": "^1.4.0",
"@funstack/static": "workspace:*",
"@types/node": "catalog:",
"react": "catalog:",
diff --git a/packages/static/e2e/fixture-fs-routing/package.json b/packages/static/e2e/fixture-fs-routing/package.json
index 6683ba8..a94d235 100644
--- a/packages/static/e2e/fixture-fs-routing/package.json
+++ b/packages/static/e2e/fixture-fs-routing/package.json
@@ -3,7 +3,7 @@
"private": true,
"type": "module",
"devDependencies": {
- "@funstack/router": "^1.2.0",
+ "@funstack/router": "^1.4.0",
"@funstack/static": "workspace:*",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
diff --git a/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/client/_page.tsx b/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/client/_page.tsx
new file mode 100644
index 0000000..448e457
--- /dev/null
+++ b/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/client/_page.tsx
@@ -0,0 +1,26 @@
+"use client";
+import { useRouteParams } from "@funstack/router";
+import type { FsRouteComponentProps } from "@funstack/static/fs-routes";
+
+// A Client Component page: the router renders it with the live params of the
+// current match (and its route object), so it stays correct across soft
+// client-side navigation.
+export default function ClientLangPage({
+ params,
+ route,
+}: FsRouteComponentProps<{ lang: string }>) {
+ const liveParams = useRouteParams(route);
+ return (
+
+ );
+}
diff --git a/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/client/page.tsx b/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/client/page.tsx
new file mode 100644
index 0000000..770f500
--- /dev/null
+++ b/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/client/page.tsx
@@ -0,0 +1,6 @@
+// generateStaticParams runs at build time, so it lives in this Server
+// Component module while the page body is a Client Component.
+export function generateStaticParams() {
+ return [{ lang: "en" }, { lang: "ja" }];
+}
+export { default } from "./_page";
diff --git a/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/layout.tsx b/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/layout.tsx
new file mode 100644
index 0000000..7e3dfe9
--- /dev/null
+++ b/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/layout.tsx
@@ -0,0 +1,16 @@
+"use client";
+import { Outlet, useLocation } from "@funstack/router";
+
+// A Client Component layout under a dynamic segment: the router renders it
+// with the params of the current match, so `params` stays live across soft
+// client-side navigation.
+export default function LangLayout({ params }: { params: { lang: string } }) {
+ const location = useLocation();
+ return (
+
+
{location.pathname}
+
{params.lang}
+
+
+ );
+}
diff --git a/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/live-lang.tsx b/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/live-lang.tsx
new file mode 100644
index 0000000..b920869
--- /dev/null
+++ b/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/live-lang.tsx
@@ -0,0 +1,14 @@
+"use client";
+import { useRouteParams } from "@funstack/router";
+import type { FsRouteObject } from "@funstack/static/fs-routes";
+
+// A Client Component under a Server Component page reading the live params
+// of the current URL through the route object.
+export function LiveLang({
+ route,
+}: {
+ route: FsRouteObject<{ lang: string }>;
+}) {
+ const params = useRouteParams(route);
+ return
{params.lang}
;
+}
diff --git a/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/page.tsx b/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/page.tsx
new file mode 100644
index 0000000..6ab1a4b
--- /dev/null
+++ b/packages/static/e2e/fixture-fs-routing/src/pages/[lang]/page.tsx
@@ -0,0 +1,30 @@
+import type { FsRouteComponentProps } from "@funstack/static/fs-routes";
+import { LiveLang } from "./live-lang";
+
+export function generateStaticParams() {
+ return [{ lang: "en" }, { lang: "ja" }];
+}
+
+// A Server Component page: rendered at build time with the concrete params,
+// and given its route object to hand to Client Components for live params.
+export default function LangPage({
+ params,
+ route,
+}: FsRouteComponentProps<{ lang: string }>) {
+ return (
+