deployment
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseDeployment and Rendering
部署与渲染
TanStack Start deploys to any hosting provider via Vite and Nitro. This skill covers hosting setup, SSR configuration, prerendering, and SEO.
TanStack Start 可通过 Vite 和 Nitro 部署到任意托管服务商。本技能涵盖托管配置、SSR 设置、预渲染和 SEO 相关内容。
Hosting Providers
托管服务商
Cloudflare Workers
Cloudflare Workers
bash
pnpm add -D @cloudflare/vite-plugin wranglerts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { cloudflare } from '@cloudflare/vite-plugin'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: 'ssr' } }),
tanstackStart(),
viteReact(),
],
})jsonc
// wrangler.jsonc
{
"name": "my-app",
"compatibility_date": "2025-09-02",
"compatibility_flags": ["nodejs_compat"],
"main": "@tanstack/react-start/server-entry",
}Deploy:
npx wrangler login && pnpm run deployWorker env is per-request. Cloudflare Workers inject env vars at request time.at module scope evaluates toprocess.env.Xeven on the server. The Cloudflare-canonical way to read env (including from module scope) is theundefinedenv binding:cloudflare:workerstsimport { env } from 'cloudflare:workers' const apiHost = env.API_HOSTOr readper-request insideprocess.env.X/ middleware.handler(). See Cloudflare's environment-variables docs and start-core/execution-model..server()
bash
pnpm add -D @cloudflare/vite-plugin wranglerts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { cloudflare } from '@cloudflare/vite-plugin'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: 'ssr' } }),
tanstackStart(),
viteReact(),
],
})jsonc
// wrangler.jsonc
{
"name": "my-app",
"compatibility_date": "2025-09-02",
"compatibility_flags": ["nodejs_compat"],
"main": "@tanstack/react-start/server-entry",
}部署命令:
npx wrangler login && pnpm run deployWorker 的环境变量是按请求注入的。 Cloudflare Workers 会在请求时注入环境变量。模块作用域下的即使在服务端也会返回process.env.X。Cloudflare 官方推荐的环境变量读取方式(包括模块作用域)是使用undefined环境绑定:cloudflare:workerstsimport { env } from 'cloudflare:workers' const apiHost = env.API_HOST或者在/ 中间件.handler()中按请求读取.server()。参见 Cloudflare 环境变量文档 和 start-core/execution-model。process.env.X
Netlify
Netlify
bash
pnpm add -D @netlify/vite-plugin-tanstack-startts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import netlify from '@netlify/vite-plugin-tanstack-start'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
plugins: [tanstackStart(), netlify(), viteReact()],
})Deploy:
npx netlify deploybash
pnpm add -D @netlify/vite-plugin-tanstack-startts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import netlify from '@netlify/vite-plugin-tanstack-start'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
plugins: [tanstackStart(), netlify(), viteReact()],
})部署命令:
npx netlify deployNitro (Vercel, Railway, Node.js, Docker)
Nitro (Vercel, Railway, Node.js, Docker)
bash
npm install nitro@npm:nitro-nightly@latestts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { nitro } from 'nitro/vite'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
plugins: [tanstackStart(), nitro(), viteReact()],
})Build and start:
npm run build && node .output/server/index.mjsbash
npm install nitro@npm:nitro-nightly@latestts
// vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { nitro } from 'nitro/vite'
import viteReact from '@vitejs/plugin-react'
export default defineConfig({
plugins: [tanstackStart(), nitro(), viteReact()],
})构建并启动:
npm run build && node .output/server/index.mjsBun
Bun
Bun deployment requires React 19. For React 18, use Node.js deployment.
ts
// vite.config.ts — add bun preset to nitro
plugins: [tanstackStart(), nitro({ preset: 'bun' }), viteReact()]Bun 部署要求使用 React 19。如果使用 React 18,请采用 Node.js 部署方式。
ts
// vite.config.ts — add bun preset to nitro
plugins: [tanstackStart(), nitro({ preset: 'bun' }), viteReact()]Selective SSR
选择性 SSR
Control SSR per route with the property.
ssr通过 属性按路由控制 SSR 行为。
ssrssr: true
(default)
ssr: truessr: true
(默认)
ssr: trueRuns and on server, renders component on server:
beforeLoadloadertsx
export const Route = createFileRoute('/posts/$postId')({
ssr: true, // default
loader: () => fetchPost(), // runs on server during SSR
component: PostPage, // rendered on server
})在服务端运行 和 ,在服务端渲染组件:
beforeLoadloadertsx
export const Route = createFileRoute('/posts/$postId')({
ssr: true, // default
loader: () => fetchPost(), // runs on server during SSR
component: PostPage, // rendered on server
})ssr: false
ssr: falsessr: false
ssr: falseDisables server execution of / and server rendering:
beforeLoadloadertsx
export const Route = createFileRoute('/dashboard')({
ssr: false,
loader: () => fetchDashboard(), // runs on client only
component: DashboardPage, // rendered on client only
})禁用服务端执行 / 以及服务端渲染:
beforeLoadloadertsx
export const Route = createFileRoute('/dashboard')({
ssr: false,
loader: () => fetchDashboard(), // runs on client only
component: DashboardPage, // rendered on client only
})ssr: 'data-only'
ssr: 'data-only'ssr: 'data-only'
ssr: 'data-only'Runs / on server but renders component on client only:
beforeLoadloadertsx
export const Route = createFileRoute('/canvas')({
ssr: 'data-only',
loader: () => fetchCanvasData(), // runs on server
component: CanvasPage, // rendered on client only
})在服务端运行 /,但仅在客户端渲染组件:
beforeLoadloadertsx
export const Route = createFileRoute('/canvas')({
ssr: 'data-only',
loader: () => fetchCanvasData(), // runs on server
component: CanvasPage, // rendered on client only
})Functional Form
函数形式
Decide SSR at runtime based on params/search:
tsx
export const Route = createFileRoute('/docs/$docType/$docId')({
ssr: ({ params }) => {
if (params.status === 'success' && params.value.docType === 'sheet') {
return false
}
},
})根据 params/search 参数在运行时决定是否启用 SSR:
tsx
export const Route = createFileRoute('/docs/$docType/$docId')({
ssr: ({ params }) => {
if (params.status === 'success' && params.value.docType === 'sheet') {
return false
}
},
})SSR Inheritance
SSR 继承规则
Children inherit parent SSR config and can only be MORE restrictive:
- →
trueordata-only(allowed)false - →
false(NOT allowed — parenttruewins)false
子路由会继承父路由的 SSR 配置,且只能比父路由更严格:
- →
true或data-only(允许)false - →
false(不允许 — 父路由的 false 优先级更高)true
Default SSR
默认 SSR 配置
Change the default for all routes in :
src/start.tstsx
import { createStart } from '@tanstack/react-start'
export const startInstance = createStart(() => ({
defaultSsr: false,
}))在 中修改所有路由的默认配置:
src/start.tstsx
import { createStart } from '@tanstack/react-start'
export const startInstance = createStart(() => ({
defaultSsr: false,
}))Static Prerendering
静态预渲染
Generate static HTML at build time:
ts
// vite.config.ts
tanstackStart({
prerender: {
enabled: true,
crawlLinks: true,
concurrency: 14,
failOnError: true,
},
})Static routes are auto-discovered. Dynamic routes (e.g. ) require or explicit config.
/users/$userIdcrawlLinkspages在构建时生成静态 HTML:
ts
// vite.config.ts
tanstackStart({
prerender: {
enabled: true,
crawlLinks: true,
concurrency: 14,
failOnError: true,
},
})静态路由会被自动发现。动态路由(例如 )需要开启 或显式配置 。
/users/$userIdcrawlLinkspagesSEO and Head Management
SEO 与头部管理
Basic Meta Tags
基础 Meta 标签
tsx
export const Route = createFileRoute('/')({
head: () => ({
meta: [
{ title: 'My App - Home' },
{ name: 'description', content: 'Welcome to My App' },
],
}),
})tsx
export const Route = createFileRoute('/')({
head: () => ({
meta: [
{ title: 'My App - Home' },
{ name: 'description', content: 'Welcome to My App' },
],
}),
})Dynamic Meta from Loader Data
基于 Loader 数据的动态 Meta
tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => fetchPost(params.postId),
head: ({ loaderData }) => ({
meta: [
{ title: loaderData.title },
{ name: 'description', content: loaderData.excerpt },
{ property: 'og:title', content: loaderData.title },
{ property: 'og:image', content: loaderData.coverImage },
],
}),
})tsx
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ params }) => fetchPost(params.postId),
head: ({ loaderData }) => ({
meta: [
{ title: loaderData.title },
{ name: 'description', content: loaderData.excerpt },
{ property: 'og:title', content: loaderData.title },
{ property: 'og:image', content: loaderData.coverImage },
],
}),
})Structured Data (JSON-LD)
结构化数据(JSON-LD)
tsx
head: ({ loaderData }) => ({
scripts: [
{
type: 'application/ld+json',
children: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: loaderData.title,
}),
},
],
})tsx
head: ({ loaderData }) => ({
scripts: [
{
type: 'application/ld+json',
children: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: loaderData.title,
}),
},
],
})Dynamic Sitemap via Server Route
通过服务端路由生成动态站点地图
ts
// src/routes/sitemap[.]xml.ts
export const Route = createFileRoute('/sitemap.xml')({
server: {
handlers: {
GET: async () => {
const posts = await fetchAllPosts()
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${posts.map((p) => `<url><loc>https://myapp.com/posts/${p.id}</loc></url>`).join('')}
</urlset>`
return new Response(sitemap, {
headers: { 'Content-Type': 'application/xml' },
})
},
},
},
})ts
// src/routes/sitemap[.]xml.ts
export const Route = createFileRoute('/sitemap.xml')({
server: {
handlers: {
GET: async () => {
const posts = await fetchAllPosts()
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${posts.map((p) => `<url><loc>https://myapp.com/posts/${p.id}</loc></url>`).join('')}
</urlset>`
return new Response(sitemap, {
headers: { 'Content-Type': 'application/xml' },
})
},
},
},
})Common Mistakes
常见错误
1. HIGH: Missing nodejs_compat flag for Cloudflare Workers
1. 高:Cloudflare Workers 缺少 nodejs_compat 标志
jsonc
// WRONG — Node.js APIs fail at runtime
{ "compatibility_flags": [] }
// CORRECT
{ "compatibility_flags": ["nodejs_compat"] }jsonc
// WRONG — Node.js APIs fail at runtime
{ "compatibility_flags": [] }
// CORRECT
{ "compatibility_flags": ["nodejs_compat"] }2. MEDIUM: Bun deployment with React 18
2. 中:Bun 部署搭配 React 18
Bun-specific deployment only works with React 19. Use Node.js deployment for React 18.
Bun 专属的部署方式仅支持 React 19。React 18 请使用 Node.js 部署。
3. MEDIUM: Child route loosening parent SSR config
3. 中:子路由放宽父路由的 SSR 配置
tsx
// Parent sets ssr: false
// WRONG — child cannot upgrade to ssr: true
const parentRoute = createFileRoute('/dashboard')({ ssr: false })
const childRoute = createFileRoute('/dashboard/stats')({
ssr: true, // IGNORED — parent false wins
})
// CORRECT — children can only be MORE restrictive
const parentRoute = createFileRoute('/dashboard')({ ssr: 'data-only' })
const childRoute = createFileRoute('/dashboard/stats')({
ssr: false, // OK — more restrictive than parent
})tsx
// Parent sets ssr: false
// WRONG — child cannot upgrade to ssr: true
const parentRoute = createFileRoute('/dashboard')({ ssr: false })
const childRoute = createFileRoute('/dashboard/stats')({
ssr: true, // IGNORED — parent false wins
})
// CORRECT — children can only be MORE restrictive
const parentRoute = createFileRoute('/dashboard')({ ssr: 'data-only' })
const childRoute = createFileRoute('/dashboard/stats')({
ssr: false, // OK — more restrictive than parent
})Cross-References
交叉参考
- start-core/server-routes — API endpoints for sitemaps, robots.txt
- start-core/execution-model — SSR affects where code runs
- start-core/server-routes — 用于站点地图、robots.txt 的 API 端点
- start-core/execution-model — SSR 会影响代码的运行位置