Skip to content

feat(start): add pre-render params + sitemap#7346

Draft
nikuscs wants to merge 9 commits intoTanStack:mainfrom
nikuscs:feat-server-side-params
Draft

feat(start): add pre-render params + sitemap#7346
nikuscs wants to merge 9 commits intoTanStack:mainfrom
nikuscs:feat-server-side-params

Conversation

@nikuscs
Copy link
Copy Markdown
Contributor

@nikuscs nikuscs commented May 5, 2026

Summary

Adds route-level prerenderParams support for TanStack Start so dynamic routes can declare which params/search combinations should be prerendered at build time.

This lets routes like /posts/$postId generate static output for known params without manually listing every final URL in the global prerender config. It also makes dynamic prerendering first-class for sitemap generation, so sitemap.xml can include URLs produced from route params instead of only statically discovered routes.

Features

  • Adds typed prerenderParams route options through Start route module augmentation.
  • Supports async prerenderParams callbacks with abort signal support.
  • Generates prerender pages from returned params and optional search values.
  • Supports route-level and per-entry sitemap metadata for generated dynamic pages.
  • Generates sitemap.xml entries for dynamic URLs produced by prerenderParams.
  • Supports sitemap metadata including priority, changefreq, lastmod, images, news, and alternate refs.
  • Preserves per-entry sitemap and prerender overrides.
  • Adds prerender.prerenderParamsTimeout for build-time callback timeout control.
  • Adds prerender.separateRouteOptionsBundle to control whether prerender-only route options are built separately from the final server bundle.

Bundle Behavior

  • Removes prerenderParams and sitemap route options from client bundles.
  • Removes prerender-only route options from final server bundles when separate route-options bundling is enabled.
  • Uses a temporary prerender route-options bundle during the prerender phase, then cleans it up after prerendering.
  • Supports opting out with prerender.separateRouteOptionsBundle: false when users prefer fewer build steps or when an adapter cannot support the extra environment.
  • Keeps Nitro compatibility by defaulting separate route-options bundling off for Nitro unless explicitly enabled.

Coverage

  • React, Solid, and Vue Start fixtures.
  • Vite and Rsbuild prerender builds.
  • SPA-only mode.
  • ssr: false / selective SSR fixtures.
  • Nested/pathless layout prerendering.
  • Dynamic params with search params.
  • Special characters and encoded delimiters in generated paths.
  • sitemap.xml generation from static routes, route-level sitemap metadata, and dynamic prerenderParams entries.
  • Final server bundle scans to ensure prerender-only route options are stripped.
  • Temporary prerender bundle cleanup.
  • Deployment adapter smoke builds for Cloudflare, Netlify, and Nitro.

Notes On Adapter Coverage

Cloudflare, Netlify, and Nitro builds were smoke-tested locally.

Bun was not covered locally in this pass. The PR keeps separateRouteOptionsBundle configurable so deployment adapters can opt out of the extra prerender route-options environment if needed, and Nitro already does this by default for compatibility.

Docs

  • Adds React and Solid static prerendering docs for route-level prerenderParams.
  • Documents sitemap.xml generation for dynamic prerendered URLs.
  • Documents prerenderParamsTimeout.
  • Documents separateRouteOptionsBundle, including the Nitro compatibility behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ba641bd-f2e7-4f52-afed-d23d2e0c33cd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds typed route-level prerenderParams and runtime to collect and run them at build time: type augments, route-tree discovery, a runner with abort/timeout/merge/dedup logic, sitemap output enhancements, client stripping of server-only markers, docs, e2e examples, and tests across React/Solid/Vue and core packages.

Changes

Dynamic Route Prerendering

