I recently moved a portfolio site over to TanStack Start and wanted to deploy it as a fully static site on Vercel. The appeal is straightforward, the build output is plain HTML, CSS, and JS, so it can go on any static host (e.g., Vercel, Netlify, Cloudflare Pages, S3) and costs next to nothing compared to a server-based approach. Getting the configuration right had a few non-obvious steps though, so I wanted to document what I ran into.
Tested with TanStack Start 1.x and Vercel's static output target as of early 2026.
Configuring prerendering in vite.config.ts
TanStack Start's Vite plugin exposes a prerender option directly on
tanstackStart():
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
export default defineConfig({
plugins: [
tanstackStart({
prerender: {
enabled: true,
crawlLinks: true,
},
}),
],
});enabled: true, switches the build target from server to static. Output lands indist/client/as a tree of.htmlfiles.crawlLinks: true, after rendering the entry point the build follows every internal<a href>it finds, rendering each discovered page. You don't have to maintain a list of routes, the crawler finds them automatically.
I found the crawler behaviour has a useful side effect: a broken internal link
fails the build. I actually had a stale /rss.xml link in the footer that would
have been a silent 404 in production, and the build caught it. Fix the link (or
remove it), and the build passes. I think this is one of the underrated benefits
of the crawl approach.
Sitemap config
The same plugin can generate a sitemap.xml alongside the prerendered pages:
tanstackStart({
prerender: {
enabled: true,
crawlLinks: true,
},
sitemap: {
enabled: true,
host: import.meta.env.VITE_DOMAIN,
},
}),I've set the host value to read from a VITE_DOMAIN environment variable so
the same config works in local builds and in Vercel's CI environment. You can set
it in your Vercel project settings under Environment Variables:
VITE_DOMAIN=https://royportas.comOne thing to watch out for, if you forget to set this variable the sitemap will
still generate, but every URL will have undefined as the host. I'd recommend
checking the output dist/client/sitemap.xml after a build to verify it looks
right before deploying.
Configuring vercel.json
This was the part that tripped me up the most. Vercel's automatic framework
detection won't pick up TanStack Start's static output correctly out of the box.
A vercel.json at the repo root fixes it:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"installCommand": "bun install",
"buildCommand": "bun run build",
"outputDirectory": "dist/client",
"cleanUrls": true
}The critical field is outputDirectory. TanStack Start builds static output to
dist/client/, not dist/. Vercel defaults to dist/, so without this
override it looks in the wrong folder and the deploy either fails or serves a
blank page. I spent a bit of time debugging this before I realised what was
happening.
I've also set installCommand and buildCommand explicitly for the same
reason. Vercel supports Bun, but being explicit means a future change to
Vercel's detection logic can't break your builds quietly.
Gotchas
These are the things that caught me off guard during the process:
dist/client/, notdist/, this one will silently break your deploy if you miss it. The TanStack Start static build always outputs todist/client/, so make sureoutputDirectoryis set accordingly invercel.json.- Broken links fail the build,
crawlLinks: trueturns every internal link into a hard build dependency. This is great for catching 404s before they reach production, but it means you have to clean up stale links whenever you remove pages or rename routes. VITE_DOMAINmust be set before the build runs, environment variables prefixed withVITE_are inlined at build time, not runtime. If the variable isn't present in Vercel's environment whenbun run buildruns, the sitemap URLs will be wrong.- Make sure it's set in Vercel's project settings, not just in a local
.envfile.
- Make sure it's set in Vercel's project settings, not just in a local
Known issues
I couldn't find a satisfying way to serve a custom not-found page. The SPA
fallback rewrite means unmatched paths get index.html, and TanStack Router
renders the correct 404 route client-side. However the initial HTML is the app
shell, not a proper 404 page. This causes a hydration mismatch where the server
sends a 200 with generic HTML, then the client corrects it.
My workaround currently is to create a custom 404 page at public/404.html that
matches the app's styling. Longer term I think Nitro's static rendering might
fit the scenario better, but when I tried it, it threw an error. If anyone has a
clean solution to this I'd like to know.
Wrapping up
The main thing I'd take away from this is to set outputDirectory to
dist/client/ in vercel.json, that's the one that will silently break
everything if you miss it. Plus, enabling crawlLinks is worth it for the
build-time link validation alone. The rest is fairly standard Vercel
configuration once you know where TanStack Start puts its output.