{"record":{"id":"c4b04bdd9eb0c110","repo":"remix-run/react-router","slug":"invalid-route-exports-found-when-prerendering-with","errorCode":null,"errorMessage":"Invalid route exports found when prerendering with `ssr:false`","messagePattern":"Invalid route exports found when prerendering with `ssr:false`","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/react-router-dev/vite/plugin.ts","lineNumber":3168,"sourceCode":"      while (parentRoute && parentRoute.id !== \"root\") {\n        if (parentRoute.hasLoader && !parentRoute.hasClientLoader) {\n          errors.push(\n            `Prerender: 1 invalid route export in \\`${parentRoute.id}\\` when ` +\n              \"pre-rendering with `ssr:false`: `loader`. \" +\n              \"See https://reactrouter.com/how-to/pre-rendering#invalid-exports for more information.\",\n          );\n        }\n        parentRoute =\n          parentRoute.parentId && parentRoute.parentId !== \"root\"\n            ? manifest.routes[parentRoute.parentId]\n            : null;\n      }\n    }\n  }\n\n  if (errors.length > 0) {\n    viteConfig.logger.error(colors.red(errors.join(\"\\n\")));\n    throw new Error(\n      \"Invalid route exports found when prerendering with `ssr:false`\",\n    );\n  }\n}\n\nfunction getAddressableRoutes(routes: RouteManifest): RouteManifestEntry[] {\n  let nonAddressableIds = new Set<string>();\n\n  for (let id in routes) {\n    let route = routes[id];\n\n    // We omit the parent route of index routes since the index route takes ownership of its parent's path\n    if (route.index) {\n      invariant(\n        route.parentId,\n        `Expected index route \"${route.id}\" to have \"parentId\" set`,\n      );\n      nonAddressableIds.add(route.parentId);","sourceCodeStart":3150,"sourceCodeEnd":3186,"githubUrl":"https://github.com/remix-run/react-router/blob/6beaca39526d5716c3c112ebb0782765baa5a9ce/packages/react-router-dev/vite/plugin.ts#L3150-L3186","documentation":"With `ssr: false`, there is no runtime server, so prerendering validates route module exports before building: `headers` and `action` are never valid (nothing can handle requests), and `loader` is only valid on routes actually matched by a `prerender` path (their data is baked in at build time). Detailed per-route messages are logged first (with a link to the pre-rendering docs), then this aggregate error stops the build.","triggerScenarios":"`ssr: false` plus any route exporting `action` or `headers`; a route with `loader` that is not covered by any path in the `prerender` config; a parent route with `loader` (and no `clientLoader`) above a prerendered child; enabling `prerender: true` only over part of the tree while other branches keep loaders.","commonSituations":"Flipping an SSR app to SPA Mode without stripping server exports; keeping form `action`s in routes during a SPA migration; prerendering only `/` while deeper routes still have loaders; inherited layout loaders.","solutions":["Remove `action` and `headers` exports from routes when `ssr: false` (forms must POST to an external API or use `clientAction`)","Convert `loader` to `clientLoader` on routes that are not prerendered","Add the affected paths to `prerender` so their `loader` runs at build time, or remove the loader","Read the logged per-route errors above the throw — they name the exact route IDs and invalid exports"],"exampleFix":"// before (app/routes/contact.tsx, ssr:false)\nexport async function action({ request }: Route.ActionArgs) {\n  await submitContact(await request.formData());\n  return null;\n}\n\n// after — client-side submission in SPA Mode\nexport async function clientAction({ request }: Route.ClientActionArgs) {\n  await submitContact(await request.formData());\n  return null;\n}","handlingStrategy":"validation","validationCode":"// Scan route modules for server-only exports before enabling ssr:false\nimport glob from 'fast-glob';\nconst bad: string[] = [];\nfor (const file of await glob('app/routes/**/*.ts*')) {\n  const src = fs.readFileSync(file, 'utf8');\n  if (/^export\\s+(async\\s+)?(function|const)\\s+(action|headers|loader)\\b/m.test(src)) {\n    bad.push(`${file}: server export found`);\n  }\n}\nif (bad.length) throw new Error(bad.join('\\n'));","typeGuard":"type ServerExport = 'loader' | 'action' | 'headers';\nconst isServerOnlyExport = (name: string): name is ServerExport =>\n  name === 'loader' || name === 'action' || name === 'headers';","tryCatchPattern":"try {\n  await build();\n} catch (e) {\n  if (e instanceof Error && e.message.includes('Invalid route exports')) {\n    // per-route details were logged just above the throw — parse them from build logs\n  }\n}","preventionTips":["When flipping `ssr: false`, grep `app/routes` for `export action|export headers|export loader` and migrate them","Prefer `clientLoader`/`clientAction` during SPA migration","Prerender every path whose loader data must be static"],"tags":["spa-mode","ssr-false","prerender","route-exports","validation"],"backgroundTag":"invalid-route-exports","analyzedSha":"6beaca39526d5716c3c112ebb0782765baa5a9ce","analyzedAt":"2026-08-18T18:04:14.938Z","contentChangedAt":"2026-08-18T18:04:14.938Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}