Layer / File(s) Summary
Type Definitions & Module Augmentation
packages/start-client-core/src/prerenderParams.ts, packages/start-client-core/src/index.tsx
Adds Prerender types, module augmentation for prerenderParams/sitemap, and re-exports the types.
Type-Level Tests
packages/start-client-core/src/tests/prerenderParams.test-d.ts
Adds d.ts tests asserting inference for routePath, AbortSignal, params/search shapes (required/optional/splat/parent inheritance).
Route Metadata Collection
packages/start-plugin-core/src/prerender-route-options.ts, packages/start-plugin-core/src/start-router-plugin/generator-plugins/prerender-routes-plugin.ts, packages/start-plugin-core/src/global.d.ts
Traverses route tree to collect per-route prerenderParams/sitemap and sets globalThis.TSS_PRERENDER_DYNAMIC_ROUTES.
Prerender Execution & Page Generation
packages/start-plugin-core/src/prerender-params-runner.ts, packages/start-plugin-core/src/prerender.ts
Implements runPrerenderParams: runs prerenderParams with abortable timeout, interpolates params, encodes/serializes search, merges sitemap/prerender options, filters/dedupes, and integrates into prerender() flow.
Build Integration & Server Route Tree
packages/start-plugin-core/src/vite/prerender.ts, packages/start-plugin-core/src/rsbuild/post-build.ts, packages/start-plugin-core/src/post-build.ts, packages/start-server-core/src/createStartHandler.ts, packages/start-server-core/src/global.d.ts
Ensures built server entry is imported/loaded and exposes globalThis.TSS_PRERENDER_ROUTE_TREE for prerender-time routeTree resolution; adjusts rsbuild/post-build handler loading.
Router Plugin, Constants & Schema
packages/start-plugin-core/src/start-router-plugin/constants.ts, packages/start-plugin-core/src/vite/start-router-plugin/plugin.ts, packages/start-plugin-core/src/rsbuild/start-router-plugin.ts, packages/start-plugin-core/src/schema.ts
Adds CLIENT_ROUTE_OPTION_DELETE_NODES (includes prerenderParams/sitemap) for client stripping, changes prerender plugin activation to enabled !== false, and adds prerenderParamsTimeout schema option.
Sitemap Enhancements
packages/start-plugin-core/src/build-sitemap.ts
Adds xmlns:image and xmlns:news namespaces and supports per-entry sitemap metadata emitted from prerenderParams.
Runner & Integration Tests
packages/start-plugin-core/tests/prerender-params-runner.test.ts, packages/start-plugin-core/tests/*
Adds comprehensive Vitest coverage for runner behavior, timeout/abort handling, sitemap merging, precedence, filtering, SSRF path-decoding behavior, and plugin constants.
E2E Example Routes & Server-Only Helpers
e2e/*/basic/src/routes/prerender-params.$slug.*, e2e/*/basic/src/routes/-prerender-params.server.*, e2e/*/basic/src/routes/_layout/_layout-2/prerender-nested.$slug.*, e2e/*/basic/src/routeTree.gen.ts
Adds example dynamic routes with prerenderParams, nested-prerender examples, server-only prerender marker helpers, and updates generated route trees/types for React/Solid/Vue.
E2E Config & Build Options
e2e/*/basic/rsbuild.config.ts, e2e/*/basic/vite.config.ts, e2e/*/basic/start-mode-config.ts, e2e/*/basic/package.json
Wires prerender config into example build configs (conditional prerender/sitemap) and updates Playwright mode entries.
E2E Tests (Playwright)
e2e/*/basic/tests/prerendering.spec.ts
Adds assertions that generated files exist, prerendered content from prerenderParams (including special chars and encoded delimiters) preserves search params, server-only marker is absent from client output, and sitemap entries contain per-entry metadata.
Docs & Examples
docs/start/framework/react/guide/static-prerendering.md, docs/start/framework/solid/guide/static-prerendering.md
Documents prerenderParams usage, prerenderParamsTimeout, dynamic route entry shape, sitemap per-entry semantics, abort signaling, search serialization, and client-bundle removal of server-only code.
Changeset
.changeset/tall-trees-prerender-params.md
Records minor version bumps and documents typed prerenderParams support.

Sequence Diagram(s)

sequenceDiagram
    actor Build as Build Process
    participant RouterGen as Router Generator
    participant RouteTree as Built Server RouteTree
    participant Runner as runPrerenderParams
    participant PreparamFn as prerenderParams Callback
    participant PageGen as Page Interpolator
    participant Sitemap as buildSitemap

    Build->>RouterGen: generate route metadata
    RouterGen->>globalThis: set TSS_PRERENDER_DYNAMIC_ROUTES

    Build->>RouteTree: import built server entry
    RouteTree-->>Build: expose routeTree via TSS_PRERENDER_ROUTE_TREE()

    Build->>Runner: runPrerenderParams({ routeTree, pages, timeout })
    Runner->>PreparamFn: invoke({ routePath, signal })
    PreparamFn-->>Runner: return entries[]
    Runner->>PageGen: interpolate params, encode path, serialize search
    PageGen-->>Runner: emit Page objects
    Runner->>Runner: merge, dedupe, apply sitemap/prerender options
    Runner-->>Build: return final pages

    Build->>Sitemap: buildSitemap(final pages)
    Sitemap->>Sitemap: apply per-entry metadata & namespaces
    Sitemap-->>Build: emit sitemap.xml
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

  • TanStack/router#5475: Modifies prerender route discovery/generator plugin and related globals; strongly related.

Suggested reviewers

  • schiller-manuel

"🐰 Prerender params bloom bright,
Dynamic pages spring to light,
At build time routes find their flight,
Sitemaps hum with metadata right,
Hopping cheer through prerender night!" ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(start): add pre-render params + sitemap' accurately summarizes the main feature additions of the changeset: support for pre-render params and sitemap functionality in TanStack Start.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented May 5, 2026

