@funstack/router 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,8 +27,7 @@ export function LearnRscPage() {
27
27
  <code>"use client"</code> because it exports components and hooks that
28
28
  depend on browser APIs (the Navigation API, React context, etc.). This
29
29
  means importing from <code>@funstack/router</code> in a server module
30
- would pull the entire router into the client bundle &mdash; defeating
31
- the purpose of RSC.
30
+ would fail.
32
31
  </p>
33
32
  <p>
34
33
  To solve this, the package provides a separate entry point:{" "}
@@ -68,9 +67,8 @@ export default function App() {
68
67
  In this example, <code>App</code> is a server component. It builds the
69
68
  route array using <code>route()</code> from{" "}
70
69
  <code>@funstack/router/server</code> and renders the{" "}
71
- <code>Router</code> component from <code>@funstack/router</code>.
72
- Since <code>Router</code> is a client component, the RSC bundler
73
- handles the client boundary automatically.
70
+ <code>Router</code> component from <code>@funstack/router</code> which
71
+ is a client component.
74
72
  </p>
75
73
  <h4>What the server entry point exports</h4>
76
74
  <ul>
@@ -90,45 +88,29 @@ export default function App() {
90
88
  </ul>
91
89
  </section>
92
90
 
93
- <section>
94
- <h3>The Client Boundary</h3>
95
- <p>
96
- The <code>Router</code> component subscribes to the Navigation API and
97
- manages React state, so it is a client component. It serves as the
98
- client boundary in your component tree &mdash; server components above
99
- it construct the route definitions, while <code>Router</code> and its
100
- runtime dependencies run in the browser.
101
- </p>
102
- <p>
103
- Route definitions (paths, component references, children) are plain
104
- serializable data, so they can be passed from a server component into{" "}
105
- <code>Router</code> as props.
106
- </p>
107
- </section>
108
-
109
91
  <section>
110
92
  <h3>Defining Routes in the Server Context</h3>
111
93
  <p>
112
- Because route definitions are plain data (paths, component references,
113
- and children), they can be constructed entirely on the server. The{" "}
114
- <code>route()</code> helper from <code>@funstack/router/server</code>{" "}
115
- produces the same <code>RouteDefinition</code> objects as the one from
116
- the main entry point &mdash; the only difference is that it does not
117
- pull in client-side code.
94
+ Route definitions can be defined in server modules because they are{" "}
95
+ <strong>plain data structures</strong> except for page components and
96
+ loader functions. Fortunately, it is possible to import both of these
97
+ from client modules which results in client references that can be
98
+ passed from the server to the client through the <code>routes</code>{" "}
99
+ prop.
118
100
  </p>
119
101
  <CodeBlock language="tsx">{`// App.tsx — Server Component
120
102
  import { Router } from "@funstack/router";
121
103
  import { route } from "@funstack/router/server";
122
104
  import { lazy } from "react";
123
105
 
124
- // Lazy-load page components these will be code-split
125
- const HomePage = lazy(() => import("./pages/HomePage.js"));
126
- const DashboardPage = lazy(() => import("./pages/DashboardPage.js"));
127
- const SettingsPage = lazy(() => import("./pages/SettingsPage.js"));
106
+ // Import page components from client modules
107
+ import HomePage from "./pages/HomePage.js";
108
+ import DashboardPage from "./pages/DashboardPage.js";
109
+ import SettingsPage from "./pages/SettingsPage.js";
128
110
 
129
111
  const routes = [
130
112
  route({
131
- component: <Layout />,
113
+ component: Layout,
132
114
  children: [
133
115
  route({ path: "/", component: HomePage }),
134
116
  route({ path: "/dashboard", component: DashboardPage }),
@@ -140,13 +122,6 @@ const routes = [
140
122
  export default function App() {
141
123
  return <Router routes={routes} />;
142
124
  }`}</CodeBlock>
143
- <p>
144
- Note that page components referenced in route definitions can be
145
- either server components or client components. When a page component
146
- uses hooks or browser APIs, it should have the{" "}
147
- <code>"use client"</code> directive. Otherwise, it can remain a server
148
- component for optimal performance.
149
- </p>
150
125
 
151
126
  <h4>Loaders in an RSC Context</h4>
152
127
  <p>
@@ -169,7 +144,7 @@ import { dashboardLoader } from "./loaders/dashboard.js";
169
144
 
170
145
  const routes = [
171
146
  route({
172
- component: <Layout />,
147
+ component: Layout,
173
148
  children: [
174
149
  route({ path: "/", component: HomePage }),
175
150
  route({
@@ -193,80 +168,78 @@ export default function App() {
193
168
  </section>
194
169
 
195
170
  <section>
196
- <h3>A Complete Example</h3>
171
+ <h3>Using Server Components as Route Components</h3>
197
172
  <p>
198
- This documentation site itself uses this pattern. Here is a simplified
199
- version of how it is structured:
173
+ All examples so far have used client components as route components,
174
+ but you can go even further and{" "}
175
+ <strong>use server components as route components</strong>. Actually,
176
+ this is the primary use case for the RSC support in FUNSTACK Router
177
+ &mdash; it allows pre-rendering each route on the server (or at build
178
+ time for static sites).
200
179
  </p>
201
- <CodeBlock language="tsx">{`// vite.config.ts
202
- import funstackStatic from "@funstack/static";
203
- import react from "@vitejs/plugin-react";
204
- import { defineConfig } from "vite";
205
180
 
206
- export default defineConfig({
207
- plugins: [
208
- funstackStatic({
209
- root: "./src/Root.tsx", // Server Component HTML shell
210
- app: "./src/App.tsx", // Server Component route definitions
211
- ssr: true,
212
- }),
213
- react(),
214
- ],
215
- });`}</CodeBlock>
216
- <CodeBlock language="tsx">{`// Root.tsx — Server Component (HTML shell)
217
- import type { ReactNode } from "react";
218
-
219
- export default function Root({ children }: { children: ReactNode }) {
220
- return (
221
- <html lang="en">
222
- <head>
223
- <meta charSet="UTF-8" />
224
- <title>My App</title>
225
- </head>
226
- <body>{children}</body>
227
- </html>
228
- );
229
- }`}</CodeBlock>
230
- <CodeBlock language="tsx">{`// App.tsx — Server Component (route definitions)
181
+ <h4>Use React Node as Route Components</h4>
182
+ <p>
183
+ When you use server components as route components, the route's{" "}
184
+ <code>component</code> must be a React node (i.e.{" "}
185
+ <code>&lt;MyComponent /&gt;</code>) instead of a component reference
186
+ (i.e. <code>MyComponent</code>) because a references to server
187
+ components cannot be passed to the client.
188
+ </p>
189
+ <CodeBlock language="tsx">{`// App.tsx — Server Component
231
190
  import { Router } from "@funstack/router";
232
191
  import { route } from "@funstack/router/server";
233
- import { Layout } from "./components/Layout.js";
234
- import { HomePage } from "./pages/HomePage.js";
235
- import { AboutPage } from "./pages/AboutPage.js";
192
+ import HomePage from "./pages/HomePage.js";
193
+ import AboutPage from "./pages/AboutPage.js";
236
194
 
237
195
  const routes = [
238
196
  route({
239
197
  component: <Layout />,
240
198
  children: [
241
- route({ path: "/", component: HomePage }),
242
- route({ path: "/about", component: AboutPage }),
199
+ route({ path: "/", component: <HomePage /> }),
200
+ route({ path: "/about", component: <AboutPage /> }),
243
201
  ],
244
202
  }),
245
203
  ];
246
204
 
247
205
  export default function App() {
248
- return <Router routes={routes} fallback="static" />;
206
+ return <Router routes={routes} />;
249
207
  }`}</CodeBlock>
250
208
  <p>
251
- In this setup, <code>Root</code> and <code>App</code> are server
252
- components. The route definitions are constructed on the server and
253
- passed into <code>Router</code>, which acts as the client boundary.
209
+ In this example, <code>HomePage</code> and <code>AboutPage</code> are
210
+ server components. They are rendered on the server and the resulting
211
+ HTML is sent to the client.
254
212
  </p>
255
213
  <p>
256
- If the server knows the requested pathname, you can pass it via the{" "}
257
- <code>ssrPathname</code> prop so that path-based routes render during
258
- SSR (see the <a href="/learn/server-side-rendering">SSR guide</a> for
259
- details):
214
+ Due to this nature, a route component defined as a server component{" "}
215
+ <em>cannot</em> receive route props (params, search params, navigation
216
+ state, etc). We are exploring ways to lift this limitation in the
217
+ future, but for now if you need to access route props you will need to
218
+ use client components as route components.
219
+ </p>
220
+ <p>
221
+ For some use cases it is enough to have a client component child as a
222
+ pathless route:
223
+ </p>
224
+ <CodeBlock language="tsx">{`const routes = [
225
+ route({
226
+ path: "/",
227
+ component: <HomePage />, // Server Component
228
+ children: [
229
+ route({
230
+ component: InteractivePartOfHomePage, // Client Component
231
+ loader: someLoaderForHomePage,
232
+ }),
233
+ ],
234
+ }),
235
+ ];`}</CodeBlock>
236
+ <p>
237
+ In this example, <code>HomePage</code> is a server component that
238
+ renders the static parts of the page while{" "}
239
+ <code>InteractivePartOfHomePage</code> is a client component that can
240
+ access route props (like loader data). <code>HomePage</code> can
241
+ render <code>&lt;Outlet /&gt;</code> to render its child routes.
260
242
  </p>
261
- <CodeBlock language="tsx">{`export default function App({ pathname }: { pathname: string }) {
262
- return (
263
- <Router
264
- routes={routes}
265
- fallback="static"
266
- ssrPathname={pathname}
267
- />
268
- );
269
- }`}</CodeBlock>
270
243
  </section>
271
244
 
272
245
  <section>
@@ -274,30 +247,27 @@ export default function App() {
274
247
  <ul>
275
248
  <li>
276
249
  Import <code>route</code> and <code>routeState</code> from{" "}
277
- <code>@funstack/router/server</code> in server modules to avoid
278
- pulling client code into the server module graph
250
+ <code>@funstack/router/server</code> to define routes in server
251
+ modules
279
252
  </li>
280
253
  <li>
281
254
  <code>Router</code> is a client component and serves as the client
282
255
  boundary &mdash; render it directly from your server component
283
256
  </li>
284
- <li>
285
- Route definitions are plain data and can be constructed entirely on
286
- the server
287
- </li>
288
257
  <li>
289
258
  Loaders run client-side &mdash; define them in{" "}
290
259
  <code>"use client"</code> modules and import them into your route
291
260
  definitions
292
261
  </li>
293
262
  <li>
294
- Page components can be either server components or client components
295
- depending on whether they need browser APIs or hooks
263
+ Page components can be either server components or client
264
+ components; if using server components, define them as React nodes
265
+ (e.g. <code>&lt;MyPage /&gt;</code>) instead of component references
266
+ (e.g. <code>MyPage</code>)
296
267
  </li>
297
268
  <li>
298
- See also the{" "}
299
- <a href="/learn/server-side-rendering">Server-Side Rendering</a>{" "}
300
- guide for how the router handles SSR and hydration
269
+ See also the <a href="/learn/ssr">Server-Side Rendering</a> guide
270
+ for how the router handles SSR and hydration
301
271
  </li>
302
272
  </ul>
303
273
  </section>
@@ -0,0 +1,133 @@
1
+ import { CodeBlock } from "../components/CodeBlock.js";
2
+
3
+ export function LearnSsgPage() {
4
+ return (
5
+ <div className="learn-content">
6
+ <h2>Static Site Generation</h2>
7
+
8
+ <p className="page-intro">
9
+ When your server or static site generator knows the URL being rendered,
10
+ you can use the <code>ssr</code> prop to match path-based routes during
11
+ SSR. This produces richer server-rendered HTML &mdash; users see page
12
+ content immediately instead of just the app shell.
13
+ </p>
14
+
15
+ <section>
16
+ <h3>
17
+ How the <code>ssr</code> Prop Works
18
+ </h3>
19
+ <p>
20
+ As described in the <a href="/learn/ssr">How SSR Works</a> guide, the
21
+ router normally has no URL during SSR and only renders pathless
22
+ routes. The <code>ssr</code> prop provides a pathname so path-based
23
+ routes can also match during SSR:
24
+ </p>
25
+ <CodeBlock language="tsx">{`// Server knows the requested URL and passes it to the router
26
+ <Router routes={routes} ssr={{ path: "/about" }} />`}</CodeBlock>
27
+ <p>
28
+ When <code>ssr</code> is provided, the router matches path-based
29
+ routes against <code>ssr.path</code> just as it would match against
30
+ the real URL on the client. Route params are extracted normally.
31
+ Routes with loaders are skipped by default &mdash; the parent route
32
+ renders as a shell, and loader content fills in after hydration.
33
+ </p>
34
+ <p>
35
+ Once the client hydrates, the real URL from the Navigation API takes
36
+ over and <code>ssr</code> is ignored.
37
+ </p>
38
+ </section>
39
+
40
+ <section>
41
+ <h3>Example</h3>
42
+ <p>
43
+ Consider a route tree with a mix of static pages and loader-based
44
+ routes:
45
+ </p>
46
+ <CodeBlock language="tsx">{`const routes = [
47
+ route({
48
+ component: AppShell,
49
+ children: [
50
+ route({ path: "/", component: HomePage }), // Matches ssr.path="/"
51
+ route({ path: "/about", component: AboutPage }),// Matches ssr.path="/about"
52
+ route({
53
+ path: "/dashboard",
54
+ component: DashboardPage,
55
+ loader: dashboardLoader, // Skipped during SSR (has loader)
56
+ }),
57
+ ],
58
+ }),
59
+ ];
60
+
61
+ // With ssr={{ path: "/about" }}:
62
+ // - AppShell renders (pathless, no loader) ✓
63
+ // - AboutPage renders (path matches, no loader) ✓
64
+ // - DashboardPage would NOT render with ssr={{ path: "/dashboard" }}
65
+ // because it has a loader`}</CodeBlock>
66
+ </section>
67
+
68
+ <section>
69
+ <h3>
70
+ When to Use <code>ssr</code>
71
+ </h3>
72
+ <p>
73
+ Use the <code>ssr</code> prop when your server or static site
74
+ generator knows the URL being rendered and you want to include
75
+ page-specific content in the SSR output. This is common for static
76
+ site generation, but can also be used in dynamic SSR scenarios where
77
+ the server can determine the URL at request time.
78
+ </p>
79
+ <p>This is particularly useful for:</p>
80
+ <ul>
81
+ <li>
82
+ Improving perceived performance by showing page content immediately
83
+ instead of a blank shell
84
+ </li>
85
+ <li>
86
+ SEO &mdash; search engine crawlers see the full page content rather
87
+ than just the app shell
88
+ </li>
89
+ <li>
90
+ Static site generation where each page is pre-rendered at a known
91
+ path
92
+ </li>
93
+ </ul>
94
+ </section>
95
+
96
+ <section>
97
+ <h3>Routes with Loaders</h3>
98
+ <p>
99
+ Routes with loaders are skipped by default during SSR. If your
100
+ application has a server runtime that can execute loaders at request
101
+ time, see the <a href="/learn/ssr/with-loaders">SSR with Loaders</a>{" "}
102
+ guide.
103
+ </p>
104
+ <p>
105
+ If you only need loaders to run at build time (not on the client),
106
+ consider using{" "}
107
+ <a href="/learn/react-server-components">React Server Components</a>{" "}
108
+ with SSG. RSC lets you fetch data on the server during the build and
109
+ send the result as static HTML, without shipping loader code to the
110
+ client.
111
+ </p>
112
+ </section>
113
+
114
+ <section>
115
+ <h3>Key Takeaways</h3>
116
+ <ul>
117
+ <li>
118
+ Use <code>ssr</code> to enable path-based route matching during SSR
119
+ for richer server-rendered output
120
+ </li>
121
+ <li>
122
+ Routes with loaders are skipped during SSR by default; the parent
123
+ route renders as a shell and loader content fills in after hydration
124
+ </li>
125
+ <li>
126
+ After hydration, the real URL from the Navigation API takes over and{" "}
127
+ <code>ssr</code> is ignored
128
+ </li>
129
+ </ul>
130
+ </section>
131
+ </div>
132
+ );
133
+ }
@@ -1,35 +1,29 @@
1
1
  import { CodeBlock } from "../components/CodeBlock.js";
2
2
 
3
- export function LearnSsrPage() {
3
+ export function LearnSsrBasicPage() {
4
4
  return (
5
5
  <div className="learn-content">
6
- <h2>Server-Side Rendering</h2>
6
+ <h2>How SSR Works</h2>
7
7
 
8
8
  <p className="page-intro">
9
9
  FUNSTACK Router supports server-side rendering with a two-stage model.
10
10
  During SSR, pathless (layout) routes without loaders render to produce
11
11
  an app shell, while path-based routes and loaders activate only after
12
- client hydration. You can optionally provide an <code>ssrPathname</code>{" "}
13
- prop to match path-based routes during SSR for richer server-rendered
14
- output.
12
+ client hydration.
15
13
  </p>
16
14
 
17
15
  <section>
18
- <h3>How SSR Works</h3>
16
+ <h3>Two-Stage Rendering</h3>
19
17
  <p>
20
18
  FUNSTACK Router uses a two-stage rendering model that separates what
21
19
  renders on the server from what renders on the client:
22
20
  </p>
23
21
  <p>
24
- <strong>Stage 1 &mdash; Server:</strong> By default, no URL is
25
- available on the server. The router matches only pathless routes
26
- (routes without a <code>path</code> property) that do not have a
27
- loader. This produces the app shell &mdash; layouts, headers,
28
- navigation chrome, and other structural markup. When{" "}
29
- <code>ssrPathname</code> is provided, the router also matches
30
- path-based routes against that pathname, enabling richer
31
- server-rendered content. In both cases, routes with loaders are always
32
- skipped during SSR.
22
+ <strong>Stage 1 &mdash; Server:</strong> No URL is available on the
23
+ server. The router matches only pathless routes (routes without a{" "}
24
+ <code>path</code> property) that do not have a loader. This produces
25
+ the app shell &mdash; layouts, headers, navigation chrome, and other
26
+ structural markup.
33
27
  </p>
34
28
  <p>
35
29
  <strong>Stage 2 &mdash; Client hydration:</strong> Once the browser
@@ -42,8 +36,7 @@ export function LearnSsrPage() {
42
36
  // Stage 1 (Server) Stage 2 (Client)
43
37
  // ─────────────────────────── ─────────────────
44
38
  // App shell (pathless routes) App shell (pathless)
45
- // + path routes if ssrPathname ✓ Path routes match
46
- // is provided (no loaders)
39
+ // ✓ Path routes match
47
40
  // ✗ No loaders ✓ Loaders execute
48
41
  // ✗ No URL available ✓ URL from Navigation API`}</CodeBlock>
49
42
  </section>
@@ -78,76 +71,6 @@ export function LearnSsrPage() {
78
71
  </p>
79
72
  </section>
80
73
 
81
- <section>
82
- <h3>
83
- Path-Based SSR with <code>ssrPathname</code>
84
- </h3>
85
- <p>
86
- By default, only pathless routes render during SSR because the server
87
- has no URL. The <code>ssrPathname</code> prop lets you provide a
88
- pathname so path-based routes can also match during SSR, producing
89
- fuller server-rendered HTML.
90
- </p>
91
- <CodeBlock language="tsx">{`// Server knows the requested URL and passes it to the router
92
- <Router routes={routes} ssrPathname="/about" />`}</CodeBlock>
93
- <p>
94
- When <code>ssrPathname</code> is provided, the router matches
95
- path-based routes against it just as it would match against the real
96
- URL on the client. Route params are extracted normally. However,
97
- routes with loaders are always skipped during SSR regardless of this
98
- setting &mdash; there is no request context available to run them.
99
- </p>
100
- <p>
101
- Once the client hydrates, the real URL from the Navigation API takes
102
- over and <code>ssrPathname</code> is ignored.
103
- </p>
104
- <CodeBlock language="tsx">{`const routes = [
105
- route({
106
- component: AppShell,
107
- children: [
108
- route({ path: "/", component: HomePage }), // Matches ssrPathname="/"
109
- route({ path: "/about", component: AboutPage }),// Matches ssrPathname="/about"
110
- route({
111
- path: "/dashboard",
112
- component: DashboardPage,
113
- loader: dashboardLoader, // Skipped during SSR (has loader)
114
- }),
115
- ],
116
- }),
117
- ];
118
-
119
- // With ssrPathname="/about":
120
- // - AppShell renders (pathless, no loader) ✓
121
- // - AboutPage renders (path matches, no loader) ✓
122
- // - DashboardPage would NOT render even with ssrPathname="/dashboard"
123
- // because it has a loader`}</CodeBlock>
124
- <h4>When to use ssrPathname</h4>
125
- <p>
126
- Use <code>ssrPathname</code> when your server or static site generator
127
- knows the URL being rendered and you want to include page-specific
128
- content in the SSR output. This is particularly useful for:
129
- </p>
130
- <ul>
131
- <li>
132
- Improving perceived performance by showing page content immediately
133
- instead of a blank shell
134
- </li>
135
- <li>
136
- SEO &mdash; search engine crawlers see the full page content rather
137
- than just the app shell
138
- </li>
139
- <li>
140
- Static site generation where each page is pre-rendered at a known
141
- path
142
- </li>
143
- </ul>
144
- <p>
145
- If your routes have loaders, those routes will still be skipped during
146
- SSR. The parent route renders as a shell, and the loader content fills
147
- in after hydration.
148
- </p>
149
- </section>
150
-
151
74
  <section>
152
75
  <h3>Hooks and SSR</h3>
153
76
  <p>
@@ -224,19 +147,40 @@ function HomePage() {
224
147
  </section>
225
148
 
226
149
  <section>
227
- <h3>Key Takeaways</h3>
150
+ <h3>Going Beyond the App Shell</h3>
151
+ <p>
152
+ The default SSR behavior produces only the app shell. This is perfect
153
+ for ordinary SPAs where only one HTML page is served and the client
154
+ takes over all routing. SSR can still be useful in this scenario,
155
+ normally with a static site generator, to improve perceived
156
+ performance by showing the shell immediately while the rest of the
157
+ page loads.
158
+ </p>
159
+ <p>
160
+ If your server or build tool knows the URL being rendered, you can use
161
+ the <code>ssr</code> prop to match path-based routes during SSR for
162
+ richer output:
163
+ </p>
228
164
  <ul>
229
165
  <li>
230
- By default, only pathless routes without loaders render during SSR
231
- (no URL is available on the server)
166
+ <a href="/learn/ssr/static-site-generation">
167
+ Static Site Generation
168
+ </a>{" "}
169
+ &mdash; pre-render pages at known paths without running loaders
232
170
  </li>
233
171
  <li>
234
- Use <code>ssrPathname</code> to enable path-based route matching
235
- during SSR for richer server-rendered output
172
+ <a href="/learn/ssr/with-loaders">SSR with Loaders</a> &mdash;
173
+ render pages with loader data on the server for fully dynamic SSR
236
174
  </li>
175
+ </ul>
176
+ </section>
177
+
178
+ <section>
179
+ <h3>Key Takeaways</h3>
180
+ <ul>
237
181
  <li>
238
- Routes with loaders are always skipped during SSR, regardless of{" "}
239
- <code>ssrPathname</code>
182
+ Only pathless routes without loaders render during SSR (no URL is
183
+ available on the server)
240
184
  </li>
241
185
  <li>
242
186
  Pathless routes are ideal for app shell markup (headers, footers,