Skip to content

Commit c3fcaa1

Browse files
Merge branch 'main' into main
2 parents 23009bf + 27d0867 commit c3fcaa1

1,044 files changed

Lines changed: 19740 additions & 4105 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/router/framework/react/api/router/NotFoundErrorType.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ The `NotFoundError` object accepts/contains the following properties:
3737
- Optional - `default: false`
3838
- If provided, will throw the not-found object instead of returning it. This can be useful in places where `throwing` in a function might cause it to have a return type of `never`. In that case, you can use `notFound({ throw: true })` to throw the not-found object instead of returning it.
3939
40-
### `route` property
40+
### `routeId` property
4141
4242
- Type: `string`
4343
- Optional

docs/router/framework/react/api/router/linkComponent.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,21 @@ function Component() {
3535
)
3636
}
3737
```
38+
39+
By default, param values with characters such as `@` will be encoded in the URL:
40+
41+
```tsx
42+
// url path will be `/%40foo`
43+
<Link to="/$username" params={{ username: '@foo' }} />
44+
```
45+
46+
To opt-out, update the [pathParamsAllowedCharacters](../router/RouterOptionsType#pathparamsallowedcharacters-property) config on the router
47+
48+
```tsx
49+
import { createRouter } from '@tanstack/react-router'
50+
51+
const router = createRouter({
52+
routeTree,
53+
pathParamsAllowedCharacters: ['@'],
54+
})
55+
```

docs/router/framework/react/guide/document-head-management.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export const Route = createRootRoute({
8989

9090
### Single-Page Applications
9191

92-
First, remove the `<title>` tag from the the index.html if you have set any.
92+
First, remove the `<title>` tag from the index.html if you have set any.
9393

9494
```tsx
9595
import { HeadContent } from '@tanstack/react-router'

docs/router/framework/react/guide/internationalization-i18n.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ This pattern relies exclusively on TanStack Router features. It is suitable when
2525

2626
Optional path parameters are ideal for implementing locale-aware routing without duplicating routes.
2727

28-
```ts
29-
;/{-$locale}/abotu
28+
```
29+
/{-$locale}/about
3030
```
3131

3232
This single route matches:

docs/router/framework/react/how-to/setup-ssr.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ hydrateRoot(document, <RouterClient router={router} />)
142142
// vite.config.ts
143143
import path from 'node:path'
144144
import url from 'node:url'
145-
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
145+
import { tanstackRouter } from '@tanstack/router-plugin/vite'
146146
import { defineConfig } from 'vite'
147147
import react from '@vitejs/plugin-react'
148148

@@ -151,7 +151,7 @@ const __dirname = path.dirname(__filename)
151151

152152
export default defineConfig(({ isSsrBuild }) => ({
153153
plugins: [
154-
TanStackRouterVite({
154+
tanstackRouter({
155155
autoCodeSplitting: true,
156156
}),
157157
react(),
@@ -407,7 +407,7 @@ For streaming SSR, update your Vite config:
407407
// vite.config.ts
408408
export default defineConfig(({ isSsrBuild }) => ({
409409
plugins: [
410-
TanStackRouterVite({
410+
tanstackRouter({
411411
autoCodeSplitting: true,
412412
enableStreaming: true, // Enable streaming support
413413
}),

docs/router/framework/react/how-to/test-file-based-routing.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ Create `vitest.config.ts` with file-based routing support:
3838
```ts
3939
import { defineConfig } from 'vitest/config'
4040
import react from '@vitejs/plugin-react'
41-
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
41+
import { tanstackRouter } from '@tanstack/router-plugin/vite'
4242

