Landing page
landing
The public marketing site for the app (codename Shuke / 述刻): home page with waitlist,
privacy policy, terms, support/FAQ, 404, robots.txt, sitemap.xml. English is the default
locale at /; Simplified Chinese lives under /zh (ADR-001 D13).
Stack: TanStack Start (React 19, Vite, TypeScript strict, file-based routes) + Tailwind v4,
deployed to Cloudflare Workers via @cloudflare/vite-plugin. No analytics, no cookies, no
external scripts or web fonts.
Develop
cd landing
pnpm install
pnpm dev # http://localhost:3000, runs inside workerd with a local simulated KV
pnpm dev runs the Worker in the real Workers runtime, so the WAITLIST KV binding works
locally out of the box (data is stored under .wrangler/state). No Cloudflare account needed.
Quick checks:
curl localhost:3000/api/health
curl -X POST localhost:3000/api/waitlist -H 'content-type: application/json' -d '{"email":"me@example.com","locale":"en"}'
pnpm wrangler kv key get "email:me@example.com" --binding WAITLIST --local
Build
pnpm build # vite build (client + worker) then `wrangler types && tsc --noEmit`
pnpm preview # serve the production build locally in workerd
CI (.github/workflows/landing.yml) runs pnpm build on every PR touching landing/.
Deploy
One-time setup:
pnpm wrangler login
pnpm wrangler kv namespace create WAITLIST
# paste the returned id into wrangler.jsonc → kv_namespaces[0].id
Then:
pnpm run deploy # build + wrangler deploy
Read the waitlist:
pnpm wrangler kv key list --binding WAITLIST --remote
Routes
Every page exists once per locale: /<page> (en) and /zh/<page> (zh-Hans).
| Path | What |
|---|---|
/, /zh |
Hero, 3-step explainer, before/after mock, waitlist form. On / only, if Accept-Language prefers another supported locale, a one-line hint links to it (never an automatic redirect). |
/privacy, /terms (+ /zh/...) |
Legal skeletons (required by the paywall, ADR-001 D8). Placeholders in [brackets] are highlighted on the page. |
/support (+ /zh/support) |
Contact + FAQ |
POST /api/waitlist |
Shared by all locales. JSON { email, locale, website } → { status: "ok" | "duplicate" | "invalid" | "error" } (201/200/400/500). Form posts (no JS) get a 303 back to the locale's home with ?waitlist=<status>#waitlist. website is a honeypot; unknown locale falls back to en. |
GET /api/health |
{ "status": "ok" } |
/robots.txt, /sitemap.xml |
Generated from brand.siteUrl; the sitemap lists every page per locale with xhtml:link hreflang alternates. |
Any other first path segment (/en, /fr/...) is a 404, so each page has exactly one URL per locale.
Every page emits <html lang>, a canonical URL, hreflang alternates for all locales plus x-default (en),
and og:locale / og:locale:alternate. The header and footer link to the same page in the other language.
KV layout: key email:<lowercased email>, value { "createdAt": ISO-8601, "locale": "en" | "zh-Hans" }.
Nothing else (no IP, no user agent) is stored, matching the privacy policy. A duplicate keeps the first record.
Copy and locales
src/content/types.ts: theContenttype every locale must satisfy.src/content/en.ts,src/content/zh-Hans.ts: all user-facing strings, one file per locale.src/content/brand.ts: locale-independent settings (Latin name, domain, App Store link).src/lib/i18n.ts: the locale registry (LOCALES,localeConfig) and path helpers. Nothing else branches on a specific locale.
Adding a locale
- Copy
src/content/en.tstosrc/content/<tag>.ts(e.g.ja.ts), export it with theContenttype and translate it. Keep[bracket]placeholders recognisable. - In
src/lib/i18n.ts, add the tag toLOCALESand an entry tolocaleConfig: URLsegment(e.g."ja"), BCP-47tag,nativeName(switch label),ogLocale, theAccept-Languageprimary subtags itmatches, and thecontent. pnpm build. TypeScript fails on any missing string. Routes,<html lang>, hreflang, the sitemap, the language switch and the waitlistlocalevalue all come from the registry.
Renaming / changing the domain
src/content/brand.ts:latinName,siteUrl(drives canonical, hreflang, og:url, sitemap, robots),appStoreUrl+appComingSoon = falseonce the app is live.nameat the top of each locale file (述刻in zh-Hans; en usesbrand.latinName).company,contactEmailand the other[bracket]placeholders in each locale's legal copy.wrangler.jsonc:name(Worker name) and, for a custom domain, uncommentroutes.public/favicon.svgandsrc/components/Logo.tsxfor the mark.