{
  "markdown": "# sigaoli.com\n\nPersonal website of **Sigao Li** — AI Product Manager · Spatial Data Scientist.\n_From maps to models, and the products in between._\n\nBilingual (English at `/`, 中文 at `/zh/`), built with **Astro + Tailwind CSS v4 + GSAP**,\ndeployed to GitHub Pages via GitHub Actions. Launched 2026-06-11, replacing the previous\nJekyll (academicpages) site.\n\n## Highlights\n\n- **Generative canvas effects on a map motif** — an interactive particle field (home), contour terrain (work), a \"river as timeline\" with a flow field (CV), and a geo-network arc map (photography); all vanilla canvas/SVG, tuned to 60fps with reduced-motion and mobile fallbacks\n- **Dotted world map** — land sampled from Natural Earth, with 76 GPS-extracted photo footprints across 6 countries; click a marker to open that country's gallery\n- **Zoe, the digital doorcat** — Sigao's cat (驺虞) lives in the corner of every page as a set of AI-generated, chroma-keyed VP9-alpha video clips pinned to shared anchor poses, driven by a state machine: she dozes off when ignored, reacts to page switches, listens while you type, \"types back\" while the assistant streams, and keeps a few easter eggs (production handbook in `docs/`)\n- **Built-in AI layer** — a floating chat assistant (fronted by Zoe) on every page — it suggests the single most relevant page as you ask, and greets a returning visitor by name (stored only in their own browser, opt-in) — plus a personal MCP server, both fed by a build-time knowledge pack generated from the same sources as the pages (see below)\n- **Machine-readable by design** — [`/llms.txt`](https://sigaoli.com/llms.txt), [`/llms-full.txt`](https://sigaoli.com/llms-full.txt), [`/resume.json`](https://sigaoli.com/resume.json) (JSON Resume), [`/knowledge.json`](https://sigaoli.com/knowledge.json), [`/.well-known/mcp.json`](https://sigaoli.com/.well-known/mcp.json), JSON-LD, and a robots.txt that explicitly welcomes AI crawlers\n- **Build-time translation pipeline** — long-form zh content generated by LLM with hash caching; human edits are protected from re-translation\n- Lighthouse (mobile): 96–100 across all categories; zero cookies, no paid services, and a plain-language privacy notice at [`/privacy`](https://sigaoli.com/privacy)\n\n## Commands\n\n| Command | Action |\n| --- | --- |\n| `npm run dev` | Dev server at `localhost:4321` (Astro 7 runs it as a daemon — stop with `npx astro dev stop`) |\n| `npm run build` | Production build to `dist/` |\n| `npm run preview` | Serve the production build locally |\n| `node scripts/translate.mjs` | Re-translate changed en content → zh (needs `.env`, see `.env.example`; manually edited zh files are never overwritten) |\n| `node scripts/check-links.mjs` | Internal link integrity check over `dist/` |\n| `node scripts/verify-nav.mjs` 等 | Playwright interaction suites (run against a local server) |\n| `npm run dev` (in `worker/`) | Chat + MCP Worker at `localhost:8787` (wrangler; secrets in `worker/.dev.vars`, never committed) |\n| `node scripts/verify-chat.mjs` | E2E chat-widget test (needs both dev servers running) |\n| `node scripts/verify-zoe.mjs` | E2E for Zoe's action state machine (append `?zoe-fast` locally to compress minute-scale timers) |\n| `node scripts/verify-typeroute.mjs` | E2E for the intent-driven typing clip and the bilingual 404 page |\n\n> Any Playwright suite that waits on Zoe's state must pin the clock\n> (`Date.prototype.getHours = () => 14`): between 23:00 and 06:00 she starts the\n> session asleep, so `state` never reaches `idle` and the run just times out.\n\n> When adding a Zoe clip, decide **who prewarms it and when** at the same time.\n> A clip that is only fetched at playback stalls on a slow connection, and the\n> stage shows nothing until it decodes. Prewarming has been missed three times\n> already. Note `warm()` takes the *file* name (`sit-to-loaf`), not the `ZOE`\n> key (`sitToLoaf`).\n\n> The chat panel is rebuilt on every navigation — `transition:persist` keeps\n> Zoe's stage, not the panel. **Anything that lives only in panel DOM is gone\n> the moment a visitor clicks a link.** The streaming reply, the guidance chip\n> and the unsent draft each had to be given module state plus a path back\n> through `paint()`; the chip was lost for weeks before anyone noticed. So when\n> adding persistent UI here, answer two questions up front: how does `paint()`\n> rebuild it, and should it ride along in `sessionStorage` with the history?\n> Measure geometry only once the panel is visible — `scrollHeight` is 0 while\n> it is hidden, which silently writes `height: 0px`.\n\n> **Turnstile guards `/chat` and `/classify`. It must never guard `/mcp`.**\n> That endpoint exists so machines can read Sigao's profile — it is in the\n> official registry — and Turnstile exists to stop machines. It also costs\n> nothing to serve: the tools read the knowledge pack and never call a model.\n> The static outlets (`llms.txt`, `knowledge.json`, `.well-known/mcp.json`) are\n> served by Pages and never reach the Worker at all.\n\n> **Locally, Turnstile uses Cloudflare's always-pass test keys** — sitekey in\n> `site.ts` behind `import.meta.env.DEV`, secret in `worker/.dev.vars`. The real\n> key rejects headless browsers, which is exactly its job, so every suite that\n> drives a real Worker would fail against it. The real secret exists only in\n> production, set with `wrangler secret put`. A corollary worth remembering:\n> **the production happy path cannot be verified from a script** — reaching it\n> needs a human in a real browser. Automation can still prove the gate is up\n> (a request with no credential must return 403).\n\n```\nsrc/\n├── pages/            # en routes + zh/ mirrors; llms.txt / resume.json / knowledge.json endpoints\n├── components/       # Nav, Hero, WorldMap, Lightbox, CommandK, ChatWidget …\n│   └── pages/        # shared page bodies rendered by both locales\n├── content/          # cases & research (en) + cases-zh & research-zh (generated, reviewed)\n├── data/             # cv.json / cv.zh.json / photos.json (GPS + bilingual alts)\n│   └── knowledge/    # persona sources for the AI assistant (about / faq / guidelines / boundaries)\n├── lib/              # i18n dict, GSAP lifecycle helper, site config\n│   └── knowledge/    # knowledge-pack pipeline (same-source layers + build-time privacy guard)\n└── assets/           # photo originals (optimized at build; originals never shipped)\nworker/               # Cloudflare Worker: /chat (SSE) + /classify (intent) + /mcp (MCP server)\n└── src/core/         # runtime-agnostic logic; Cloudflare specifics live only in src/adapter/\npublic/zoe/           # Zoe's clip library (600p VP9 alpha, lazy-loaded; idle loads first)\ndocs/                 # zoe-production-handbook.md — clip production specs & prompt cards\n```\n\n## AI layer\n\nOne knowledge layer, three outlets: `/llms-full.txt` for passive crawlers, a chat assistant\n(`POST /chat`, SSE) for humans, and an MCP server (`/mcp`, Streamable HTTP, no auth — tools:\n`get_profile` / `list_experience` / `get_case_study`) for visiting agents, both served from\n`api.sigaoli.com` (Cloudflare Worker, code in `worker/`). The knowledge pack\n([`/knowledge.json`](https://sigaoli.com/knowledge.json)) is assembled at build time from the\nsame sources as the pages — persona markdown, `cv.json`, case studies, photo stats — so any\ncontent edit propagates to all three outlets on the next deploy, no manual step. A privacy\nguard fails the build if sensitive patterns (phone numbers, IDs, coordinates) ever leak into\nthe pack.\n\nAlongside each reply the chat runs a lightweight intent classifier (`POST /classify`, a small\nmodel) to suggest the single most relevant page, and can remember a returning visitor's name —\nboth kept entirely in the visitor's own browser (opt-in, clearable via \"Forget me\"), never on a\nserver. Visitors in the EU/EEA/UK have their chat and classification routed to an EU-hosted\nprovider, never the China-direct API. What the site stores and sends is described in plain\nlanguage at [`/privacy`](https://sigaoli.com/privacy).\n\n## Editing content\n\n- **Case studies / research**: edit `src/content/cases/*.md` (en), then run the translate\n  script — or edit the `-zh` files directly (they're override-protected afterwards).\n- **CV**: edit `src/data/cv.json` (+ `cv.zh.json`); the timeline, `/resume.json` and\n  `/llms-full.txt` all render from it. Replace `public/files/pdf/CV__Sigao_Li.pdf` alongside.\n- **UI strings & hero copy**: hand-written bilingual dictionary in `src/lib/i18n.ts`.\n- **Photos**: drop JPGs into `src/assets/photos/<country>/`, add entries to\n  `src/data/photos.json` (run `node scripts/extract-gps.mjs` for coordinates). Photo stats\n  in the AI knowledge pack update automatically.\n- **AI assistant persona**: edit `src/data/knowledge/*.md`; the knowledge pack rebuilds on\n  every deploy and the assistant follows within ~10 minutes (Worker-side cache TTL).\n- **Zoe's actions**: source clips live outside the repo; the pipeline\n  (`scripts/zoe-board2.mjs` → `zoe-qc2.mjs` → `zoe-prod2.mjs`) keys, QCs, mirrors and\n  encodes them into `public/zoe/`. New actions = one clip + one row in the `ZOE` table in\n  `ChatWidget.astro`; specs and prompt cards in `docs/zoe-production-handbook.md`.\n\n## Deployment\n\nPush to `master` → GitHub Actions (`.github/workflows/deploy.yml`) audits, builds and\ndeploys to Pages. Pushes to `v2` build without deploying (verification).\n\nThe Worker deploys separately: `cd worker && npx wrangler deploy` (secrets via\n`wrangler secret put`; custom domain `api.sigaoli.com` bound in the Cloudflare dashboard).\nWhen a batch changes both, deploy the Worker **first** — the chat UI calls its endpoints, so a\nsite push ahead of the Worker leaves a brief window where those calls 404.\n\nA deploy takes up to a minute to reach every edge location. Checking immediately reads the\nprevious version, which has twice looked like a broken deploy when nothing was wrong — wait,\nthen check.\n\nDaily chat usage is at `https://api.sigaoli.com/usage` (last seven days, plus whether today has\nhit the cap). The cap itself is `DAILY_CAP` in `worker/src/core/quota.ts`; when it trips it emails\nonce via Cloudflare Email Routing.\n\n> ⚠️ **Never click \"Sync fork\".** This repository began as an academicpages fork; syncing\n> would reset `master` to the upstream template. If that ever happens again:\n> `git push --force origin <good-commit>:master`.\n",
  "bytes": 10361,
  "sha": "341cd1543ad462ddf578daedaee36402a5f5b3ffdfbf84ee2f6b123c0017ead4",
  "repo_slug": "sigaoli/sigaoli.github.io",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_com_sigaoli_profile_4ad624aa/readme"
}