Jichuan

§ 09 — Colophon

How this site is built

All of it, including the parts that went wrong. If we are going to sell the claim that we know how to build a site, the site had better be inspectable.

Stack

LayerChoiceWhy
FrameworkAstro, static outputAnything without a client directive ships as plain HTML. That is an architectural property, not a configuration trick
StylingTailwind CSS v4 + hand-written tokensTokens carry the two surfaces; Tailwind carries the rest
MotionNative CSS onlyView Transitions and scroll-driven animations run on the compositor, cost zero JS, and cannot affect what a crawler reads
ContentAstro Content CollectionsPrerendered at build time
Iconsastro-icon, inlinedNo icon font, no CDN request
Client JS0 bytesVerified on every build, not aspirational

Type

Latin display and UI is Archivo (OFL 1.1), self-hosted. It was drawn for print and wayfinding signage — built to stay legible at a glance, which is what a price list needs. Numerals, readings and code are IBM Plex Mono (OFL 1.1). Both are subset and served from our own origin, because the CSP allows font-src 'self' and nothing else.

Chinese body text uses the reader's system stack — PingFang SC, HarmonyOS Sans, Microsoft YaHei. This is deliberate. A full self-hosted Chinese face is measured in megabytes and the swap is visible; system Chinese faces on Chinese devices are already good. Where a distinctive Chinese face is genuinely wanted, LXGW WenKai (OFL 1.1) is subset to the exact characters used, which is a few kilobytes.

We are not using Geist. Its licence is fine; its recognisability is the problem. Same reasoning excludes the shadcn default palette.

Deployment

Static output served from a single VPS in Los Angeles, behind a Cloudflare Tunnel, with Cloudflare proxying in front. The API lives on the same origin under /api/, reverse-proxied to a hand-written Node server — same origin means no CORS configuration and no cross-site cookie problems.

There is no CI. The gate lives in deploy.sh, which is where a one-person project can actually maintain it.

What runs before anything ships

scripts/geo-check.mjs runs against dist/ — the real artefact, not the source — and exits non-zero on any fatal finding:

  1. Body text per page with script and style blocks removed, against a floor
  2. Client JavaScript byte count and any external script references
  3. canonical present, unique, and matching the page's own URL
  4. hreflang bidirectionality, and that x-default lands on a real page
  5. Every JSON-LD block parses, and inLanguage matches <html lang>
  6. Internal links resolve
  7. One h1, title and description present
  8. Banned-phrase scan across the rendered copy
  9. sitemap reconciled against the canonical set
  10. robots.txt lists the crawlers it is supposed to list

Mistakes, kept on the record

Counting bytes and calling them characters

An early measurement of our own production site reported 14,524 characters of body text. Two errors compounded: wc -c counts bytes, and a tag-stripping regex that removes <script> tags but not their contentsleaves the JSON-LD and inline JavaScript in the count. The real figure is 3,999. The conclusion drawn from it — "close to a full score" — had to be downgraded to "structurally sound, corpus nearly empty."

canonical and sitemap disagreeing by one slash

The first green build emitted /zh in the canonical tag and/zh/ in the sitemap. A canonical pointing at an address that redirects is worse than no canonical at all. Nothing in the build output suggested a problem — it was found by diffing the two sets. Check number nine exists because of this.

Nearly copying a blocklist that blocks the wrong thing

A research pass recommended the nginx-block-ai-bots.conf from a well-known AI-crawler repository, described as blocking training crawlers while allowing citation crawlers. Reading the twelve lines shows it is an undifferentiated blocklist: GPTBot, ChatGPT-User, ClaudeBot, Claude-User and Claude-SearchBot all return 403. We take the user-agent list from that repository and nothing else.

Open source used

Astro, Tailwind CSS, @astrojs/sitemap, @astrojs/mdx,astro-icon, Archivo, IBM Plex Mono, LXGW WenKai, and the user-agent list from ai-robots-txt/ai.robots.txt.

Licences are checked at adoption and again at deploy time — 338 production dependencies, all permissive. The allowlist stays strict and copyleft does not get in; where something genuinely has to be allowed it goes in as a named exception rather than by widening the list, because widening it means the next copyleft package slips through silently and the gate stops being a gate.

There is one such exception. @img/sharp-* isApache-2.0 AND LGPL-3.0-or-later — the LGPL half is libvips, bundled inside the prebuilt binary that Astro's image pipeline uses. It runs at build time only, the library itself is not redistributed, LGPL permits linking without reaching our own source, and a resized image is not a derivative work of the resizer.