@rangojs/router 0.0.0-experimental.10
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.
- package/CLAUDE.md +43 -0
- package/README.md +19 -0
- package/dist/bin/rango.js +227 -0
- package/dist/vite/index.js +3039 -0
- package/package.json +171 -0
- package/skills/caching/SKILL.md +191 -0
- package/skills/debug-manifest/SKILL.md +108 -0
- package/skills/document-cache/SKILL.md +180 -0
- package/skills/fonts/SKILL.md +165 -0
- package/skills/hooks/SKILL.md +442 -0
- package/skills/intercept/SKILL.md +190 -0
- package/skills/layout/SKILL.md +213 -0
- package/skills/links/SKILL.md +180 -0
- package/skills/loader/SKILL.md +246 -0
- package/skills/middleware/SKILL.md +202 -0
- package/skills/mime-routes/SKILL.md +124 -0
- package/skills/parallel/SKILL.md +228 -0
- package/skills/prerender/SKILL.md +283 -0
- package/skills/rango/SKILL.md +54 -0
- package/skills/response-routes/SKILL.md +358 -0
- package/skills/route/SKILL.md +173 -0
- package/skills/router-setup/SKILL.md +346 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +78 -0
- package/skills/typesafety/SKILL.md +394 -0
- package/src/__internal.ts +175 -0
- package/src/bin/rango.ts +24 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +913 -0
- package/src/browser/navigation-client.ts +165 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +600 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +346 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/mount-context.ts +32 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +203 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +140 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +352 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/segment-structure-assert.ts +67 -0
- package/src/browser/server-action-bridge.ts +762 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +478 -0
- package/src/build/generate-manifest.ts +377 -0
- package/src/build/generate-route-types.ts +828 -0
- package/src/build/index.ts +36 -0
- package/src/build/route-trie.ts +239 -0
- package/src/cache/cache-scope.ts +563 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +392 -0
- package/src/client.rsc.tsx +83 -0
- package/src/client.tsx +643 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/debug.ts +233 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +295 -0
- package/src/handle.ts +130 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/host/cookie-handler.ts +159 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +56 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +330 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +138 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +202 -0
- package/src/href-context.ts +33 -0
- package/src/index.rsc.ts +121 -0
- package/src/index.ts +165 -0
- package/src/loader.rsc.ts +207 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/prerender/param-hash.ts +35 -0
- package/src/prerender/store.ts +40 -0
- package/src/prerender.ts +156 -0
- package/src/reverse.ts +267 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +193 -0
- package/src/route-definition.ts +1431 -0
- package/src/route-map-builder.ts +242 -0
- package/src/route-types.ts +220 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/intercept-resolution.ts +387 -0
- package/src/router/loader-resolution.ts +327 -0
- package/src/router/manifest.ts +216 -0
- package/src/router/match-api.ts +621 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +382 -0
- package/src/router/match-middleware/cache-store.ts +276 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +281 -0
- package/src/router/match-middleware/segment-resolution.ts +184 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +213 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.ts +791 -0
- package/src/router/pattern-matching.ts +407 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +301 -0
- package/src/router/segment-resolution.ts +1315 -0
- package/src/router/trie-matching.ts +172 -0
- package/src/router/types.ts +163 -0
- package/src/router.gen.ts +6 -0
- package/src/router.ts +2423 -0
- package/src/rsc/handler.ts +1443 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +236 -0
- package/src/segment-system.tsx +442 -0
- package/src/server/context.ts +466 -0
- package/src/server/handle-store.ts +229 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +171 -0
- package/src/ssr/index.tsx +296 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +59 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1757 -0
- package/src/urls.gen.ts +8 -0
- package/src/urls.ts +1282 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +426 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/expose-prerender-handler-id.ts +429 -0
- package/src/vite/index.ts +2068 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +114 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: intercept
|
|
3
|
+
description: Define intercept routes for modals, slide-overs, and soft navigation patterns in @rangojs/router
|
|
4
|
+
argument-hint: [@slot-name] [route-to-intercept]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Intercept Routes
|
|
8
|
+
|
|
9
|
+
Intercept routes render a different component during soft navigation (client-side) while preserving the background route. Hard navigation (direct URL) shows the full page.
|
|
10
|
+
|
|
11
|
+
## Basic Intercept
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { urls } from "@rangojs/router";
|
|
15
|
+
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
16
|
+
|
|
17
|
+
function ShopLayout() {
|
|
18
|
+
return (
|
|
19
|
+
<div className="shop">
|
|
20
|
+
<Outlet />
|
|
21
|
+
<ParallelOutlet name="@modal" />
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const urlpatterns = urls(({ path, layout, intercept, loader }) => [
|
|
27
|
+
layout(<ShopLayout />, () => [
|
|
28
|
+
// Intercept product detail - shows modal during soft navigation
|
|
29
|
+
intercept(
|
|
30
|
+
"@modal", // Slot name
|
|
31
|
+
"product", // Route name to intercept
|
|
32
|
+
<ProductModal />, // Modal component
|
|
33
|
+
() => [
|
|
34
|
+
loader(ProductLoader),
|
|
35
|
+
loading(<ProductModalSkeleton />),
|
|
36
|
+
]
|
|
37
|
+
),
|
|
38
|
+
|
|
39
|
+
// Normal routes
|
|
40
|
+
path("/shop", ShopIndex, { name: "index" }),
|
|
41
|
+
path("/shop/product/:slug", ProductPage, { name: "product" }),
|
|
42
|
+
]),
|
|
43
|
+
]);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Navigation Behavior
|
|
47
|
+
|
|
48
|
+
| Navigation Type | What Renders |
|
|
49
|
+
|-----------------|--------------|
|
|
50
|
+
| Click link `/shop/product/abc` | `<ProductModal />` in `@modal`, background preserved |
|
|
51
|
+
| Direct URL `/shop/product/abc` | Full `<ProductPage />` page |
|
|
52
|
+
| Browser back | Close modal, restore previous state |
|
|
53
|
+
|
|
54
|
+
## Intercept with Layout
|
|
55
|
+
|
|
56
|
+
Wrap intercept content in a modal layout:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
intercept(
|
|
60
|
+
"@modal",
|
|
61
|
+
"product",
|
|
62
|
+
<ProductModalContent />,
|
|
63
|
+
() => [
|
|
64
|
+
layout(<ModalWrapper />), // Wraps the modal content
|
|
65
|
+
loader(ProductLoader),
|
|
66
|
+
loading(<ProductModalSkeleton />),
|
|
67
|
+
]
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Conditional Intercept with when()
|
|
72
|
+
|
|
73
|
+
Only intercept based on navigation context:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
intercept(
|
|
77
|
+
"@modal",
|
|
78
|
+
"product",
|
|
79
|
+
<ProductModal />,
|
|
80
|
+
() => [
|
|
81
|
+
// Only intercept when coming from a different section
|
|
82
|
+
when(({ from }) => !from.pathname.startsWith("/shop/product/")),
|
|
83
|
+
loader(ProductLoader),
|
|
84
|
+
]
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Multiple Loaders in Intercept
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
intercept(
|
|
92
|
+
"@modal",
|
|
93
|
+
"product",
|
|
94
|
+
<ProductModal />,
|
|
95
|
+
() => [
|
|
96
|
+
loader(ProductLoader, () => [cache()]),
|
|
97
|
+
loader(ProductCartLoader, () => [revalidate(() => true)]),
|
|
98
|
+
loader(RecommendationsLoader),
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Closing the Modal
|
|
104
|
+
|
|
105
|
+
Use navigation to close:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
"use client";
|
|
109
|
+
import { useNavigation } from "@rangojs/router/client";
|
|
110
|
+
|
|
111
|
+
function ModalWrapper({ children }) {
|
|
112
|
+
const { goBack } = useNavigation();
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div className="modal-overlay" onClick={goBack}>
|
|
116
|
+
<div className="modal" onClick={(e) => e.stopPropagation()}>
|
|
117
|
+
<button onClick={goBack}>Close</button>
|
|
118
|
+
{children}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Complete Example
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// components/ProductModal.tsx
|
|
129
|
+
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
130
|
+
|
|
131
|
+
function ShopLayout() {
|
|
132
|
+
return (
|
|
133
|
+
<div className="shop">
|
|
134
|
+
<ParallelOutlet name="@promoBanner" />
|
|
135
|
+
<main>
|
|
136
|
+
<Outlet />
|
|
137
|
+
</main>
|
|
138
|
+
<ParallelOutlet name="@modal" />
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ModalWrapper({ children }) {
|
|
144
|
+
return (
|
|
145
|
+
<div className="modal-overlay">
|
|
146
|
+
<div className="modal">{children}</div>
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// urls/shop.tsx
|
|
152
|
+
import { urls } from "@rangojs/router";
|
|
153
|
+
|
|
154
|
+
export const shopPatterns = urls(({
|
|
155
|
+
path,
|
|
156
|
+
layout,
|
|
157
|
+
parallel,
|
|
158
|
+
intercept,
|
|
159
|
+
loader,
|
|
160
|
+
loading,
|
|
161
|
+
when,
|
|
162
|
+
}) => [
|
|
163
|
+
layout(<ShopLayout />, () => [
|
|
164
|
+
parallel({
|
|
165
|
+
"@promoBanner": () => <PromoBanner />,
|
|
166
|
+
}),
|
|
167
|
+
|
|
168
|
+
// Intercept product detail into modal
|
|
169
|
+
intercept(
|
|
170
|
+
"@modal",
|
|
171
|
+
"product", // Route name (without prefix)
|
|
172
|
+
<ProductModalContent />,
|
|
173
|
+
() => [
|
|
174
|
+
when(({ from }) => !from.pathname.startsWith("/shop/product/")),
|
|
175
|
+
layout(<ModalWrapper />),
|
|
176
|
+
loading(<ProductModalSkeleton />),
|
|
177
|
+
loader(ProductLoader, () => [cache()]),
|
|
178
|
+
loader(RecommendationsLoader),
|
|
179
|
+
]
|
|
180
|
+
),
|
|
181
|
+
|
|
182
|
+
// Normal routes
|
|
183
|
+
path("/", ShopIndex, { name: "index" }),
|
|
184
|
+
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
185
|
+
loader(ProductLoader),
|
|
186
|
+
loading(<ProductPageSkeleton />),
|
|
187
|
+
]),
|
|
188
|
+
]),
|
|
189
|
+
]);
|
|
190
|
+
```
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: layout
|
|
3
|
+
description: Define layout routes that wrap child routes in @rangojs/router
|
|
4
|
+
argument-hint: [component]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Layouts with layout()
|
|
8
|
+
|
|
9
|
+
Layouts wrap child routes and persist during navigation within their scope.
|
|
10
|
+
|
|
11
|
+
## Basic Layout
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { urls } from "@rangojs/router";
|
|
15
|
+
import { Outlet } from "@rangojs/router/client";
|
|
16
|
+
|
|
17
|
+
function ShopLayout() {
|
|
18
|
+
return (
|
|
19
|
+
<div className="shop">
|
|
20
|
+
<nav>Shop Navigation</nav>
|
|
21
|
+
<Outlet /> {/* Child routes render here */}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const urlpatterns = urls(({ path, layout }) => [
|
|
27
|
+
layout(<ShopLayout />, () => [
|
|
28
|
+
path("/shop", ShopIndex, { name: "shop.index" }),
|
|
29
|
+
path("/shop/cart", CartPage, { name: "shop.cart" }),
|
|
30
|
+
path("/shop/product/:slug", ProductPage, { name: "shop.product" }),
|
|
31
|
+
]),
|
|
32
|
+
]);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Layout Patterns
|
|
36
|
+
|
|
37
|
+
### JSX Element
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
layout(<ShopLayout />, () => [
|
|
41
|
+
path("/shop", ShopIndex, { name: "shop" }),
|
|
42
|
+
])
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Component Function
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
layout(ShopLayout, () => [
|
|
49
|
+
path("/shop", ShopIndex, { name: "shop" }),
|
|
50
|
+
])
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Handler with Context
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
layout((ctx) => {
|
|
57
|
+
const push = ctx.use(Breadcrumbs);
|
|
58
|
+
push({ label: "Shop", href: "/shop" });
|
|
59
|
+
return <ShopLayout />;
|
|
60
|
+
}, () => [
|
|
61
|
+
path("/shop", ShopIndex, { name: "shop" }),
|
|
62
|
+
])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Nested Layouts
|
|
66
|
+
|
|
67
|
+
Layouts compose by wrapping order (first layout wraps outer):
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
urls(({ path, layout }) => [
|
|
71
|
+
layout(<RootLayout />, () => [ // Outer
|
|
72
|
+
layout(<ShopLayout />, () => [ // Inner
|
|
73
|
+
path("/shop", ShopIndex, { name: "shop" }),
|
|
74
|
+
]),
|
|
75
|
+
]),
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
// Result: RootLayout > ShopLayout > ShopIndex
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Layout with Children DSL
|
|
82
|
+
|
|
83
|
+
Add loaders, parallel routes, or revalidation to layouts:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
layout(<ShopLayout />, () => [
|
|
87
|
+
// Loaders for layout
|
|
88
|
+
loader(CartLoader),
|
|
89
|
+
loader(UserLoader),
|
|
90
|
+
|
|
91
|
+
// Revalidation rules for layout
|
|
92
|
+
revalidate(shopRevalidation),
|
|
93
|
+
|
|
94
|
+
// Child routes
|
|
95
|
+
path("/shop", ShopIndex, { name: "shop" }),
|
|
96
|
+
path("/shop/cart", CartPage, { name: "cart" }),
|
|
97
|
+
])
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## The Outlet Component
|
|
101
|
+
|
|
102
|
+
`<Outlet />` renders child content. Import from `@rangojs/router/client`:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { Outlet } from "@rangojs/router/client";
|
|
106
|
+
|
|
107
|
+
function ShopLayout() {
|
|
108
|
+
return (
|
|
109
|
+
<div className="shop-layout">
|
|
110
|
+
<header>Shop Header</header>
|
|
111
|
+
<main>
|
|
112
|
+
<Outlet /> {/* Child routes render here */}
|
|
113
|
+
</main>
|
|
114
|
+
<footer>Shop Footer</footer>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Named Outlets
|
|
121
|
+
|
|
122
|
+
For parallel routes, use named outlets:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
126
|
+
|
|
127
|
+
function DashboardLayout() {
|
|
128
|
+
return (
|
|
129
|
+
<div className="dashboard">
|
|
130
|
+
<aside>
|
|
131
|
+
<ParallelOutlet name="@sidebar" />
|
|
132
|
+
</aside>
|
|
133
|
+
<main>
|
|
134
|
+
<Outlet /> {/* Main content */}
|
|
135
|
+
</main>
|
|
136
|
+
<aside>
|
|
137
|
+
<ParallelOutlet name="@notifications" />
|
|
138
|
+
</aside>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Layout Revalidation
|
|
145
|
+
|
|
146
|
+
Layouts don't revalidate by default. Control with `revalidate()`:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
layout(<ShopLayout />, () => [
|
|
150
|
+
// Never revalidate (default behavior)
|
|
151
|
+
revalidate(() => false),
|
|
152
|
+
|
|
153
|
+
path("/shop", ShopIndex, { name: "shop" }),
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
// Or revalidate based on conditions
|
|
157
|
+
layout(<CartLayout />, () => [
|
|
158
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
|
|
159
|
+
|
|
160
|
+
path("/cart", CartPage, { name: "cart" }),
|
|
161
|
+
])
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Complete Example
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { urls } from "@rangojs/router";
|
|
168
|
+
import { Outlet, ParallelOutlet } from "@rangojs/router/client";
|
|
169
|
+
|
|
170
|
+
function ShopLayout() {
|
|
171
|
+
return (
|
|
172
|
+
<div className="shop">
|
|
173
|
+
<ParallelOutlet name="@promoBanner" />
|
|
174
|
+
<nav>
|
|
175
|
+
<a href="/shop">Home</a>
|
|
176
|
+
<a href="/shop/cart">Cart</a>
|
|
177
|
+
</nav>
|
|
178
|
+
<div className="content">
|
|
179
|
+
<aside>
|
|
180
|
+
<ParallelOutlet name="@sidebar" />
|
|
181
|
+
</aside>
|
|
182
|
+
<main>
|
|
183
|
+
<Outlet />
|
|
184
|
+
</main>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const shopPatterns = urls(({ path, layout, parallel, loader, revalidate }) => [
|
|
191
|
+
layout((ctx) => {
|
|
192
|
+
const push = ctx.use(Breadcrumbs);
|
|
193
|
+
push({ label: "Shop", href: "/shop" });
|
|
194
|
+
return <ShopLayout />;
|
|
195
|
+
}, () => [
|
|
196
|
+
// Layout loaders
|
|
197
|
+
loader(CartLoader, () => [
|
|
198
|
+
revalidate(({ actionId }) => actionId?.includes("Cart") ?? false),
|
|
199
|
+
]),
|
|
200
|
+
|
|
201
|
+
// Parallel routes
|
|
202
|
+
parallel({
|
|
203
|
+
"@promoBanner": () => <PromoBanner />,
|
|
204
|
+
"@sidebar": () => <CategorySidebar />,
|
|
205
|
+
}),
|
|
206
|
+
|
|
207
|
+
// Child routes
|
|
208
|
+
path("/shop", ShopIndex, { name: "index" }),
|
|
209
|
+
path("/shop/cart", CartPage, { name: "cart" }),
|
|
210
|
+
path("/shop/product/:slug", ProductPage, { name: "product" }),
|
|
211
|
+
]),
|
|
212
|
+
]);
|
|
213
|
+
```
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: links
|
|
3
|
+
description: URL generation with ctx.reverse (server), href (client), useHref (mounted), useMount, and scopedReverse
|
|
4
|
+
argument-hint: [href|useHref|useMount|scopedReverse]
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Links & URL Generation
|
|
8
|
+
|
|
9
|
+
@rangojs/router provides different href APIs for server and client contexts.
|
|
10
|
+
|
|
11
|
+
## Server: ctx.reverse()
|
|
12
|
+
|
|
13
|
+
Available in route handlers via HandlerContext. Resolves named routes using the full route map.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { urls, scopedReverse } 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.reverse("cart"); // "/shop/cart" (local)
|
|
37
|
+
ctx.reverse("product", { slug: "widget" }); // "/shop/product/widget" (local + params)
|
|
38
|
+
ctx.reverse("blog.post", { slug: "hi" }); // "/blog/hi" (absolute)
|
|
39
|
+
ctx.reverse("/about"); // "/about" (path-based)
|
|
40
|
+
|
|
41
|
+
return <ProductPage slug={ctx.params.slug} />;
|
|
42
|
+
}, { name: "product" })
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### scopedReverse() - type-safe ctx.reverse
|
|
46
|
+
|
|
47
|
+
Wraps `ctx.reverse` with local route type information for autocomplete and validation:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { scopedReverse } from "@rangojs/router";
|
|
51
|
+
|
|
52
|
+
path("/product/:slug", (ctx) => {
|
|
53
|
+
const reverse = scopedReverse<typeof shopPatterns>(ctx.reverse);
|
|
54
|
+
|
|
55
|
+
reverse("cart"); // Type-safe local name
|
|
56
|
+
reverse("product", { slug: "widget" }); // Type-safe with params
|
|
57
|
+
reverse("blog.post"); // Absolute names (dot notation) always allowed
|
|
58
|
+
reverse("/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.reverse("name")` | Named routes (local + absolute) | Server-side URL generation |
|
|
132
|
+
| Server handler | `scopedReverse<T>(ctx.reverse)` | 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, scopedReverse } from "@rangojs/router";
|
|
142
|
+
|
|
143
|
+
export const shopPatterns = urls(({ path, layout }) => [
|
|
144
|
+
layout((ctx) => {
|
|
145
|
+
const reverse = scopedReverse<typeof shopPatterns>(ctx.reverse);
|
|
146
|
+
return <ShopLayout cartUrl={reverse("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
|
+
```
|