@rangojs/router 0.0.0-experimental.3 → 0.0.0-experimental.4

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.
Files changed (63) hide show
  1. package/CLAUDE.md +40 -0
  2. package/dist/vite/index.js +53 -4
  3. package/package.json +13 -3
  4. package/skills/caching/SKILL.md +5 -5
  5. package/skills/document-cache/SKILL.md +7 -7
  6. package/skills/hooks/SKILL.md +83 -0
  7. package/skills/intercept/SKILL.md +2 -2
  8. package/skills/layout/SKILL.md +2 -2
  9. package/skills/links/SKILL.md +180 -0
  10. package/skills/loader/SKILL.md +32 -5
  11. package/skills/middleware/SKILL.md +4 -4
  12. package/skills/parallel/SKILL.md +2 -2
  13. package/skills/rango/SKILL.md +2 -1
  14. package/skills/route/SKILL.md +2 -2
  15. package/skills/router-setup/SKILL.md +83 -14
  16. package/skills/theme/SKILL.md +4 -4
  17. package/skills/typesafety/SKILL.md +140 -41
  18. package/src/browser/partial-update.ts +24 -24
  19. package/src/browser/react/NavigationProvider.tsx +0 -18
  20. package/src/browser/react/mount-context.ts +14 -0
  21. package/src/browser/react/use-handle.ts +34 -3
  22. package/src/browser/react/use-href.tsx +20 -188
  23. package/src/browser/react/use-mount.ts +31 -0
  24. package/src/browser/react/use-navigation.ts +10 -20
  25. package/src/browser/rsc-router.tsx +4 -0
  26. package/src/browser/segment-structure-assert.ts +67 -0
  27. package/src/browser/server-action-bridge.ts +18 -3
  28. package/src/browser/types.ts +2 -10
  29. package/src/build/generate-manifest.ts +227 -0
  30. package/src/build/index.ts +22 -0
  31. package/src/client.rsc.tsx +9 -14
  32. package/src/client.tsx +10 -10
  33. package/src/handle.ts +33 -4
  34. package/src/handles/MetaTags.tsx +4 -0
  35. package/src/href-client.ts +17 -21
  36. package/src/href.ts +1 -1
  37. package/src/index.rsc.ts +2 -3
  38. package/src/index.ts +1 -1
  39. package/src/loader.rsc.ts +6 -0
  40. package/src/route-content-wrapper.tsx +3 -8
  41. package/src/route-definition.ts +48 -8
  42. package/src/route-map-builder.ts +43 -2
  43. package/src/route-types.ts +12 -2
  44. package/src/router/handler-context.ts +1 -1
  45. package/src/router/manifest.ts +33 -23
  46. package/src/router/match-context.ts +1 -1
  47. package/src/router/match-result.ts +0 -1
  48. package/src/router/pattern-matching.ts +124 -1
  49. package/src/router.ts +763 -210
  50. package/src/rsc/handler.ts +48 -30
  51. package/src/rsc/index.ts +5 -5
  52. package/src/rsc/types.ts +3 -4
  53. package/src/segment-system.tsx +31 -50
  54. package/src/server/context.ts +20 -0
  55. package/src/server/route-manifest-cache.ts +173 -0
  56. package/src/server.ts +9 -0
  57. package/src/ssr/index.tsx +66 -4
  58. package/src/types.ts +57 -11
  59. package/src/urls.ts +114 -38
  60. package/src/vite/expose-loader-id.ts +71 -2
  61. package/src/vite/virtual-entries.ts +4 -1
  62. package/src/warmup/connection-warmup.tsx +94 -0
  63. package/src/warmup/warmup-context.ts +35 -0
package/CLAUDE.md CHANGED
@@ -1,3 +1,43 @@
1
1
  # @rangojs/router
2
2
 
3
3
  Run `/rango` first to understand the API. Skills are in `node_modules/@rangojs/router/skills/`.
