Skip to content

Commit 475707e

Browse files
FatahChanautofix-ci[bot]schiller-manuel
authored
fix: auto discover static routes for prerendering
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Manuel Schiller <manuel.schiller@caligano.de>
1 parent 9bc2969 commit 475707e

File tree

14 files changed

+251
-24
lines changed

14 files changed

+251
-24
lines changed

docs/start/framework/react/guide/static-prerendering.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export default defineConfig({
2525
// Enable if you need pages to be at `/page/index.html` instead of `/page.html`
2626
autoSubfolderIndex: true,
2727

28+
// If disabled, only the root path or the paths defined in the pages config will be prerendered
29+
autoStaticPathsDiscovery: true,
30+
2831
// How many prerender jobs to run at once
2932
concurrency: 14,
3033

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

46+
// Maximum number of redirects to follow during prerendering
47+
maxRedirects: 5,
48+
49+
// Fail if an error occurs during prerendering
50+
failOnError: true,
51+
4352
// Callback when page is successfully rendered
4453
onSuccess: ({ page }) => {
4554
console.log(`Rendered ${page.path}!`)
4655
},
4756
},
48-
// Optional configuration for specific pages (without this it will still automatically
49-
// prerender all routes)
57+
// Optional configuration for specific pages
58+
// Note: When autoStaticPathsDiscovery is enabled (default), discovered static
59+
// routes will be merged with the pages specified below
5060
pages: [
5161
{
5262
path: '/my-page',
@@ -58,3 +68,21 @@ export default defineConfig({
5868
],
5969
})
6070
```
71+
72+
## Automatic Static Route Discovery
73+
74+
All static paths will be automatically discovered and seamlessly merged with the specified `pages` config
75+
76+
Routes are excluded from automatic discovery in the following cases:
77+
78+
- Routes with path parameters (e.g., `/users/$userId`) since they require specific parameter values
79+
- Layout routes (prefixed with `_`) since they don't render standalone pages
80+
- Routes without components (e.g., API routes)
81+
82+
Note: Dynamic routes can still be prerendered if they are linked from other pages when `crawlLinks` is enabled.
83+
84+
## Crawling Links
85+
86+
When `crawlLinks` is enabled (default: `true`), TanStack Start will extract links from prerendered pages and prerender those linked pages as well.
87+
88+
For example, if `/` contains a link to `/posts`, then `/posts` will also be automatically prerendered.

docs/start/framework/solid/guide/static-prerendering.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export default defineConfig({
2525
// Enable if you need pages to be at `/page/index.html` instead of `/page.html`
2626
autoSubfolderIndex: true,
2727

28+
// If disabled, only the root path or the paths defined in the pages config will be prerendered
29+
autoStaticPathsDiscovery: true,
30+
2831
// How many prerender jobs to run at once
2932
concurrency: 14,
3033

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

46+
// Maximum number of redirects to follow during prerendering
47+
maxRedirects: 5,
48+
49+
// Fail if an error occurs during prerendering
50+
failOnError: true,
51+
4352
// Callback when page is successfully rendered
4453
onSuccess: ({ page }) => {
4554
console.log(`Rendered ${page.path}!`)
4655
},
4756
},
48-
// Optional configuration for specific pages (without this it will still automatically
49-
// prerender all routes)
57+
// Optional configuration for specific pages
58+
// Note: When autoStaticPathsDiscovery is enabled (default), discovered static
59+
// routes will be merged with the pages specified below
5060
pages: [
5161
{
5262
path: '/my-page',
@@ -58,3 +68,21 @@ export default defineConfig({
5868
],
5969
})
6070
```
71+
72+
## Automatic Static Route Discovery
73+
74+
All static paths will be automatically discovered and seamlessly merged with the specified `pages` config
75+
76+
Routes are excluded from automatic discovery in the following cases:
77+
78+
- Routes with path parameters (e.g., `/users/$userId`) since they require specific parameter values
79+
- Layout routes (prefixed with `_`) since they don't render standalone pages
80+
- Routes without components (e.g., API routes)
81+
82+
Note: Dynamic routes can still be prerendered if they are linked from other pages when `crawlLinks` is enabled.
83+
84+
## Crawling Links
85+
86+
When `crawlLinks` is enabled (default: `true`), TanStack Start will extract links from prerendered pages and prerender those linked pages as well.
87+
88+
For example, if `/` contains a link to `/posts`, then `/posts` will also be automatically prerendered.

e2e/react-start/basic/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@
88
"dev:e2e": "vite dev",
99
"build": "vite build && tsc --noEmit",
1010
"build:spa": "MODE=spa vite build && tsc --noEmit",
11+
"build:prerender": "MODE=prerender vite build && tsc --noEmit",
1112
"start": "pnpx srvx --prod -s ../client dist/server/server.js",
1213
"start:spa": "node server.js",
14+
"test:e2e:startDummyServer": "node -e 'import(\"./tests/setup/global.setup.ts\").then(m => m.default())' &",
15+
"test:e2e:stopDummyServer": "node -e 'import(\"./tests/setup/global.teardown.ts\").then(m => m.default())'",
1316
"test:e2e:spaMode": "rm -rf port*.txt; MODE=spa playwright test --project=chromium",
1417
"test:e2e:ssrMode": "rm -rf port*.txt; playwright test --project=chromium",
15-
"test:e2e": "pnpm run test:e2e:spaMode && pnpm run test:e2e:ssrMode"
18+
"test:e2e:prerender": "rm -rf port*.txt; MODE=prerender playwright test --project=chromium",
19+
"test:e2e": "pnpm run test:e2e:spaMode && pnpm run test:e2e:ssrMode && pnpm run test:e2e:prerender"
1620
},
1721
"dependencies": {
1822
"@tanstack/react-router": "workspace:^",

e2e/react-start/basic/playwright.config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getTestServerPort,
55
} from '@tanstack/router-e2e-utils'
66
import { isSpaMode } from './tests/utils/isSpaMode'
7+
import { isPrerender } from './tests/utils/isPrerender'
78
import packageJson from './package.json' with { type: 'json' }
89

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

22+
const getCommand = () => {
23+
if (isSpaMode) return spaModeCommand
24+
if (isPrerender) return prerenderModeCommand
25+
return ssrModeCommand
26+
}
2027
console.log('running in spa mode: ', isSpaMode.toString())
28+
console.log('running in prerender mode: ', isPrerender.toString())
2129
/**
2230
* See https://playwright.dev/docs/test-configuration.
2331
*/
@@ -35,7 +43,7 @@ export default defineConfig({
3543
},
3644

3745
webServer: {
38-
command: isSpaMode ? spaModeCommand : ssrModeCommand,
46+
command: getCommand(),
3947
url: baseURL,
4048
reuseExistingServer: !process.env.CI,
4149
stdout: 'pipe',
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { existsSync, readFileSync } from 'node:fs'
2+
import { join } from 'node:path'
3+
import { expect } from '@playwright/test'
4+
import { test } from '@tanstack/router-e2e-utils'
5+
import { isPrerender } from './utils/isPrerender'
6+
7+
test.describe('Prerender Static Path Discovery', () => {
8+
test.skip(!isPrerender, 'Skipping since not in prerender mode')
9+
test.describe('Build Output Verification', () => {
10+
test('should automatically discover and prerender static routes', () => {
11+
// Check that static routes were automatically discovered and prerendered
12+
const distDir = join(process.cwd(), 'dist', 'client')
13+
14+
// These static routes should be automatically discovered and prerendered
15+
expect(existsSync(join(distDir, 'index.html'))).toBe(true)
16+
expect(existsSync(join(distDir, 'posts/index.html'))).toBe(true)
17+
expect(existsSync(join(distDir, 'users/index.html'))).toBe(true)
18+
expect(existsSync(join(distDir, 'deferred/index.html'))).toBe(true)
19+
expect(existsSync(join(distDir, 'scripts/index.html'))).toBe(true)
20+
expect(existsSync(join(distDir, 'inline-scripts/index.html'))).toBe(true)
21+
expect(existsSync(join(distDir, '대한민국/index.html'))).toBe(true)
22+
23+
// Pathless layouts should NOT be prerendered (they start with _)
24+
expect(existsSync(join(distDir, '_layout', 'index.html'))).toBe(false) // /_layout
25+
26+
// API routes should NOT be prerendered
27+
28+
expect(existsSync(join(distDir, 'api', 'users', 'index.html'))).toBe(
29+
false,
30+
) // /api/users
31+
})
32+
})
33+
34+
test.describe('Static Files Verification', () => {
35+
test('should contain prerendered content in posts.html', () => {
36+
const distDir = join(process.cwd(), 'dist', 'client')
37+
expect(existsSync(join(distDir, 'posts/index.html'))).toBe(true)
38+
39+
// "Select a post." should be in the prerendered HTML
40+
const html = readFileSync(join(distDir, 'posts/index.html'), 'utf-8')
41+
expect(html).toContain('Select a post.')
42+
})
43+
44+
test('should contain prerendered content in users.html', () => {
45+
const distDir = join(process.cwd(), 'dist', 'client')
46+
expect(existsSync(join(distDir, 'users/index.html'))).toBe(true)
47+
48+
// "Select a user." should be in the prerendered HTML
49+
const html = readFileSync(join(distDir, 'users/index.html'), 'utf-8')
50+
expect(html).toContain('Select a user.')
51+
})
52+
})
53+
})

e2e/react-start/basic/tests/search-params.spec.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect } from '@playwright/test'
22
import { test } from '@tanstack/router-e2e-utils'
33
import { isSpaMode } from 'tests/utils/isSpaMode'
4+
import { isPrerender } from './utils/isPrerender'
45
import type { Response } from '@playwright/test'
56

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

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

@@ -52,7 +53,7 @@ test.describe('/search-params/default', () => {
5253
page,
5354
}) => {
5455
const response = await page.goto('/search-params/default')
55-
if (!isSpaMode) {
56+
if (!isSpaMode && !isPrerender) {
5657
expectRedirect(response, '/search-params/default?default=d1')
5758
}
5859
await expect(page.getByTestId('search-default')).toContainText('d1')
@@ -65,7 +66,7 @@ test.describe('/search-params/default', () => {
6566
test('Directly visiting the route with search param set', async ({
6667
page,
6768
}) => {
68-
const response = await page.goto('/search-params/default/?default=d2')
69+
const response = await page.goto('/search-params/default?default=d2')
6970
expectNoRedirect(response)
7071

7172
await expect(page.getByTestId('search-default')).toContainText('d2')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const isPrerender: boolean = process.env.MODE === 'prerender'

e2e/react-start/basic/vite.config.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import tsConfigPaths from 'vite-tsconfig-paths'
33
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
44
import viteReact from '@vitejs/plugin-react'
55
import { isSpaMode } from './tests/utils/isSpaMode'
6+
import { isPrerender } from './tests/utils/isPrerender'
67

78
const spaModeConfiguration = {
89
enabled: true,
@@ -11,6 +12,19 @@ const spaModeConfiguration = {
1112
},
1213
}
1314

15+
const prerenderConfiguration = {
16+
enabled: true,
17+
filter: (page: { path: string }) =>
18+
![
19+
'/this-route-does-not-exist',
20+
'/redirect',
21+
'/i-do-not-exist',
22+
'/not-found/via-beforeLoad',
23+
'/not-found/via-loader',
24+
].some((p) => page.path.includes(p)),
25+
maxRedirects: 100,
26+
}
27+
1428
export default defineConfig({
1529
server: {
1630
port: 3000,
@@ -22,6 +36,7 @@ export default defineConfig({
2236
// @ts-ignore we want to keep one test with verboseFileRoutes off even though the option is hidden
2337
tanstackStart({
2438
spa: isSpaMode ? spaModeConfiguration : undefined,
39+
prerender: isPrerender ? prerenderConfiguration : undefined,
2540
}),
2641
viteReact(),
2742
],

packages/router-generator/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export {
2727
format,
2828
removeExt,
2929
checkRouteFullPathUniqueness,
30+
inferFullPath,
3031
} from './utils'
3132

3233
export type {

packages/start-plugin-core/src/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ import type { Manifest } from '@tanstack/router-core'
33
/* eslint-disable no-var */
44
declare global {
55
var TSS_ROUTES_MANIFEST: Manifest
6+
var TSS_PRERENDABLE_PATHS: Array<{ path: string }> | undefined
67
}
78
export {}

0 commit comments

Comments
 (0)