@timber-js/app 0.2.0-alpha.154 → 0.2.0-alpha.155
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/agent-skill.md +344 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +41 -4
- package/dist/cli.js.map +1 -1
- package/docs/api/30-api-server.mdx +233 -0
- package/docs/api/31-api-client.mdx +161 -0
- package/docs/api/32-api-cache.mdx +193 -0
- package/docs/api/33-api-search-params.mdx +141 -0
- package/docs/api/34-api-config.mdx +189 -0
- package/docs/api/35-api-typescript.mdx +170 -0
- package/docs/api/36-cli.mdx +143 -0
- package/docs/learn/00-introduction.mdx +81 -0
- package/docs/learn/01-quick-start.mdx +94 -0
- package/docs/learn/02-pages-and-layouts.mdx +138 -0
- package/docs/learn/03-fetching-data.mdx +211 -0
- package/docs/learn/03b-access-control.mdx +130 -0
- package/docs/learn/04-loading-states.mdx +67 -0
- package/docs/learn/04b-the-flush-point.mdx +115 -0
- package/docs/learn/05-typed-params.mdx +275 -0
- package/docs/learn/06-forms-and-actions.mdx +150 -0
- package/docs/learn/08-streaming.mdx +146 -0
- package/docs/learn/09-caching.mdx +121 -0
- package/docs/learn/10-middleware.mdx +156 -0
- package/docs/learn/11-error-handling.mdx +147 -0
- package/docs/learn/12-client-navigation.mdx +168 -0
- package/docs/learn/13-configuration.mdx +117 -0
- package/docs/learn/14-deploying.mdx +206 -0
- package/docs/more/01-advanced-routing.mdx +139 -0
- package/docs/more/02-advanced-forms.mdx +137 -0
- package/docs/more/03-coming-from-nextjs.mdx +186 -0
- package/docs/more/04-metadata-and-fonts.mdx +193 -0
- package/docs/more/04b-mdx.mdx +209 -0
- package/docs/more/05-content-collections.mdx +90 -0
- package/docs/more/06-instrumentation.mdx +188 -0
- package/docs/more/07-security.mdx +123 -0
- package/docs/more/40-why-timber.mdx +50 -0
- package/docs/more/41-timber-vs-nextjs.mdx +81 -0
- package/docs/more/42-timber-vs-others.mdx +68 -0
- package/llms.txt +55 -0
- package/package.json +6 -2
- package/src/cli.ts +62 -3
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Client Navigation'
|
|
3
|
+
description: 'The Link component, useRouter, segment cache, and scroll restoration.'
|
|
4
|
+
slug: 'client-navigation'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Client Navigation
|
|
8
|
+
|
|
9
|
+
timber.js supports client-side navigation via the `<Link>` component. Clicking a link fetches an RSC payload from the server and reconciles the DOM without a full page reload.
|
|
10
|
+
|
|
11
|
+
## `<Link>`
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Link } from '@timber-js/app/client';
|
|
15
|
+
|
|
16
|
+
<Link href="/about">About</Link>
|
|
17
|
+
<Link href="/products/123">Product</Link>
|
|
18
|
+
<Link href="/dashboard" replace>Dashboard</Link>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`<Link>` type-checks `href` against the generated route map. Invalid routes produce a TypeScript error.
|
|
22
|
+
|
|
23
|
+
## `useRouter`
|
|
24
|
+
|
|
25
|
+
For programmatic navigation:
|
|
26
|
+
|
|
27
|
+
```tsx title="app/components/logout-button.tsx"
|
|
28
|
+
'use client';
|
|
29
|
+
|
|
30
|
+
import { useRouter } from '@timber-js/app/client';
|
|
31
|
+
|
|
32
|
+
declare function logout(): Promise<void>;
|
|
33
|
+
|
|
34
|
+
export function LogoutButton() {
|
|
35
|
+
const router = useRouter();
|
|
36
|
+
|
|
37
|
+
async function handleLogout() {
|
|
38
|
+
await logout();
|
|
39
|
+
router.push('/login');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return <button onClick={handleLogout}>Log out</button>;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| Method | Description |
|
|
47
|
+
| ---------------------- | ---------------------------------------- |
|
|
48
|
+
| `router.push(href)` | Navigate to a new URL |
|
|
49
|
+
| `router.replace(href)` | Navigate without adding a history entry |
|
|
50
|
+
| `router.refresh()` | Re-fetch the current route's RSC payload |
|
|
51
|
+
| `router.back()` | Go back in history |
|
|
52
|
+
| `router.forward()` | Go forward in history |
|
|
53
|
+
|
|
54
|
+
## `usePathname`
|
|
55
|
+
|
|
56
|
+
Returns the current pathname:
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
'use client';
|
|
60
|
+
import { usePathname, Link } from '@timber-js/app/client';
|
|
61
|
+
|
|
62
|
+
export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
|
|
63
|
+
const pathname = usePathname();
|
|
64
|
+
const isActive = pathname === href;
|
|
65
|
+
return (
|
|
66
|
+
<Link href={href} className={isActive ? 'font-bold' : ''}>
|
|
67
|
+
{children}
|
|
68
|
+
</Link>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## `useSelectedLayoutSegment`
|
|
74
|
+
|
|
75
|
+
Returns the active segment within a layout — useful for highlighting nav items:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
'use client';
|
|
79
|
+
import { useSelectedLayoutSegment } from '@timber-js/app/client';
|
|
80
|
+
|
|
81
|
+
export function DashboardNav() {
|
|
82
|
+
const segment = useSelectedLayoutSegment();
|
|
83
|
+
// segment is "settings", "projects", etc.
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## `useLinkStatus`
|
|
88
|
+
|
|
89
|
+
Track per-link pending state during navigation:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
'use client';
|
|
93
|
+
import { Link, useLinkStatus } from '@timber-js/app/client';
|
|
94
|
+
|
|
95
|
+
function NavItemInner({ children }: { children: React.ReactNode }) {
|
|
96
|
+
const { pending } = useLinkStatus();
|
|
97
|
+
return <span className={pending ? 'opacity-50' : ''}>{children}</span>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function NavItem({ href, children }: { href: string; children: React.ReactNode }) {
|
|
101
|
+
return (
|
|
102
|
+
<Link href={href}>
|
|
103
|
+
<NavItemInner>{children}</NavItemInner>
|
|
104
|
+
</Link>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Segment Cache
|
|
110
|
+
|
|
111
|
+
The client maintains a mirror of the server's segment tree. On navigation, only changed segments are re-fetched:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
/dashboard/settings → /dashboard/team
|
|
115
|
+
|
|
116
|
+
Root Layout ← sync, mounted → skip
|
|
117
|
+
Auth Layout ← sync, mounted → skip
|
|
118
|
+
Dashboard Layout ← sync, mounted → skip
|
|
119
|
+
Team Page ← new → fetch from server
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Sync layouts stay cached while mounted. Async layouts always re-render. Pages always re-render. Client component state in shared layouts is preserved — counters, form inputs, scroll positions survive navigation.
|
|
123
|
+
|
|
124
|
+
Back/forward navigation replays cached RSC payloads instantly — no server roundtrip.
|
|
125
|
+
|
|
126
|
+
## Scroll Restoration
|
|
127
|
+
|
|
128
|
+
- **Forward navigation** scrolls to the top of the page.
|
|
129
|
+
- **Back/forward** restores the saved scroll position.
|
|
130
|
+
- **Hash fragments** — `<Link href="/docs/api#install">` commits the full URL (including the `#fragment`) to the address bar and scrolls to the matching element after render, just like a plain `<a>` without JavaScript. If no element matches, navigation falls back to scroll-to-top.
|
|
131
|
+
|
|
132
|
+
Pass `scroll={false}` to `<Link>` or `router.push` to preserve the current scroll position:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
<Link href="/dashboard/settings" scroll={false}>Settings</Link>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
router.push('/dashboard/settings', { scroll: false });
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Custom Scroll Containers
|
|
143
|
+
|
|
144
|
+
If your layout uses a custom scrollable container, add `data-timber-scroll-restoration`:
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<main className="overflow-y-auto h-screen" data-timber-scroll-restoration>
|
|
148
|
+
{children}
|
|
149
|
+
</main>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## `Link.onNavigate`
|
|
153
|
+
|
|
154
|
+
Intercept navigation for view transitions:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
<Link
|
|
158
|
+
href="/gallery"
|
|
159
|
+
onNavigate={(e) => {
|
|
160
|
+
if (document.startViewTransition) {
|
|
161
|
+
e.preventDefault();
|
|
162
|
+
document.startViewTransition(() => e.navigate());
|
|
163
|
+
}
|
|
164
|
+
}}
|
|
165
|
+
>
|
|
166
|
+
Gallery
|
|
167
|
+
</Link>
|
|
168
|
+
```
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Configuration'
|
|
3
|
+
description: 'timber.config.ts — output mode, adapters, caching, and the two-phase model.'
|
|
4
|
+
slug: 'configuration'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Configuration
|
|
8
|
+
|
|
9
|
+
timber.js is configured through `timber.config.ts` at the project root. Vite configuration lives separately in `vite.config.ts`.
|
|
10
|
+
|
|
11
|
+
## Minimal Config
|
|
12
|
+
|
|
13
|
+
```ts title="timber.config.ts"
|
|
14
|
+
export default {
|
|
15
|
+
output: 'server',
|
|
16
|
+
};
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That's it for most projects. The defaults are sensible.
|
|
20
|
+
|
|
21
|
+
## How the Config Is Loaded
|
|
22
|
+
|
|
23
|
+
`timber.config.ts` is loaded by Node.js itself using built-in type stripping — it is not bundled by Vite. This requires Node.js 22.18 or newer, and the file must stick to erasable TypeScript syntax: type annotations, interfaces, and `satisfies` are fine, but enums, namespaces, and constructor parameter properties will fail to load. Your `package.json` should declare `"type": "module"` (the default for new timber projects) so the `export default` syntax parses correctly.
|
|
24
|
+
|
|
25
|
+
If the config file fails to load for any reason, every command — `timber dev`, `timber build`, and `timber preview` — stops with an error naming the file. A broken config never silently falls back to defaults.
|
|
26
|
+
|
|
27
|
+
## Output Modes
|
|
28
|
+
|
|
29
|
+
| Mode | Server required | Client JS | Server Actions |
|
|
30
|
+
| -------------------------------------- | --------------- | ------------------------- | -------------- |
|
|
31
|
+
| `'server'` | Yes | Yes | Yes |
|
|
32
|
+
| `'static'` | No | Yes (hydration + SPA nav) | Via adapter |
|
|
33
|
+
| `'static'` + `clientJavascript: false` | No | None | Build error |
|
|
34
|
+
|
|
35
|
+
```ts title="timber.config.ts"
|
|
36
|
+
// Static site with no JavaScript
|
|
37
|
+
export default {
|
|
38
|
+
output: 'static',
|
|
39
|
+
clientJavascript: false,
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Build Time vs Request Time
|
|
44
|
+
|
|
45
|
+
The `output` field shifts work between two phases:
|
|
46
|
+
|
|
47
|
+
**Build time** happens once when you run `timber build`. There's no request, no user, no cookies. Route scanning, client boundary discovery, server action extraction, font downloading, and asset bundling all happen here.
|
|
48
|
+
|
|
49
|
+
**Request time** happens per incoming HTTP request. Middleware, access checks, server components, caching, cookies, and server actions all run here.
|
|
50
|
+
|
|
51
|
+
| `output` | `middleware.ts` | Server components | Server actions |
|
|
52
|
+
| ---------- | --------------------- | ------------------------------ | ------------------------------------------- |
|
|
53
|
+
| `'server'` | Request time | Request time | Request time |
|
|
54
|
+
| `'static'` | **Build time only** | **Build time** (once, to HTML) | **Request time**, split-deployed as endpoints |
|
|
55
|
+
|
|
56
|
+
This is why `cookies()` and `headers()` raise warnings during a static build — there's no request at the moment components render.
|
|
57
|
+
|
|
58
|
+
## Adapters
|
|
59
|
+
|
|
60
|
+
Adapters transform the build output for your deployment platform:
|
|
61
|
+
|
|
62
|
+
```ts title="timber.config.ts"
|
|
63
|
+
import { cloudflare } from '@timber-js/app/adapters/cloudflare';
|
|
64
|
+
|
|
65
|
+
export default {
|
|
66
|
+
output: 'server',
|
|
67
|
+
adapter: cloudflare(),
|
|
68
|
+
};
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```ts title="timber.config.ts"
|
|
72
|
+
import { nitro } from '@timber-js/app/adapters/nitro';
|
|
73
|
+
|
|
74
|
+
export default {
|
|
75
|
+
output: 'server',
|
|
76
|
+
adapter: nitro({ preset: 'node-server' }),
|
|
77
|
+
};
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`cloudflare()` for Cloudflare Workers. `nitro({ preset })` for everything else — Node.js, Vercel, Netlify, AWS Lambda, Deno, Bun, Azure.
|
|
81
|
+
|
|
82
|
+
## Cache Handler
|
|
83
|
+
|
|
84
|
+
```ts title="timber.config.ts"
|
|
85
|
+
import { MemoryCacheHandler } from '@timber-js/app/cache';
|
|
86
|
+
|
|
87
|
+
export default {
|
|
88
|
+
output: 'server',
|
|
89
|
+
cacheHandler: new MemoryCacheHandler(),
|
|
90
|
+
};
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The default is in-memory. See [Caching](/docs/caching) for Redis, KV, and custom handlers.
|
|
94
|
+
|
|
95
|
+
## All Options
|
|
96
|
+
|
|
97
|
+
| Option | Type | Default | Description |
|
|
98
|
+
| ------------------ | ----------------------- | ---------------------------- | ------------------------- |
|
|
99
|
+
| `output` | `'server' \| 'static'` | `'server'` | Output mode |
|
|
100
|
+
| `adapter` | `TimberPlatformAdapter` | — | Deployment adapter |
|
|
101
|
+
| `cacheHandler` | `CacheHandler` | `MemoryCacheHandler` | Cache backend |
|
|
102
|
+
| `clientJavascript` | `boolean \| object` | `true` | Control client-side JS |
|
|
103
|
+
| `pageExtensions` | `string[]` | `['tsx', 'ts', 'jsx', 'js']` | File extensions for pages |
|
|
104
|
+
| `mdx` | `object` | — | MDX remark/rehype plugins |
|
|
105
|
+
|
|
106
|
+
For the full type definition, see the [Config API Reference](/docs/api-config).
|
|
107
|
+
|
|
108
|
+
## .gitignore
|
|
109
|
+
|
|
110
|
+
Add these to your `.gitignore` — they're generated at build/dev time and should not be committed:
|
|
111
|
+
|
|
112
|
+
```txt title=".gitignore"
|
|
113
|
+
.timber
|
|
114
|
+
.content-collections
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`.timber` contains the build output (default `.timber/dist`). `.content-collections` contains generated modules from [content collections](/docs/content-collections). `create-timber-app` adds both automatically.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Deploying'
|
|
3
|
+
description: 'Deploy timber.js to Cloudflare Workers, Node.js, Vercel, Netlify, AWS Lambda, and more.'
|
|
4
|
+
slug: 'deploying'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Deploying
|
|
8
|
+
|
|
9
|
+
timber.js uses **adapters** to transform the build output for your platform. There are exactly two first-party adapters.
|
|
10
|
+
|
|
11
|
+
## The Two Adapters
|
|
12
|
+
|
|
13
|
+
**`cloudflare()`** — Cloudflare Workers and Pages. Workers are architecturally different from Node.js (no file system, no `node:http`, different lifecycle model), so they get a dedicated adapter with direct binding access and native `waitUntil()`.
|
|
14
|
+
|
|
15
|
+
**`nitro({ preset })`** — Everything else. Pick the preset that matches your platform:
|
|
16
|
+
|
|
17
|
+
| Preset | Platform | `waitUntil()` |
|
|
18
|
+
| ----------------- | ---------------------- | ------------- |
|
|
19
|
+
| `node-server` | Node.js, Docker, VPS | ✅ |
|
|
20
|
+
| `bun` | Bun.serve() | ✅ |
|
|
21
|
+
| `vercel` | Vercel Serverless | ✅ |
|
|
22
|
+
| `vercel-edge` | Vercel Edge Functions | ✅ |
|
|
23
|
+
| `netlify` | Netlify Functions | ❌ |
|
|
24
|
+
| `netlify-edge` | Netlify Edge Functions | ✅ |
|
|
25
|
+
| `aws-lambda` | AWS Lambda | ❌ |
|
|
26
|
+
| `deno-deploy` | Deno Deploy | ✅ |
|
|
27
|
+
| `azure-functions` | Azure Functions | ❌ |
|
|
28
|
+
|
|
29
|
+
## Build Commands
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
timber build # Production build
|
|
33
|
+
timber preview # Local preview of the build output
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Cloudflare Workers
|
|
37
|
+
|
|
38
|
+
```ts title="timber.config.ts"
|
|
39
|
+
import { cloudflare } from '@timber-js/app/adapters/cloudflare';
|
|
40
|
+
|
|
41
|
+
export default {
|
|
42
|
+
output: 'server',
|
|
43
|
+
adapter: cloudflare(),
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
timber build
|
|
49
|
+
wrangler deploy
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The adapter generates a `wrangler.jsonc` with sensible defaults. Access KV, D1, R2, and Queues bindings via `getCloudflareBindings()`:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { getCloudflareBindings } from '@timber-js/app/adapters/cloudflare';
|
|
56
|
+
|
|
57
|
+
export default async function Page() {
|
|
58
|
+
const { MY_KV, MY_DB } = getCloudflareBindings();
|
|
59
|
+
const data = await MY_KV.get('key');
|
|
60
|
+
// ...
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For caching, use `CloudflareKVCacheHandler`:
|
|
65
|
+
|
|
66
|
+
```ts title="timber.config.ts"
|
|
67
|
+
import { cloudflare } from '@timber-js/app/adapters/cloudflare';
|
|
68
|
+
import { CloudflareKVCacheHandler } from '@timber-js/app/adapters/cloudflare/cache';
|
|
69
|
+
|
|
70
|
+
export default {
|
|
71
|
+
output: 'server',
|
|
72
|
+
adapter: cloudflare(),
|
|
73
|
+
cacheHandler: new CloudflareKVCacheHandler({ bindingName: 'TIMBER_CACHE' }),
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Constraints:** no file system, 10ms/30s CPU time limits, 10MB/25MB bundle size.
|
|
78
|
+
|
|
79
|
+
## Node.js & Docker
|
|
80
|
+
|
|
81
|
+
```ts title="timber.config.ts"
|
|
82
|
+
import { nitro } from '@timber-js/app/adapters/nitro';
|
|
83
|
+
|
|
84
|
+
export default {
|
|
85
|
+
output: 'server',
|
|
86
|
+
adapter: nitro({ preset: 'node-server' }),
|
|
87
|
+
};
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
timber build
|
|
92
|
+
node .timber/dist/nitro/.output/server/index.mjs
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A dedicated server is timber's sweet spot: no cold starts, warm in-process cache, and the ability to co-locate with your database. Five sequential fetches that take 1000ms from serverless take 10ms co-located.
|
|
96
|
+
|
|
97
|
+
### Docker
|
|
98
|
+
|
|
99
|
+
```dockerfile
|
|
100
|
+
FROM node:24-slim AS build
|
|
101
|
+
RUN corepack enable
|
|
102
|
+
WORKDIR /app
|
|
103
|
+
COPY package.json pnpm-lock.yaml ./
|
|
104
|
+
RUN pnpm install --frozen-lockfile
|
|
105
|
+
COPY . .
|
|
106
|
+
RUN pnpm run build
|
|
107
|
+
|
|
108
|
+
FROM node:24-slim AS runtime
|
|
109
|
+
RUN corepack enable
|
|
110
|
+
RUN groupadd --system timber && useradd --system --gid timber --create-home timber
|
|
111
|
+
WORKDIR /app
|
|
112
|
+
COPY --from=build --chown=timber:timber /app/.timber/dist/nitro/.output ./.output
|
|
113
|
+
COPY --from=build --chown=timber:timber /app/package.json /app/pnpm-lock.yaml ./
|
|
114
|
+
RUN pnpm install --prod --frozen-lockfile
|
|
115
|
+
USER timber
|
|
116
|
+
ENV NODE_ENV=production PORT=3000 HOST=0.0.0.0
|
|
117
|
+
EXPOSE 3000
|
|
118
|
+
CMD ["node", ".output/server/index.mjs"]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
For multiple replicas, switch from `MemoryCacheHandler` to `RedisCacheHandler`.
|
|
122
|
+
|
|
123
|
+
## Vercel
|
|
124
|
+
|
|
125
|
+
```ts title="timber.config.ts"
|
|
126
|
+
import { nitro } from '@timber-js/app/adapters/nitro';
|
|
127
|
+
|
|
128
|
+
export default {
|
|
129
|
+
output: 'server',
|
|
130
|
+
adapter: nitro({ preset: 'vercel' }),
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
timber build
|
|
136
|
+
vercel deploy --prebuilt
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Or connect your git repository — Vercel deploys automatically on push. Use `vercel-edge` preset for Edge Functions. Serverless functions are ephemeral — always use an external cache handler.
|
|
140
|
+
|
|
141
|
+
## Netlify
|
|
142
|
+
|
|
143
|
+
```ts title="timber.config.ts"
|
|
144
|
+
import { nitro } from '@timber-js/app/adapters/nitro';
|
|
145
|
+
|
|
146
|
+
export default {
|
|
147
|
+
output: 'server',
|
|
148
|
+
adapter: nitro({ preset: 'netlify' }),
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
timber build
|
|
154
|
+
netlify deploy --prod
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Netlify Functions (Lambda-based) don't support `waitUntil()`. Use `netlify-edge` for Deno-based edge execution with `waitUntil()` support.
|
|
158
|
+
|
|
159
|
+
## AWS Lambda
|
|
160
|
+
|
|
161
|
+
```ts title="timber.config.ts"
|
|
162
|
+
import { nitro } from '@timber-js/app/adapters/nitro';
|
|
163
|
+
|
|
164
|
+
export default {
|
|
165
|
+
output: 'server',
|
|
166
|
+
adapter: nitro({ preset: 'aws-lambda' }),
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Deploy with CDK, SAM, Serverless Framework, or Terraform. Lambda doesn't support `waitUntil()` — use SQS or EventBridge for background work. Use provisioned concurrency to mitigate cold starts.
|
|
171
|
+
|
|
172
|
+
## Deno, Azure & Bun
|
|
173
|
+
|
|
174
|
+
```ts title="timber.config.ts"
|
|
175
|
+
import { nitro } from '@timber-js/app/adapters/nitro';
|
|
176
|
+
|
|
177
|
+
// Deno Deploy
|
|
178
|
+
export default { output: 'server', adapter: nitro({ preset: 'deno-deploy' }) };
|
|
179
|
+
|
|
180
|
+
// Azure Functions
|
|
181
|
+
export default { output: 'server', adapter: nitro({ preset: 'azure-functions' }) };
|
|
182
|
+
|
|
183
|
+
// Bun
|
|
184
|
+
export default { output: 'server', adapter: nitro({ preset: 'bun' }) };
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Static Export
|
|
188
|
+
|
|
189
|
+
For sites that don't need a server:
|
|
190
|
+
|
|
191
|
+
```ts title="timber.config.ts"
|
|
192
|
+
export default {
|
|
193
|
+
output: 'static',
|
|
194
|
+
};
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Pre-renders every route to HTML at build time. Add `clientJavascript: false` for zero-JavaScript output. Deploy to any static file host — S3, CloudFront, Cloudflare Pages, GitHub Pages, Netlify, or any HTTP server.
|
|
198
|
+
|
|
199
|
+
## Cache Handler Selection
|
|
200
|
+
|
|
201
|
+
| Deployment | Recommended Handler | Why |
|
|
202
|
+
| --------------------------- | -------------------- | --------------------------------------------- |
|
|
203
|
+
| Single server (VPS, Docker) | `MemoryCacheHandler` | In-process LRU, no external dependency |
|
|
204
|
+
| Multi-instance (k8s, ECS) | `RedisCacheHandler` | Shared store, tag invalidation across replicas |
|
|
205
|
+
| Cloudflare Workers | `KVCacheHandler` | Workers KV, global edge distribution |
|
|
206
|
+
| Serverless (Vercel, Lambda) | `RedisCacheHandler` | Ephemeral functions need an external store |
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 'Advanced Routing'
|
|
3
|
+
description: 'Route groups, parallel routes (slots), intercepting routes, catch-all segments, and API routes.'
|
|
4
|
+
slug: 'advanced-routing'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Advanced Routing
|
|
8
|
+
|
|
9
|
+
This page covers routing patterns beyond the basics in [Pages and Layouts](/docs/pages-and-layouts). It assumes you understand `page.tsx`, `layout.tsx`, and nesting.
|
|
10
|
+
|
|
11
|
+
## Dynamic Segments
|
|
12
|
+
|
|
13
|
+
Wrap a directory name in brackets to capture a URL parameter:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
app/
|
|
17
|
+
products/
|
|
18
|
+
[id]/
|
|
19
|
+
page.tsx # /products/42, /products/abc
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Access the param with `getSegmentParams(SEGMENT_PATH)` using the `$segment` module — see [Typed Params](/docs/typed-params) for the full codec system.
|
|
23
|
+
|
|
24
|
+
## Catch-All Segments
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
app/docs/[...slug]/page.tsx # /docs/a, /docs/a/b, /docs/a/b/c
|
|
28
|
+
app/docs/[[...slug]]/page.tsx # Same, plus /docs (zero segments)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
With optional catch-all segments (`[[...slug]]`), the param is `undefined` when no segments match — not an empty array:
|
|
32
|
+
|
|
33
|
+
| URL | `slug` value |
|
|
34
|
+
| ------------- | ----------------- |
|
|
35
|
+
| `/docs` | `undefined` |
|
|
36
|
+
| `/docs/intro` | `['intro']` |
|
|
37
|
+
| `/docs/a/b/c` | `['a', 'b', 'c']` |
|
|
38
|
+
|
|
39
|
+
### Not Supported: Optional Single Segments
|
|
40
|
+
|
|
41
|
+
timber does not support `[[foo]]` (optional single dynamic segment). Use `[[...foo]]` and check `slug?.[0]`, or create two separate pages:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
app/blog/page.tsx # /blog (index)
|
|
45
|
+
app/blog/[year]/page.tsx # /blog/2024 (filtered)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Parallel Routes (Slots)
|
|
49
|
+
|
|
50
|
+
Directories prefixed with `@` are slots — they render simultaneously within a parent layout:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
app/
|
|
54
|
+
dashboard/
|
|
55
|
+
layout.tsx # Receives { children, sidebar }
|
|
56
|
+
page.tsx # Renders as children
|
|
57
|
+
@sidebar/
|
|
58
|
+
page.tsx # Renders as sidebar prop
|
|
59
|
+
default.tsx # Fallback when no match
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```tsx title="app/dashboard/layout.tsx"
|
|
63
|
+
export default function DashboardLayout({
|
|
64
|
+
children,
|
|
65
|
+
sidebar,
|
|
66
|
+
}: {
|
|
67
|
+
children: React.ReactNode;
|
|
68
|
+
sidebar: React.ReactNode;
|
|
69
|
+
}) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex">
|
|
72
|
+
<aside className="w-64">{sidebar}</aside>
|
|
73
|
+
<main className="flex-1">{children}</main>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Slots don't add URL depth. Each slot independently matches the current URL and can have its own `access.ts`, `error.tsx`, and layouts. If a slot's access check fails, the slot degrades gracefully — the rest of the page is unaffected (see [Access Control](/docs/access-control)).
|
|
80
|
+
|
|
81
|
+
Each slot is also its own collision space: a slot page sharing a URL with the parent page is the point of parallel routes, but two route groups inside one slot producing pages at the same URL (`@side/(a)/x/page.tsx` + `@side/(b)/x/page.tsx`) fail the build, just like the same collision outside a slot.
|
|
82
|
+
|
|
83
|
+
## Intercepting Routes
|
|
84
|
+
|
|
85
|
+
Intercepting routes show a different component on soft navigation (client-side link click) vs hard navigation (direct URL). This enables the modal pattern:
|
|
86
|
+
|
|
87
|
+
| Marker | Meaning |
|
|
88
|
+
| ---------- | ------------- |
|
|
89
|
+
| `(.)` | Same level |
|
|
90
|
+
| `(..)` | One level up |
|
|
91
|
+
| `(..)(..)` | Two levels up |
|
|
92
|
+
| `(...)` | Root level |
|
|
93
|
+
|
|
94
|
+
## API Routes
|
|
95
|
+
|
|
96
|
+
Export HTTP method handlers from `route.ts`:
|
|
97
|
+
|
|
98
|
+
```ts title="app/api/hello/route.ts"
|
|
99
|
+
export async function GET(ctx: RouteContext) {
|
|
100
|
+
return Response.json({ message: 'Hello' });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function POST(ctx: RouteContext) {
|
|
104
|
+
const body = await ctx.request.json();
|
|
105
|
+
return Response.json({ received: body });
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## The Segment Tree
|
|
110
|
+
|
|
111
|
+
Every directory in your `app/` directory is a **segment**. Segments nest into a tree that connects your URL structure, component structure, and HTML structure:
|
|
112
|
+
|
|
113
|
+
| Concern | Segment mapping |
|
|
114
|
+
| ----------------- | ------------------------------------------------------------- |
|
|
115
|
+
| URL matching | Each segment consumes URL segments (except groups and slots) |
|
|
116
|
+
| Params | Dynamic segments extract params, accumulate top-down |
|
|
117
|
+
| Authorization | `access.ts` per segment, top-down, shallowest failure wins |
|
|
118
|
+
| Layouts | `layout.tsx` per segment, persist across navigation |
|
|
119
|
+
| Streaming | `<Suspense>` placement controls flush boundary per segment |
|
|
120
|
+
| Error handling | `error.tsx` / status files per segment, leaf-to-root fallback |
|
|
121
|
+
| Client navigation | Segment cache mirrors the tree, skips mounted sync layouts |
|
|
122
|
+
| Slot isolation | `@slot` branches render independently, fail gracefully |
|
|
123
|
+
|
|
124
|
+
## The Request Lifecycle
|
|
125
|
+
|
|
126
|
+
``` leading="none"
|
|
127
|
+
Request
|
|
128
|
+
├── URL canonicalization
|
|
129
|
+
├── proxy.ts (global — every request)
|
|
130
|
+
├── Route matching
|
|
131
|
+
├── Param coercion (schema.ts codecs)
|
|
132
|
+
├── middleware.ts chain (root → leaf)
|
|
133
|
+
└── Single renderToReadableStream call
|
|
134
|
+
Root Segment → AccessGate → Layout
|
|
135
|
+
Child Segment → AccessGate → Layout
|
|
136
|
+
Leaf Segment → Page
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The entire route renders in a **single `renderToReadableStream` call**. One React tree, one `React.cache` scope.
|