4
+
5
+ ## Tree-Structure-Critical Files (DO NOT MODIFY without understanding)
6
+
7
+ The following files control the React tree structure. Changing the tree structure
8
+ (element types, nesting depth, or keys at any position) between SSR, navigation,
9
+ and action renders will cause React to remount components, destroying client state
10
+ like `useActionState`, refs, and local state. This is extremely hard to debug.
11
+
12
+ **Protected files:**
13
+
14
+ - `src/segment-system.tsx` - `renderSegments()` builds the React tree from segments.
15
+ The `loading` property determines tree structure:
16
+ - `undefined` / `null` -> OutletProvider directly (no boundary)
17
+ - `false` -> LoaderBoundary + OutletProvider (boundary, no RouteContentWrapper)
18
+ - truthy (ReactNode) -> LoaderBoundary + OutletProvider + RouteContentWrapper
19
+
20
+ - `src/route-content-wrapper.tsx` - `LoaderBoundary` and `RouteContentWrapper`.
21
+ These add structural depth (Suspense boundaries) to the React tree.
22
+
23
+ - `src/browser/server-action-bridge.ts` - Merges server action segments with
24
+ cached segments. Must preserve cached `loading` values to prevent tree drift.
25
+
26
+ - `src/browser/partial-update.ts` - Merges navigation segments with cached segments.
27
+
28
+ **Rules:**
29
+
30
+ 1. Never change the conditional logic in `renderSegments()` that decides between
31
+ LoaderBoundary/RouteContentWrapper/OutletProvider without verifying all three
32
+ render paths (SSR, navigation, action) produce identical tree structures.
33
+
34
+ 2. Never add or remove wrapper elements (Suspense, div, Fragment) around segment
35
+ content without checking that the same wrappers exist in ALL render paths.
36
+
37
+ 3. When merging segments (action bridge, partial update), always preserve the
38
+ cached `loading` value if it differs from the server value. The server may
39
+ return different `loading` values based on `isSSR` context.
40
+
41
+ 4. Run `pnpm --filter @rangojs/router exec playwright test loader-behavior` after
42
+ any changes to these files. The skipSSR action tests specifically catch tree
43
+ structure regressions.
@@ -211,6 +211,36 @@ function countCreateLoaderArgs(code, startPos, endPos) {
211
211
  }
212
212
  return hasContent ? argCount + 1 : 0;
213
213
  }
214
+ function generateClientLoaderStubs(code, filePath, isBuild) {
215
+ const loaderPattern = /export\s+const\s+(\w+)\s*=\s*createLoader\s*\(/g;
216
+ const loaders = [];
217
+ let match;
218
+ while ((match = loaderPattern.exec(code)) !== null) {
219
+ loaders.push(match[1]);
220
+ }
221
+ if (loaders.length === 0) {
222
+ return null;
223
+ }
224
+ const allExports = /export\s+(const|let|var|function|class|default)\s+(\w+)/g;
225
+ let exportMatch;
226
+ const nonLoaderExports = [];
227
+ while ((exportMatch = allExports.exec(code)) !== null) {
228
+ const name = exportMatch[2];
229
+ if (!loaders.includes(name)) {
230
+ nonLoaderExports.push(name);
231
+ }
232
+ }
233
+ if (nonLoaderExports.length > 0) {
234
+ return null;
235
+ }
236
+ const stubs = loaders.map((name) => {
237
+ const loaderId = isBuild ? hashLoaderId(filePath, name) : `${filePath}#${name}`;
238
+ return `export const ${name} = { __brand: "loader", $$id: "${loaderId}" };`;
239
+ });
240
+ return {
241
+ code: stubs.join("\n") + "\n"
242
+ };
243
+ }
214
244
  function transformLoaderExports(code, filePath, sourceId, isBuild = false) {
215
245
  if (!code.includes("createLoader")) {
216
246
  return null;
@@ -376,6 +406,12 @@ ${lazyImports.join(",\n")}
376
406
  loaderRegistry.set(hashedId, { filePath: relativePath, exportName });
377
407
  }
378
408
  }
409
+ if (!isRscEnv) {
410
+ const stubResult = generateClientLoaderStubs(code, relativePath, isBuild);
411
+ if (stubResult) {
412
+ return stubResult;
413
+ }
414
+ }
379
415
  return transformLoaderExports(code, relativePath, id, isBuild);
380
416
  }
381
417
  };
@@ -634,7 +670,7 @@ import {
634
670
  decodeFormState,
635
671
  } from "@rangojs/router/internal/deps/rsc";
636
672
  import { router } from "${routerPath}";
637
- import { createRSCHandler } from "@rangojs/router/rsc";
673
+ import { createRSCHandler } from "@rangojs/router/internal/rsc-handler";
638
674
  import { VERSION } from "@rangojs/router:version";
639
675
 
