@timber-js/app 0.2.0-alpha.153 → 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/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/server/internal.js +6 -2
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline-metadata.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/sitemap-handler.d.ts.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
- package/src/client/browser-entry/action-dispatch.ts +14 -0
- package/src/server/pipeline-metadata.ts +1 -0
- package/src/server/pipeline-phases.ts +4 -1
- package/src/server/sitemap-handler.ts +12 -3
|
@@ -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.
|
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.
|
|
3
|
+
"version": "0.2.0-alpha.155",
|
|
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
|
],
|
|
@@ -159,7 +162,8 @@
|
|
|
159
162
|
"node": ">=22.18.0"
|
|
160
163
|
},
|
|
161
164
|
"scripts": {
|
|
162
|
-
"
|
|
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
|
}
|
package/src/cli.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// timber preview — Serve the production build
|
|
12
12
|
// timber check — Validate types + routes without building
|
|
13
13
|
|
|
14
|
-
const COMMANDS = ['dev', 'build', 'preview', 'check', 'schema'] as const;
|
|
14
|
+
const COMMANDS = ['dev', 'build', 'preview', 'check', 'schema', 'init'] as const;
|
|
15
15
|
type Command = (typeof COMMANDS)[number];
|
|
16
16
|
|
|
17
17
|
export interface ParsedArgs {
|
|
@@ -37,7 +37,7 @@ export interface CommandOptions {
|
|
|
37
37
|
export function parseArgs(args: string[]): ParsedArgs {
|
|
38
38
|
if (args.length === 0) {
|
|
39
39
|
throw new Error(
|
|
40
|
-
'No command provided. Usage: timber <dev|build|preview|check|schema> [--config <path>]'
|
|
40
|
+
'No command provided. Usage: timber <dev|build|preview|check|schema|init> [--config <path>]'
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -64,7 +64,7 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
64
64
|
throw new Error(
|
|
65
65
|
`Unknown option: ${arg}. The timber CLI only accepts --config <path>.${portHint}`
|
|
66
66
|
);
|
|
67
|
-
} else if (command === 'schema') {
|
|
67
|
+
} else if (command === 'schema' || command === 'init') {
|
|
68
68
|
rest.push(arg);
|
|
69
69
|
} else {
|
|
70
70
|
throw new Error(`Unexpected argument: ${arg}. Usage: timber ${command} [--config <path>]`);
|
|
@@ -235,6 +235,62 @@ export async function runSchema(args: string[]): Promise<void> {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// ─── Init Subcommand ─────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* `timber init` — write AGENTS.md (and CLAUDE.md pointer) to the project root
|
|
242
|
+
* so AI coding agents can discover timber.js documentation and conventions.
|
|
243
|
+
*
|
|
244
|
+
* The content is read from the installed @timber-js/app package itself, so it
|
|
245
|
+
* stays in sync with the installed version.
|
|
246
|
+
*/
|
|
247
|
+
export async function runInit(): Promise<void> {
|
|
248
|
+
const { readFileSync, writeFileSync, existsSync } = await import('node:fs');
|
|
249
|
+
const { resolve, dirname } = await import('node:path');
|
|
250
|
+
const { fileURLToPath } = await import('node:url');
|
|
251
|
+
|
|
252
|
+
const root = process.cwd();
|
|
253
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
254
|
+
|
|
255
|
+
const agentSkillPath = resolve(packageRoot, 'agent-skill.md');
|
|
256
|
+
if (!existsSync(agentSkillPath)) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
'agent-skill.md not found in @timber-js/app package. ' +
|
|
259
|
+
'This may be a development install — try reinstalling.'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const agentSkill = readFileSync(agentSkillPath, 'utf-8');
|
|
264
|
+
|
|
265
|
+
const agentsPath = resolve(root, 'AGENTS.md');
|
|
266
|
+
const claudePath = resolve(root, 'CLAUDE.md');
|
|
267
|
+
|
|
268
|
+
const wrote: string[] = [];
|
|
269
|
+
|
|
270
|
+
if (existsSync(agentsPath)) {
|
|
271
|
+
console.log('[timber] AGENTS.md already exists — skipping (delete it first to regenerate)');
|
|
272
|
+
} else {
|
|
273
|
+
writeFileSync(agentsPath, agentSkill);
|
|
274
|
+
wrote.push('AGENTS.md');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (existsSync(claudePath)) {
|
|
278
|
+
console.log('[timber] CLAUDE.md already exists — skipping (delete it first to regenerate)');
|
|
279
|
+
} else {
|
|
280
|
+
writeFileSync(claudePath, 'Read AGENTS.md for project context and conventions.\n');
|
|
281
|
+
wrote.push('CLAUDE.md');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (wrote.length > 0) {
|
|
285
|
+
console.log(
|
|
286
|
+
`[timber] Created ${wrote.join(' and ')} — AI agents can now discover timber.js docs.`
|
|
287
|
+
);
|
|
288
|
+
console.log('[timber] Full documentation: node_modules/@timber-js/app/docs/');
|
|
289
|
+
} else {
|
|
290
|
+
console.log('[timber] Nothing to do — both files already exist.');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
238
294
|
// ─── Main Entry Point ────────────────────────────────────────────────────────
|
|
239
295
|
|
|
240
296
|
async function main(): Promise<void> {
|
|
@@ -257,6 +313,9 @@ async function main(): Promise<void> {
|
|
|
257
313
|
case 'schema':
|
|
258
314
|
await runSchema(parsed.rest);
|
|
259
315
|
break;
|
|
316
|
+
case 'init':
|
|
317
|
+
await runInit();
|
|
318
|
+
break;
|
|
260
319
|
}
|
|
261
320
|
}
|
|
262
321
|
|
|
@@ -66,6 +66,20 @@ export function setupServerActions(): void {
|
|
|
66
66
|
// instead of returning HTTP 302. Perform a client-side SPA navigation.
|
|
67
67
|
if (hasRedirect) {
|
|
68
68
|
const wrapper = decoded as { _redirect: string; _status: number };
|
|
69
|
+
|
|
70
|
+
// External redirects (e.g., OAuth flows via redirectExternal()) must
|
|
71
|
+
// use a full page navigation — the RSC router can only fetch same-origin
|
|
72
|
+
// payloads. On browsers with the Navigation API, navigation.navigate()
|
|
73
|
+
// happens to perform a real cross-origin navigation, but browsers
|
|
74
|
+
// without it (iPad Safari) would attempt an RSC fetch that fails on CORS.
|
|
75
|
+
const isExternal =
|
|
76
|
+
wrapper._redirect.startsWith('https://') || wrapper._redirect.startsWith('http://');
|
|
77
|
+
if (isExternal) {
|
|
78
|
+
setHardNavigating(true);
|
|
79
|
+
window.location.href = wrapper._redirect;
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
try {
|
|
70
84
|
const router = getRouter();
|
|
71
85
|
void router.navigate(wrapper._redirect);
|
|
@@ -41,6 +41,7 @@ export async function serveStaticMetadataFile(
|
|
|
41
41
|
const headers: Record<string, string> = {
|
|
42
42
|
'Content-Type': isText ? `${contentType}; charset=utf-8` : contentType,
|
|
43
43
|
'Content-Length': String(body.byteLength),
|
|
44
|
+
'Cache-Control': 'public, max-age=14400, must-revalidate',
|
|
44
45
|
};
|
|
45
46
|
|
|
46
47
|
return new Response(body, { status: 200, headers });
|
|
@@ -316,7 +316,10 @@ export async function handleRequest(
|
|
|
316
316
|
}
|
|
317
317
|
return new Response(body, {
|
|
318
318
|
status: 200,
|
|
319
|
-
headers: {
|
|
319
|
+
headers: {
|
|
320
|
+
'Content-Type': `${contentType}; charset=utf-8`,
|
|
321
|
+
'Cache-Control': 'public, max-age=14400, must-revalidate',
|
|
322
|
+
},
|
|
320
323
|
});
|
|
321
324
|
} catch (error) {
|
|
322
325
|
// Control-flow signals from the metadata handler (e.g. a sitemap that
|
|
@@ -111,7 +111,10 @@ export function createAutoSitemapHandler(
|
|
|
111
111
|
const xml = result.type === 'single' ? result.xml : result.indexXml;
|
|
112
112
|
return new Response(xml, {
|
|
113
113
|
status: 200,
|
|
114
|
-
headers: {
|
|
114
|
+
headers: {
|
|
115
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
116
|
+
'Cache-Control': 'public, max-age=14400, must-revalidate',
|
|
117
|
+
},
|
|
115
118
|
});
|
|
116
119
|
}
|
|
117
120
|
|
|
@@ -125,7 +128,10 @@ export function createAutoSitemapHandler(
|
|
|
125
128
|
if (pageIndex === 0) {
|
|
126
129
|
return new Response(result.xml, {
|
|
127
130
|
status: 200,
|
|
128
|
-
headers: {
|
|
131
|
+
headers: {
|
|
132
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
133
|
+
'Cache-Control': 'public, max-age=14400, must-revalidate',
|
|
134
|
+
},
|
|
129
135
|
});
|
|
130
136
|
}
|
|
131
137
|
return new Response(null, { status: 404 });
|
|
@@ -138,7 +144,10 @@ export function createAutoSitemapHandler(
|
|
|
138
144
|
}
|
|
139
145
|
return new Response(pageXml, {
|
|
140
146
|
status: 200,
|
|
141
|
-
headers: {
|
|
147
|
+
headers: {
|
|
148
|
+
'Content-Type': 'application/xml; charset=utf-8',
|
|
149
|
+
'Cache-Control': 'public, max-age=14400, must-revalidate',
|
|
150
|
+
},
|
|
142
151
|
});
|
|
143
152
|
}
|
|
144
153
|
|