The apex marketing site for Sailing Naturali — an all-electric expedition charter being built in the open in the Pacific Northwest.
Built with TanStack Start (React + Vite), prerendered to static HTML, deployed on Vercel. No CMS: content lives in the repo and ships via PR → Vercel preview → merge.
This repo also carries a small, copyable SEO + GEO module for TanStack Start. If you found your way here looking for "how do I do SEO / answer-engine visibility on TanStack Start," the SEO/GEO pattern section is for you.
SEO = ranking in classic search (Googlebot, Bingbot). GEO (Generative Engine Optimization) = getting cited by AI answer engines (GPTBot, PerplexityBot, ClaudeBot, Google AI Overviews).
They share one spine: a bot that receives clean, prerendered HTML with good structured data wins both. TanStack Start client-renders by default, which is exactly the thing both SEO and GEO need fixed — so the single highest-leverage move is prerendering every public route to static HTML. Everything else (meta, JSON-LD, sitemap, llms.txt) layers on top of that.
In vite.config.ts, enable prerendering on the tanstackStart() plugin and add nitro() for the Vercel build target:
tanstackStart({
prerender: {
enabled: true,
autoSubfolderIndex: true,
autoStaticPathsDiscovery: true,
crawlLinks: true,
concurrency: 14,
},
}),
nitro(),Verify it worked by inspecting the built HTML — the page text must be in the file, not just an empty <div id="root">:
pnpm build
grep -a 'your hero text' .output/public/index.html # must matchNote: Nitro emits
index.htmlas a single UTF-8 line; macOS/BSDgreptreats it as binary, so usegrep -a.
One file defines the site and the page registry. Everything downstream (meta, JSON-LD, sitemap, llms.txt) reads from it, so adding a page is a one-line change:
export const siteConfig = { url, name, title, description, ... } as const
export interface PageDef { path; title; description; lastmod }
export const pages: PageDef[] = [ { path: '/', title, description, lastmod } ]pageHead(page) returns a TanStack Router head object (title, description, canonical, Open Graph, Twitter). organizationJsonLd() / websiteJsonLd() return schema.org objects. A route wires them in:
export const Route = createFileRoute('/')({
head: () => ({
...pageHead(home),
scripts: [
{ type: 'application/ld+json', children: JSON.stringify(organizationJsonLd()) },
{ type: 'application/ld+json', children: JSON.stringify(websiteJsonLd()) },
],
}),
component: Home,
})Finding worth knowing: TanStack Start's route head.scripts does emit inline application/ld+json into the prerendered <head> — no need to render the script tag in the component body. (If a future version regresses, the fallback is a <script type="application/ld+json" dangerouslySetInnerHTML={...} /> directly in the component JSX.)
src/lib/seo-assets.ts holds three pure string builders (buildSitemap, buildRobots, buildLlmsTxt). scripts/generate-seo-assets.mjs reads site.ts and writes the three files into public/ at build time, so they stay in lockstep with the page registry. The build runs the generator first:
llms.txt is the GEO-specific lever — a plain-Markdown map of the site (see llmstxt.org) that answer engines can read without executing JS or parsing your layout. It's cheap to emit and it's the most direct "make this site legible to LLMs" move available today.
Lift these four files and the build wiring:
src/lib/site.ts # edit: your site + pages
src/lib/seo.ts # per-route head + JSON-LD helpers
src/lib/seo-assets.ts # sitemap / robots / llms.txt builders (pure, unit-tested)
scripts/generate-seo-assets.mjs
Then enable prerender + nitro() in vite.config.ts (step 1) and set the build script (step 4). The builders in seo-assets.ts are framework-agnostic pure functions; seo.ts's return shape is TanStack-Router-specific but trivial to adapt.
pnpm install
pnpm dev # http://localhost:5173 (3000 is reserved for SignalK)
pnpm test # vitest — unit tests for the SEO/GEO builders
pnpm build # generates SEO assets, then prerenders to .output/public/Tests run under vitest.config.ts (Node environment, isolated from the app's Vite plugin stack so pure-function tests don't drag in tanstackStart/nitro/React).
Vercel auto-detects TanStack Start. Build command pnpm build; Nitro auto-targets Vercel in CI. DNS lives on Cloudflare (DNS-only / grey-cloud, so Vercel provisions its own TLS cert).
No dashboard. Change copy in src/routes/* or src/lib/site.ts, open a PR, review the Vercel preview deploy, merge. To add a page: create the route and add a row to pages in site.ts — the sitemap and llms.txt follow automatically.
MIT.