色系: 中文
Public · Technical readers

Anatomy of a Four-Page Site

One config file grows the whole site. Taste is taste; engineering is engineering.

v1.0.02026-08-19 Mila Chen

This document covers how mila-site is built. The design decisions (why watercolor, why an aurora) live in the blog post; this is engineering only.

01Architecture overview

A static Astro site on a free Cloudflare Pages subdomain: hosting cost 0, domain cost 0. The case for Astro is simple: zero JS by default, content collections turn posts into Markdown files in a folder, and the build output is plain static files with no server to babysit. A four-page site needs nothing heavier.

02One config file grows all the styling

Not a single color or font is hard-coded in any component. The single source of truth is one brand.json: palette, fonts, both light/dark palettes, channel links. A pre-build script emits it as CSS variables plus metadata; components only ever reference variables.

The payoff comes at revision time Swapping the palette, the fonts, or the tagline means editing one JSON file and rebuilding.

This skeleton will later generalize into a template: a new brand site equals a fresh brand.json plus a content directory.

03How light/dark mode is engineered

Dual mode is not a filter. Every color token has separate light and dark values, and even the cover images are a matched pair of paintings; the browser only loads the one for the active theme.

There are three states: an explicit user choice (data-theme on the html element, stored in localStorage), following the system (prefers-color-scheme), and an inline script that applies the theme before render to avoid a white flash.

The easiest thing to miss Every themed CSS rule needs two selectors to cover both paths, "manually chose dark" and "following the system"; write only one and the toggle button fights the OS setting.

04i18n: a four-page site doesn't deserve a framework

Bilingual, / for Traditional Chinese and /en/ for English. No i18n framework: both languages' copy live in one dictionary file, routes are hand-written, and a post ships in a single language by setting lang in its frontmatter. I evaluated frameworks; for four pages, the abstraction is bigger than the problem.

So when does a framework earn its place? My three tripwires — hit any one and it's time to re-evaluate:

  • A third language. Two languages are an if/else; three become a matrix, and the dictionary file buckles first.
  • Too many routes to hand-write safely. With enough pages, hreflang, sitemaps, and language fallbacks are mechanical work a framework should own; humans eventually miss one.
  • Translation stops being a one-person job. Translation workflows, fallback to a default language, tracking what's untranslated: that is framework territory.

Until then, every added layer of abstraction is interest paid on a problem that doesn't exist.

05Motion engineering: keeping the aurora alive across pages

Motion has one goal: the whole site should carry the aurora's flow. Three pieces of engineering:

  • Page changes use View Transitions; the background aurora layer carries transition:persist, so the element survives navigation untouched and its animation never restarts. Without this, the aurora blinks out on every page change.
  • Scroll reveals use IntersectionObserver, with a fuse: if nothing fires within 2.5 seconds, everything is force-shown. A dead animation is acceptable; invisible content is not.
  • Turn on prefers-reduced-motion and everything goes still.

Motion discipline Slow and light, one metaphor at a time (the aurora). The first version layered colored fog over the page: technically animated, visually zero. Tearing it down produced the current light-and-mist approach.

06The image pipeline

The cover auroras come from an image model, with style held by anchors rather than luck: every generation attaches my own watercolor paintings as reference images with a fixed set of style terms, so the output stays in one family. Everything is re-saved through PIL before it ships, stripping all metadata. Generation runs on a subscription quota at zero API cost — a pipeline that deserves its own write-up.

← Back to: Why the site looks like this