View your CI Pipeline Execution ↗ for commit 1c27261

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 2m 49s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-06 08:01:44 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 5, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7346

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7346

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7346

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7346

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7346

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7346

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7346

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7346

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7346

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7346

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7346

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7346

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7346

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7346

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7346

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7346

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7346

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7346

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7346

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7346

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7346

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7346

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7346

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7346

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7346

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7346

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7346

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7346

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7346

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7346

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7346

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7346

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7346

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7346

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7346

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7346

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7346

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7346

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7346

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7346

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7346

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7346

commit: 1c27261

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Bundle Size Benchmarks

  • Commit: ee96e25d1487
  • Measured at: 2026-05-06T07:59:58.607Z
  • Baseline source: history:35e88f04996d
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 87.15 KiB 0 B (0.00%) 273.94 KiB 75.70 KiB ▅▅▅▅▅▅▅▅▅▅▅
react-router.full 90.68 KiB 0 B (0.00%) 285.45 KiB 78.71 KiB ▅▅▅▅▅▅▅▅▅▅▅
solid-router.minimal 35.38 KiB 0 B (0.00%) 106.25 KiB 31.81 KiB ▅▅▅▅▅▅▅▅▅▅▅
solid-router.full 40.10 KiB 0 B (0.00%) 120.46 KiB 36.04 KiB ▅▅▅▅▅▅▅▅▅▅▅
vue-router.minimal 53.15 KiB 0 B (0.00%) 151.39 KiB 47.73 KiB ▅▅▅▅▅▅▅▅▅▅▅
vue-router.full 58.28 KiB 0 B (0.00%) 167.56 KiB 52.18 KiB ▅▅▅▅▅▅▅▅▅▅▅
react-start.minimal 101.84 KiB 0 B (0.00%) 322.38 KiB 88.03 KiB ▁▁▁▁▁▁▁▁▁▁█
react-start.full 105.27 KiB 0 B (0.00%) 332.71 KiB 91.00 KiB ▁▁▁▁▁▁▁▁▁▁█
react-start.rsbuild.minimal 99.43 KiB 0 B (0.00%) 316.76 KiB 85.51 KiB ▁▁▁▁▁▁▁▁▁▁█
react-start.rsbuild.full 102.72 KiB 0 B (0.00%) 327.19 KiB 88.31 KiB ▁▁▁▁▁▁▁▁▁▁█
solid-start.minimal 49.48 KiB 0 B (0.00%) 152.36 KiB 43.69 KiB ▁▁▁▁▁▁▁▁▁▁█
solid-start.full 55.27 KiB 0 B (0.00%) 169.27 KiB 48.60 KiB ▁▁▁▁▁▁▁▁▁▁█

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

