Skip to content

Repository files navigation

sailingnaturali.com

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 GitHub Pages. No CMS: content lives in the repo and ships via PR → merge → Pages.

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.


The SEO/GEO pattern

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.

1. Prerender to static HTML (the load-bearing decision)

In vite.config.ts, enable prerendering on the tanstackStart() plugin and add nitro() for the 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 match

Note: Nitro emits index.html as a single UTF-8 line; macOS/BSD grep treats it as binary, so use grep -a.

2. Single source of truth — src/lib/site.ts

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 } ]

3. Per-route <head> + JSON-LD — src/lib/seo.ts

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.)

4. Generated sitemap.xml, robots.txt, llms.txt

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:

"build": "tsx scripts/generate-seo-assets.mjs && vite build"

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.

Copy it into your own site

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.


Develop

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).

Deploy

GitHub Pages, built by .github/workflows/deploy.yml on push to main. Every route is prerendered, so .output/public is the entire site — .output/server is never deployed. The workflow copies index.html to 404.html so unknown paths boot the router.

public/CNAME holds the custom domain. DNS lives on Cloudflare and must stay DNS-only / grey-cloud, or Pages can't provision its TLS cert.

No preview deploys — run pnpm dev locally to review a change before merging.

Editing content

No dashboard. Change copy in src/routes/* or src/lib/site.ts, review locally with pnpm dev, open a PR, merge. To add a page: create the route and add a row to pages in site.ts — the sitemap and llms.txt follow automatically.

License

MIT.

About

sailingnaturali.com — the apex site (TanStack Start prerendered to static, on GitHub Pages)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages