@timber-js/app 0.2.0-alpha.154 → 0.2.0-alpha.156

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 (70) hide show
  1. package/agent-skill.md +344 -0
  2. package/dist/_chunks/{build-output-helper-B20ZuTSA.js → build-output-helper-Rp9r7gSA.js} +62 -9
  3. package/dist/_chunks/build-output-helper-Rp9r7gSA.js.map +1 -0
  4. package/dist/adapters/build-output-helper.d.ts +1 -0
  5. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  6. package/dist/adapters/cloudflare.d.ts.map +1 -1
  7. package/dist/adapters/cloudflare.js +35 -5
  8. package/dist/adapters/cloudflare.js.map +1 -1
  9. package/dist/adapters/nitro.js +1 -1
  10. package/dist/adapters/shared.d.ts +1 -11
  11. package/dist/adapters/shared.d.ts.map +1 -1
  12. package/dist/cli.d.ts +9 -1
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +41 -4
  15. package/dist/cli.js.map +1 -1
  16. package/dist/fonts/google.d.ts.map +1 -1
  17. package/dist/index.js +43 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugins/fonts.d.ts.map +1 -1
  20. package/dist/server/asset-headers.d.ts +7 -3
  21. package/dist/server/asset-headers.d.ts.map +1 -1
  22. package/dist/server/internal.js +12 -6
  23. package/dist/server/internal.js.map +1 -1
  24. package/dist/server/pipeline-phases.d.ts.map +1 -1
  25. package/docs/api/30-api-server.mdx +233 -0
  26. package/docs/api/31-api-client.mdx +161 -0
  27. package/docs/api/32-api-cache.mdx +193 -0
  28. package/docs/api/33-api-search-params.mdx +141 -0
  29. package/docs/api/34-api-config.mdx +189 -0
  30. package/docs/api/35-api-typescript.mdx +170 -0
  31. package/docs/api/36-cli.mdx +143 -0
  32. package/docs/learn/00-introduction.mdx +81 -0
  33. package/docs/learn/01-quick-start.mdx +94 -0
  34. package/docs/learn/02-pages-and-layouts.mdx +138 -0
  35. package/docs/learn/03-fetching-data.mdx +211 -0
  36. package/docs/learn/03b-access-control.mdx +130 -0
  37. package/docs/learn/04-loading-states.mdx +67 -0
  38. package/docs/learn/04b-the-flush-point.mdx +115 -0
  39. package/docs/learn/05-typed-params.mdx +275 -0
  40. package/docs/learn/06-forms-and-actions.mdx +150 -0
  41. package/docs/learn/08-streaming.mdx +146 -0
  42. package/docs/learn/09-caching.mdx +121 -0
  43. package/docs/learn/10-middleware.mdx +156 -0
  44. package/docs/learn/11-error-handling.mdx +147 -0
  45. package/docs/learn/12-client-navigation.mdx +168 -0
  46. package/docs/learn/13-configuration.mdx +117 -0
  47. package/docs/learn/14-deploying.mdx +206 -0
  48. package/docs/more/01-advanced-routing.mdx +139 -0
  49. package/docs/more/02-advanced-forms.mdx +137 -0
  50. package/docs/more/03-coming-from-nextjs.mdx +186 -0
  51. package/docs/more/04-metadata-and-fonts.mdx +193 -0
  52. package/docs/more/04b-mdx.mdx +209 -0
  53. package/docs/more/05-content-collections.mdx +90 -0
  54. package/docs/more/06-instrumentation.mdx +188 -0
  55. package/docs/more/07-security.mdx +123 -0
  56. package/docs/more/40-why-timber.mdx +50 -0
  57. package/docs/more/41-timber-vs-nextjs.mdx +81 -0
  58. package/docs/more/42-timber-vs-others.mdx +68 -0
  59. package/docs/more/50-ai-agent-instructions.mdx +169 -0
  60. package/llms.txt +55 -0
  61. package/package.json +8 -4
  62. package/src/adapters/build-output-helper.ts +52 -2
  63. package/src/adapters/cloudflare.ts +34 -4
  64. package/src/adapters/shared.ts +34 -6
  65. package/src/cli.ts +62 -3
  66. package/src/fonts/google.ts +56 -5
  67. package/src/plugins/fonts.ts +6 -0
  68. package/src/server/asset-headers.ts +7 -3
  69. package/src/server/pipeline-phases.ts +10 -2
  70. package/dist/_chunks/build-output-helper-B20ZuTSA.js.map +0 -1
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: 'timber.js vs Next.js'
3
+ description: 'An honest comparison of timber.js and Next.js — where they differ and why.'
4
+ slug: 'timber-vs-nextjs'
5
+ ---
6
+
7
+ # timber.js vs Next.js
8
+
9
+ Next.js is the most widely used React framework. It's mature, well-documented, and backed by Vercel. timber.js would not exist without it — the app directory routing model, server components, and many API patterns are directly inspired by Next.js.
10
+
11
+ That said, we made different design decisions in a few areas. Here's an honest comparison.
12
+
13
+ ## HTTP Semantics
14
+
15
+ | Behavior | Next.js | timber.js |
16
+ | ------------------ | ------------------------------------------------ | ------------------------------------------------ |
17
+ | Status codes | Always 200 (streaming commits early) | Real status codes (flush held until shell ready) |
18
+ | 404 pages | 200 + `not-found.tsx` error boundary | HTTP 404 + `404.tsx` |
19
+ | Redirects in pages | Client-side via `redirect()` after 200 | HTTP 302/301 before any bytes sent |
20
+ | Works without JS | Partially (primary content yes, interactions no) | Fully (forms, navigation, status codes all work) |
21
+
22
+ This is the core philosophical difference. Next.js optimizes for earliest possible TTFB. timber.js optimizes for correct HTTP responses, at the cost of ~20ms additional server buffering.
23
+
24
+ ## Streaming
25
+
26
+ Both frameworks support React Suspense streaming. The difference is when it starts:
27
+
28
+ - **Next.js:** Streams immediately. `loading.tsx` renders while data loads. Status code is already committed.
29
+ - **timber.js:** Holds until the shell is ready (everything outside `<Suspense>`). Then streams Suspense content. No `loading.tsx` — `<Suspense>` is opt-in only.
30
+
31
+ ## Routing
32
+
33
+ Both use file-system routing in an `app/` directory. The models are similar:
34
+
35
+ - Layouts, pages, dynamic segments, catch-all routes — same patterns
36
+ - Parallel routes (slots) — both support `@sidebar`, `@modal` patterns
37
+ - Route groups — both support `(group)` directories
38
+
39
+ **Differences:**
40
+
41
+ - **Middleware:** Next.js has a single global `middleware.ts`. timber.js has per-segment `middleware.ts` files.
42
+ - **Authorization:** timber.js has `access.ts` files for per-segment access control with `deny()`. Next.js uses middleware or in-component checks.
43
+ - **Typed routes:** timber.js generates route types at build time — `<Link>` type-checks `href` and params.
44
+
45
+ ## Caching
46
+
47
+ | Behavior | Next.js | timber.js |
48
+ | ----------------- | ---------------------------------------------------- | ----------------------------------------------------- |
49
+ | `fetch()` caching | Cached by default (opt out with `cache: 'no-store'`) | Never cached (explicit is better) |
50
+ | Cache API | `unstable_cache` / `'use cache'` | `timber.cache()` with TTL, tags, staleWhileRevalidate |
51
+ | Revalidation | `revalidateTag()`, `revalidatePath()` | `revalidateTag()` |
52
+
53
+ timber.js takes the position that implicit caching causes more bugs than it prevents. Nothing is cached unless you wrap it in `timber.cache()`.
54
+
55
+ ## Build System
56
+
57
+ | | Next.js | timber.js |
58
+ | ---------------- | ------------------- | ------------------------------------- |
59
+ | Bundler | Turbopack / Webpack | Vite 7 (Rolldown) |
60
+ | Config | `next.config.js` | `vite.config.ts` + `timber.config.ts` |
61
+ | Plugin ecosystem | Next.js-specific | Full Vite plugin ecosystem |
62
+
63
+ ## Platform Support
64
+
65
+ Next.js is optimized for Vercel and works on other platforms through community adapters. timber.js ships a first-party Cloudflare Workers adapter and a Nitro adapter that covers Node.js, Vercel, Bun, Netlify, AWS Lambda, and more.
66
+
67
+ ## When to Use Next.js
68
+
69
+ - You need the largest ecosystem and community support
70
+ - You're deploying to Vercel
71
+ - You need features timber.js hasn't built yet (image optimization, ISR, etc.)
72
+ - Your team is already productive with Next.js
73
+
74
+ ## When to Use timber.js
75
+
76
+ - You care about correct HTTP status codes and headers
77
+ - You want pages that work fully without JavaScript
78
+ - You're deploying to Cloudflare Workers
79
+ - You prefer explicit caching over implicit
80
+ - You want per-segment middleware and authorization
81
+ - You want Vite's build speed and plugin ecosystem
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: 'timber.js vs Other Frameworks'
3
+ description: 'How timber.js compares to Vinext, Remix, and other React server frameworks.'
4
+ slug: 'timber-vs-others'
5
+ ---
6
+
7
+ # timber.js vs Other Frameworks
8
+
9
+ Beyond Next.js, there are several React frameworks worth comparing against. Each makes different trade-offs.
10
+
11
+ ## Vinext (Cloudflare)
12
+
13
+ Vinext is Cloudflare's implementation of Next.js on Vite. It aims for Next.js API compatibility running natively on Cloudflare Workers.
14
+
15
+ **Shared ground:** Both timber.js and Vinext target Cloudflare Workers, use Vite, and support React Server Components.
16
+
17
+ **Key differences:**
18
+
19
+ | | Vinext | timber.js |
20
+ | ------------ | -------------------------------------- | ---------------------------------------- |
21
+ | Goal | Next.js compatibility on Vite | Independent design, different trade-offs |
22
+ | Streaming | Streams immediately (Next.js behavior) | Holds until shell ready |
23
+ | Status codes | 200 for everything (Next.js behavior) | Real HTTP status codes |
24
+ | API surface | Next.js-compatible | Similar but divergent where we disagree |
25
+ | Caching | Next.js caching model | Explicit-only caching |
26
+
27
+ If you want Next.js on Cloudflare with minimal code changes, Vinext is the right choice. If you want a different rendering model with correct HTTP semantics, that's timber.js.
28
+
29
+ ## Remix / React Router
30
+
31
+ Remix (now React Router v7) pioneered many of the ideas timber.js agrees with: progressive enhancement, forms that work without JavaScript, and server-first rendering.
32
+
33
+ **Shared philosophy:**
34
+
35
+ - Forms should work without JS
36
+ - Server rendering is the default
37
+ - Progressive enhancement over client-side-first
38
+
39
+ **Key differences:**
40
+
41
+ | | Remix / React Router | timber.js |
42
+ | ------------- | ------------------------------------ | ---------------------------------------- |
43
+ | Routing model | Flat route config or file convention | Nested `app/` directory (Next.js-style) |
44
+ | Data loading | `loader` / `action` functions | Async server components + server actions |
45
+ | RSC support | Not yet (planned) | Built on RSC from day one |
46
+ | Streaming | Supports `defer()` for streaming | `<Suspense>` with explicit flush point |
47
+ | Build system | Vite (React Router v7) | Vite |
48
+
49
+ Remix's `loader`/`action` model is elegant and well-understood. timber.js bets on RSC as the data loading primitive — your components are your data layer.
50
+
51
+ ## TanStack Start
52
+
53
+ TanStack Start is a full-stack React framework from the TanStack Router team.
54
+
55
+ **Shared ground:** Both use Vite, both aim for good TypeScript support, both support server-side rendering.
56
+
57
+ **Key differences:**
58
+
59
+ | | TanStack Start | timber.js |
60
+ | -------------- | --------------------------------------- | ------------------------------------------ |
61
+ | Routing | TanStack Router (code-based, type-safe) | File-system routing with generated types |
62
+ | Data loading | TanStack Query integration | Async server components + `timber.cache()` |
63
+ | RSC support | Experimental | Core architecture |
64
+ | Platform focus | General-purpose | Cloudflare-first, works everywhere |
65
+
66
+ ## Summary
67
+
68
+ Every framework makes trade-offs. timber.js trades streaming speed for HTTP correctness and trades API surface size for explicitness. If those trade-offs align with how you think about web development, it's worth trying. If they don't, the frameworks above are all excellent choices.
@@ -0,0 +1,169 @@
1
+ ---
2
+ title: 'AI Agent Instructions'
3
+ description: 'Structured reference for AI coding agents working with timber.js — key differences from Next.js, import paths, common mistakes, and reading order.'
4
+ slug: 'ai-agent-instructions'
5
+ ---
6
+
7
+ # AI Agent Instructions
8
+
9
+ This page is for AI coding agents (Claude Code, Cursor, Copilot, Codex, etc.) and developers who paste context into agent prompts. It is concise and structured for machine consumption — but useful as a quick reference for humans too.
10
+
11
+ ## Fetching These Docs as Markdown
12
+
13
+ Any timber.js documentation page can be fetched as plain markdown by sending an `Accept: text/markdown` header:
14
+
15
+ ```bash
16
+ curl -H 'Accept: text/markdown' https://timberjs.com/docs/ai-agent-instructions
17
+ ```
18
+
19
+ This returns the page content as `text/markdown` with a `203` status code, suitable for piping into agent context or saving to a file.
20
+
21
+ ## How timber.js Docs Are Organized
22
+
23
+ | Location | Purpose | Audience |
24
+ | --- | --- | --- |
25
+ | `packages/website/content/docs/` | User-facing guides, tutorials, API reference | Developers building with timber |
26
+ | `design/` | Design decisions, architecture, internal specs | Contributors and agents implementing features |
27
+ | `CLAUDE.md` | Contributor workflow, commands, code style | Agents working on timber itself |
28
+
29
+ **Read the user-facing docs for building apps.** Read the design docs only when you need to understand _why_ something works the way it does or when contributing to timber itself.
30
+
31
+ ## Reading Order by Task
32
+
33
+ **Starting a new project:**
34
+ 1. [Introduction](/docs/introduction) — what timber is and its philosophy
35
+ 2. [Quick Start](/docs/quick-start) — project setup
36
+ 3. [Pages and Layouts](/docs/pages-and-layouts) — routing basics
37
+ 4. [Configuration](/docs/configuration) — `timber.config.ts`
38
+
39
+ **Adding a feature:**
40
+ 1. [API: @timber-js/app/server](/docs/api-server) — server exports
41
+ 2. [API: @timber-js/app/client](/docs/api-client) — client exports
42
+ 3. The relevant guide (caching, forms, middleware, etc.)
43
+
44
+ **Debugging:**
45
+ 1. [Coming from Next.js](/docs/coming-from-nextjs) — if the code assumes Next.js patterns
46
+ 2. [The Flush Point](/docs/the-flush-point) — if status codes or headers are wrong
47
+ 3. [Error Handling](/docs/error-handling) — deny, error boundaries, status files
48
+
49
+ ## Key Differences from Next.js
50
+
51
+ These are the things you will get wrong if you assume Next.js behavior.
52
+
53
+ | Concept | Next.js | timber.js |
54
+ | --- | --- | --- |
55
+ | Default component type | Client components need `'use client'` | **All components are server components** by default. Only add `'use client'` when you need browser APIs or hooks. |
56
+ | Fetch caching | `fetch()` is patched with implicit caching | `fetch()` is **never patched**. Use `cache()` from `@timber-js/app/cache` explicitly. |
57
+ | ISR | `revalidate` option | **Does not exist.** Use `cache()` with TTL and tags. |
58
+ | `loading.tsx` | Convention for auto-Suspense | **Does not exist.** Use `<Suspense>` explicitly. |
59
+ | Middleware | Single global `middleware.ts` with matchers | Per-segment `middleware.ts` files + global `proxy.ts`. One-arg signature: `middleware(ctx)`. |
60
+ | `notFound()` | `notFound()` from `next/navigation` | `deny(404)` from `@timber-js/app/server`. Sends a real HTTP 404. |
61
+ | `redirect()` | `redirect()` from `next/navigation` | `redirect()` from `@timber-js/app/server`. Same concept, different import. |
62
+ | Search params | Untyped `useSearchParams()` | Typed `defineSearchParams()` with Zod schemas in `params.ts`. |
63
+ | Auth pattern | In middleware or components | `access.ts` per segment with slot degradation. |
64
+ | Forms | `useFormState` / `useFormStatus` | `createActionClient` + `useActionState` returns a 4-tuple. Forms work without JS. |
65
+
66
+ ## Import Map
67
+
68
+ | What you need | Import from |
69
+ | --- | --- |
70
+ | `deny`, `redirect`, `getHeaders`, `getCookieJar`, `createActionClient` | `@timber-js/app/server` |
71
+ | `Link`, `useRouter`, `usePathname`, `useActionState` | `@timber-js/app/client` |
72
+ | `cache`, `CacheHandler` | `@timber-js/app/cache` |
73
+ | `defineSearchParams` | `@timber-js/app/search-params` |
74
+ | `defineSchema`, `defineSegmentParams` | `@timber-js/app/params` |
75
+ | Content collection data | `content-collections` |
76
+ | `Metadata` type | `@timber-js/app/server` |
77
+
78
+ There is **no** `@timber-js/app` default import. Always use the subpath exports above.
79
+
80
+ ## Project Structure
81
+
82
+ ```
83
+ app/ # Route tree — file-system routing
84
+ proxy.ts # Global request handler (like global middleware)
85
+ layout.tsx # Root layout
86
+ page.tsx # Home page
87
+ [segment]/
88
+ page.tsx # Dynamic route
89
+ middleware.ts # Per-segment middleware
90
+ access.ts # Per-segment authorization
91
+ denied.tsx # Rendered when access.ts denies
92
+ error.tsx # Error boundary for this segment
93
+ params.ts # Search param definitions
94
+ schema.ts # Segment param schemas (from @timber-js/app/params)
95
+ content/ # Content collections (outside route tree)
96
+ timber.config.ts # Framework configuration
97
+ mdx-components.tsx # Custom MDX component mappings (optional)
98
+ content-collections.ts # Collection definitions (optional)
99
+ ```
100
+
101
+ ## Common Mistakes
102
+
103
+ 1. **Importing from `next/*`** — there is no `next` package. Use `@timber-js/app/server` and `@timber-js/app/client`. Exception: `next/navigation`'s `redirect()` and `notFound()` are shimmed for compatibility.
104
+
105
+ 2. **Adding `'use client'` to everything** — components are server components by default. Only add `'use client'` when you need browser APIs, event handlers, or React hooks like `useState`/`useEffect`.
106
+
107
+ 3. **Using `fetch()` and expecting caching** — timber never patches `fetch`. Wrap data-fetching functions in `cache()` from `@timber-js/app/cache` when you want caching.
108
+
109
+ 4. **Creating `loading.tsx` files** — this convention does not exist. Use `<Suspense>` with an explicit fallback where you want streaming.
110
+
111
+ 5. **Using ISR patterns** (`revalidate`, `unstable_cache`) — ISR does not exist. Use `cache()` with `{ ttl: seconds, tags: ['tag'] }`.
112
+
113
+ 6. **Calling `notFound()`** — use `deny(404)` from `@timber-js/app/server` instead. It sends a real HTTP 404 status code.
114
+
115
+ 7. **Putting all middleware in one file** — middleware is per-segment. Place `middleware.ts` at the route level that matches the concern. Use `proxy.ts` for global concerns.
116
+
117
+ 8. **Assuming `useSearchParams()` exists** — use `defineSearchParams()` in a `params.ts` file with Zod schemas, then access via `.get()` (server) or `.useQueryStates()` (client).
118
+
119
+ ## Quick Patterns
120
+
121
+ ### Data fetching with caching
122
+
123
+ ```tsx
124
+ import { cache } from '@timber-js/app/cache';
125
+
126
+ const getProducts = cache(
127
+ async () => db.products.findMany(),
128
+ { ttl: 300, tags: ['products'] }
129
+ );
130
+
131
+ export default async function Page() {
132
+ const products = await getProducts();
133
+ return <ProductList products={products} />;
134
+ }
135
+ ```
136
+
137
+ ### 404 handling
138
+
139
+ ```tsx
140
+ import { deny } from '@timber-js/app/server';
141
+
142
+ export default async function Page({ params }: { params: Promise<{ id: string }> }) {
143
+ const { id } = await params;
144
+ const item = await getItem(id);
145
+ if (!item) deny(404);
146
+ return <Item item={item} />;
147
+ }
148
+ ```
149
+
150
+ ### Per-segment middleware
151
+
152
+ ```ts title="app/dashboard/middleware.ts"
153
+ import type { MiddlewareContext } from '@timber-js/app/server';
154
+
155
+ export default async function middleware(ctx: MiddlewareContext) {
156
+ ctx.headers.set('X-Dashboard', 'true');
157
+ }
158
+ ```
159
+
160
+ ### Access control
161
+
162
+ ```ts title="app/(auth)/access.ts"
163
+ import { redirect, getCookieJar } from '@timber-js/app/server';
164
+
165
+ export default async function access() {
166
+ const session = getSession(getCookieJar());
167
+ if (!session) redirect('/login');
168
+ }
169
+ ```
package/llms.txt ADDED
@@ -0,0 +1,55 @@
1
+ # @timber-js/app
2
+
3
+ > Vite-native React Server Components framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript.
4
+
5
+ timber.js is a React framework built on Vite and React Server Components. It uses file-system routing (app/ directory), server components by default, and produces real HTTP status codes. It deploys to Cloudflare Workers, Node.js, Vercel, Netlify, and more via adapters.
6
+
7
+ It is inspired by, but _not_ next.js. Do not assume next.js features work in timber, though the routing is similar, much of the rest diverges.
8
+
9
+ ## Docs
10
+
11
+ Documentation ships with this package in the `docs/` directory. All files are MDX.
12
+
13
+ ### Learn (core concepts, read in order)
14
+
15
+ - [Introduction](docs/learn/00-introduction.mdx)
16
+ - [Quick Start](docs/learn/01-quick-start.mdx)
17
+ - [Pages and Layouts](docs/learn/02-pages-and-layouts.mdx)
18
+ - [Fetching Data](docs/learn/03-fetching-data.mdx)
19
+ - [Access Control](docs/learn/03b-access-control.mdx)
20
+ - [Loading States](docs/learn/04-loading-states.mdx)
21
+ - [The Flush Point](docs/learn/04b-the-flush-point.mdx)
22
+ - [Typed Params](docs/learn/05-typed-params.mdx)
23
+ - [Forms and Actions](docs/learn/06-forms-and-actions.mdx)
24
+ - [Streaming](docs/learn/08-streaming.mdx)
25
+ - [Caching](docs/learn/09-caching.mdx)
26
+ - [Middleware](docs/learn/10-middleware.mdx)
27
+ - [Error Handling](docs/learn/11-error-handling.mdx)
28
+ - [Client Navigation](docs/learn/12-client-navigation.mdx)
29
+ - [Configuration](docs/learn/13-configuration.mdx)
30
+ - [Deploying](docs/learn/14-deploying.mdx)
31
+
32
+ ### More (advanced topics)
33
+
34
+ - [Advanced Routing](docs/more/01-advanced-routing.mdx)
35
+ - [Advanced Forms](docs/more/02-advanced-forms.mdx)
36
+ - [Coming from Next.js](docs/more/03-coming-from-nextjs.mdx)
37
+ - [Metadata and Fonts](docs/more/04-metadata-and-fonts.mdx)
38
+ - [MDX](docs/more/04b-mdx.mdx)
39
+ - [Content Collections](docs/more/05-content-collections.mdx)
40
+ - [Instrumentation & Tracing](docs/more/06-instrumentation.mdx)
41
+ - [Security](docs/more/07-security.mdx)
42
+
43
+ ### API Reference
44
+
45
+ - [@timber-js/app/server](docs/api/30-api-server.mdx)
46
+ - [@timber-js/app/client](docs/api/31-api-client.mdx)
47
+ - [@timber-js/app/cache](docs/api/32-api-cache.mdx)
48
+ - [Search & Segment Params](docs/api/33-api-search-params.mdx)
49
+ - [timber.config.ts](docs/api/34-api-config.mdx)
50
+ - [TypeScript](docs/api/35-api-typescript.mdx)
51
+ - [CLI](docs/api/36-cli.mdx)
52
+
53
+ ## AI Agent Skill
54
+
55
+ A Claude Code skill is included at `agent-skill.md`. Load it for guidance on building timber.js applications.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.154",
3
+ "version": "0.2.0-alpha.156",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -26,6 +26,9 @@
26
26
  "dist",
27
27
  "src",
28
28
  "bin",
29
+ "docs",
30
+ "agent-skill.md",
31
+ "llms.txt",
29
32
  "README.md",
30
33
  "LICENSE"
31
34
  ],
@@ -122,9 +125,9 @@
122
125
  "nitro": "3.0.260610-beta"
123
126
  },
124
127
  "peerDependencies": {
125
- "@content-collections/core": "^0.14.0",
128
+ "@content-collections/core": "^0.14.0 || ^0.15.0",
126
129
  "@content-collections/mdx": "^0.2.0",
127
- "@content-collections/vite": "^0.2.0",
130
+ "@content-collections/vite": "^0.2.0 || ^0.3.0",
128
131
  "@mdx-js/rollup": "^3.0.0",
129
132
  "@vitejs/plugin-react": "^6.0.0",
130
133
  "@vitejs/plugin-rsc": ">=0.5.27",
@@ -159,7 +162,8 @@
159
162
  "node": ">=22.18.0"
160
163
  },
161
164
  "scripts": {
162
- "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
165
+ "copy-docs": "node scripts/copy-docs.js",
166
+ "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist && pnpm run copy-docs",
163
167
  "typecheck": "tsgo --noEmit"
164
168
  }
165
169
  }
@@ -12,7 +12,7 @@
12
12
  // ../client/, or Vite-dependent code. See adapters/shared.ts header.
13
13
 
14
14
  import { writeFile, mkdir, cp, readdir, readFile } from 'node:fs/promises';
15
- import { join, basename } from 'node:path';
15
+ import { join, basename, relative } from 'node:path';
16
16
  import { generateHeadersFile } from './shared.js';
17
17
  import type { TimberConfig } from './types';
18
18
 
@@ -74,7 +74,11 @@ export async function runSharedBuildSteps(opts: BuildOutputBaseOptions): Promise
74
74
  });
75
75
 
76
76
  // 3. Write _headers file for static asset cache control.
77
- await writeFile(join(publicDir, '_headers'), generateHeadersFile());
77
+ // Scan for unhashed files so they get per-file Cache-Control rules in the
78
+ // _headers file. On platforms like Cloudflare Workers Static Assets, the
79
+ // worker never sees static file requests — _headers is the only mechanism.
80
+ const unhashedPaths = await findUnhashedStaticFiles(publicDir);
81
+ await writeFile(join(publicDir, '_headers'), generateHeadersFile(unhashedPaths));
78
82
 
79
83
  // 4. Copy RSC + SSR server bundles into the output directory.
80
84
  await cp(join(buildDir, 'rsc'), join(outDir, 'rsc'), { recursive: true });
@@ -93,6 +97,52 @@ export async function runSharedBuildSteps(opts: BuildOutputBaseOptions): Promise
93
97
  return publicDir;