nx-cloud[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
e2e/vue-start/basic/rsbuild.config.ts (1)

8-37: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add sitemap option to rsbuild prerender config for feature parity with vite

The Vue vite.config.ts conditionally enables sitemap generation when isPrerender is true, but rsbuild.config.ts omits this option. The prerendering test includes sitemap assertions, but uses test.skip() to gracefully skip the test if the sitemap is missing — so this won't cause a test failure. However, it creates an inconsistency where users building with rsbuild won't get sitemap.xml output during prerendering, unlike the vite build.

🛠️ Proposed fix (mirror the vite config)
 const prerenderConfiguration = { ... }

 export default defineConfig({
   plugins: [
     pluginBabel({ include: /\.(?:jsx|tsx)$/ }),
     pluginVue(),
     pluginVueJsx(),
-    tanstackStart({
-      prerender: isPrerender ? prerenderConfiguration : undefined,
-    }),
+    tanstackStart({
+      prerender: isPrerender ? prerenderConfiguration : undefined,
+      sitemap: isPrerender
+        ? { enabled: true, host: 'https://example.com' }
+        : undefined,
+    }),
   ],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/vue-start/basic/rsbuild.config.ts` around lines 8 - 37, The rsbuild
prerender config omits the sitemap option so rsbuild builds won't produce
sitemap.xml; update the prerender configuration passed to tanstackStart in
rsbuild.config.ts by adding sitemap: true when prerendering is enabled (e.g.,
extend prerenderConfiguration or the object passed to tanstackStart to include
sitemap: true when isPrerender is true), referencing prerenderConfiguration,
tanstackStart and isPrerender so the rsbuild prerender behavior matches the vite
config.
🧹 Nitpick comments (5)
e2e/solid-start/basic/rsbuild.config.ts (2)

7-23: 💤 Low value

Duplicate prerender filter list with vite.config.ts — consider extracting a shared config helper

The prerenderConfiguration (filter list + maxRedirects) defined here is byte-for-byte identical to the prerenderConfiguration in e2e/solid-start/basic/vite.config.ts. The React e2e app already centralizes this in e2e/react-start/basic/start-mode-config.ts via getStartModeConfig(). Applying the same pattern for Solid (and Vue) prevents silent filter-list drift when new routes are added.

♻️ Proposed refactor — extract a shared start-mode-config.ts for Solid

Create e2e/solid-start/basic/start-mode-config.ts:

import { isPrerender } from './tests/utils/isPrerender'
import { isSpaMode } from './tests/utils/isSpaMode'

export function getStartModeConfig() {
  return {
    spa: isSpaMode ? { enabled: true, prerender: { outputPath: 'index.html' } } : undefined,
    prerender: isPrerender
      ? {
          enabled: true,
          filter: (page: { path: string }) =>
            ![
              '/this-route-does-not-exist',
              '/redirect',
              '/i-do-not-exist',
              '/not-found',
              '/specialChars/search',
              '/specialChars/hash',
              '/specialChars/malformed',
              '/search-params/default',
              '/transition',
              '/users',
            ].some((p) => page.path.includes(p)),
          maxRedirects: 100,
        }
      : undefined,
    sitemap: isPrerender ? { enabled: true, host: 'https://example.com' } : undefined,
  }
}

Then in both vite.config.ts and rsbuild.config.ts:

-import { isPrerender } from './tests/utils/isPrerender'
+import { getStartModeConfig } from './start-mode-config'

-const prerenderConfiguration = { ... }
 ...
-tanstackStart({ prerender: isPrerender ? prerenderConfiguration : undefined, sitemap: ... })
+tanstackStart(getStartModeConfig())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/solid-start/basic/rsbuild.config.ts` around lines 7 - 23, The
prerenderConfiguration object (including filter and maxRedirects) is duplicated;
extract it into a shared getStartModeConfig() helper (like the React approach)
that returns the prerender config (with enabled, filter function, maxRedirects
and sitemap/spa variants) and import that helper into both vite.config.ts and
rsbuild.config.ts, then replace the local prerenderConfiguration constant with
the shared value (referencing getStartModeConfig and its prerender property) so
both build configs use the same filter array and settings.

7-35: ⚡ Quick win

sitemap option missing from rsbuild prerender config — feature parity gap

e2e/solid-start/basic/vite.config.ts includes sitemap: { enabled: true, host: 'https://example.com' } when isPrerender is true, but the rsbuild config does not. While the sitemap test in prerendering.spec.ts (line 147) includes a test.skip() guard that gracefully skips when the sitemap is disabled, the inconsistency means the rsbuild build lacks this feature compared to vite. Consider adding the sitemap option for parity:

Optional enhancement
 tanstackStart({
   prerender: isPrerender ? prerenderConfiguration : undefined,
+  sitemap: isPrerender
+    ? { enabled: true, host: 'https://example.com' }
+    : undefined,
 }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/solid-start/basic/rsbuild.config.ts` around lines 7 - 35, The rsbuild
config is missing the sitemap option for prerender parity; update the
tanstackStart call in rsbuild.config.ts (where tanstackStart({ prerender:
isPrerender ? prerenderConfiguration : undefined })) so that when isPrerender is
true the prerender config includes sitemap: { enabled: true, host:
'https://example.com' } (either by adding sitemap to the existing
prerenderConfiguration object or by merging it when passing into tanstackStart).
packages/start-client-core/src/prerenderParams.ts (1)

61-90: ⚡ Quick win

Document expected date string formats.

lastmod and news.publicationDate accept string | Date, but the sitemap writer normalizes to YYYY-MM-DD (per build-sitemap.test.ts). Adding short JSDoc on these fields (e.g., "ISO 8601 date or YYYY-MM-DD; Date is formatted to W3C date format") would prevent users from passing arbitrary strings that get emitted verbatim into the XML.

📝 Proposed JSDoc additions
   alternateRefs?: Array<{
     href: string
     hreflang: string
   }>
+  /**
+   * Last modification date. Accepts a `Date` (formatted as `YYYY-MM-DD`)
+   * or a W3C/ISO 8601 date string. Strings are emitted verbatim, so
+   * pass `YYYY-MM-DD` or full ISO 8601 to remain spec-compliant.
+   */
   lastmod?: string | Date
   ...
   news?: {
     publication: {
       name: string
       language: string
     }
+    /**
+     * Publication date. `Date` is formatted as `YYYY-MM-DD`;
+     * strings should be W3C/ISO 8601 to remain spec-compliant.
+     */
     publicationDate: string | Date
     title: string
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-client-core/src/prerenderParams.ts` around lines 61 - 90,
Update the RouteSitemapOptions interface JSDoc to document expected date string
formats for lastmod and news.publicationDate: add brief comments on the lastmod
property and the nested news.publicationDate explaining they accept ISO 8601
strings or YYYY-MM-DD and that Date values will be formatted to W3C/ISO
(normalized to YYYY-MM-DD by the sitemap writer). Reference the
RouteSitemapOptions interface and its lastmod and news.publicationDate fields so
consumers know strings will be emitted verbatim unless they follow the
documented formats.
packages/start-plugin-core/tests/prerender-routes-plugin.test.ts (1)

39-69: 💤 Low value

LGTM – consider adding a TSS_PRERENDER_DYNAMIC_ROUTES assertion to test 2.

Test 2 verifies that /posts/$slug (which has component but not prerenderParams) stays out of TSS_PRERENDABLE_PATHS, but doesn't confirm it's also absent from TSS_PRERENDER_DYNAMIC_ROUTES. Test 3 covers this indirectly, so it's not a bug — just a small coverage gap.

✨ Optional assertion to make test 2 self-contained
     expect(globalThis.TSS_PRERENDABLE_PATHS).toEqual([{ path: '/' }])
+    expect(globalThis.TSS_PRERENDER_DYNAMIC_ROUTES).toEqual([])
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-plugin-core/tests/prerender-routes-plugin.test.ts` around
lines 39 - 69, Add an assertion in the test "does not store API, layout, or
dynamic routes as static paths" to also verify that dynamic routes without
prerenderParams are not recorded in TSS_PRERENDER_DYNAMIC_ROUTES: after invoking
plugin.onRouteTreeChanged (using prerenderRoutesPlugin and the route node with
routePath '/posts/$slug'), assert that globalThis.TSS_PRERENDER_DYNAMIC_ROUTES
does not contain an entry for '/posts/$slug' (or is empty), so the test checks
both TSS_PRERENDABLE_PATHS and globalThis.TSS_PRERENDER_DYNAMIC_ROUTES
consistency.
packages/start-plugin-core/src/vite/prerender.ts (1)

38-50: ⚡ Quick win

Address the fragile output filename assumption.

The code computes the server output filename as ${basename(serverInput, extname(serverInput))}.js, which assumes the built file uses the same base name as the input and always ends in .js. A project that sets rollupOptions.output.entryFileNames with a hash pattern or emits .cjs/.mjs would produce an ERR_MODULE_NOT_FOUND crash at prerender time. Consider reading from a Vite manifest or build metadata to get the actual output filename instead of computing it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-plugin-core/src/vite/prerender.ts` around lines 38 - 50, The
code that builds serverEntryPath from serverInput (using basename + ".js") is
fragile for hashed or non-.js outputs; instead look up the actual built entry
filename from the Vite/Rollup manifest or build output metadata and use that to
form serverEntryPath. Concretely: when preparing to import the built server
entry, read the build manifest (or Rollup output info) in the same output
directory returned by getServerOutputDirectory(serverEnv.config), find the entry
whose source or file corresponds to serverInput (use serverInput as the original
input key), and use the manifest-provided filename (which may be .cjs/.mjs or
hashed) for import; update the logic around serverInput, serverOutputDir,
serverEntryPath and the await import(...) to use the manifest-resolved filename
instead of `${basename(...)}.js`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/vue-start/basic/tests/prerendering.spec.ts`:
- Line 119: The assertion expect(html).toContain('2') is too broad; update the
test to assert the specific element or text that indicates the search param
page=2 was rendered instead of any occurrence of the character '2'. Replace the
loose expect(html).toContain('2') check (referencing the html variable and the
expect call) with a targeted assertion—either assert the exact HTML substring
for the element that should show "2" (e.g., the specific heading or pager
markup) or parse html with a DOM parser (e.g., JSDOM) and use querySelector to
assert the element's textContent equals '2' (or matches a precise regex like
/\bpage[:\s]*2\b/).

In `@packages/start-plugin-core/src/prerender-params-runner.ts`:
- Around line 131-142: The promise executor currently checks signal.aborted once
but then schedules callback via Promise.resolve().then(callback) allowing a
race; change the scheduled invocation to first re-check signal.aborted and
throw/reject if set before calling callback (e.g. replace .then(callback) with
.then(() => { if (signal.aborted) throw signal.reason ?? new
Error('prerenderParams aborted'); return callback(); })) and ensure the abort
listener (abort) is removed/cleanup occurs after resolve/reject so the handler
and callback cannot both run; reference the Promise executor, signal, callback,
and abort variables in prerender-params-runner.ts.

---

Outside diff comments:
In `@e2e/vue-start/basic/rsbuild.config.ts`:
- Around line 8-37: The rsbuild prerender config omits the sitemap option so
rsbuild builds won't produce sitemap.xml; update the prerender configuration
passed to tanstackStart in rsbuild.config.ts by adding sitemap: true when
prerendering is enabled (e.g., extend prerenderConfiguration or the object
passed to tanstackStart to include sitemap: true when isPrerender is true),
referencing prerenderConfiguration, tanstackStart and isPrerender so the rsbuild
prerender behavior matches the vite config.

---

Nitpick comments:
In `@e2e/solid-start/basic/rsbuild.config.ts`:
- Around line 7-23: The prerenderConfiguration object (including filter and
maxRedirects) is duplicated; extract it into a shared getStartModeConfig()
helper (like the React approach) that returns the prerender config (with
enabled, filter function, maxRedirects and sitemap/spa variants) and import that
helper into both vite.config.ts and rsbuild.config.ts, then replace the local
prerenderConfiguration constant with the shared value (referencing
getStartModeConfig and its prerender property) so both build configs use the
same filter array and settings.
- Around line 7-35: The rsbuild config is missing the sitemap option for
prerender parity; update the tanstackStart call in rsbuild.config.ts (where
tanstackStart({ prerender: isPrerender ? prerenderConfiguration : undefined }))
so that when isPrerender is true the prerender config includes sitemap: {
enabled: true, host: 'https://example.com' } (either by adding sitemap to the
existing prerenderConfiguration object or by merging it when passing into
tanstackStart).

In `@packages/start-client-core/src/prerenderParams.ts`:
- Around line 61-90: Update the RouteSitemapOptions interface JSDoc to document
expected date string formats for lastmod and news.publicationDate: add brief
comments on the lastmod property and the nested news.publicationDate explaining
they accept ISO 8601 strings or YYYY-MM-DD and that Date values will be
formatted to W3C/ISO (normalized to YYYY-MM-DD by the sitemap writer). Reference
the RouteSitemapOptions interface and its lastmod and news.publicationDate
fields so consumers know strings will be emitted verbatim unless they follow the
documented formats.

In `@packages/start-plugin-core/src/vite/prerender.ts`:
- Around line 38-50: The code that builds serverEntryPath from serverInput
(using basename + ".js") is fragile for hashed or non-.js outputs; instead look
up the actual built entry filename from the Vite/Rollup manifest or build output
metadata and use that to form serverEntryPath. Concretely: when preparing to
import the built server entry, read the build manifest (or Rollup output info)
in the same output directory returned by
getServerOutputDirectory(serverEnv.config), find the entry whose source or file
corresponds to serverInput (use serverInput as the original input key), and use
the manifest-provided filename (which may be .cjs/.mjs or hashed) for import;
update the logic around serverInput, serverOutputDir, serverEntryPath and the
await import(...) to use the manifest-resolved filename instead of
`${basename(...)}.js`.

In `@packages/start-plugin-core/tests/prerender-routes-plugin.test.ts`:
- Around line 39-69: Add an assertion in the test "does not store API, layout,
or dynamic routes as static paths" to also verify that dynamic routes without
prerenderParams are not recorded in TSS_PRERENDER_DYNAMIC_ROUTES: after invoking
plugin.onRouteTreeChanged (using prerenderRoutesPlugin and the route node with
routePath '/posts/$slug'), assert that globalThis.TSS_PRERENDER_DYNAMIC_ROUTES
does not contain an entry for '/posts/$slug' (or is empty), so the test checks
both TSS_PRERENDABLE_PATHS and globalThis.TSS_PRERENDER_DYNAMIC_ROUTES
consistency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b58420f7-00a8-4fa8-b77b-ab44168b7d58

📥 Commits

Reviewing files that changed from the base of the PR and between bcefc84 and cb7a05d.

📒 Files selected for processing (48)
  • .changeset/tall-trees-prerender-params.md
  • docs/start/framework/react/guide/static-prerendering.md
  • docs/start/framework/solid/guide/static-prerendering.md
  • e2e/react-start/basic/src/routeTree.gen.ts
  • e2e/react-start/basic/src/routes/-prerender-params.server.ts
  • e2e/react-start/basic/src/routes/_layout/_layout-2/prerender-nested.$slug.tsx
  • e2e/react-start/basic/src/routes/prerender-params.$slug.tsx
  • e2e/react-start/basic/start-mode-config.ts
  • e2e/react-start/basic/tests/prerendering.spec.ts
  • e2e/solid-start/basic/package.json
  • e2e/solid-start/basic/rsbuild.config.ts
  • e2e/solid-start/basic/src/routeTree.gen.ts
  • e2e/solid-start/basic/src/routes/-prerender-params.server.ts
  • e2e/solid-start/basic/src/routes/_layout/_layout-2/prerender-nested.$slug.tsx
  • e2e/solid-start/basic/src/routes/prerender-params.$slug.tsx
  • e2e/solid-start/basic/tests/prerendering.spec.ts
  • e2e/solid-start/basic/vite.config.ts
  • e2e/vue-start/basic/package.json
  • e2e/vue-start/basic/rsbuild.config.ts
  • e2e/vue-start/basic/src/routeTree.gen.ts
  • e2e/vue-start/basic/src/routes/-prerender-params.server.ts
  • e2e/vue-start/basic/src/routes/_layout/_layout-2/prerender-nested.$slug.tsx
  • e2e/vue-start/basic/src/routes/prerender-params.$slug.tsx
  • e2e/vue-start/basic/tests/prerendering.spec.ts
  • e2e/vue-start/basic/vite.config.ts
  • packages/start-client-core/src/index.tsx
  • packages/start-client-core/src/prerenderParams.ts
  • packages/start-client-core/src/tests/prerenderParams.test-d.ts
  • packages/start-plugin-core/src/build-sitemap.ts
  • packages/start-plugin-core/src/global.d.ts
  • packages/start-plugin-core/src/post-build.ts
  • packages/start-plugin-core/src/prerender-params-runner.ts
  • packages/start-plugin-core/src/prerender-route-options.ts
  • packages/start-plugin-core/src/prerender.ts
  • packages/start-plugin-core/src/rsbuild/post-build.ts
  • packages/start-plugin-core/src/rsbuild/start-router-plugin.ts
  • packages/start-plugin-core/src/schema.ts
  • packages/start-plugin-core/src/start-router-plugin/constants.ts
  • packages/start-plugin-core/src/start-router-plugin/generator-plugins/prerender-routes-plugin.ts
  • packages/start-plugin-core/src/vite/prerender.ts
  • packages/start-plugin-core/src/vite/start-router-plugin/plugin.ts
  • packages/start-plugin-core/tests/build-sitemap.test.ts
  • packages/start-plugin-core/tests/prerender-params-runner.test.ts
  • packages/start-plugin-core/tests/prerender-routes-plugin.test.ts
  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
  • packages/start-plugin-core/tests/start-router-plugin-constants.test.ts
  • packages/start-server-core/src/createStartHandler.ts
  • packages/start-server-core/src/global.d.ts

Comment thread e2e/vue-start/basic/tests/prerendering.spec.ts Outdated
Comment thread packages/start-plugin-core/src/prerender-params-runner.ts
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 5, 2026

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing nikuscs:feat-server-side-params (1c27261) with main (35e88f0)2

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

  2. No successful run was found on main (ee96e25) during the generation of this report, so 35e88f0 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/start-plugin-core/src/prerender-params-runner.ts (2)

68-89: 💤 Low value

Defensive validation for non-iterable entries.

for (const entry of entries) will throw a non-descriptive TypeError: entries is not iterable if a user-authored prerenderParams mistakenly returns undefined/null or a non-array. A small guard with a clearer error message attributing the offending route improves DX without changing the happy path.

♻️ Suggested guard
       const entries = await call(
         () =>
           options.prerenderParams!({
             routePath: route.routePath,
             signal: controller.signal,
           }),
         controller.signal,
       ).finally(cleanupTimeout)

+      if (!Array.isArray(entries)) {
+        throw new Error(
+          `prerenderParams for route ${route.routePath} must return an array; got ${typeof entries}`,
+        )
+      }
+
       for (const entry of entries) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-plugin-core/src/prerender-params-runner.ts` around lines 68 -
89, The loop assumes the result of calling options.prerenderParams via call(...)
is iterable; add a defensive validation after the await that checks entries is
an array (or at least iterable) and throw a clear Error that includes
route.routePath (or skip non-iterable with a logged warning) so users get a
descriptive message instead of "entries is not iterable"; update the block
around the awaited call in prerender-params-runner.ts (where entries is assigned
from call(...), which uses controller.signal and options.prerenderParams) to
validate entries before the for (const entry of entries) loop and only proceed
to call create(...), filter, pagesByPath.set(...), and merge(...) when entries
is valid.

155-186: 💤 Low value

Route-level options.prerender is silently dropped on generated pages.

create() merges options.sitemap with entry.sitemap but assigns prerender: entry.prerender directly, ignoring options.prerender entirely. If a route declares prerender: { headers: { … } } at the route level and an entry omits prerender, those headers are lost on the generated page even though the analogous sitemap config is preserved. If this asymmetry is intentional (e.g., prerender options are applied later in the pipeline), a brief comment would help; otherwise consider symmetric merging.

♻️ Symmetric merge sketch
   return {
     path: interpolatedPath + search(entry.search),
     sitemap: sitemap(options.sitemap, entry.sitemap),
-    prerender: entry.prerender,
+    prerender: prerender(options.prerender, entry.prerender),
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-plugin-core/src/prerender-params-runner.ts` around lines 155 -
186, The create function currently drops route-level prerender options by
returning prerender: entry.prerender instead of merging with options.prerender;
update create (in prerender-params-runner.ts) to merge options.prerender with
entry.prerender (similar to how sitemap(options.sitemap, entry.sitemap) is
merged) so route-level defaults (e.g., headers) are preserved when
entry.prerender is absent—either call an existing merge helper or
shallow/deep-merge options.prerender and entry.prerender and return the merged
result under prerender.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/start-plugin-core/src/prerender-params-runner.ts`:
- Around line 68-89: The loop assumes the result of calling
options.prerenderParams via call(...) is iterable; add a defensive validation
after the await that checks entries is an array (or at least iterable) and throw
a clear Error that includes route.routePath (or skip non-iterable with a logged
warning) so users get a descriptive message instead of "entries is not
iterable"; update the block around the awaited call in
prerender-params-runner.ts (where entries is assigned from call(...), which uses
controller.signal and options.prerenderParams) to validate entries before the
for (const entry of entries) loop and only proceed to call create(...), filter,
pagesByPath.set(...), and merge(...) when entries is valid.
- Around line 155-186: The create function currently drops route-level prerender
options by returning prerender: entry.prerender instead of merging with
options.prerender; update create (in prerender-params-runner.ts) to merge
options.prerender with entry.prerender (similar to how sitemap(options.sitemap,
entry.sitemap) is merged) so route-level defaults (e.g., headers) are preserved
when entry.prerender is absent—either call an existing merge helper or
shallow/deep-merge options.prerender and entry.prerender and return the merged
result under prerender.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e8757849-427a-4490-af73-adf1d0583e55

📥 Commits

Reviewing files that changed from the base of the PR and between cb7a05d and 7990d41.

📒 Files selected for processing (6)
  • e2e/react-start/basic/tests/prerendering.spec.ts
  • e2e/solid-start/basic/tests/prerendering.spec.ts
  • e2e/vue-start/basic/tests/prerendering.spec.ts
  • packages/start-plugin-core/src/post-build.ts
  • packages/start-plugin-core/src/prerender-params-runner.ts
  • packages/start-plugin-core/tests/post-server-build.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • e2e/react-start/basic/tests/prerendering.spec.ts
  • e2e/solid-start/basic/tests/prerendering.spec.ts
  • e2e/vue-start/basic/tests/prerendering.spec.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/start-plugin-core/src/post-build.ts (1)

17-18: 💤 Low value

spaOnly is inferred as boolean | undefined rather than boolean

When startConfig.spa is undefined, the ?.enabled short-circuits the && to undefined (not false). All current usages (if (spaOnly), ternary) treat it as a falsy check, so there's no runtime defect, but the inferred type boolean | undefined is weaker than needed under strict mode.

♻️ Suggested refinement
- const spaOnly =
-   startConfig.spa?.enabled && startConfig.prerender?.enabled !== true
+ const spaOnly =
+   !!startConfig.spa?.enabled && startConfig.prerender?.enabled !== true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-plugin-core/src/post-build.ts` around lines 17 - 18, The
variable spaOnly is inferred as boolean | undefined because
startConfig.spa?.enabled can short-circuit to undefined; coerce the result to a
true boolean so its type is boolean (e.g., wrap the whole expression with a
boolean coercion such as Boolean(...) or double-negation) and keep the same
logical check using startConfig.spa?.enabled and startConfig.prerender?.enabled
!== true so usages like if (spaOnly) remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/start-plugin-core/tests/post-server-build.test.ts`:
- Around line 14-41: The test is misleading because postBuild does not read
globalThis.TSS_PRERENDER_DYNAMIC_ROUTES; remove the global setup and adjust the
test to assert the actual condition under test by deleting the
globalThis.TSS_PRERENDER_DYNAMIC_ROUTES assignment (and its cleanup) and rename
the it(...) description to something like "does not enable prerendering when
pages array is empty and prerender config is absent" while keeping the same call
to postBuild, the mocked adapter.prerender and the expectation that prerender
was not called; alternatively move this scenario into an integration test that
invokes the real prerender implementation if you intend to assert behavior
around TSS_PRERENDER_DYNAMIC_ROUTES.

---

Nitpick comments:
In `@packages/start-plugin-core/src/post-build.ts`:
- Around line 17-18: The variable spaOnly is inferred as boolean | undefined
because startConfig.spa?.enabled can short-circuit to undefined; coerce the
result to a true boolean so its type is boolean (e.g., wrap the whole expression
with a boolean coercion such as Boolean(...) or double-negation) and keep the
same logical check using startConfig.spa?.enabled and
startConfig.prerender?.enabled !== true so usages like if (spaOnly) remain
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3acc396d-cd42-4fb7-9e36-8cac7d3d593d

📥 Commits

Reviewing files that changed from the base of the PR and between 7990d41 and 6f5afd7.

📒 Files selected for processing (4)
  • packages/start-plugin-core/src/post-build.ts
  • packages/start-plugin-core/src/prerender.ts
  • packages/start-plugin-core/src/vite/prerender.ts
  • packages/start-plugin-core/tests/post-server-build.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/start-plugin-core/src/vite/prerender.ts
  • packages/start-plugin-core/src/prerender.ts

Comment thread packages/start-plugin-core/tests/post-server-build.test.ts Outdated
nx-cloud[bot]

This comment was marked as outdated.

@nikuscs nikuscs marked this pull request as draft May 5, 2026 18:40
nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud has identified a flaky task in your failed CI:

Since the failure was identified as flaky, the solution is to rerun CI. Because this branch comes from a fork, it is not possible for us to push directly, but you can rerun by pushing an empty commit:

git commit --allow-empty -m "chore: trigger rerun"
git push

Nx Cloud View detailed reasoning in Nx Cloud ↗


🎓 Learn more about Self-Healing CI on nx.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant