---
title: "Landing page"
source: "landing/README.md"
---

> Part of [Landing page](/landing/llms.txt). Every page: [llms.txt](/llms.txt).
> View: [clarisay.com](https://clarisay.com)

# 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

```bash
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:

```bash
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

```bash
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:

```bash
pnpm wrangler login
pnpm wrangler kv namespace create WAITLIST
# paste the returned id into wrangler.jsonc → kv_namespaces[0].id
```

Then:

```bash
pnpm run deploy     # build + wrangler deploy
```

Read the waitlist:

```bash
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`: the `Content` type 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

1. Copy `src/content/en.ts` to `src/content/<tag>.ts` (e.g. `ja.ts`), export it with the `Content` type and translate it. Keep `[bracket]` placeholders recognisable.
2. In `src/lib/i18n.ts`, add the tag to `LOCALES` and an entry to `localeConfig`: URL `segment` (e.g. `"ja"`), BCP-47 `tag`, `nativeName` (switch label), `ogLocale`, the `Accept-Language` primary subtags it `matches`, and the `content`.
3. `pnpm build`. TypeScript fails on any missing string. Routes, `<html lang>`, hreflang, the sitemap, the language switch and the waitlist `locale` value all come from the registry.

## Renaming / changing the domain

1. `src/content/brand.ts`: `latinName`, `siteUrl` (drives canonical, hreflang, og:url, sitemap, robots), `appStoreUrl` + `appComingSoon = false` once the app is live.
2. `name` at the top of each locale file (`述刻` in zh-Hans; en uses `brand.latinName`).
3. `company`, `contactEmail` and the other `[bracket]` placeholders in each locale's legal copy.
4. `wrangler.jsonc`: `name` (Worker name) and, for a custom domain, uncomment `routes`.
5. `public/favicon.svg` and `src/components/Logo.tsx` for the mark.