94
98
  }
95
99
 
100
+ /**
101
+ * Find unhashed static files in the public directory that need per-file
102
+ * Cache-Control rules in the `_headers` file.
103
+ *
104
+ * Excludes:
105
+ * - Files under `assets/` and `_timber/` (covered by wildcard rules)
106
+ * - Underscore-prefixed files (`_headers`, `_worker.js`, etc.)
107
+ *
108
+ * @internal Exported for testing.
109
+ */
110
+ // Well-known metadata files that should be prioritized when capping
111
+ // _headers rules, so they're never dropped in favor of less important files.
112
+ const PRIORITY_FILES = new Set([
113
+ '/favicon.ico',
114
+ '/robots.txt',
115
+ '/sitemap.xml',
116
+ '/manifest.json',
117
+ '/manifest.webmanifest',
118
+ '/apple-touch-icon.png',
119
+ ]);
120
+
121
+ export async function findUnhashedStaticFiles(publicDir: string): Promise<string[]> {
122
+ let files: string[];
123
+ try {
124
+ files = await collectFiles(publicDir);
125
+ } catch {
126
+ return [];
127
+ }
128
+
129
+ const priority: string[] = [];
130
+ const rest: string[] = [];
131
+ for (const file of files) {
132
+ const rel = relative(publicDir, file).replace(/\\/g, '/');
133
+ if (rel.startsWith('assets/') || rel.startsWith('_timber/')) continue;
134
+ if (basename(rel).startsWith('_')) continue;
135
+ const urlPath = '/' + rel;
136
+ if (PRIORITY_FILES.has(urlPath)) {
137
+ priority.push(urlPath);
138
+ } else {
139
+ rest.push(urlPath);
140
+ }
141
+ }
142
+
143
+ return [...priority.sort(), ...rest.sort()];
144
+ }
145
+
96
146
  /**
97
147
  * Scan client JS files for references to .wasm filenames.
98
148
  * Returns the set of WASM basenames that are actually imported/referenced
@@ -338,12 +338,42 @@ if (!bindingsAls) {
338
338
 
339
339
  export default {
340
340
  ${handlersSpread} async fetch(request, env, ctx) {
341
- // Bridge ctx.waitUntil into the per-request ALS so waitUntil() calls in
342
- // user code keep the isolate alive after the response is sent.
343
- // See design/11-platform.md §"waitUntil()".
344
- return bindingsAls.run(env, () =>
341
+ // Edge-cache GET/HEAD responses via the Cache API. RSC navigation
342
+ // requests (Accept: text/x-component) are excluded — they vary by
343
+ // X-Timber-State-Tree which the cache key doesn't capture.
344
+ const isRsc = request.headers.get('Accept')?.includes('text/x-component')
345
+ const reqCc = request.headers.get('Cache-Control') || ''
346
+ const bypassCache = reqCc.includes('no-cache') || reqCc.includes('max-age=0')
347
+ || request.headers.has('Pragma')
348
+ const cacheable = !isRsc && !bypassCache
349
+ && (request.method === 'GET' || request.method === 'HEAD')
350
+ const cache = cacheable && typeof caches !== 'undefined' ? caches.default : null
351
+ if (cache) {
352
+ const cached = await cache.match(request)
353
+ if (cached) return cached
354
+ }
355
+
356
+ const response = await bindingsAls.run(env, () =>
345
357
  runWithWaitUntil((promise) => ctx.waitUntil(promise), () => handler(request))
346
358
  )
359
+
360
+ // Cache public responses at the edge. A response is edge-cacheable if
361
+ // it has 'public' or 's-maxage' (a shared-cache TTL directive) and no
362
+ // 'no-store' or 'private'. Cloudflare's cache.put() only accepts GET
363
+ // requests and rejects 206, so only store GET 200s. HEAD requests
364
+ // benefit from cache.match() which handles HEAD→GET matching automatically.
365
+ // See design/25-production-deployments.md §"CDN / Edge Cache".
366
+ if (cache && request.method === 'GET' && response.status === 200) {
367
+ const cc = response.headers.get('Cache-Control')
368
+ const vary = response.headers.get('Vary')
369
+ if (cc && !cc.includes('no-store') && !cc.includes('private')
370
+ && (cc.includes('public') || cc.includes('s-maxage'))
371
+ && vary !== '*' && !response.headers.has('Set-Cookie')) {
372
+ ctx.waitUntil(cache.put(request, response.clone()))
373
+ }
374
+ }
375
+
376
+ return response
347
377
  }
348
378
  }
349
379
  `;
@@ -24,17 +24,45 @@ export const STATIC_CACHE = 'public, max-age=3600, must-revalidate';
24
24
  * Static Assets, Cloudflare Pages, and Netlify. It maps URL patterns to
25
25
  * HTTP response headers.
26
26
  *
27
- * Vite places all hashed chunks under `/assets/` — these get immutable caching.
28
- * Everything else (favicon.ico, robots.txt, etc.) gets a shorter cache.
27
+ * Only hashed (immutable) asset paths are listed here by default. When
28
+ * `unhashedPaths` is provided, per-file rules are appended for unhashed
29
+ * static assets (favicon.ico, robots.txt, etc.) so they get Cache-Control
30
+ * on platforms like Cloudflare Workers Static Assets where the server
31
+ * handler never sees static file requests.
32
+ *
33
+ * A `/*` catch-all is NOT used — it would be additive on Cloudflare,
34
+ * producing duplicate Cache-Control directives on hashed assets
35
+ * (invalid per RFC 9111).
29
36
  */
