Documentation and community site for Codewhale — lives at codewhale.net.
Next.js 15 (App Router) + Tailwind, deployed to Cloudflare Workers via @opennextjs/cloudflare. Curated "Today's Dispatch" content is regenerated every 6 hours by a Cloudflare Cron Trigger that calls deepseek-v4-flash to summarise recent repo activity, and stored in Workers KV.
cd web
npm install
cp .env.example .env.local # fill in the keys you have
npm run dev # http://localhost:3000Env (mirrors .env.example):
| Variable | What | Required? |
|---|---|---|
DEEPSEEK_API_KEY |
DeepSeek platform key (sk-...) |
only for the /api/cron tasks (summarization + community agent) |
GITHUB_TOKEN |
Fine-grained PAT, public-repo read scope | optional (raises rate limit 60 → 5000 req/h) |
GITHUB_REPO |
Defaults to Hmbown/CodeWhale |
optional |
CRON_SECRET |
Shared secret for manual /api/cron invocation |
optional (Cloudflare cron triggers don't need it) |
DEEPSEEK_MODEL |
Defaults to deepseek-v4-flash |
optional |
DEEPSEEK_BASE_URL |
Defaults to https://api.deepseek.com |
optional |
MAINTAINER_TOKEN |
Admin panel auth; access /admin?token=<value> |
only for /admin |
MAINTAINER_GITHUB_PAT |
PAT with issues:write, for posting comments via /admin |
only for /admin posting |
NEXT_PUBLIC_GITEE_ENABLED |
Set to 1 once the Gitee mirror exists; blank hides Gitee links |
optional |
The site renders fine without any of them — Today's Dispatch falls back to a static editorial; the GitHub feed shows "feed not yet loaded".
Ordinary pushes and pull requests run the web checks and production build, but
they do not deploy. The deploy job in .github/workflows/web.yml runs
only for a maintainer-triggered workflow_dispatch on main. Before approval,
record the exact 40-character origin/main SHA and trigger that ref:
git fetch origin main
git rev-parse origin/main
gh workflow run web.yml --repo Hmbown/CodeWhale --ref mainEvery green push to main also emits a Deployment approval needed workflow
notice with that command. The reminder does not receive Cloudflare credentials
and cannot deploy; it keeps the manual production approval boundary visible.
The manual job records the pre-deploy source drift, builds the OpenNext bundle,
deploys only after the protected Cloudflare inputs pass, and then requires the
public /api/facts receipt to report the exact workflow SHA. A credential-free
local comparison is available without starting a deployment:
npm run compare:deployed-facts -- --expected-revision <exact-40-character-sha>You already own codewhale.net on Cloudflare and have a Workers Paid plan. The deploy is two steps:
-
Provision KV namespaces once:
npx wrangler kv namespace create CURATED_KV npx wrangler kv namespace create NEXT_INC_CACHE_KV
Copy the printed
idvalues into the matchingwrangler.jsoncbindings (replace eachREPLACE_WITH_KV_ID). -
Set secrets and deploy:
npx wrangler secret put DEEPSEEK_API_KEY npx wrangler secret put GITHUB_TOKEN # optional npx wrangler secret put CRON_SECRET # optional, for manual /api/cron?task=curate hits npm run deploy # builds with OpenNext + uploads
-
Point the domain: in the Cloudflare dashboard, add a Worker route for
codewhale.net/*→ the deployed Worker, namedcodewhale-web(seewrangler.jsonc).
The first cron run happens within 6 hours; you can also kick it manually:
curl -H "x-cron-secret: $CRON_SECRET" "https://codewhale.net/api/cron?task=curate"Pages are bilingual by default: each app/[locale]/ page renders both
English and Chinese from the same file, keyed by the [locale] segment
(see lib/i18n/config.ts). Copy changes must update both locales. The
v0.9.2 wave adds routed partial locales (ja, vi, ko, ru, uk, es, pt-BR):
their shared chrome (nav/footer/switcher) and home-page copy live in
lib/i18n/dictionaries/<code>/ (checked by npm run check:locales), and
everything else falls back to the English copy. Routing, middleware
detection (lib/i18n/detect.ts), sitemap, and hreflang all derive from the
one registry.
web/
├── app/
│ ├── globals.css ocean portal, docs layout, type, and shared surfaces
│ ├── [locale]/ 10 routed locales; zh has native page bodies,
│ │ the rest fall back to the English body
│ │ ├── layout.tsx root + locale layout: html shell, fonts, nav, footer
│ │ ├── page.tsx home — hero, ticker, proof, decides, workflow,
│ │ │ start, boundaries, surfaces, install band, community
│ │ ├── install/page.tsx per-OS install with auto-detection
│ │ ├── docs/page.tsx modes / tools / approval / config / mcp / providers
│ │ ├── faq/page.tsx frequently asked questions
│ │ ├── feed/page.tsx live mirror of issues + PRs
│ │ ├── roadmap/page.tsx shipped / underway / considered / ruled out
│ │ ├── contribute/page.tsx how to PR + house rules + dev loop
│ │ └── admin/ maintainer panel (page.tsx + admi
4915
n-client.tsx)
│ └── api/
│ ├── cron/route.ts cron tasks: curate, triage, facts-drift, …
│ ├── facts/route.ts public source/deployment receipt
│ ├── github/feed/route.ts cached JSON endpoint
│ └── admin/ login, logout, post (MAINTAINER_TOKEN-gated)
├── data/
│ └── latest-published-release.json manually advanced only after publication
├── components/
│ ├── nav.tsx sticky header w/ date strip + CJK accents
│ ├── footer.tsx dense 5-column footer
│ ├── whale.tsx shared Codewhale mark
│ ├── ticker.tsx live wire: merges, issues, releases + handles
│ ├── feed-card.tsx one issue/PR card
│ ├── locale-switcher.tsx N-locale dropdown with partial badges
│ └── install-*.tsx install page blocks (binary, code block, tiles)
├── lib/
│ ├── types.ts shared types
│ ├── i18n/ locale config, en/zh dictionaries
│ ├── github.ts REST client + relative-time formatter
│ ├── deepseek.ts v4-flash chat client + curate() prompt
│ ├── facts.ts getFacts(): KV value, else build-time FACTS
│ ├── facts.generated.ts GENERATED — do not edit by hand
│ ├── facts-drift.ts runtime re-derivation for the drift cron
│ ├── community-agent.ts triage / pr-review / digest cron tasks
│ └── kv.ts Cloudflare KV access via OpenNext bindings
├── scripts/
│ ├── derive-facts.mjs prebuild: repo sources → lib/facts.generated.ts
│ ├── compare-deployed-facts.mjs credential-free exact-SHA receipt check
│ └── check-kv-id.mjs predeploy guard for KV namespace ids
├── wrangler.jsonc CF Worker config + cron + KV binding
├── open-next.config.ts OpenNext adapter config
└── tailwind.config.ts design tokens
Mechanical facts (version, provider list, sandbox backends, crate names, default model, Node engines) are never hand-written into pages:
- Build time —
scripts/derive-facts.mjsruns asprebuild(and beforenpm run dev), parses the parent repo (Cargo.toml,crates/tui/src/config.rs,crates/tui/src/sandbox/mod.rs,npm/codewhale/package.json) and writeslib/facts.generated.ts. Never edit that file by hand. - Published release —
data/latest-published-release.jsonrecords the latest GitHub Release separately from the source candidate. Install commands use this published tag; they never turn the workspace version into a release before publication. The credential-free deployed-facts comparison checks the record against the public receipt. - Runtime — the
/api/cron?task=facts-driftcron (lib/facts-drift.ts) resolves an exactmainrevision, derives every source fact from that SHA, and writes changes toCURATED_KVunderfacts:current. Pages accept that snapshot only when its source provenance is the same as or newer than the deployed build. Legacy, malformed, or older KV data cannot replace newer build facts; published-release metadata is resolved independently. Public fact pages revalidate their cached HTML every five minutes.
/api/facts exposes only public provenance and counts: deployed/resolved source
revision, version, provider count, tool count, selection reason, and latest
published release. It contains no environment values, tokens, or KV contents.
When a new ApiProvider variant lands in crates/tui/src/config.rs, it must
be added to the labelMap in both scripts/derive-facts.mjs and
lib/facts-drift.ts (or to the EXCLUDED set if deliberately hidden). Both
fail loudly on unmapped variants, so the build / cron will tell you.
The public site is a documentation portal with a restrained underwater atmosphere. Content and navigation come first; ocean depth, currents, and the whale mark provide identity without turning every section into a themed card.
- Palette: cool paper and mist for reading surfaces, deep navy for terminal and community sections, muted current blue for links, and small gold/coral signals where status needs contrast.
- Type: Space Grotesk for headings, IBM Plex Sans for body copy, and JetBrains Mono for commands and compact interface labels.
- Structure: compact documentation rows, quiet hairline dividers, generous but bounded reading widths, and responsive layouts that remove chrome before content.
If you want to retune the palette, edit :root in app/globals.css and the colors block in tailwind.config.ts.