Writing

Deploying a static TanStack Start app to Vercel

How to configure TanStack Start's static prerender target and deploy it correctly to Vercel, including the non-obvious vercel.json settings that make routing work.

23 February 2026

·

5 min read

·
TanStackVercelDeployment

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():

vite.config.ts
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 in dist/client/ as a tree of .html files.
  • 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:

vite.config.ts
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.com

One 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:

vercel.json
{
  "$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/, not dist/, this one will silently break your deploy if you miss it. The TanStack Start static build always outputs to dist/client/, so make sure outputDirectory is set accordingly in vercel.json.
  • Broken links fail the build, crawlLinks: true turns 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_DOMAIN must be set before the build runs, environment variables prefixed with VITE_ are inlined at build time, not runtime. If the variable isn't present in Vercel's environment when bun run build runs, the sitemap URLs will be wrong.
    • Make sure it's set in Vercel's project settings, not just in a local .env file.

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.