30
- export function generateHeadersFile(): string {
31
- return `# Auto-generated by @timber-js/app — static asset cache headers.
37
+ // Cloudflare Workers Static Assets caps _headers at 100 rules.
38
+ // Reserve 2 for the wildcard hashed-asset rules.
39
+ const MAX_UNHASHED_RULES = 98;
40
+
41
+ export function generateHeadersFile(unhashedPaths?: string[]): string {
42
+ let content = `# Auto-generated by @timber-js/app — static asset cache headers.
32
43
  # See design/25-production-deployments.md §"CDN / Edge Cache"
33
44
 
34
45
  /assets/*
35
46
  Cache-Control: ${IMMUTABLE_CACHE}
36
47
 
37
- /*
38
- Cache-Control: ${STATIC_CACHE}
48
+ /_timber/fonts/*
49
+ Cache-Control: ${IMMUTABLE_CACHE}
39
50
  `;
51
+
52
+ if (unhashedPaths && unhashedPaths.length > 0) {
53
+ const capped = unhashedPaths.slice(0, MAX_UNHASHED_RULES);
54
+ if (unhashedPaths.length > MAX_UNHASHED_RULES) {
55
+ console.warn(
56
+ `[timber] _headers: ${unhashedPaths.length} unhashed static files found, ` +
57
+ `but Cloudflare limits _headers to 100 rules. Only the first ${MAX_UNHASHED_RULES} ` +
58
+ `unhashed files will get Cache-Control headers.`
59
+ );
60
+ }
61
+ content += `\n# Unhashed static assets — short-lived cache with revalidation\n`;
62
+ for (const p of capped) {
63
+ content += `\n${p}\n Cache-Control: ${STATIC_CACHE}\n`;
64
+ }
65
+ }
66
+
67
+ return content;
40
68
  }