@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.
Files changed (42) hide show
  1. package/agent-skill.md +344 -0
  2. package/dist/cli.d.ts +9 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +41 -4
  5. package/dist/cli.js.map +1 -1
  6. package/docs/api/30-api-server.mdx +233 -0
  7. package/docs/api/31-api-client.mdx +161 -0
  8. package/docs/api/32-api-cache.mdx +193 -0
  9. package/docs/api/33-api-search-params.mdx +141 -0
  10. package/docs/api/34-api-config.mdx +189 -0
  11. package/docs/api/35-api-typescript.mdx +170 -0
  12. package/docs/api/36-cli.mdx +143 -0
  13. package/docs/learn/00-introduction.mdx +81 -0
  14. package/docs/learn/01-quick-start.mdx +94 -0
  15. package/docs/learn/02-pages-and-layouts.mdx +138 -0
  16. package/docs/learn/03-fetching-data.mdx +211 -0
  17. package/docs/learn/03b-access-control.mdx +130 -0
  18. package/docs/learn/04-loading-states.mdx +67 -0
  19. package/docs/learn/04b-the-flush-point.mdx +115 -0
  20. package/docs/learn/05-typed-params.mdx +275 -0
  21. package/docs/learn/06-forms-and-actions.mdx +150 -0
  22. package/docs/learn/08-streaming.mdx +146 -0
  23. package/docs/learn/09-caching.mdx +121 -0
  24. package/docs/learn/10-middleware.mdx +156 -0
  25. package/docs/learn/11-error-handling.mdx +147 -0
  26. package/docs/learn/12-client-navigation.mdx +168 -0
  27. package/docs/learn/13-configuration.mdx +117 -0
  28. package/docs/learn/14-deploying.mdx +206 -0
  29. package/docs/more/01-advanced-routing.mdx +139 -0
  30. package/docs/more/02-advanced-forms.mdx +137 -0
  31. package/docs/more/03-coming-from-nextjs.mdx +186 -0
  32. package/docs/more/04-metadata-and-fonts.mdx +193 -0
  33. package/docs/more/04b-mdx.mdx +209 -0
  34. package/docs/more/05-content-collections.mdx +90 -0
  35. package/docs/more/06-instrumentation.mdx +188 -0
  36. package/docs/more/07-security.mdx +123 -0
  37. package/docs/more/40-why-timber.mdx +50 -0
  38. package/docs/more/41-timber-vs-nextjs.mdx +81 -0
  39. package/docs/more/42-timber-vs-others.mdx +68 -0
  40. package/llms.txt +55 -0
  41. package/package.json +6 -2
  42. package/src/cli.ts +62 -3
