From ea4802f777e1c23a4447e2a49f9a81ef452784b1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Jul 2025 21:19:18 +0000 Subject: [PATCH 01/11] Add SSR how-to guide for TanStack Router with comprehensive setup Co-authored-by: tannerlinsley --- docs/router/framework/react/how-to/README.md | 1 + .../react/how-to/deploy-to-production.md | 3 +- .../framework/react/how-to/setup-ssr.md | 448 ++++++++++++++++++ how-to-guides-implementation-plan.md | 5 +- 4 files changed, 454 insertions(+), 3 deletions(-) create mode 100644 docs/router/framework/react/how-to/setup-ssr.md diff --git a/docs/router/framework/react/how-to/README.md b/docs/router/framework/react/how-to/README.md index dabd163751..f468d16a27 100644 --- a/docs/router/framework/react/how-to/README.md +++ b/docs/router/framework/react/how-to/README.md @@ -11,6 +11,7 @@ This directory contains focused, step-by-step instructions for common TanStack R - [Install TanStack Router](./install.md) - Basic installation steps - [Deploy to Production](./deploy-to-production.md) - Deploy your app to hosting platforms +- [Set Up Server-Side Rendering (SSR)](./setup-ssr.md) - Implement SSR with TanStack Router ## Using These Guides diff --git a/docs/router/framework/react/how-to/deploy-to-production.md b/docs/router/framework/react/how-to/deploy-to-production.md index a7c19b3557..b093c2f311 100644 --- a/docs/router/framework/react/how-to/deploy-to-production.md +++ b/docs/router/framework/react/how-to/deploy-to-production.md @@ -432,8 +432,9 @@ Before deploying, ensure you have: After deployment, you might want to: - diff --git a/docs/router/framework/react/how-to/setup-ssr.md b/docs/router/framework/react/how-to/setup-ssr.md new file mode 100644 index 0000000000..29fdedccc8 --- /dev/null +++ b/docs/router/framework/react/how-to/setup-ssr.md @@ -0,0 +1,448 @@ +# How to Set Up Server-Side Rendering (SSR) + +This guide shows you how to set up Server-Side Rendering (SSR) with TanStack Router. SSR improves initial page load performance by rendering HTML on the server before sending it to the client. + +## Quick Start + +SSR with TanStack Router involves: + +1. **Creating shared router configuration** that works on both server and client +2. **Setting up a server entry point** to render your app server-side +3. **Setting up a client entry point** to hydrate the server-rendered HTML +4. **Configuring your bundler** (Vite, Webpack, etc.) for SSR builds + +## Step-by-Step Setup + +### 1. Create Shared Router Configuration + +Create a router factory that can be used by both server and client: + +```tsx +// src/router.tsx +import { createRouter as createTanstackRouter } from '@tanstack/react-router' +import { routeTree } from './routeTree.gen' + +export function createRouter() { + return createTanstackRouter({ + routeTree, + context: { + head: '', // For server-side head injection + }, + defaultPreload: 'intent', + scrollRestoration: true, + }) +} + +declare module '@tanstack/react-router' { + interface Register { + router: ReturnType + } +} +``` + +### 2. Set Up Server Entry Point + +Create a server entry that renders your app to HTML: + +```tsx +// src/entry-server.tsx +import { pipeline } from 'node:stream/promises' +import { + RouterServer, + createRequestHandler, + renderRouterToString, +} from '@tanstack/react-router/ssr/server' +import { createRouter } from './router' +import type express from 'express' + +export async function render({ + req, + res, + head = '', +}: { + head?: string + req: express.Request + res: express.Response +}) { + // Convert Express request to Web API Request + const url = new URL(req.originalUrl || req.url, 'https://localhost:3000').href + + const request = new Request(url, { + method: req.method, + headers: (() => { + const headers = new Headers() + for (const [key, value] of Object.entries(req.headers)) { + headers.set(key, value as any) + } + return headers + })(), + }) + + // Create request handler + const handler = createRequestHandler({ + request, + createRouter: () => { + const router = createRouter() + + // Inject server context (like head tags from Vite) + router.update({ + context: { + ...router.options.context, + head: head, + }, + }) + return router + }, + }) + + // Render to string (non-streaming) + const response = await handler(({ responseHeaders, router }) => + renderRouterToString({ + responseHeaders, + router, + children: , + }), + ) + + // Convert Web API Response back to Express response + res.statusMessage = response.statusText + res.status(response.status) + + response.headers.forEach((value, name) => { + res.setHeader(name, value) + }) + + // Stream response body + return pipeline(response.body as any, res) +} +``` + +### 3. Set Up Client Entry Point + +Create a client entry that hydrates the server-rendered HTML: + +```tsx +// src/entry-client.tsx +import { hydrateRoot } from 'react-dom/client' +import { RouterClient } from '@tanstack/react-router/ssr/client' +import { createRouter } from './router' + +const router = createRouter() + +hydrateRoot(document, ) +``` + +### 4. Configure Vite for SSR + +Update your Vite configuration: + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import { TanStackRouterVite } from '@tanstack/router-plugin/vite' + +export default defineConfig({ + plugins: [ + react(), + TanStackRouterVite(), + ], + build: { + rollupOptions: { + input: { + main: './index.html', + server: './src/entry-server.tsx', + }, + }, + }, + ssr: { + noExternal: ['@tanstack/react-router'], + }, +}) +``` + +### 5. Create Express Server + +Set up an Express server to handle SSR: + +```js +// server.js +import path from 'node:path' +import express from 'express' +import * as zlib from 'node:zlib' + +const isProduction = process.env.NODE_ENV === 'production' + +export async function createServer() { + const app = express() + + let vite + if (!isProduction) { + // Development: Use Vite dev server + vite = await (await import('vite')).createServer({ + server: { middlewareMode: true }, + appType: 'custom', + }) + app.use(vite.middlewares) + } else { + // Production: Serve static files and use compression + app.use( + (await import('compression')).default({ + brotli: { flush: zlib.constants.BROTLI_OPERATION_FLUSH }, + flush: zlib.constants.Z_SYNC_FLUSH, + }), + ) + app.use(express.static('./dist/client')) + } + + app.use('*', async (req, res) => { + try { + const url = req.originalUrl + + // Skip non-route requests + if (path.extname(url) !== '') { + console.warn(`${url} is not valid router path`) + res.status(404).end(`${url} is not valid router path`) + return + } + + // Extract head tags from Vite's transformation + let viteHead = '' + if (!isProduction) { + const transformed = await vite.transformIndexHtml( + url, + ``, + ) + viteHead = transformed.substring( + transformed.indexOf('') + 6, + transformed.indexOf(''), + ) + } + + // Load entry module + const entry = !isProduction + ? await vite.ssrLoadModule('/src/entry-server.tsx') + : await import('./dist/server/entry-server.js') + + console.info('Rendering:', url) + await entry.render({ req, res, head: viteHead }) + } catch (e) { + !isProduction && vite.ssrFixStacktrace(e) + console.error(e.stack) + res.status(500).end(e.stack) + } + }) + + return { app, vite } +} + +// Start server +if (process.env.NODE_ENV !== 'test') { + createServer().then(({ app }) => { + app.listen(3000, () => { + console.info('Server running at http://localhost:3000') + }) + }) +} +``` + +### 6. Update Package Scripts + +Add SSR build scripts to your `package.json`: + +```json +{ + "scripts": { + "dev": "node server.js", + "build": "npm run build:client && npm run build:server", + "build:client": "vite build --outDir dist/client", + "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server", + "start": "NODE_ENV=production node server.js" + } +} +``` + +## Streaming SSR (Optional) + +For better performance with slower data fetching, enable streaming SSR: + +```tsx +// src/entry-server.tsx - Replace renderRouterToString with: +import { renderRouterToStream } from '@tanstack/react-router/ssr/server' + +const response = await handler(({ request, responseHeaders, router }) => + renderRouterToStream({ + request, + responseHeaders, + router, + children: , + }), +) +``` + +## Different Deployment Targets + +### Node.js Production + +```bash +# Build for Node.js +npm run build +NODE_ENV=production node server.js +``` + +### Bun Runtime + +Update your server to handle Bun-specific requirements: + +```ts +// Add to entry-server.tsx top +import './fetch-polyfill' // If using custom fetch polyfill + +// For Bun, ensure react-dom/server exports are available +if (typeof process !== 'undefined' && process.versions?.bun) { + // Bun-specific setup if needed +} +``` + +### Edge Runtime + +For edge deployments, use Web APIs instead of Node.js APIs: + +```tsx +// src/entry-server-edge.tsx +import { + createRequestHandler, + renderRouterToString, + RouterServer, +} from '@tanstack/react-router/ssr/server' +import { createRouter } from './router' + +export async function handleRequest(request: Request): Promise { + const handler = createRequestHandler({ + request, + createRouter, + }) + + return handler(({ responseHeaders, router }) => + renderRouterToString({ + responseHeaders, + router, + children: , + }), + ) +} +``` + +## Common Problems + +### React Import Errors + +**Problem:** `ReferenceError: React is not defined` during SSR + +**Solution:** Ensure React is properly imported in components: + +```tsx +// In your route components +import React from 'react' // Add explicit import +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/')({ + component: () =>
Hello
// React is now available +}) +``` + +### Hydration Mismatches + +**Problem:** Client HTML doesn't match server HTML + +**Solution:** Ensure consistent rendering between server and client: + +```tsx +// Use useIsomorphicLayoutEffect for browser-only effects +import { useLayoutEffect, useEffect } from 'react' + +const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect + +function MyComponent() { + useIsomorphicLayoutEffect(() => { + // Browser-only code + }, []) +} +``` + +### Bun Runtime Issues + +**Problem:** `Cannot find module "react-dom/server"` with Bun + +**Solution:** Add Node.js compatibility or use Bun-specific builds: + +```json +{ + "scripts": { + "build:bun": "bun build --target=bun --outdir=dist/bun src/entry-server.tsx" + } +} +``` + +### Module Resolution Errors + +**Problem:** SSR modules not resolving correctly + +**Solution:** Configure Vite SSR externals: + +```ts +// vite.config.ts +export default defineConfig({ + ssr: { + noExternal: [ + '@tanstack/react-router', + // Add other packages that need to be bundled + ], + external: [ + // Add packages that should remain external + ], + }, +}) +``` + +### Build Output Issues + +**Problem:** Server build missing dependencies + +**Solution:** Ensure correct Rollup input configuration: + +```ts +// vite.config.ts +export default defineConfig({ + build: { + rollupOptions: { + input: { + main: './index.html', + server: './src/entry-server.tsx', + }, + output: { + entryFileNames: (chunkInfo) => { + return chunkInfo.name === 'server' + ? '[name].js' + : 'assets/[name]-[hash].js' + }, + }, + }, + }, +}) +``` + +## Related Resources + +- [SSR Guide (Detailed)](../guide/ssr.md) - Comprehensive SSR concepts +- [TanStack Start](../guide/tanstack-start.md) - Full-stack React framework +- [Data Loading](../guide/data-loading.md) - SSR-compatible data patterns + + \ No newline at end of file diff --git a/how-to-guides-implementation-plan.md b/how-to-guides-implementation-plan.md index 21300827ae..f120fe089e 100644 --- a/how-to-guides-implementation-plan.md +++ b/how-to-guides-implementation-plan.md @@ -5,7 +5,8 @@ This document outlines the multi-PR process for implementing the remaining how-t ## Progress Tracking - ✅ **Guide #1: Deploy to Production** - COMPLETED in docs/router/framework/react/how-to/deploy-to-production.md -- ⏳ **Guides #2-11** - Pending implementation +- ✅ **Guide #2: Setup SSR** - COMPLETED in docs/router/framework/react/how-to/setup-ssr.md +- ⏳ **Guides #3-11** - Pending implementation ## Implementation Process @@ -197,7 +198,7 @@ Update this section as guides are completed: ``` ✅ Guide #1: Deploy to Production - COMPLETED -⏳ Guide #2: Setup SSR - IN PROGRESS +✅ Guide #2: Setup SSR - COMPLETED ⏳ Guide #3: Migrate from React Router - PENDING ⏳ Guide #4: Fix Build Issues - PENDING ⏳ Guide #5: Integrate UI Libraries - PENDING From fdd87c67a02c7c6d28e87e5aef9a013923c28f56 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Jul 2025 21:27:03 +0000 Subject: [PATCH 02/11] Update SSR docs to recommend TanStack Start and clarify manual setup Co-authored-by: tannerlinsley --- .../framework/react/how-to/setup-ssr.md | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/docs/router/framework/react/how-to/setup-ssr.md b/docs/router/framework/react/how-to/setup-ssr.md index 29fdedccc8..1bfd8f0c7f 100644 --- a/docs/router/framework/react/how-to/setup-ssr.md +++ b/docs/router/framework/react/how-to/setup-ssr.md @@ -2,9 +2,33 @@ This guide shows you how to set up Server-Side Rendering (SSR) with TanStack Router. SSR improves initial page load performance by rendering HTML on the server before sending it to the client. +> [!IMPORTANT] +> **TanStack Start is the recommended way to use SSR with TanStack Router.** +> +> [TanStack Start](../guide/tanstack-start.md) is our official full-stack React framework that provides SSR out-of-the-box with zero configuration. It handles all the complexity shown in this guide automatically. +> +> **Use this manual setup guide only if:** +> - You need to integrate with an existing server setup +> - You're migrating from another SSR solution gradually +> - You have specific custom requirements that Start doesn't cover +> +> **For new projects, start with [TanStack Start](../guide/tanstack-start.md) instead.** + ## Quick Start -SSR with TanStack Router involves: +**Option 1: TanStack Start (Recommended)** + +```bash +npx create-start@latest my-app +cd my-app +npm run dev +``` + +That's it! You get SSR, streaming, file-based routing, and much more with zero configuration. + +**Option 2: Manual SSR Setup** + +If you need manual SSR setup, it involves: 1. **Creating shared router configuration** that works on both server and client 2. **Setting up a server entry point** to render your app server-side @@ -262,7 +286,32 @@ Add SSR build scripts to your `package.json`: } ``` -## Streaming SSR (Optional) +## Why TanStack Start Instead? + +Before diving into more advanced configurations, consider that **TanStack Start** gives you all of this and more with zero configuration: + +- ✅ **Automatic SSR & Streaming** - Works out of the box +- ✅ **File-based routing** - No manual route tree configuration +- ✅ **Built-in deployment** - Deploy to Vercel, Netlify, Cloudflare, etc. +- ✅ **API routes** - Full-stack development +- ✅ **TypeScript** - Fully typed by default +- ✅ **Optimized builds** - Production-ready bundling +- ✅ **Development experience** - Hot reload, error boundaries, dev tools + +**Quick Start with TanStack Start:** + +```bash +# Create a new Start project +npx create-start@latest my-start-app +cd my-start-app + +# Already includes SSR, routing, and everything you need +npm run dev +``` + +[Learn more about TanStack Start →](../guide/tanstack-start.md) + +## Streaming SSR (Manual Setup) For better performance with slower data fetching, enable streaming SSR: @@ -335,6 +384,9 @@ export async function handleRequest(request: Request): Promise { ## Common Problems +> [!TIP] +> **Most of these problems are automatically solved by [TanStack Start](../guide/tanstack-start.md).** The issues below are primarily relevant for manual SSR setups. + ### React Import Errors **Problem:** `ReferenceError: React is not defined` during SSR @@ -435,8 +487,8 @@ export default defineConfig({ ## Related Resources +- [TanStack Start](../guide/tanstack-start.md) - **Recommended full-stack React framework with SSR** - [SSR Guide (Detailed)](../guide/ssr.md) - Comprehensive SSR concepts -- [TanStack Start](../guide/tanstack-start.md) - Full-stack React framework - [Data Loading](../guide/data-loading.md) - SSR-compatible data patterns \ No newline at end of file +--> From 7237fd2d8a1e37ab54c60d511787ddfde9e0fcba Mon Sep 17 00:00:00 2001 From: Nico Lynzaad Date: Sun, 20 Jul 2025 18:26:59 +0200 Subject: [PATCH 10/11] update to align with examples --- .../framework/react/how-to/setup-ssr.md | 423 +++++++++++++----- 1 file changed, 304 insertions(+), 119 deletions(-) diff --git a/docs/router/framework/react/how-to/setup-ssr.md b/docs/router/framework/react/how-to/setup-ssr.md index ee3039d6a3..d3652c2123 100644 --- a/docs/router/framework/react/how-to/setup-ssr.md +++ b/docs/router/framework/react/how-to/setup-ssr.md @@ -7,11 +7,20 @@ ## Quick Start with TanStack Start ```bash -npx create-tsrouter-app@latest my-app --add-ons=start +npx create-tsrouter-app@latest my-app --template file-router cd my-app npm run dev ``` +## Install dependencies + +To server render the content, we will require a web server instance. In this guide we will be using express as our server, let us install these dependencies so long. + +```bash +npm i express compression +npm i --save-dev @types/express +``` + ## Manual SSR Setup ### 1. Create Shared Router Configuration @@ -39,8 +48,17 @@ declare module '@tanstack/react-router' { } ``` +```tsx +// src/routerContext.tsx +export type RouterContext = { + head: string +} +``` + ### 2. Set Up Server Entry Point +When a new request is received, the server entry point will be responsible for rendering the content on the first render. + ```tsx // src/entry-server.tsx import { pipeline } from 'node:stream/promises' @@ -116,6 +134,8 @@ export async function render({ ### 3. Set Up Client Entry Point +After the initial server rendering has completed, subsequent renders will be done on the client using the client entry point. + ```tsx // src/entry-client.tsx import { hydrateRoot } from 'react-dom/client' @@ -129,130 +149,279 @@ hydrateRoot(document, ) ### 4. Configure Vite for SSR +When setting up server rendering, we need to ensure that vite builds both a client side and server side bundle. +Our server and client bundles will be saved to and served from dist/server and dist/client respectively. + ```ts // vite.config.ts -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import { TanStackRouterVite } from '@tanstack/router-plugin/vite' - -export default defineConfig({ - plugins: [react(), TanStackRouterVite()], - build: { - rollupOptions: { - input: { - main: './index.html', - server: './src/entry-server.tsx', - }, - }, - }, - ssr: { - noExternal: ['@tanstack/react-router'], - }, -}) +import path from "node:path"; +import url from "node:url"; +import { tanstackRouter } from "@tanstack/router-plugin/vite"; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import type { BuildEnvironmentOptions } from "vite"; + +const __filename = url.fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// SSR configuration +const ssrBuildConfig: BuildEnvironmentOptions = { + ssr: true, + outDir: "dist/server", + ssrEmitAssets: true, + copyPublicDir: false, + emptyOutDir: true, + rollupOptions: { + input: path.resolve(__dirname, "src/entry-server.tsx"), + output: { + entryFileNames: "[name].js", + chunkFileNames: "assets/[name]-[hash].js", + assetFileNames: "assets/[name]-[hash][extname]", + }, + }, +}; + +// Client-specific configuration +const clientBuildConfig: BuildEnvironmentOptions = { + outDir: "dist/client", + emitAssets: true, + copyPublicDir: true, + emptyOutDir: true, + rollupOptions: { + input: path.resolve(__dirname, "src/entry-client.tsx"), + output: { + entryFileNames: "[name].js", + chunkFileNames: "assets/[name]-[hash].js", + assetFileNames: "assets/[name]-[hash][extname]", + }, + }, +}; + +// https://vitejs.dev/config/ +export default defineConfig((configEnv) => { + return { + plugins: [ + tanstackRouter({ target: "react", autoCodeSplitting: true }), + react(), + ], + build: configEnv.isSsrBuild ? ssrBuildConfig : clientBuildConfig, + }; +}); ``` -### 5. Create Express Server +### 5. Update our project files -```js -// server.js -import path from 'node:path' -import express from 'express' -import * as zlib from 'node:zlib' - -const isProduction = process.env.NODE_ENV === 'production' - -export async function createServer() { - const app = express() - - let vite - if (!isProduction) { - // Development: Use Vite dev server - vite = await ( - await import('vite') - ).createServer({ - server: { middlewareMode: true }, - appType: 'custom', - }) - app.use(vite.middlewares) - } else { - // Production: Serve static files and use compression - app.use( - (await import('compression')).default({ - brotli: { flush: zlib.constants.BROTLI_OPERATION_FLUSH }, - flush: zlib.constants.Z_SYNC_FLUSH, - }), - ) - app.use(express.static('./dist/client')) - } - - app.use('*', async (req, res) => { - try { - const url = req.originalUrl +Since the HTML will be rendered on the server before being sent to the client, we can use the root route to provide all our HTML needs that we would usually include in our index.html. - // Skip non-route requests - if (path.extname(url) !== '') { - console.warn(`${url} is not valid router path`) - res.status(404).end(`${url} is not valid router path`) - return - } +```tsx +//src/routes/__root.tsx +import type { RouterContext } from "@/routerContext"; +import { + HeadContent, + Outlet, + createRootRouteWithContext, +} from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; +import appCss from "../App.css?url"; + +export const Route = createRootRouteWithContext()({ + head: () => ({ + links: [ + { rel: "icon", href: "/favicon.ico" }, + { rel: "apple-touch-icon", href: "/logo192.png" }, + { rel: "manifest", href: "/manifest.json" }, + { rel: "stylesheet", href: appCss }, + ], + meta: [ + { + name: "theme-color", + content: "#000000", + }, + { + title: "TanStack Router SSR File Based", + }, + { + charSet: "UTF-8", + }, + { + name: "viewport", + content: "width=device-width, initial-scale=1.0", + }, + ], + scripts: [ + ...(!import.meta.env.PROD + ? [ + { + type: "module", + children: `import RefreshRuntime from "/@react-refresh" + RefreshRuntime.injectIntoGlobalHook(window) + window.$RefreshReg$ = () => {} + window.$RefreshSig$ = () => (type) => type + window.__vite_plugin_react_preamble_installed__ = true`, + }, + { + type: "module", + src: "/@vite/client", + }, + ] + : []), + { + type: "module", + src: import.meta.env.PROD + ? "/entry-client.js" + : "/src/entry-client.tsx", + }, + ], + }), + component: RootComponent, +}); + +function RootComponent() { + return ( + + + + + + + + + + ); +} +``` - // Extract head tags from Vite's transformation - let viteHead = '' - if (!isProduction) { - const transformed = await vite.transformIndexHtml( - url, - ``, - ) - viteHead = transformed.substring( - transformed.indexOf('') + 6, - transformed.indexOf(''), - ) - } +Now we can remove safely index.html and main.tsx - // Load entry module - const entry = !isProduction - ? await vite.ssrLoadModule('/src/entry-server.tsx') - : await import('./dist/server/entry-server.js') - - console.info('Rendering:', url) - await entry.render({ req, res, head: viteHead }) - } catch (e) { - !isProduction && vite.ssrFixStacktrace(e) - console.error(e.stack) - res.status(500).end(e.stack) - } - }) +### 6. Create Express Server - return { app, vite } +```js +// server.js +import path from "node:path"; +import express from "express"; +import * as zlib from "node:zlib"; + +const isTest = process.env.NODE_ENV === "test" || !!process.env.VITE_TEST_BUILD; + +export async function createServer( + root = process.cwd(), + isProd = process.env.NODE_ENV === "production", + hmrPort = process.env.VITE_DEV_SERVER_PORT +) { + const app = express(); + + /** + * @type {import('vite').ViteDevServer} + */ + let vite; + if (!isProd) { + vite = await (await import("vite")).createServer({ + root, + logLevel: isTest ? "error" : "info", + server: { + middlewareMode: true, + watch: { + // During tests we edit the files too fast and sometimes chokidar + // misses change events, so enforce polling for consistency + usePolling: true, + interval: 100, + }, + hmr: { + port: hmrPort, + }, + }, + appType: "custom", + }); + // use vite's connect instance as middleware + app.use(vite.middlewares); + } else { + app.use( + (await import("compression")).default({ + brotli: { + flush: zlib.constants.BROTLI_OPERATION_FLUSH, + }, + flush: zlib.constants.Z_SYNC_FLUSH, + }), + ); + } + + if (isProd) app.use(express.static("./dist/client")); + + app.use("/{*splat}", async (req, res) => { + try { + const url = req.originalUrl; + + if (path.extname(url) !== "") { + console.warn(`${url} is not valid router path`); + res.status(404); + res.end(`${url} is not valid router path`); + return; + } + + // Best effort extraction of the head from vite's index transformation hook + let viteHead = !isProd + ? await vite.transformIndexHtml( + url, + ``, + ) + : ""; + + viteHead = viteHead.substring( + viteHead.indexOf("") + 6, + viteHead.indexOf(""), + ); + + const entry = await (async () => { + if (!isProd) { + return vite.ssrLoadModule("/src/entry-server.tsx"); + } else { + return import("./dist/server/entry-server.js"); + } + })(); + + console.info("Rendering: ", url, "..."); + entry.render({ req, res, head: viteHead }); + } catch (e) { + !isProd && vite.ssrFixStacktrace(e); + console.info(e.stack); + res.status(500).end(e.stack); + } + }); + + return { app, vite }; } -// Start server -if (process.env.NODE_ENV !== 'test') { - createServer().then(({ app }) => { - app.listen(3000, () => { - console.info('Server running at http://localhost:3000') - }) - }) +if (!isTest) { + createServer().then(async ({ app }) => + app.listen(3000, () => { + console.info("Client Server: http://localhost:3000"); + }), + ); } ``` -### 6. Update Package Scripts +### 7. Update Package Scripts + +The below update ensures that: +1) During development our express server will serve the app using the vite dev server using vite middleware mode. +2) Separate build processes are used for client and server bundles. +3) In production, it will be served over the express server directly. ```json { "scripts": { "dev": "node server.js", "build": "npm run build:client && npm run build:server", - "build:client": "vite build --outDir dist/client", - "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server", - "start": "NODE_ENV=production node server.js" + "build:client": "vite build", + "build:server": "vite build --ssr", + "start": "NODE_ENV=production node server" } } ``` ## Streaming SSR -Replace `renderRouterToString` with `renderRouterToStream` for better performance: +To enable streaming rendering for better performance, replace `renderRouterToString` with `renderRouterToStream`: ```tsx // src/entry-server.tsx @@ -333,8 +502,7 @@ function MyComponent() { export default defineConfig({ ssr: { noExternal: [ - '@tanstack/react-router', - // Add other packages that need to be bundled + // Add packages that need to be bundled ], external: [ // Add packages that should remain external @@ -347,27 +515,44 @@ export default defineConfig({ **Problem:** Server build missing dependencies -**Solution:** Ensure correct Rollup input configuration: +**Solution:** Ensure correct Rollup input configuration for either client/server assets: ```ts // vite.config.ts -export default defineConfig({ - build: { - rollupOptions: { - input: { - main: './index.html', - server: './src/entry-server.tsx', - }, - output: { - entryFileNames: (chunkInfo) => { - return chunkInfo.name === 'server' - ? '[name].js' - : 'assets/[name]-[hash].js' - }, - }, - }, - }, -}) + +// SSR configuration +const ssrBuildConfig: BuildEnvironmentOptions = { + // server specific config is here + rollupOptions: { + input: path.resolve(__dirname, "src/entry-server.tsx"), + output: { + entryFileNames: "[name].js", + chunkFileNames: "assets/[name]-[hash].js", + assetFileNames: "assets/[name]-[hash][extname]", + }, + }, +}; + +// Client-specific configuration +const clientBuildConfig: BuildEnvironmentOptions = { + // client specific config is here + rollupOptions: { + input: path.resolve(__dirname, "src/entry-client.tsx"), + output: { + entryFileNames: "[name].js", + chunkFileNames: "assets/[name]-[hash].js", + assetFileNames: "assets/[name]-[hash][extname]", + }, + }, +}; + +export default defineConfig((configEnv) => { + return { + // global config + build: configEnv.isSsrBuild ? ssrBuildConfig : clientBuildConfig, + }; +}); + ``` ## Related Resources From 38853d062d7b820cbbae9ae78d2f6e209ba7a543 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 18:08:10 +0000 Subject: [PATCH 11/11] ci: apply automated fixes --- .../framework/react/how-to/setup-ssr.md | 464 +++++++++--------- 1 file changed, 233 insertions(+), 231 deletions(-) diff --git a/docs/router/framework/react/how-to/setup-ssr.md b/docs/router/framework/react/how-to/setup-ssr.md index d3652c2123..e338506683 100644 --- a/docs/router/framework/react/how-to/setup-ssr.md +++ b/docs/router/framework/react/how-to/setup-ssr.md @@ -51,7 +51,7 @@ declare module '@tanstack/react-router' { ```tsx // src/routerContext.tsx export type RouterContext = { - head: string + head: string } ``` @@ -149,64 +149,64 @@ hydrateRoot(document, ) ### 4. Configure Vite for SSR -When setting up server rendering, we need to ensure that vite builds both a client side and server side bundle. +When setting up server rendering, we need to ensure that vite builds both a client side and server side bundle. Our server and client bundles will be saved to and served from dist/server and dist/client respectively. ```ts // vite.config.ts -import path from "node:path"; -import url from "node:url"; -import { tanstackRouter } from "@tanstack/router-plugin/vite"; -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import type { BuildEnvironmentOptions } from "vite"; +import path from 'node:path' +import url from 'node:url' +import { tanstackRouter } from '@tanstack/router-plugin/vite' +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import type { BuildEnvironmentOptions } from 'vite' -const __filename = url.fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +const __filename = url.fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) // SSR configuration const ssrBuildConfig: BuildEnvironmentOptions = { - ssr: true, - outDir: "dist/server", - ssrEmitAssets: true, - copyPublicDir: false, - emptyOutDir: true, - rollupOptions: { - input: path.resolve(__dirname, "src/entry-server.tsx"), - output: { - entryFileNames: "[name].js", - chunkFileNames: "assets/[name]-[hash].js", - assetFileNames: "assets/[name]-[hash][extname]", - }, - }, -}; + ssr: true, + outDir: 'dist/server', + ssrEmitAssets: true, + copyPublicDir: false, + emptyOutDir: true, + rollupOptions: { + input: path.resolve(__dirname, 'src/entry-server.tsx'), + output: { + entryFileNames: '[name].js', + chunkFileNames: 'assets/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash][extname]', + }, + }, +} // Client-specific configuration const clientBuildConfig: BuildEnvironmentOptions = { - outDir: "dist/client", - emitAssets: true, - copyPublicDir: true, - emptyOutDir: true, - rollupOptions: { - input: path.resolve(__dirname, "src/entry-client.tsx"), - output: { - entryFileNames: "[name].js", - chunkFileNames: "assets/[name]-[hash].js", - assetFileNames: "assets/[name]-[hash][extname]", - }, - }, -}; + outDir: 'dist/client', + emitAssets: true, + copyPublicDir: true, + emptyOutDir: true, + rollupOptions: { + input: path.resolve(__dirname, 'src/entry-client.tsx'), + output: { + entryFileNames: '[name].js', + chunkFileNames: 'assets/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash][extname]', + }, + }, +} // https://vitejs.dev/config/ export default defineConfig((configEnv) => { - return { - plugins: [ - tanstackRouter({ target: "react", autoCodeSplitting: true }), - react(), - ], - build: configEnv.isSsrBuild ? ssrBuildConfig : clientBuildConfig, - }; -}); + return { + plugins: [ + tanstackRouter({ target: 'react', autoCodeSplitting: true }), + react(), + ], + build: configEnv.isSsrBuild ? ssrBuildConfig : clientBuildConfig, + } +}) ``` ### 5. Update our project files @@ -215,79 +215,79 @@ Since the HTML will be rendered on the server before being sent to the client, w ```tsx //src/routes/__root.tsx -import type { RouterContext } from "@/routerContext"; +import type { RouterContext } from '@/routerContext' import { - HeadContent, - Outlet, - createRootRouteWithContext, -} from "@tanstack/react-router"; -import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; -import appCss from "../App.css?url"; + HeadContent, + Outlet, + createRootRouteWithContext, +} from '@tanstack/react-router' +import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' +import appCss from '../App.css?url' export const Route = createRootRouteWithContext()({ - head: () => ({ - links: [ - { rel: "icon", href: "/favicon.ico" }, - { rel: "apple-touch-icon", href: "/logo192.png" }, - { rel: "manifest", href: "/manifest.json" }, - { rel: "stylesheet", href: appCss }, - ], - meta: [ - { - name: "theme-color", - content: "#000000", - }, - { - title: "TanStack Router SSR File Based", - }, - { - charSet: "UTF-8", - }, - { - name: "viewport", - content: "width=device-width, initial-scale=1.0", - }, - ], - scripts: [ - ...(!import.meta.env.PROD - ? [ - { - type: "module", - children: `import RefreshRuntime from "/@react-refresh" + head: () => ({ + links: [ + { rel: 'icon', href: '/favicon.ico' }, + { rel: 'apple-touch-icon', href: '/logo192.png' }, + { rel: 'manifest', href: '/manifest.json' }, + { rel: 'stylesheet', href: appCss }, + ], + meta: [ + { + name: 'theme-color', + content: '#000000', + }, + { + title: 'TanStack Router SSR File Based', + }, + { + charSet: 'UTF-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1.0', + }, + ], + scripts: [ + ...(!import.meta.env.PROD + ? [ + { + type: 'module', + children: `import RefreshRuntime from "/@react-refresh" RefreshRuntime.injectIntoGlobalHook(window) window.$RefreshReg$ = () => {} window.$RefreshSig$ = () => (type) => type window.__vite_plugin_react_preamble_installed__ = true`, - }, - { - type: "module", - src: "/@vite/client", - }, - ] - : []), - { - type: "module", - src: import.meta.env.PROD - ? "/entry-client.js" - : "/src/entry-client.tsx", - }, - ], - }), - component: RootComponent, -}); + }, + { + type: 'module', + src: '/@vite/client', + }, + ] + : []), + { + type: 'module', + src: import.meta.env.PROD + ? '/entry-client.js' + : '/src/entry-client.tsx', + }, + ], + }), + component: RootComponent, +}) function RootComponent() { - return ( - - - - - - - - - - ); + return ( + + + + + + + + + + ) } ``` @@ -297,115 +297,118 @@ Now we can remove safely index.html and main.tsx ```js // server.js -import path from "node:path"; -import express from "express"; -import * as zlib from "node:zlib"; +import path from 'node:path' +import express from 'express' +import * as zlib from 'node:zlib' -const isTest = process.env.NODE_ENV === "test" || !!process.env.VITE_TEST_BUILD; +const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD export async function createServer( - root = process.cwd(), - isProd = process.env.NODE_ENV === "production", - hmrPort = process.env.VITE_DEV_SERVER_PORT + root = process.cwd(), + isProd = process.env.NODE_ENV === 'production', + hmrPort = process.env.VITE_DEV_SERVER_PORT, ) { - const app = express(); - - /** - * @type {import('vite').ViteDevServer} - */ - let vite; - if (!isProd) { - vite = await (await import("vite")).createServer({ - root, - logLevel: isTest ? "error" : "info", - server: { - middlewareMode: true, - watch: { - // During tests we edit the files too fast and sometimes chokidar - // misses change events, so enforce polling for consistency - usePolling: true, - interval: 100, - }, - hmr: { - port: hmrPort, - }, - }, - appType: "custom", - }); - // use vite's connect instance as middleware - app.use(vite.middlewares); - } else { - app.use( - (await import("compression")).default({ - brotli: { - flush: zlib.constants.BROTLI_OPERATION_FLUSH, - }, - flush: zlib.constants.Z_SYNC_FLUSH, - }), - ); - } - - if (isProd) app.use(express.static("./dist/client")); - - app.use("/{*splat}", async (req, res) => { - try { - const url = req.originalUrl; - - if (path.extname(url) !== "") { - console.warn(`${url} is not valid router path`); - res.status(404); - res.end(`${url} is not valid router path`); - return; - } - - // Best effort extraction of the head from vite's index transformation hook - let viteHead = !isProd - ? await vite.transformIndexHtml( - url, - ``, - ) - : ""; - - viteHead = viteHead.substring( - viteHead.indexOf("") + 6, - viteHead.indexOf(""), - ); - - const entry = await (async () => { - if (!isProd) { - return vite.ssrLoadModule("/src/entry-server.tsx"); - } else { - return import("./dist/server/entry-server.js"); - } - })(); - - console.info("Rendering: ", url, "..."); - entry.render({ req, res, head: viteHead }); - } catch (e) { - !isProd && vite.ssrFixStacktrace(e); - console.info(e.stack); - res.status(500).end(e.stack); - } - }); - - return { app, vite }; + const app = express() + + /** + * @type {import('vite').ViteDevServer} + */ + let vite + if (!isProd) { + vite = await ( + await import('vite') + ).createServer({ + root, + logLevel: isTest ? 'error' : 'info', + server: { + middlewareMode: true, + watch: { + // During tests we edit the files too fast and sometimes chokidar + // misses change events, so enforce polling for consistency + usePolling: true, + interval: 100, + }, + hmr: { + port: hmrPort, + }, + }, + appType: 'custom', + }) + // use vite's connect instance as middleware + app.use(vite.middlewares) + } else { + app.use( + (await import('compression')).default({ + brotli: { + flush: zlib.constants.BROTLI_OPERATION_FLUSH, + }, + flush: zlib.constants.Z_SYNC_FLUSH, + }), + ) + } + + if (isProd) app.use(express.static('./dist/client')) + + app.use('/{*splat}', async (req, res) => { + try { + const url = req.originalUrl + + if (path.extname(url) !== '') { + console.warn(`${url} is not valid router path`) + res.status(404) + res.end(`${url} is not valid router path`) + return + } + + // Best effort extraction of the head from vite's index transformation hook + let viteHead = !isProd + ? await vite.transformIndexHtml( + url, + ``, + ) + : '' + + viteHead = viteHead.substring( + viteHead.indexOf('') + 6, + viteHead.indexOf(''), + ) + + const entry = await (async () => { + if (!isProd) { + return vite.ssrLoadModule('/src/entry-server.tsx') + } else { + return import('./dist/server/entry-server.js') + } + })() + + console.info('Rendering: ', url, '...') + entry.render({ req, res, head: viteHead }) + } catch (e) { + !isProd && vite.ssrFixStacktrace(e) + console.info(e.stack) + res.status(500).end(e.stack) + } + }) + + return { app, vite } } if (!isTest) { - createServer().then(async ({ app }) => - app.listen(3000, () => { - console.info("Client Server: http://localhost:3000"); - }), - ); + createServer().then(async ({ app }) => + app.listen(3000, () => { + console.info('Client Server: http://localhost:3000') + }), + ) } ``` ### 7. Update Package Scripts The below update ensures that: -1) During development our express server will serve the app using the vite dev server using vite middleware mode. -2) Separate build processes are used for client and server bundles. -3) In production, it will be served over the express server directly. + +1. During development our express server will serve the app using the vite dev server using vite middleware mode. +2. Separate build processes are used for client and server bundles. +3. In production, it will be served over the express server directly. ```json { @@ -522,37 +525,36 @@ export default defineConfig({ // SSR configuration const ssrBuildConfig: BuildEnvironmentOptions = { - // server specific config is here - rollupOptions: { - input: path.resolve(__dirname, "src/entry-server.tsx"), - output: { - entryFileNames: "[name].js", - chunkFileNames: "assets/[name]-[hash].js", - assetFileNames: "assets/[name]-[hash][extname]", - }, - }, -}; + // server specific config is here + rollupOptions: { + input: path.resolve(__dirname, 'src/entry-server.tsx'), + output: { + entryFileNames: '[name].js', + chunkFileNames: 'assets/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash][extname]', + }, + }, +} // Client-specific configuration const clientBuildConfig: BuildEnvironmentOptions = { - // client specific config is here - rollupOptions: { - input: path.resolve(__dirname, "src/entry-client.tsx"), - output: { - entryFileNames: "[name].js", - chunkFileNames: "assets/[name]-[hash].js", - assetFileNames: "assets/[name]-[hash][extname]", - }, - }, -}; + // client specific config is here + rollupOptions: { + input: path.resolve(__dirname, 'src/entry-client.tsx'), + output: { + entryFileNames: '[name].js', + chunkFileNames: 'assets/[name]-[hash].js', + assetFileNames: 'assets/[name]-[hash][extname]', + }, + }, +} export default defineConfig((configEnv) => { - return { - // global config - build: configEnv.isSsrBuild ? ssrBuildConfig : clientBuildConfig, - }; -}); - + return { + // global config + build: configEnv.isSsrBuild ? ssrBuildConfig : clientBuildConfig, + } +}) ``` ## Related Resources