4343
export default defineConfig({
4444
plugins: [
45-
TanStackRouterVite({
45+
tanstackRouter({
4646
// Configure for test environment
4747
routesDirectory: './src/routes',
4848
generatedRouteTree: './src/routeTree.gen.ts',
@@ -898,7 +898,7 @@ Error: Cannot find module '../routeTree.gen'
898898
// vitest.config.ts
899899
export default defineConfig({
900900
plugins: [
901-
TanStackRouterVite(), // Ensure this runs before tests
901+
tanstackRouter(), // Ensure this runs before tests
902902
react(),
903903
],
904904
test: {

docs/router/framework/react/how-to/use-environment-variables.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,13 +436,13 @@ export const Route = createFileRoute('/api-data')({
436436
// vite.config.ts
437437
import { defineConfig } from 'vite'
438438
import react from '@vitejs/plugin-react'
439-
import { TanStackRouterVite } from '@tanstack/router-vite-plugin'
439+
import { tanstackRouter } from '@tanstack/router-plugin/vite'
440440

441441
export default defineConfig({
442442
plugins: [
443443
react(),
444-
// TanStackRouterVite generates route tree and enables file-based routing
445-
TanStackRouterVite(),
444+
// tanstackRouter generates route tree and enables file-based routing
445+
tanstackRouter(),
446446
],
447447
// Environment variables are handled automatically
448448
// Custom environment variable handling:

docs/start/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@
137137
"label": "Server Entry Point",
138138
"to": "framework/react/guide/server-entry-point"
139139
},
140+
{
141+
"label": "CDN Asset URLs",
142+
"to": "framework/react/guide/cdn-asset-urls"
143+
},
140144
{
141145
"label": "Client Entry Point",
142146
"to": "framework/react/guide/client-entry-point"

docs/start/framework/react/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ title: Getting Started
1515
Choose one of the following options to start building a _new_ TanStack Start project:
1616

1717
- [TanStack Start CLI](./quick-start) - Just run `npm create @tanstack/start@latest`. Local, fast, and optionally customizable
18-
- [TanStack Builder](#) (coming soon!) - A visual interface to configure new TanStack projects with a few clicks
18+
- [TanStack Builder](https://tanstack.com/builder) - A visual interface to configure new TanStack projects with a few clicks
1919
- [Quick Start Examples](./quick-start) Download or clone one of our official examples
2020
- [Build a project from scratch](./build-from-scratch) - A guide to building a TanStack Start project line-by-line, file-by-file.
2121

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
---
2+
id: cdn-asset-urls
3+
title: CDN Asset URLs
4+
---
5+
6+
# CDN Asset URLs
7+
8+
> **Experimental:** `transformAssetUrls` is experimental and subject to change.
9+
10+
When deploying to production, you may want to serve your static assets (JavaScript, CSS) from a CDN. The `transformAssetUrls` option on `createStartHandler` lets you rewrite asset URLs at runtime — for example, prepending a CDN origin that is only known when the server starts.
11+
12+
## Why Runtime URL Rewriting?
13+
14+
Vite's `base` config is evaluated at build time. If your CDN URL is determined at deploy time (via environment variables, dynamic configuration, etc.), you need a way to rewrite URLs at runtime. `transformAssetUrls` solves this for the URLs that TanStack Start manages in its manifest:
15+
16+
- `<link rel="modulepreload">` tags (JS preloads)
17+
- `<link rel="stylesheet">` tags (CSS)
18+
- The client entry `<script>` tag
19+
20+
## Basic Usage
21+
22+
### String Prefix
23+
24+
The simplest usage is passing a string. Every manifest asset URL will be prefixed with it:
25+
26+
```tsx
27+
// src/server.ts
28+
import {
29+
createStartHandler,
30+
defaultStreamHandler,
31+
} from '@tanstack/react-start/server'
32+
import { createServerEntry } from '@tanstack/react-start/server-entry'
33+
34+
const handler = createStartHandler({
35+
handler: defaultStreamHandler,
36+
transformAssetUrls: process.env.CDN_ORIGIN || '',
37+
})
38+
39+
export default createServerEntry({ fetch: handler })
40+
```
41+
42+
If `CDN_ORIGIN` is `https://cdn.example.com` and an asset URL is `/assets/index-abc123.js`, the resulting URL will be `https://cdn.example.com/assets/index-abc123.js`.
43+
44+
When the string is empty (or not set), the URLs are left unchanged.
45+
46+
### Callback
47+
48+
For more control, pass a callback that receives `{ url, type }` and returns a new URL (or a `Promise` of one). By default, the transformed manifest is cached after the first request (`cache: true`), so the callback only runs once in production:
49+
50+
```tsx
51+
// src/server.ts
52+
import {
53+
createStartHandler,
54+
defaultStreamHandler,
55+
} from '@tanstack/react-start/server'
56+
import { createServerEntry } from '@tanstack/react-start/server-entry'
57+
58+
const handler = createStartHandler({
59+
handler: defaultStreamHandler,
60+
transformAssetUrls: ({ url, type }) => {
61+
// Only rewrite JS and CSS, leave client entry unchanged
62+
if (type === 'clientEntry') return url
63+
return `https://cdn.example.com${url}`
64+
},
65+
})
66+
67+
export default createServerEntry({ fetch: handler })
68+
```
69+
70+
If you need per-request behavior (for example, choosing a CDN based on a header), use the object form with `cache: false`.
71+
72+
The `type` parameter tells you what kind of asset URL is being transformed:
73+
74+
| `type` | Description |
75+
| ----------------- | ---------------------------------------------------- |
76+
| `'modulepreload'` | JS module preload URL (`<link rel="modulepreload">`) |
77+
| `'stylesheet'` | CSS stylesheet URL (`<link rel="stylesheet">`) |
78+
| `'clientEntry'` | Client entry module URL (used in `import('...')`) |
79+
80+
### Object Form (Explicit Cache Control)
81+
82+
For per-request transforms — where the CDN URL depends on request-specific data like headers — use the object form with `cache: false`:
83+
84+
```tsx
85+
// src/server.ts
86+
import {
87+
createStartHandler,
88+
defaultStreamHandler,
89+
} from '@tanstack/react-start/server'
90+
import { createServerEntry } from '@tanstack/react-start/server-entry'
91+
import { getRequest } from '@tanstack/react-start/server'
92+
93+
const handler = createStartHandler({
94+
handler: defaultStreamHandler,
95+
transformAssetUrls: {
96+
transform: ({ url, type }) => {
97+
const region = getRequest().headers.get('x-region') || 'us'
98+
const cdnBase =
99+
region === 'eu'
100+
? 'https://cdn-eu.example.com'
101+
: 'https://cdn-us.example.com'
102+
return `${cdnBase}${url}`
103+
},
104+
cache: false,
105+
},
106+
})
107+
108+
export default createServerEntry({ fetch: handler })
109+
```
110+
111+
The object form accepts:
112+
113+
| Property | Type | Description |
114+
| ----------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
115+
| `transform` | `string \| (asset) => string \| Promise<string>` | A string prefix or callback, same as the shorthand forms above. |
116+
| `createTransform` | `(ctx: { warmup: true } \| { warmup: false; request: Request }) => (asset) => string \| Promise<string>` | Async factory that runs once per manifest computation and returns a per-asset transform. Mutually exclusive with `transform`. |
117+
| `cache` | `boolean` | Whether to cache the transformed manifest. Defaults to `true`. |
118+
| `warmup` | `boolean` | When `true`, warms up the cached manifest on server startup (prod only). Defaults to `false`. |
119+
120+
If you need to do async work once per manifest computation (e.g. fetch a CDN origin from a service) and then transform many URLs, prefer `createTransform`:
121+
122+
```ts
123+
transformAssetUrls: {
124+
cache: false,
125+
async createTransform(ctx) {
126+
if (ctx.warmup) {
127+
// optional: return a default transform during warmup
128+
return ({ url }) => url
129+
}
130+
131+
const region = ctx.request.headers.get('x-region') || 'us'
132+
const cdnBase = await fetchCdnBaseForRegion(region)
133+
return ({ url }) => `${cdnBase}${url}`
134+
},
135+
}
136+
```
137+
138+
## Caching Behavior
139+
140+
By default, **all forms** of `transformAssetUrls` cache the transformed manifest after the first request (`cache: true`). This means the transform function runs once on the first request, and the result is reused for every subsequent request in production.
141+
142+
| Form | Default cache | Behavior |
143+
| -------------------------------------- | ------------- | ---------------------------------------------------------- |
144+
| String prefix | `true` | Computed once, cached forever in prod. |
145+
| Callback | `true` | Runs once on first request, cached forever in prod. |
146+
| Object with `cache: true` (or omitted) | `true` | Same as above. |
147+
| Object with `cache: false` | `false` | Deep-clones base manifest and transforms on every request. |
148+
149+
Use `cache: false` only when the transform depends on per-request data (e.g., geo-routing based on request headers). For static CDN prefixes, the default `cache: true` is recommended.
150+
151+
### Optional Warmup (Avoid First-Request Latency)
152+
153+
If you're using the object form with `cache: true`, you can set `warmup: true`
154+
to compute the transformed manifest in the background at server startup.
155+
156+
```ts
157+
transformAssetUrls: {
158+
transform: process.env.CDN_ORIGIN || '',
159+
cache: true,
160+
warmup: true,
161+
}
162+
```
163+
164+
This has no effect in development mode, or when `cache: false`.
165+
166+
> **Note:** In development mode (`TSS_DEV_SERVER`), caching is always skipped regardless of the `cache` setting, so you always get fresh manifests.
167+
168+
## Recommended: Set `base: ''` for Client-Side Navigation
169+
170+
`transformAssetUrls` rewrites the URLs in the SSR HTML — modulepreload hints, stylesheets, and the client entry script. This means the browser's initial page load fetches all assets from the CDN.
171+
172+
However, when users navigate client-side (e.g., clicking a `<Link>`), TanStack Router lazy-loads route chunks using `import()` calls with paths that were baked in at **build time** by Vite. By default, Vite uses `base: '/'`, which produces absolute paths like `/assets/about-abc123.js`. These resolve against the **app server's origin**, not the CDN — even though the entry module was loaded from the CDN.
173+
174+
To fix this, set `base: ''` in your Vite config:
175+
176+
```ts
177+
// vite.config.ts
178+
export default defineConfig({
179+
base: '',
180+
// ... plugins, etc.
181+
})
182+
```
183+
184+
With `base: ''`, Vite generates **relative** import paths for client-side chunks. Since the client entry module was loaded from the CDN (thanks to `transformAssetUrls`), all relative `import()` calls resolve against the CDN origin. This ensures that lazy-loaded route chunks during client-side navigation are also served from the CDN.
185+
186+
Using an empty string rather than `'./'` is important — both produce relative client-side imports, but `base: ''` preserves the correct root-relative paths (`/assets/...`) in the SSR manifest so that `transformAssetUrls` can properly prepend the CDN origin.
187+
188+
| `base` setting | SSR assets (initial load) | Client-side navigation chunks |
189+
| --------------- | ------------------------------ | ------------------------------ |
190+
| `'/'` (default) | CDN (via `transformAssetUrls`) | App server |
191+
| `''` | CDN (via `transformAssetUrls`) | CDN (relative to entry module) |
192+
193+
> **Tip:** `base: ''` is recommended whenever you use `transformAssetUrls` so that all assets — both on initial load and during client-side navigation — are consistently served from the CDN.
194+
195+
## What This Does NOT Cover
196+
197+
`transformAssetUrls` only rewrites URLs in the TanStack Start manifest — the tags emitted during SSR for preloading and bootstrapping the application.
198+
199+
It does **not** rewrite asset URLs that are imported directly in your components:
200+
201+
```tsx
202+
// This import resolves to a URL at build time by Vite
203+
import logo from './logo.svg'
204+
205+
function Header() {
206+
return <img src={logo} /> // This URL is NOT affected by transformAssetUrls
207+
}
208+
```
209+
210+
For these asset imports, use Vite's `experimental.renderBuiltUrl` in your `vite.config.ts`:
211+
212+
```ts
213+
// vite.config.ts
214+
export default defineConfig({
215+
experimental: {
216+
renderBuiltUrl(filename, { hostType }) {
217+
if (hostType === 'js') {
218+
return { relative: true }
219+
}
220+
return `https://cdn.example.com/${filename}`
221+
},
222+
},
223+
})
224+
```

0 commit comments

Comments
 (0)