Contact EN 中文

Tzu Chi Annual Report

Static annual-report site with a gated live-draft preview and a staff-editable CMS

At a glance

Problem
The Tzu Chi Foundation publishes an annual report every year.
What I built
A public report site per year (2021–2025 content) plus an admin dashboard.
Outcome
Live; a staff save reaches the site in about 2 minutes by design, and drafts are now visible only to signed-in editors.
My role
Solo
Period
2026-06 → 2026-09

Abilities shown

Each links to the ability and its level on the Skills page.

The problem

The Tzu Chi Foundation publishes an annual report every year. The old site was a Next.js app that fetched its content in the browser through server-side proxies. Drafts could be previewed through an open ?preview=1 link that anyone could use, and published pages depended on backend reads on every visit. Staff already edited content in an admin dashboard; the goal was to keep that, make published pages static and cheap to serve, and close the draft leak.

What I built

A public report site per year (2021–2025 content) plus an admin dashboard. Published years are prerendered to static HTML on Cloudflare Pages. A parallel /preview/{year} tree renders drafts on demand, only for signed-in editors. The dashboard is static HTML on an existing schema-driven form builder, so a new field is a JSON config change. I rewired it to talk to drust directly with Google OAuth login and added the icon picker. The site has a 3-tier light/dark design-token system. The build also detects transparent-background PNG charts so they stay readable in dark mode.

How it works

  1. An editor signs in with Google OAuth through drust; only an allowlist of accounts can log in.
  2. The dashboard writes straight to drust with the editor’s own token. No server holds a privileged key.
  3. Drafts are visible at once under /preview. Middleware checks the session cookie against drust before rendering; any failure redirects to login, and no draft markup is sent.
  4. Every save fires a webhook that starts a GitLab pipeline: build, 3 build assertions, deploy to preview, then production. The change is live in about 2 minutes by design.
  5. An hourly scheduled rebuild catches any missed webhook.

Decisions

  • Static published pages, server-rendered previews. Static pages cut per-visit backend and Worker calls. The old ?preview=1 link was open to anyone; a gated route closed that hole.
  • Dashboard talks to drust directly. I deleted the proxy layer that held a service token. A build gate and a unit test now assert that token never appears in src/ or dist/.
  • Fail the build, not the site. If drust is unreachable or build env is missing, the build stops loudly instead of prerendering an empty site, and the last good deployment stays live.
  • Code decides where images go, not an LLM. For the 2025 report I built a Python ingest pipeline over the source .docx. Image placement comes from each image’s heading path in the document, because earlier LLM-guessed placement had mixed up photos. An LLM only tidies text inside one record, and a fidelity check falls back to the plain version if images or text go missing.

Outcome

The site is live, with 2024 and 2025 published and 2021–2025 content imported. 2025 is published but still missing some authored content (report PDFs, homepage copy, portraits), and the site is set to noindex for now. Built solo from June to September 2026, with Claude Code in a spec → plan → test-first → review loop; 13 specs and 11 plans are in the repo.