640
676
  // Import loader manifest to ensure all fetchable loaders are registered at startup
@@ -642,6 +678,9 @@ import { VERSION } from "@rangojs/router:version";
642
678
  // might not be imported before a GET request arrives
643
679
  import "virtual:rsc-router/loader-manifest";
644
680
 
681
+ // Route manifest is now loaded at runtime on first request via getRouteManifestData()
682
+ // This eliminates the need for build-time manifest generation
683
+
645
684
  export default createRSCHandler({
646
685
  router,
647
686
  version: VERSION,
@@ -675,7 +714,7 @@ import { resolve } from "node:path";
675
714
  // package.json
676
715
  var package_default = {
677
716
  name: "@rangojs/router",
678
- version: "0.0.0-experimental.3",
717
+ version: "0.0.0-experimental.4",
679
718
  type: "module",
680
719
  description: "Django-inspired RSC router with composable URL patterns",
681
720
  author: "Ivo Todorov",
@@ -760,6 +799,11 @@ var package_default = {
760
799
  types: "./src/deps/html-stream-server.ts",
761
800
  default: "./src/deps/html-stream-server.ts"
762
801
  },
802
+ "./internal/rsc-handler": {
803
+ "react-server": "./src/rsc/handler.ts",
804
+ types: "./src/rsc/handler.ts",
805
+ default: "./src/rsc/handler.ts"
806
+ },
763
807
  "./cache": {
764
808
  "react-server": "./src/cache/index.ts",
765
809
  types: "./src/cache/index.ts",
@@ -768,6 +812,10 @@ var package_default = {
768
812
  "./theme": {
769
813
  types: "./src/theme/index.ts",
770
814
  default: "./src/theme/index.ts"
815
+ },
816
+ "./build": {
817
+ types: "./src/build/index.ts",
818
+ import: "./src/build/index.ts"
771
819
  }
772
820
  },
773
821
  files: [
@@ -814,12 +862,13 @@ var package_default = {
814
862
  "@types/node": "^24.10.1",
815
863
  "@types/react": "catalog:",
816
864
  "@types/react-dom": "catalog:",
865
+ esbuild: "^0.27.0",
866
+ jiti: "^2.6.1",
817
867
  react: "catalog:",
818
868
  "react-dom": "catalog:",
819
- esbuild: "^0.27.0",
820
869
  tinyexec: "^0.3.2",
821
870
  typescript: "^5.3.0",
822
- vitest: "^2.1.8"
871
+ vitest: "^4.0.0"
823
872
  }
824
873
  };
825
874
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.3",
3
+ "version": "0.0.0-experimental.4",
4
4
  "type": "module",
5
5
  "description": "Django-inspired RSC router with composable URL patterns",
6
6
  "author": "Ivo Todorov",
@@ -85,6 +85,11 @@
85
85
  "types": "./src/deps/html-stream-server.ts",
86
86
  "default": "./src/deps/html-stream-server.ts"
87
87
  },
88
+ "./internal/rsc-handler": {
89
+ "react-server": "./src/rsc/handler.ts",
90
+ "types": "./src/rsc/handler.ts",
91
+ "default": "./src/rsc/handler.ts"
92
+ },
88
93
  "./cache": {
89
94
  "react-server": "./src/cache/index.ts",
90
95
  "types": "./src/cache/index.ts",
@@ -93,6 +98,10 @@
93
98
  "./theme": {
94
99
  "types": "./src/theme/index.ts",
95
100
  "default": "./src/theme/index.ts"
101
+ },
102
+ "./build": {
103
+ "types": "./src/build/index.ts",
104
+ "import": "./src/build/index.ts"
96
105
  }
97
106
  },
98
107
  "files": [
@@ -130,12 +139,13 @@
130
139
  "@types/node": "^24.10.1",
131
140
  "@types/react": "^19.2.7",
132
141
  "@types/react-dom": "^19.2.3",
142
+ "esbuild": "^0.27.0",
143
+ "jiti": "^2.6.1",
133
144
  "react": "^19.2.1",
134
145
  "react-dom": "^19.2.1",
135
- "esbuild": "^0.27.0",
136
146
  "tinyexec": "^0.3.2",
137
147
  "typescript": "^5.3.0",
138
- "vitest": "^2.1.8"
148
+ "vitest": "^4.0.0"
139
149
  },
140
150
  "scripts": {
141
151
  "build": "pnpm dlx esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external",
@@ -13,7 +13,7 @@ argument-hint: [setup]
13
13
  Use the `cache()` DSL function to cache routes:
14
14
 
15
15
  ```typescript
16
- import { urls } from "@rangojs/router/server";
16
+ import { urls } from "@rangojs/router";
17
17
 
18
18
  export const urlpatterns = urls(({ path, cache }) => [
19
19
  // Cache these routes for 60 seconds, SWR for 5 minutes
@@ -59,14 +59,14 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
59
59
  Configure a cache store in the router:
60
60
 
61
61
  ```typescript
62
- import { createRSCRouter } from "@rangojs/router/server";
62
+ import { createRouter } from "@rangojs/router";
63
63
  import { MemorySegmentCacheStore } from "@rangojs/router/rsc";
64
64
 
65
65
  const store = new MemorySegmentCacheStore({
66
66
  defaults: { ttl: 60, swr: 300 },
67
67
  });
68
68
 
69
- const router = createRSCRouter({
69
+ const router = createRouter({
70
70
  document: Document,
71
71
  urls: urlpatterns,
72
72
  cache: {
@@ -98,7 +98,7 @@ For distributed caching on Cloudflare Workers:
98
98
  ```typescript
99
99
  import { CFCacheStore } from "@rangojs/router/cache/cf";
100
100
 
101
- const router = createRSCRouter({
101
+ const router = createRouter({
102
102
  document: Document,
103
103
  urls: urlpatterns,
104
104
  cache: (env) => ({
@@ -145,7 +145,7 @@ cache({ store: checkoutCache }, () => [
145
145
  ## Complete Example
146
146
 
147
147
  ```typescript
148
- import { urls } from "@rangojs/router/server";
148
+ import { urls } from "@rangojs/router";
149
149
  import { MemorySegmentCacheStore } from "@rangojs/router/rsc";
150
150
 
151
151
  // Custom store for checkout (short TTL)
@@ -13,11 +13,11 @@ Caches complete HTTP responses (HTML/RSC) at the edge based on Cache-Control hea
13
13
  Configure document cache in router:
14
14
 
15
15
  ```typescript
16
- import { createRSCRouter } from "@rangojs/router/server";
16
+ import { createRouter } from "@rangojs/router";
17
17
  import { CFCacheStore } from "@rangojs/router/cache/cf";
18
18
  import { urlpatterns } from "./urls";
19
19
 
20
- const router = createRSCRouter<AppEnv>({
20
+ const router = createRouter<AppEnv>({
21
21
  document: Document,
22
22
  urls: urlpatterns,
23
23
  documentCache: (env) => ({
@@ -35,7 +35,7 @@ export default router;
35
35
  Routes opt-in to document caching using the `cache()` DSL with `documentCache` option:
36
36
 
37
37
  ```typescript
38
- import { urls } from "@rangojs/router/server";
38
+ import { urls } from "@rangojs/router";
39
39
 
40
40
  export const urlpatterns = urls(({ path, cache }) => [
41
41
  // Cache full page for 5 min, serve stale for 1 hour
@@ -56,7 +56,7 @@ export const urlpatterns = urls(({ path, cache }) => [
56
56
  ## Document Cache Options
57
57
 
58
58
  ```typescript
59
- createRSCRouter({
59
+ createRouter({
60
60
  // ...
61
61
  documentCache: (env) => ({
62
62
  // Cache store (required)
@@ -131,11 +131,11 @@ Segment hash ensures different cached responses for navigations from different s
131
131
 
132
132
  ```typescript
133
133
  // router.tsx
134
- import { createRSCRouter } from "@rangojs/router/server";
134
+ import { createRouter } from "@rangojs/router";
135
135
  import { CFCacheStore } from "@rangojs/router/cache/cf";
136
136
  import { urlpatterns } from "./urls";
137
137
 
138
- const router = createRSCRouter<AppEnv>({
138
+ const router = createRouter<AppEnv>({
139
139
  document: Document,
140
140
  urls: urlpatterns,
141
141
  documentCache: (env) => ({
@@ -148,7 +148,7 @@ const router = createRSCRouter<AppEnv>({
148
148
  export default router;
149
149
 
150
150
  // urls.tsx
151
- import { urls } from "@rangojs/router/server";
151
+ import { urls } from "@rangojs/router";
152
152
 
153
153
  export const urlpatterns = urls(({ path, layout, cache, loader }) => [
154
154
  // Blog with document caching
@@ -108,6 +108,20 @@ function ProductPrice() {
108
108
 
109
109
  **Precondition**: Loader must be registered on route via `loader()` helper.
110
110
 
111
+ Loaders can also be passed as props from server to client components:
112
+
113
+ ```tsx
114
+ "use client";
115
+ import { useLoader } from "@rangojs/router/client";
116
+ import type { ProductLoader } from "../loaders";
117
+
118
+ // typeof infers the full data type from the loader definition
119
+ function ProductCard({ loader }: { loader: typeof ProductLoader }) {
120
+ const { data } = useLoader(loader);
121
+ return <h2>{data.product.name}</h2>;
122
+ }
123
+ ```
124
+
111
125
  ### useFetchLoader()
112
126
 
113
127
  Access loader with on-demand fetching (flexible):
@@ -194,6 +208,31 @@ function BreadcrumbNav() {
194
208
  const lastCrumb = useHandle(Breadcrumbs, data => data.at(-1));
195
209
  ```
196
210
 
211
+ Handles can be passed as props from server to client components:
212
+
213
+ ```tsx
214
+ // Server component
215
+ path("/dashboard", (ctx) => {
216
+ const push = ctx.use(Breadcrumbs);
217
+ push({ label: "Dashboard", href: "/dashboard" });
218
+ return <DashboardNav handle={Breadcrumbs} />;
219
+ })
220
+
221
+ // Client component — typeof infers the full Handle<T> type
222
+ "use client";
223
+ import { useHandle } from "@rangojs/router/client";
224
+ import type { Breadcrumbs } from "../handles";
225
+
226
+ function DashboardNav({ handle }: { handle: typeof Breadcrumbs }) {
227
+ const crumbs = useHandle(handle);
228
+ return <nav>{crumbs.map(c => <a href={c.href}>{c.label}</a>)}</nav>;
229
+ }
230
+ ```
231
+
232
+ RSC serialization strips the `collect` function via `toJSON()`. On the client,
233
+ `useHandle()` recovers it from the module-level registry (populated when
234
+ `createHandle()` runs during module initialization).
235
+
197
236
  ## Action Hooks
198
237
 
199
238
  ### useAction()
@@ -343,10 +382,54 @@ function ConditionalLayout() {
343
382
  }
344
383
  ```
345
384
 
385
+ ## URL Hooks
386
+
387
+ ### useHref()
388
+
389
+ Mount-aware href for client components inside `include()` scopes:
390
+
391
+ ```tsx
392
+ "use client";
393
+ import { useHref, href, Link } from "@rangojs/router/client";
394
+
395
+ // Inside include("/shop", shopPatterns)
396
+ function ShopNav() {
397
+ const href = useHref();
398
+
399
+ return (
400
+ <>
401
+ {/* Local paths - auto-prefixed with /shop */}
402
+ <Link to={href("/cart")}>Cart</Link>
403
+ <Link to={href("/product/widget")}>Widget</Link>
404
+ </>
405
+ );
406
+ }
407
+ ```
408
+
409
+ Use `useHref()` for local navigation. Use the bare `href()` function for absolute paths.
410
+
411
+ ### useMount()
412
+
413
+ Returns the current `include()` mount path:
414
+
415
+ ```tsx
416
+ "use client";
417
+ import { useMount } from "@rangojs/router/client";
418
+
419
+ function MountInfo() {
420
+ const mount = useMount(); // "/shop" inside include("/shop", ...)
421
+ return <span>Mounted at: {mount}</span>;
422
+ }
423
+ ```
424
+
425
+ See `/links` for full URL generation guide including server-side `ctx.href`.
426
+
346
427
  ## Hook Summary
347
428
 
348
429
  | Hook | Purpose | Returns |
349
430
  |------|---------|---------|
431
+ | `useHref()` | Mount-aware href | `(path) => string` |
432
+ | `useMount()` | Current include() mount path | `string` |
350
433
  | `useNavigation()` | Navigation state & control | state, navigate, refresh |
351
434
  | `useSegments()` | URL path & segment IDs | path, segmentIds, location |
352
435
  | `useLinkStatus()` | Link pending state | { pending } |
@@ -11,7 +11,7 @@ Intercept routes render a different component during soft navigation (client-sid
11
11
  ## Basic Intercept
12
12
 
13
13
  ```typescript
14
- import { urls } from "@rangojs/router/server";
14
+ import { urls } from "@rangojs/router";
15
15
  import { Outlet, ParallelOutlet } from "@rangojs/router/client";
16
16
 
17
17
  function ShopLayout() {
@@ -149,7 +149,7 @@ function ModalWrapper({ children }) {
149
149
  }
150
150
 
151
151
  // urls/shop.tsx
152
- import { urls } from "@rangojs/router/server";
152
+ import { urls } from "@rangojs/router";
153
153
 
154
154
  export const shopPatterns = urls(({
155
155
  path,
@@ -11,7 +11,7 @@ Layouts wrap child routes and persist during navigation within their scope.
11
11
  ## Basic Layout
12
12
 
13
13
  ```typescript
14
- import { urls } from "@rangojs/router/server";
14
+ import { urls } from "@rangojs/router";
15
15
  import { Outlet } from "@rangojs/router/client";
16
16
 
17
17
  function ShopLayout() {
@@ -164,7 +164,7 @@ layout(<CartLayout />, () => [
164
164
  ## Complete Example
165
165
 
166
166
  ```typescript
167
- import { urls } from "@rangojs/router/server";
167
+ import { urls } from "@rangojs/router";
168
168
  import { Outlet, ParallelOutlet } from "@rangojs/router/client";
169
169
 
170
170
  function ShopLayout() {
@@ -0,0 +1,180 @@
1
+ ---
2
+ name: links
3
+ description: URL generation with ctx.href (server), href (client), useHref (mounted), useMount, and scopedHref
4
+ argument-hint: [href|useHref|useMount|scopedHref]
5
+ ---
6
+
7
+ # Links & URL Generation
8
+
9
+ @rangojs/router provides different href APIs for server and client contexts.
10
+
11
+ ## Server: ctx.href()
12
+
13
+ Available in route handlers via HandlerContext. Resolves named routes using the full route map.
14
+
15
+ ```typescript
16
+ import { urls, scopedHref } from "@rangojs/router";
17
+
18
+ export const shopPatterns = urls(({ path, layout }) => [
19
+ layout(<ShopLayout />, () => [
20
+ path("/", ShopIndex, { name: "index" }),
21
+ path("/cart", CartPage, { name: "cart" }),
22
+ path("/product/:slug", ProductPage, { name: "product" }),
23
+ ]),
24
+ ]);
25
+ ```
26
+
27
+ ### Resolution priority
28
+
29
+ 1. **Path-based** (`/...`) - returned as-is
30
+ 2. **Absolute name** (contains dot: `blog.post`) - global lookup
31
+ 3. **Local name** (`cart`) - resolved relative to current route's namespace
32
+
33
+ ```typescript
34
+ // Inside a handler within shopPatterns (mounted at /shop)
35
+ path("/product/:slug", (ctx) => {
36
+ ctx.href("cart"); // "/shop/cart" (local)
37
+ ctx.href("product", { slug: "widget" }); // "/shop/product/widget" (local + params)
38
+ ctx.href("blog.post", { slug: "hi" }); // "/blog/hi" (absolute)
39
+ ctx.href("/about"); // "/about" (path-based)
40
+
41
+ return <ProductPage slug={ctx.params.slug} />;
42
+ }, { name: "product" })
43
+ ```
44
+
45
+ ### scopedHref() - type-safe ctx.href
46
+
47
+ Wraps `ctx.href` with local route type information for autocomplete and validation:
48
+
49
+ ```typescript
50
+ import { scopedHref } from "@rangojs/router";
51
+
52
+ path("/product/:slug", (ctx) => {
53
+ const href = scopedHref<typeof shopPatterns>(ctx.href);
54
+
55
+ href("cart"); // Type-safe local name
56
+ href("product", { slug: "widget" }); // Type-safe with params
57
+ href("blog.post"); // Absolute names (dot notation) always allowed
58
+ href("/about"); // Path-based always allowed
59
+
60
+ return <ProductPage slug={ctx.params.slug} />;
61
+ }, { name: "product" })
62
+ ```
63
+
64
+ ## Client: href()
65
+
66
+ Plain function for absolute path-based URLs. No hook needed - works anywhere.
67
+
68
+ ```typescript
69
+ "use client";
70
+ import { href, Link } from "@rangojs/router/client";
71
+
72
+ function GlobalNav() {
73
+ return (
74
+ <nav>
75
+ <Link to={href("/")}>Home</Link>
76
+ <Link to={href("/about")}>About</Link>
77
+ <Link to={href("/blog/hello")}>Post</Link>
78
+ </nav>
79
+ );
80
+ }
81
+ ```
82
+
83
+ `href()` is an identity function at runtime but provides compile-time validation via `ValidPaths` type. Paths are validated against registered route patterns using `PatternToPath`.
84
+
85
+ ## Client: useHref()
86
+
87
+ Hook that returns a mount-aware href function. Automatically prepends the `include()` mount prefix.
88
+
89
+ ```typescript
90
+ "use client";
91
+ import { useHref, href, Link } from "@rangojs/router/client";
92
+
93
+ // Inside include("/shop", shopPatterns)
94
+ function ShopNav() {
95
+ const href = useHref();
96
+
97
+ return (
98
+ <nav>
99
+ <Link to={href("/")}>Shop Home</Link> {/* "/shop/" */}
100
+ <Link to={href("/cart")}>Cart</Link> {/* "/shop/cart" */}
101
+ <Link to={href("/product/widget")}>W</Link> {/* "/shop/product/widget" */}
102
+ </nav>
103
+ );
104
+ }
105
+ ```
106
+
107
+ Use `useHref()` for local navigation within a mounted module. Use the bare `href()` function for absolute paths outside the current mount.
108
+
109
+ ## Client: useMount()
110
+
111
+ Returns the current `include()` mount path. Useful for building custom logic based on mount location.
112
+
113
+ ```typescript
114
+ "use client";
115
+ import { useMount } from "@rangojs/router/client";
116
+
117
+ function MountInfo() {
118
+ const mount = useMount(); // "/shop" inside include("/shop", ...)
119
+ // "/" at root level
120
+
121
+ return <span>Mounted at: {mount}</span>;
122
+ }
123
+ ```
124
+
125
+ `useMount()` reads from `MountContext`, which is automatically set by `include()` in the segment tree.
126
+
127
+ ## When to use what
128
+
129
+ | Context | API | Resolves | Use for |
130
+ |---------|-----|----------|---------|
131
+ | Server handler | `ctx.href("name")` | Named routes (local + absolute) | Server-side URL generation |
132
+ | Server handler | `scopedHref<T>(ctx.href)` | Same, with type safety | Type-safe server URLs |
133
+ | Client component | `href("/path")` | Absolute paths | Global navigation |
134
+ | Client component | `useHref()` | Mount-prefixed paths | Local navigation inside `include()` |
135
+ | Client component | `useMount()` | Raw mount path | Custom mount-aware logic |
136
+
137
+ ## Complete example: mounted module
138
+
139
+ ```typescript
140
+ // urls/shop.tsx (server)
141
+ import { urls, scopedHref } from "@rangojs/router";
142
+
143
+ export const shopPatterns = urls(({ path, layout }) => [
144
+ layout((ctx) => {
145
+ const href = scopedHref<typeof shopPatterns>(ctx.href);
146
+ return <ShopLayout cartUrl={href("cart")} />;
147
+ }, () => [
148
+ path("/", ShopIndex, { name: "index" }),
149
+ path("/cart", CartPage, { name: "cart" }),
150
+ path("/product/:slug", ProductPage, { name: "product" }),
151
+ ]),
152
+ ]);
153
+
154
+ // urls.tsx (server)
155
+ export const urlpatterns = urls(({ path, include }) => [
156
+ path("/", HomePage, { name: "home" }),
157
+ include("/shop", shopPatterns),
158
+ ]);
159
+ ```
160
+
161
+ ```tsx
162
+ // components/ShopNav.tsx (client)
163
+ "use client";
164
+ import { useHref, href, Link } from "@rangojs/router/client";
165
+
166
+ export function ShopNav() {
167
+ const localHref = useHref();
168
+
169
+ return (
170
+ <nav>
171
+ {/* Local paths - auto-prefixed with /shop */}
172
+ <Link to={localHref("/cart")}>Cart</Link>
173
+ <Link to={localHref("/product/widget")}>Widget</Link>
174
+
175
+ {/* Absolute path - no prefix */}
176
+ <Link to={href("/")}>Home</Link>
177
+ </nav>
178
+ );
179
+ }
180
+ ```