@@ -0,0 +1,137 @@
1
+ ---
2
+ title: 'Advanced Forms'
3
+ description: 'Coercion helpers, parseFormData, form flash internals, and sensitive field stripping.'
4
+ slug: 'advanced-forms'
5
+ ---
6
+
7
+ # Advanced Forms
8
+
9
+ This page covers advanced form patterns beyond [Forms and Actions](/docs/forms-and-actions): coercion helpers, raw FormData parsing, form flash internals, and sensitive field stripping.
10
+
11
+ ## Coercing FormData Values
12
+
13
+ HTML forms send everything as strings. Use `coerce` helpers to convert FormData values before schema validation:
14
+
15
+ ```ts
16
+ import { coerce } from '@timber-js/app/server';
17
+ import { z } from 'zod/v4';
18
+
19
+ const eventSchema = z.object({
20
+ title: z.string().min(1),
21
+ maxAttendees: z.preprocess(coerce.number, z.number().min(1)),
22
+ isPublic: z.preprocess(coerce.checkbox, z.boolean()),
23
+ date: z.preprocess(coerce.date, z.date()),
24
+ });
25
+ ```
26
+
27
+ | Helper | Input | Output |
28
+ | ----------------- | ------------------- | ------------------- |
29
+ | `coerce.number` | `"42"`, `""` | `42`, `undefined` |
30
+ | `coerce.checkbox` | `"on"`, `undefined` | `true`, `false` |
31
+ | `coerce.json` | `'{"a":1}'` | `{ a: 1 }` |
32
+ | `coerce.date` | `"2024-01-15"` | `Date` |
33
+ | `coerce.file` | `File`, empty input | `File`, `undefined` |
34
+
35
+ `coerce.file` accepts options for validation:
36
+
37
+ ```ts
38
+ z.preprocess(
39
+ coerce.file({ maxSize: 5 * 1024 * 1024, accept: ['image/png', 'image/jpeg'] }),
40
+ z.instanceof(File)
41
+ );
42
+ ```
43
+
44
+ ## `parseFormData`
45
+
46
+ For advanced cases, `parseFormData` converts raw `FormData` into a plain object before schema validation:
47
+
48
+ ```ts
49
+ import { parseFormData } from '@timber-js/app/server';
50
+
51
+ const obj = parseFormData(formData);
52
+ ```
53
+
54
+ It handles common patterns:
55
+
56
+ - **Duplicate keys → arrays**: `tags=js&tags=ts` → `{ tags: ["js", "ts"] }`
57
+ - **Dot-path nesting**: `user.name=Alice` → `{ user: { name: "Alice" } }`
58
+ - **Empty strings → undefined**: Enables `.optional()` semantics
59
+ - **Empty file inputs → undefined**: File inputs with no selection become `undefined`
60
+
61
+ `createActionClient` calls `parseFormData` automatically — you only need it for manual FormData handling.
62
+
63
+ ## Form Flash Internals
64
+
65
+ When JavaScript is disabled and a server action returns validation errors, timber re-renders the page with flash data available via `getFormFlash()`:
66
+
67
+ ```ts
68
+ interface FormFlashData {
69
+ validationErrors: Record<string, string[]>;
70
+ submittedValues: Record<string, unknown>;
71
+ serverError?: { code: string; data?: Record<string, unknown> };
72
+ }
73
+ ```
74
+
75
+ - `validationErrors` — field-level errors keyed by field name
76
+ - `submittedValues` — raw input for repopulating form fields (File objects excluded, sensitive fields stripped)
77
+ - `serverError` — present if an `ActionError` was thrown
78
+
79
+ Flash data is server-side only — stored in AsyncLocalStorage for the duration of the re-render. Never serialized to cookies or headers.
80
+
81
+ ## Sensitive Field Stripping
82
+
83
+ On validation failure timber echoes submitted form values back so users don't re-type everything. To prevent plaintext passwords or credit-card numbers from leaking through the RSC payload or into HTML `defaultValue` attributes, timber automatically strips a deny-list of sensitive field names.
84
+
85
+ **Built-in deny-list** (case-insensitive, nested objects walked recursively):
86
+
87
+ - `password`, `passwd`, `pwd` (and variants like `currentPassword`, `newPassword`)
88
+ - `secret`, `apiKey`, `accessToken`, `refreshToken`, `privateKey`
89
+ - `cvv`, `cvc`, `cardNumber`, `cardCvc`
90
+ - `ssn`, `socialSecurityNumber`
91
+ - `otp`, `totp`, `mfaCode`, `twoFactorCode`
92
+ - `token` (exact match only — `csrfToken` is preserved)
93
+
94
+ Stripped fields are omitted from `submittedValues` entirely — not replaced with empty strings.
95
+
96
+ ### Global Configuration
97
+
98
+ ```ts title="timber.config.ts"
99
+ import { defineConfig } from '@timber-js/app';
100
+
101
+ export default defineConfig({
102
+ forms: {
103
+ // true / omitted — use built-in deny-list (default)
104
+ // false — disable all stripping (NEVER in prod)
105
+ // string[] — extra field names merged with built-in list
106
+ stripSensitiveFields: ['internalCode', 'bankAccount'],
107
+ },
108
+ });
109
+ ```
110
+
111
+ ### Per-Action Override
112
+
113
+ ```ts
114
+ import { createActionClient } from '@timber-js/app/server';
115
+
116
+ const action = createActionClient({
117
+ stripSensitiveFields: (name) => name.startsWith('secret_'),
118
+ });
119
+ ```
120
+
121
+ Per-action config takes precedence over global config, which takes precedence over the built-in default.
122
+
123
+ ## Shared Schemas for Client + Server Validation
124
+
125
+ Define Zod schemas in shared files and import them on both sides for instant client-side validation while keeping the server as the source of truth:
126
+
127
+ ```ts title="lib/schemas/contact.ts"
128
+ import { z } from 'zod/v4';
129
+
130
+ export const contactSchema = z.object({
131
+ name: z.string().min(1, 'Name is required').max(100),
132
+ email: z.string().email('Invalid email address'),
133
+ message: z.string().min(10).max(2000),
134
+ });
135
+ ```
136
+
137
+ Use in the server action via `.schema(contactSchema)` and in the client form for `onBlur` validation. Client validation is a UX enhancement — always validate on the server.
@@ -0,0 +1,186 @@
1
+ ---
2
+ title: 'Coming from Next.js'
3
+ description: 'A practical mapping for Next.js developers — in Next.js you did X, in timber you do Y.'
4
+ slug: 'coming-from-nextjs'
5
+ ---
6
+
7
+ # Coming from Next.js
8
+
9
+ This page maps Next.js patterns to timber equivalents. It's not a comparison (see [timber vs Next.js](/docs/timber-vs-nextjs) for that) — it's a migration mental model.
10
+
11
+ ## Data Fetching
12
+
13
+ **Next.js:** `getServerSideProps`, `getStaticProps`, or `fetch()` with implicit caching.
14
+
15
+ **timber:** Async server components. No loaders, no special functions. `fetch()` is never patched or cached — use `timber.cache()` explicitly when you want caching.
16
+
17
+ ```tsx title="app/products/page.tsx"
18
+ // timber — just an async component
19
+ export default async function ProductsPage() {
20
+ const products = await db.products.findMany();
21
+ return <ProductList products={products} />;
22
+ }
23
+ ```
24
+
25
+ ## `loading.tsx`
26
+
27
+ **Next.js:** `loading.tsx` convention inserts a Suspense boundary automatically.
28
+
29
+ **timber:** No `loading.tsx`. Place `<Suspense>` explicitly where you want it. This keeps you in control of what streams and what blocks the HTTP status code.
30
+
31
+ ```tsx
32
+ // timber — explicit Suspense
33
+ import { Suspense } from 'react';
34
+
35
+ export default async function Page() {
36
+ const product = await getProduct(id); // blocks, can affect status code
37
+ return (
38
+ <div>
39
+ <h1>{product.name}</h1>
40
+ <Suspense fallback={<Skeleton />}>
41
+ <Reviews productId={id} /> {/* streams in */}
42
+ </Suspense>
43
+ </div>
44
+ );
45
+ }
46
+ ```
47
+
48
+ ## Middleware
49
+
50
+ **Next.js:** Single global `middleware.ts` at the project root. Matcher patterns select routes.
51
+
52
+ **timber:** Per-segment `middleware.ts` files. Place them at the route level that matches the concern. `proxy.ts` for global concerns (CORS, rate limiting, security headers).
53
+
54
+ ```
55
+ app/
56
+ proxy.ts # Global — every request (like Next.js middleware but with next())
57
+ dashboard/
58
+ middleware.ts # Only runs for /dashboard/*
59
+ settings/
60
+ middleware.ts # Only runs for /dashboard/settings/*
61
+ ```
62
+
63
+ ## `useSearchParams`
64
+
65
+ **Next.js:** `useSearchParams()` hook returns a `URLSearchParams` object.
66
+
67
+ **timber:** Typed search params via `defineSearchParams` in a `params.ts` file. Server reads via `.get()`, client reads/writes via `.useQueryStates()`. Segment params are defined globally in `app/schema.ts` via `defineSchema()`, accessed with `getSegmentParams(SEGMENT_PATH)` using the `$segment` module.
68
+
69
+ ```ts title="app/products/params.ts"
70
+ import { defineSearchParams } from '@timber-js/app/search-params';
71
+ import { z } from 'zod/v4';
72
+
73
+ export const searchParams = defineSearchParams({
74
+ page: z.coerce.number().default(1),
75
+ sort: z.enum(['price', 'name']).default('name'),
76
+ });
77
+ ```
78
+
79
+ ## Caching
80
+
81
+ **Next.js:** Implicit fetch caching, `unstable_cache`, ISR with `revalidate`.
82
+
83
+ **timber:** No implicit caching. No ISR. Explicit `timber.cache()` with TTL and tags:
84
+
85
+ ```ts
86
+ import { cache } from '@timber-js/app/cache';
87
+
88
+ declare const db: { products: { findPopular(): Promise<{ id: string; name: string }[]> } };
89
+
90
+ const getProducts = cache(
91
+ async () => db.products.findPopular(),
92
+ { ttl: 300, tags: ['products'] }
93
+ );
94
+ ```
95
+
96
+ ## Authorization
97
+
98
+ **Next.js:** Usually done in middleware or layout components. No built-in access gate pattern.
99
+
100
+ **timber:** `access.ts` per segment. Runs inside the React tree with `React.cache` sharing. Supports slot degradation (a denied slot renders `denied.tsx` instead of failing the whole page).
101
+
102
+ ```ts title="app/(auth)/access.ts"
103
+ import { redirect, getCookieJar } from '@timber-js/app/server';
104
+
105
+ declare function getSessionFromCookie(jar: ReturnType<typeof getCookieJar>): { userId: string } | null;
106
+
107
+ export default async function access() {
108
+ const session = getSessionFromCookie(getCookieJar());
109
+ if (!session) redirect('/login');
110
+ }
111
+ ```
112
+
113
+ ## Server Actions
114
+
115
+ **Next.js:** Server actions with `'use server'`. `useFormState` / `useFormStatus`.
116
+
117
+ **timber:** `createActionClient` for reusable middleware (auth, validation). `useActionState` from `@timber-js/app/client` returns a 4-tuple with auto-derived errors. Forms work without JavaScript via form flash.
118
+
119
+ ```ts title="lib/action.ts"
120
+ import { createActionClient, ActionError } from '@timber-js/app/server';
121
+ import { getUser } from '@/lib/auth';
122
+
123
+ export const action = createActionClient({
124
+ middleware: async () => {
125
+ const user = await getUser();
126
+ if (!user) throw new ActionError('UNAUTHORIZED');
127
+ return { user };
128
+ },
129
+ });
130
+ ```
131
+
132
+ ## `notFound()`
133
+
134
+ **Next.js:** `notFound()` function.
135
+
136
+ **timber:** `deny(404)` — sends a real HTTP 404 status code:
137
+
138
+ ```tsx
139
+ import { deny } from '@timber-js/app/server';
140
+
141
+ if (!product) deny(404);
142
+ ```
143
+
144
+ ## `redirect()`
145
+
146
+ **Next.js:** `redirect()` from `next/navigation`.
147
+
148
+ **timber:** `redirect()` from `@timber-js/app/server`. Same concept, different import:
149
+
150
+ ```ts
151
+ import { redirect } from '@timber-js/app/server';
152
+
153
+ redirect('/login'); // 302 by default
154
+ redirect('/login', 301); // Permanent redirect
155
+ ```
156
+
157
+ Code that imports `redirect()` or `notFound()` from `next/navigation` works unmodified — including calls during client component render, which timber converts into an SPA navigation (`redirect`) or your nearest 404 boundary (`notFound`), matching Next.js behavior.
158
+
159
+ ## Metadata
160
+
161
+ **Next.js:** `export const metadata` or `export async function generateMetadata()`.
162
+
163
+ **timber:** Same pattern — export `metadata` (static) or a `metadata()` function (dynamic) from any page or layout.
164
+
165
+ ## Imports
166
+
167
+ | Next.js import | timber import |
168
+ | ------------------------- | ------------------------------------------ |
169
+ | `next/link` | `@timber-js/app/client` (`Link`) |
170
+ | `next/navigation` | `@timber-js/app/client` (`useRouter`, etc) |
171
+ | `next/headers` | `@timber-js/app/server` (`getHeaders`) |
172
+ | `next/server` | `@timber-js/app/server` |
173
+ | `unstable_cache` | `@timber-js/app/cache` (`cache`) |
174
+
175
+ ## Key Differences Summary
176
+
177
+ | Concept | Next.js | timber |
178
+ | -------------------- | -------------------------------- | ------------------------------------- |
179
+ | Fetch caching | Implicit, patched `fetch` | Explicit `timber.cache()` |
180
+ | ISR | Built-in | Not supported (use cache + TTL) |
181
+ | `loading.tsx` | Convention | Use explicit `<Suspense>` |
182
+ | Middleware | Single global file | Per-segment + global `proxy.ts` |
183
+ | Status codes | Often 200 for errors | Real HTTP status codes always |
184
+ | Auth | In middleware or components | `access.ts` with slot degradation |
185
+ | Search params | Untyped `URLSearchParams` | Typed codecs with validation |
186
+ | Forms without JS | Limited support | Full form flash support |
@@ -0,0 +1,193 @@
1
+ ---
2
+ title: 'Metadata and Fonts'
3
+ description: 'Page titles, Open Graph, metadata routes, Google Fonts, and local fonts.'
4
+ slug: 'metadata-and-fonts'
5
+ ---
6
+
7
+ # Metadata and Fonts
8
+
9
+ ## Metadata
10
+
11
+ Export `metadata` from any page or layout to control `<head>` tags.
12
+
13
+ ### Static Metadata
14
+
15
+ ```tsx title="app/page.tsx"
16
+ import type { Metadata } from '@timber-js/app/server';
17
+
18
+ export const metadata: Metadata = {
19
+ title: 'Dashboard',
20
+ description: 'Your project dashboard',
21
+ };
22
+ ```
23
+
24
+ ### Dynamic Metadata
25
+
26
+ Export `metadata` as an async function to compute values at request time:
27
+
28
+ ```tsx title="app/products/[id]/page.tsx"
29
+ import type { Metadata } from '@timber-js/app/server';
30
+ import { SEGMENT_PATH } from './$segment';
31
+ import { getSegmentParams } from '@timber-js/app/server';
32
+
33
+ declare function getProduct(id: string): Promise<{ name: string; summary: string; imageUrl: string }>;
34
+
35
+ export async function metadata(): Promise<Metadata> {
36
+ const { id } = getSegmentParams(SEGMENT_PATH);
37
+ const product = await getProduct(id);
38
+ return {
39
+ title: product.name,
40
+ description: product.summary,
41
+ openGraph: { images: [product.imageUrl] },
42
+ };
43
+ }
44
+ ```
45
+
46
+ Dynamic `metadata` runs during the render pass — `React.cache` is active, so data fetched here and in the page component is deduplicated.
47
+
48
+ ### Title Templates
49
+
50
+ Layouts can define a title template. Pages fill in the `%s` placeholder:
51
+
52
+ ```tsx title="app/layout.tsx"
53
+ export const metadata: Metadata = {
54
+ title: { template: '%s | My App', default: 'My App' },
55
+ };
56
+ ```
57
+
58
+ ### Open Graph & Twitter Cards
59
+
60
+ ```tsx
61
+ export const metadata: Metadata = {
62
+ openGraph: {
63
+ title: 'My Product',
64
+ description: 'The best product ever.',
65
+ images: [{ url: '/og-image.png', width: 1200, height: 630 }],
66
+ },
67
+ twitter: {
68
+ card: 'summary_large_image',
69
+ title: 'My Product',
70
+ images: ['/og-image.png'],
71
+ },
72
+ };
73
+ ```
74
+
75
+ ### Metadata Routes
76
+
77
+ Special files in your route tree generate metadata endpoints:
78
+
79
+ | File | Output |
80
+ | --------------------- | ------------------------- |
81
+ | `sitemap.ts` | `/sitemap.xml` |
82
+ | `robots.ts` | `/robots.txt` |
83
+ | `manifest.ts` | `/manifest.json` |
84
+ | `opengraph-image.tsx` | `/opengraph-image` + auto `og:image` and `twitter:image` meta tags |
85
+ | `favicon.ico` / `favicon.tsx` | `/favicon.ico` |
86
+ | `icon.tsx` | `/icon` + auto `<link rel="icon">` |
87
+
88
+ When `opengraph-image.tsx` exists in a segment, timber automatically emits both `<meta property="og:image">` and `<meta name="twitter:image">` in `<head>` — no need to declare them in your `metadata` export. The auto-linked URL includes a cache-busting hash so social platforms pick up changes on redeploy.
89
+
90
+ If you declare `openGraph.images` in your metadata, the auto-linked tags are suppressed (user-declared metadata wins).
91
+
92
+ `favicon.tsx` lets you generate favicons dynamically (e.g. tenant branding). The URL stays `/favicon.ico` for browser compatibility.
93
+
94
+ For OG image generation, use `@takumi-rs/image-response` (BYO package — not bundled with timber):
95
+
96
+ ```tsx title="app/opengraph-image.tsx"
97
+ import { ImageResponse } from 'takumi-js/response';
98
+
99
+ export default async function OpenGraphImage() {
100
+ return new ImageResponse(
101
+ <div style={{ fontSize: 48, background: 'white', width: '100%', height: '100%',
102
+ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
103
+ My App
104
+ </div>,
105
+ { width: 1200, height: 630 }
106
+ );
107
+ }
108
+ ```
109
+
110
+ Metadata is always complete before the flush — no client-side injection, no partial `<head>`.
111
+
112
+ ---
113
+
114
+ ## Fonts
115
+
116
+ timber.js downloads, subsets, and self-hosts fonts at build time. No requests to Google at runtime. No layout shift.
117
+
118
+ ### Google Fonts
119
+
120
+ ```tsx title="app/layout.tsx"
121
+ import { Inter, JetBrains_Mono } from '@timber-js/app/fonts/google';
122
+
123
+ const inter = Inter({
124
+ subsets: ['latin'],
125
+ display: 'swap',
126
+ variable: '--font-sans',
127
+ });
128
+
129
+ const jetbrainsMono = JetBrains_Mono({
130
+ subsets: ['latin'],
131
+ display: 'swap',
132
+ variable: '--font-mono',
133
+ });
134
+
135
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
136
+ return (
137
+ <html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}>
138
+ <body className={inter.className}>{children}</body>
139
+ </html>
140
+ );
141
+ }
142
+ ```
143
+
144
+ ### Local Fonts
145
+
146
+ ```tsx
147
+ import localFont from '@timber-js/app/fonts/local';
148
+
149
+ const myFont = localFont({
150
+ src: [
151
+ { path: './fonts/MyFont-Regular.woff2', weight: '400' },
152
+ { path: './fonts/MyFont-Bold.woff2', weight: '700' },
153
+ ],
154
+ display: 'swap',
155
+ variable: '--font-custom',
156
+ });
157
+ ```
158
+
159
+ ### Static Analysis Constraints
160
+
161
+ Font calls look like runtime functions but are a build-time transform, so they must be statically analyzable. The canonical shape is a module-scope `const` (or `export const`) with a literal config object:
162
+
163
+ ```tsx
164
+ const inter = Inter({ subsets: ['latin'] }); // ✅
165
+ export const mono = JetBrains_Mono({ subsets: ['latin'] }); // ✅
166
+ ```
167
+
168
+ Anything else fails the build with a descriptive error instead of silently breaking at runtime:
169
+
170
+ ```tsx
171
+ export default Inter({ subsets: ['latin'] }); // ❌ build error
172
+ const theme = { font: Inter({ subsets: ['latin'] }) }; // ❌ build error
173
+ const cls = Inter({ subsets: ['latin'] }).className; // ❌ build error
174
+ import * as fonts from 'next/font/google'; // ❌ build error
175
+ ```
176
+
177
+ ### CSS Variables
178
+
179
+ Use the `variable` option and reference it in your styles:
180
+
181
+ ```css title="globals.css"
182
+ body { font-family: var(--font-sans), system-ui, sans-serif; }
183
+ code { font-family: var(--font-mono); }
184
+ ```
185
+
186
+ ### Next.js Compatibility
187
+
188
+ Imports from `next/font/google` and `next/font/local` are shimmed — existing code works without changes:
189
+
190
+ ```tsx
191
+ import { Inter } from 'next/font/google';
192
+ import localFont from 'next/font/local';
193
+ ```