Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
dd5f473
fix: pre-rendering all static pages
FatahChan Oct 12, 2025
4037b04
fix: build test
FatahChan Oct 12, 2025
19f4746
debugging
FatahChan Oct 14, 2025
55f0a97
fix: update prerendering logic to use generator for static paths and …
FatahChan Oct 14, 2025
b6b4d04
fix: update prerenderPages to track prerendered pages instead of seen…
FatahChan Oct 14, 2025
6fc92c9
feat: add dummy server scripts and update test commands for prerendering
FatahChan Oct 14, 2025
a38ed5c
docs: enhance static prerendering documentation with automatic route …
FatahChan Oct 14, 2025
63b2305
ci: apply automated fixes
autofix-ci[bot] Oct 14, 2025
08be4d7
fix: remove unnecessary logging of startConfig.pages in prerender fun…
FatahChan Oct 14, 2025
1c50e2c
feat: implement prerenderRoutesPlugin
FatahChan Oct 15, 2025
5be64ab
fix: rename redirectCount to maxRedirect and add warning
FatahChan Oct 15, 2025
d45f2df
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
3290ddd
chore: align naming maxRedirect
FatahChan Oct 15, 2025
14c48ed
chore: Add fallback for empty discovery results.
FatahChan Oct 15, 2025
779f9ad
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
32bda4a
feat: enhance prerendering configuration with automatic static paths …
FatahChan Oct 15, 2025
6ac943c
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
76288e0
fix: handle undefined TSS_PRERENDABLE_PATHS in prerender function
FatahChan Oct 15, 2025
f6f1f12
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
9624ad1
fix: correct typo in prerendering error handling comment
FatahChan Oct 15, 2025
969992c
fix: add missing comma in prerender configuration
FatahChan Oct 15, 2025
9d691d3
fix: handle undefined headers in prerender options
FatahChan Oct 15, 2025
9f8b48f
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
28c20fc
test: add test to e2e:basic instead of a new e2e
FatahChan Oct 15, 2025
aa3d962
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
1a47b76
fix: clarify automatic static route discovery and crawling links sect…
FatahChan Oct 15, 2025
0617f8f
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
42a963c
fix: remove deprecated e2e/react-start/basic-prerendering dependencies
FatahChan Oct 15, 2025
c1ee33d
fix: restore lock file
FatahChan Oct 15, 2025
616f4ff
fix: update static prerendering documentation for clarity and additio…
FatahChan Oct 15, 2025
0ae2e67
fix: remove unnecessary --ui flag from prerender test command
FatahChan Oct 15, 2025
a3c8408
fix: clarify autoStaticPathsDiscovery behavior in static prerendering…
FatahChan Oct 15, 2025
2653c87
fix: enable autoStaticPathsDiscovery and update Solid plugin imports …
FatahChan Oct 15, 2025
a9f3720
fix: improve clarity in static prerendering documentation regarding p…
FatahChan Oct 15, 2025
d5093c1
fix: add prerendering tests for static path discovery and content ver…
FatahChan Oct 15, 2025
71334bc
fix: standardize maxRedirect option naming to maxRedirects across doc…
FatahChan Oct 18, 2025
67888a6
fix: enforce minimum value for maxRedirects option in start plugin sc…
FatahChan Oct 18, 2025
00a811c
fix tests
schiller-manuel Oct 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions docs/start/framework/react/guide/static-prerendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export default defineConfig({
// Enable if you need pages to be at `/page/index.html` instead of `/page.html`
autoSubfolderIndex: true,

// If disabled, only the root path or the paths defined in the pages config will be prerendered
autoStaticPathsDiscovery: true,

// How many prerender jobs to run at once
concurrency: 14,

Expand All @@ -40,13 +43,20 @@ export default defineConfig({
// Delay between retries in milliseconds
retryDelay: 1000,

// Maximum number of redirects to follow during prerendering
maxRedirects: 5,

// Fail if an error occurs during prerendering
failOnError: true,

// Callback when page is successfully rendered
onSuccess: ({ page }) => {
console.log(`Rendered ${page.path}!`)
},
},
// Optional configuration for specific pages (without this it will still automatically
// prerender all routes)
// Optional configuration for specific pages
// Note: When autoStaticPathsDiscovery is enabled (default), discovered static
// routes will be merged with the pages specified below
pages: [
{
path: '/my-page',
Expand All @@ -58,3 +68,21 @@ export default defineConfig({
],
})
```

## Automatic Static Route Discovery

All static paths will be automatically discovered and seamlessly merged with the specified `pages` config

Routes are excluded from automatic discovery in the following cases:

- Routes with path parameters (e.g., `/users/$userId`) since they require specific parameter values
- Layout routes (prefixed with `_`) since they don't render standalone pages
- Routes without components (e.g., API routes)

Note: Dynamic routes can still be prerendered if they are linked from other pages when `crawlLinks` is enabled.

## Crawling Links

When `crawlLinks` is enabled (default: `true`), TanStack Start will extract links from prerendered pages and prerender those linked pages as well.

For example, if `/` contains a link to `/posts`, then `/posts` will also be automatically prerendered.
32 changes: 30 additions & 2 deletions docs/start/framework/solid/guide/static-prerendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export default defineConfig({
// Enable if you need pages to be at `/page/index.html` instead of `/page.html`
autoSubfolderIndex: true,

// If disabled, only the root path or the paths defined in the pages config will be prerendered
autoStaticPathsDiscovery: true,

// How many prerender jobs to run at once
concurrency: 14,

Expand All @@ -40,13 +43,20 @@ export default defineConfig({
// Delay between retries in milliseconds
retryDelay: 1000,

// Maximum number of redirects to follow during prerendering
maxRedirects: 5,

// Fail if an error occurs during prerendering
failOnError: true,

// Callback when page is successfully rendered
onSuccess: ({ page }) => {
console.log(`Rendered ${page.path}!`)
},
},
// Optional configuration for specific pages (without this it will still automatically
// prerender all routes)
// Optional configuration for specific pages
// Note: When autoStaticPathsDiscovery is enabled (default), discovered static
// routes will be merged with the pages specified below
pages: [
{
path: '/my-page',
Expand All @@ -58,3 +68,21 @@ export default defineConfig({
],
})
```

## Automatic Static Route Discovery

All static paths will be automatically discovered and seamlessly merged with the specified `pages` config

Routes are excluded from automatic discovery in the following cases:

- Routes with path parameters (e.g., `/users/$userId`) since they require specific parameter values
- Layout routes (prefixed with `_`) since they don't render standalone pages
- Routes without components (e.g., API routes)

Note: Dynamic routes can still be prerendered if they are linked from other pages when `crawlLinks` is enabled.

## Crawling Links

When `crawlLinks` is enabled (default: `true`), TanStack Start will extract links from prerendered pages and prerender those linked pages as well.

For example, if `/` contains a link to `/posts`, then `/posts` will also be automatically prerendered.
6 changes: 5 additions & 1 deletion e2e/react-start/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
"dev:e2e": "vite dev",
"build": "vite build && tsc --noEmit",
"build:spa": "MODE=spa vite build && tsc --noEmit",
"build:prerender": "MODE=prerender vite build && tsc --noEmit",
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
"start:spa": "node server.js",
"test:e2e:startDummyServer": "node -e 'import(\"./tests/setup/global.setup.ts\").then(m => m.default())' &",
"test:e2e:stopDummyServer": "node -e 'import(\"./tests/setup/global.teardown.ts\").then(m => m.default())'",
"test:e2e:spaMode": "rm -rf port*.txt; MODE=spa playwright test --project=chromium",
"test:e2e:ssrMode": "rm -rf port*.txt; playwright test --project=chromium",
"test:e2e": "pnpm run test:e2e:spaMode && pnpm run test:e2e:ssrMode"
"test:e2e:prerender": "rm -rf port*.txt; MODE=prerender playwright test --project=chromium",
"test:e2e": "pnpm run test:e2e:spaMode && pnpm run test:e2e:ssrMode && pnpm run test:e2e:prerender"
},
"dependencies": {
"@tanstack/react-router": "workspace:^",
Expand Down
10 changes: 9 additions & 1 deletion e2e/react-start/basic/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getTestServerPort,
} from '@tanstack/router-e2e-utils'
import { isSpaMode } from './tests/utils/isSpaMode'
import { isPrerender } from './tests/utils/isPrerender'
import packageJson from './package.json' with { type: 'json' }

const PORT = await getTestServerPort(
Expand All @@ -16,8 +17,15 @@ const EXTERNAL_PORT = await getDummyServerPort(packageJson.name)
const baseURL = `http://localhost:${PORT}`
const spaModeCommand = `pnpm build:spa && pnpm start:spa`
const ssrModeCommand = `pnpm build && pnpm start`
const prerenderModeCommand = `pnpm run test:e2e:startDummyServer && pnpm build:prerender && pnpm run test:e2e:stopDummyServer && pnpm start`

const getCommand = () => {
if (isSpaMode) return spaModeCommand
if (isPrerender) return prerenderModeCommand
return ssrModeCommand
}
console.log('running in spa mode: ', isSpaMode.toString())
console.log('running in prerender mode: ', isPrerender.toString())
/**
* See https://playwright.dev/docs/test-configuration.
*/
Expand All @@ -35,7 +43,7 @@ export default defineConfig({
},

webServer: {
command: isSpaMode ? spaModeCommand : ssrModeCommand,
command: getCommand(),
url: baseURL,
reuseExistingServer: !process.env.CI,
stdout: 'pipe',
Expand Down
53 changes: 53 additions & 0 deletions e2e/react-start/basic/tests/prerendering.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'
import { isPrerender } from './utils/isPrerender'

test.describe('Prerender Static Path Discovery', () => {
test.skip(!isPrerender, 'Skipping since not in prerender mode')
test.describe('Build Output Verification', () => {
test('should automatically discover and prerender static routes', () => {
// Check that static routes were automatically discovered and prerendered
const distDir = join(process.cwd(), 'dist', 'client')

// These static routes should be automatically discovered and prerendered
expect(existsSync(join(distDir, 'index.html'))).toBe(true)
expect(existsSync(join(distDir, 'posts/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'users/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'deferred/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'scripts/index.html'))).toBe(true)
expect(existsSync(join(distDir, 'inline-scripts/index.html'))).toBe(true)
expect(existsSync(join(distDir, '대한민국/index.html'))).toBe(true)

// Pathless layouts should NOT be prerendered (they start with _)
expect(existsSync(join(distDir, '_layout', 'index.html'))).toBe(false) // /_layout

// API routes should NOT be prerendered

expect(existsSync(join(distDir, 'api', 'users', 'index.html'))).toBe(
false,
) // /api/users
})
})

test.describe('Static Files Verification', () => {
test('should contain prerendered content in posts.html', () => {
const distDir = join(process.cwd(), 'dist', 'client')
expect(existsSync(join(distDir, 'posts/index.html'))).toBe(true)

// "Select a post." should be in the prerendered HTML
const html = readFileSync(join(distDir, 'posts/index.html'), 'utf-8')
expect(html).toContain('Select a post.')
})

test('should contain prerendered content in users.html', () => {
const distDir = join(process.cwd(), 'dist', 'client')
expect(existsSync(join(distDir, 'users/index.html'))).toBe(true)

// "Select a user." should be in the prerendered HTML
const html = readFileSync(join(distDir, 'users/index.html'), 'utf-8')
expect(html).toContain('Select a user.')
})
})
})
7 changes: 4 additions & 3 deletions e2e/react-start/basic/tests/search-params.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from '@playwright/test'
import { test } from '@tanstack/router-e2e-utils'
import { isSpaMode } from 'tests/utils/isSpaMode'
import { isPrerender } from './utils/isPrerender'
import type { Response } from '@playwright/test'

function expectRedirect(response: Response | null, endsWith: string) {
Expand All @@ -27,7 +28,7 @@ test.describe('/search-params/loader-throws-redirect', () => {
}) => {
const response = await page.goto('/search-params/loader-throws-redirect')

if (!isSpaMode) {
if (!isSpaMode && !isPrerender) {
expectRedirect(response, '/search-params/loader-throws-redirect?step=a')
}

Expand All @@ -52,7 +53,7 @@ test.describe('/search-params/default', () => {
page,
}) => {
const response = await page.goto('/search-params/default')
if (!isSpaMode) {
if (!isSpaMode && !isPrerender) {
expectRedirect(response, '/search-params/default?default=d1')
}
await expect(page.getByTestId('search-default')).toContainText('d1')
Expand All @@ -65,7 +66,7 @@ test.describe('/search-params/default', () => {
test('Directly visiting the route with search param set', async ({
page,
}) => {
const response = await page.goto('/search-params/default/?default=d2')
const response = await page.goto('/search-params/default?default=d2')
expectNoRedirect(response)

await expect(page.getByTestId('search-default')).toContainText('d2')
Expand Down
1 change: 1 addition & 0 deletions e2e/react-start/basic/tests/utils/isPrerender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isPrerender: boolean = process.env.MODE === 'prerender'
15 changes: 15 additions & 0 deletions e2e/react-start/basic/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import tsConfigPaths from 'vite-tsconfig-paths'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import { isSpaMode } from './tests/utils/isSpaMode'
import { isPrerender } from './tests/utils/isPrerender'

const spaModeConfiguration = {
enabled: true,
Expand All @@ -11,6 +12,19 @@ const spaModeConfiguration = {
},
}

const prerenderConfiguration = {
enabled: true,
filter: (page: { path: string }) =>
![
'/this-route-does-not-exist',
'/redirect',
'/i-do-not-exist',
'/not-found/via-beforeLoad',
'/not-found/via-loader',
].some((p) => page.path.includes(p)),
maxRedirects: 100,
}

export default defineConfig({
server: {
port: 3000,
Expand All @@ -22,6 +36,7 @@ export default defineConfig({
// @ts-ignore we want to keep one test with verboseFileRoutes off even though the option is hidden
tanstackStart({
spa: isSpaMode ? spaModeConfiguration : undefined,
prerender: isPrerender ? prerenderConfiguration : undefined,
}),
viteReact(),
],
Expand Down
1 change: 1 addition & 0 deletions packages/router-generator/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export {
format,
removeExt,
checkRouteFullPathUniqueness,
inferFullPath,
} from './utils'

export type {
Expand Down
1 change: 1 addition & 0 deletions packages/start-plugin-core/src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import type { Manifest } from '@tanstack/router-core'
/* eslint-disable no-var */
declare global {
var TSS_ROUTES_MANIFEST: Manifest
var TSS_PRERENDABLE_PATHS: Array<{ path: string }> | undefined
}
export {}
Loading
Loading