Zhang Lu.

ARCHITECTURE

How this site works

Content is plain text files in a git repo; one command compiles it into a full set of static artifacts; pushing deploys it. No CMS, no database, no server runtime — production is static files on a CDN.

That single constraint is the root of everything else: with no server, search has to run in the client, endpoints have to be files written at build time, there can be no auth, and nothing can depend on a third-party API at request time.

Big picture: one file to production

Four layers — source → contract → build → artifacts — then a push to the CDN. Only the first layer is written by hand.

① Source · the only hand-written part

src/content/<coll>/

Chinese content

src/content/<coll>En/

English parallel

src/data/*.json

bio / socials

src/i18n/ui.ts

UI strings

② Contract · Zod schema

src/content/config.ts

missing field / wrong type → build fails (not a blank page in production)


pnpm build

③ Build · Astro

field shapes: src/lib/api.ts (one copy, shared by zh / en)

④ Artifacts · all static files

5 content types × 2 languages

59 entries × 2

HTML pages

every page in both languages + 404

24 endpoint kinds

12 × 2 languages → 118 JSON files

2 RSS feeds

/rss.xml · /en/rss.xml

2 llms.txt

agent discovery entry

sitemap · robots · _headers

hreflang / Content-Signal / CORS


git push origin main

Cloudflare Pages

live in 1–2 minutes

humans

browser

zhanglu.net · /en/

agents

JSON endpoints

/api · /en/api

CLI

npm package

npx zhanglu-net

crawlers

sitemap + robots

Content-Signal

5 content types

Each type has parallel Chinese and English content, 1:1 in count. One collection = one directory; one entry = one markdown file.

Collection Entries Pages Endpoints Notes
projects Projects 8 × 2 list + detail list + detail carries loc / persona / cover
articles Writing 5 × 2 list only list only a writing index — links point to the original source
presentations Decks 4 × 2 list only list only cards link straight out
skills Skills 41 × 2 list + detail list + detail 16 auto-synced + 14 hand-written
weekly Weekly 1 × 2 list + detail list + detail redacted public notes

Write once, appear in 7 places

This is where the structure pays for itself. Take src/content/projectsEn/boss.md — write one file and it shows up in every one of these, with nothing to sync by hand.

projectsEn/boss.md

│ pnpm build

1 /en/projects/boss page human
2 /en/projects list card human
3 homepage “Featured projects” human
4 /en/api/projects.json agent
5 /en/api/projects/boss.json (full text) agent
6 /en/api/search.json corpus agent / CLI
7 RSS + sitemap entry subscribers / search

The Chinese projects/boss.md produces 7 more

How the bilingual setup works

Routing

Chinese at the root /, English under /en/. Assets, /api, external links and anchors are never prefixed — only page routes are.

Detection

Components call getLangFromUrl(Astro.url) and detect for themselves, rather than threading a prop down every level.

Content

Parallel *En collections, not one collection with a language field — the latter needs filtering in a dozen consumers, and missing one leaks the wrong language. The cost is writing content twice.

UI strings

A dictionary in src/i18n/ui.ts (zh / en). Page-specific prose stays in each language’s own page file, out of the dictionary.

First visit

Redirects by navigator.language — an English browser opening zhanglu.net lands on /en/.

Manual switch

The header “中 / EN” writes the choice to localStorage; after that your choice wins and the auto-redirect stops.

SEO

<html lang>, hreflang (zh-CN / en / x-default), og:locale, per-language RSS, sitemap i18n.

The layer built for machines

Everything is published as JSON too, so agents never have to parse HTML. Full usage lives at /en/agents.

Discovery entry

/en/llms.txt · /llms.txt

manifest

/en/api/index.json — counts + every endpoint + cross-language links

Lists · 8

projects · articles · presentations · skills · weekly · about · social · search

Details · 3 (with body_md)

projects/{slug} · skills/{slug} · weekly/{slug}

A few constraints, and why

These aren't abstract principles — each one came out of a real piece of rework.

No server

Production is nothing but static files. Always up, zero ops, a build is a snapshot. Every other decision follows from this — that is why search runs client-side, why endpoints are files written at build time, and why there is no auth.

Schema is enforced

Zod schemas live in src/content/config.ts. A missing field or wrong type fails the build — instead of shipping a blank page to production.

Field shapes defined once

All endpoint field shapes live in src/lib/api.ts, shared by the zh and en endpoints. Early on each endpoint inlined its own fields and they drifted: the list had loc/persona/cover, the detail did not.

Drifty numbers are computed

Every count on this page, and the CLI line count and version on /agents, are read at build time. The copy once hardcoded “270 lines” long after it had passed 500.

Status codes are honest

A missing path returns a real 404, not 200 plus the homepage. An agent can trust the status code to tell whether an endpoint exists.

Table-driven

The CLI's KINDS table: adding a content type touches one place, and list / get / help text all follow automatically.

From one edit to production

edit src/content/... or src/data/...
  │
  ├── pnpm dev      hot reload locally
  ├── pnpm build    must pass (schema check + artifact generation)
  ├── docs/dev-log/ leave a process record
  └── git push main → Cloudflare Pages → live in 1–2 minutes

The full working rules live in AGENTS.md — the authoritative guide for AI agents and for future me, covering schemas, traps already hit, and the release flow. The architecture doc behind this page: docs/architecture.md.