Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
77fc5cf
fix: pre-rendering all static pages
FatahChan Oct 12, 2025
c1798fa
fix: build test
FatahChan Oct 12, 2025
02f1298
debugging
FatahChan Oct 14, 2025
a86d140
fix: update prerendering logic to use generator for static paths and …
FatahChan Oct 14, 2025
e319ea1
fix: update prerenderPages to track prerendered pages instead of seen…
FatahChan Oct 14, 2025
5bf61f8
feat: add dummy server scripts and update test commands for prerendering
FatahChan Oct 14, 2025
de3a355
docs: enhance static prerendering documentation with automatic route …
FatahChan Oct 14, 2025
1e486a3
ci: apply automated fixes
autofix-ci[bot] Oct 14, 2025
04c2890
fix: remove unnecessary logging of startConfig.pages in prerender fun…
FatahChan Oct 14, 2025
0192c7b
feat: implement prerenderRoutesPlugin
FatahChan Oct 15, 2025
a1d8e47
fix: rename redirectCount to maxRedirect and add warning
FatahChan Oct 15, 2025
df99119
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
349d719
chore: align naming maxRedirect
FatahChan Oct 15, 2025
a43b1a2
chore: Add fallback for empty discovery results.
FatahChan Oct 15, 2025
e9c62f1
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
30abba8
feat: enhance prerendering configuration with automatic static paths …
FatahChan Oct 15, 2025
12b91ff
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
a175f54
fix: handle undefined TSS_PRERENDABLE_PATHS in prerender function
FatahChan Oct 15, 2025
0eba284
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
42d82ee
fix: correct typo in prerendering error handling comment
FatahChan Oct 15, 2025
aec1ea0
fix: add missing comma in prerender configuration
FatahChan Oct 15, 2025
6a2cd74
fix: handle undefined headers in prerender options
FatahChan Oct 15, 2025
4cf866c
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
6be5de2
test: add test to e2e:basic instead of a new e2e
FatahChan Oct 15, 2025
f63f8ee
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
7c6fb62
fix: clarify automatic static route discovery and crawling links sect…
FatahChan Oct 15, 2025
4dd5c5f
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2025
c02af7b
fix: remove deprecated e2e/react-start/basic-prerendering dependencies
FatahChan Oct 15, 2025
bd59aaa
fix: restore lock file
FatahChan Oct 15, 2025
a6dac9d
fix: update static prerendering documentation for clarity and additio…
FatahChan Oct 15, 2025
f27c5d3
fix: remove unnecessary --ui flag from prerender test command
FatahChan Oct 15, 2025
bb188a4
fix: clarify autoStaticPathsDiscovery behavior in static prerendering…
FatahChan Oct 15, 2025
af74015
fix: enable autoStaticPathsDiscovery and update Solid plugin imports …
FatahChan Oct 15, 2025
442a5ec
fix: improve clarity in static prerendering documentation regarding p…
FatahChan Oct 15, 2025
5e32fa5
fix: add prerendering tests for static path discovery and content ver…
FatahChan Oct 15, 2025
1cf05b1
fix: standardize maxRedirect option naming to maxRedirects across doc…
FatahChan Oct 18, 2025
8d25746
fix: enforce minimum value for maxRedirects option in start plugin sc…
FatahChan Oct 18, 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.html'))).toBe(true)
expect(existsSync(join(distDir, 'users.html'))).toBe(true)
expect(existsSync(join(distDir, 'deferred.html'))).toBe(true)
expect(existsSync(join(distDir, 'scripts.html'))).toBe(true)
expect(existsSync(join(distDir, 'inline-scripts.html'))).toBe(true)
expect(existsSync(join(distDir, '대한민국.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.html'))).toBe(true)

// "Select a post." should be in the prerendered HTML
const html = readFileSync(join(distDir, 'posts.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.html'))).toBe(true)

// "Select a user." should be in the prerendered HTML
const html = readFileSync(join(distDir, 'users.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