← writing
29 July 2026

Astro: rules before pages, then publishing

The whole path, private repository included: design system first, implementation second, deployment on every push. With the five traps that actually bit, because none of them is in any documentation.

Two tools, two moments. Claude Design is for setting the rules: colour and spacing tokens, surfaces, what the brand refuses. Claude Code is for applying them. Separating the two is not method for its own sake: it is what later lets you say a page is wrong against a written rule rather than against a taste.

01the rule before the page

The system comes out as CSS variables and a rules file. The second is the more useful: it lists what is forbidden. No hard-coded hex, no drop shadow, no border radius. A checker reads the whole repository and fails the build on any breach. It has already rejected code I wrote myself, which is precisely its job.

json
// package.json — the repository's front door
{
  "scripts": {
    "check": "astro check",
    "adherence": "node scripts/check-adherence.mjs",
    "verify": "npm run check && npm run adherence && npm run build"
  }
}
02static, actually

Astro in static output produces only HTML, CSS and a few kilobytes of script. No server rendering, so nothing to keep alive in production: the page served is the page that was built. That is what makes the hosting nearly free and the handover possible.

03the repository stays private

The repository is private, the site public. GitHub Actions builds, then uploads the output directory to Cloudflare Pages. Two secrets are enough, an API token and an account id. Deployment is gated on the check: if a system rule is breached, nothing ships.

yaml
- name: Verify
  run: npm run verify
  working-directory: site

- name: Publish to Cloudflare Pages
  if: github.event_name != 'pull_request'
  uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
    workingDirectory: site
    command: pages deploy dist --project-name=preview-commutator --branch=main
04the five traps
  • You cannot add the Git integration to a Pages project that already exists. If it was created for direct upload, it stays that way; either recreate it or drive it from Actions, which is what this site does.
  • The wrangler action splits the command on whitespace. An option holding a multi-line message shatters it, and the error says nothing useful. The command has to stay a single line of plain arguments.
  • Cloudflare appends a suffix to the project subdomain: the name you choose is not the address you get. Read the real address rather than infer it.
  • A custom domain is not settled by DNS alone. Until the domain is bound to the project on Cloudflare, resolution is fine and the certificate fails: a symptom that sends you looking in the wrong place.
  • The edge cache serves for a long time. A renamed page went on being served a week after deletion, with a week-long cache-control and a HIT status. Deleting a route does not reach it: you have to redirect explicitly.
plaintext
# public/_redirects — old addresses go straight to the final destination,
# not chained one into the next
/journal/*  /points-de-vue/:splat  301
/journal    /points-de-vue         301
05what it costs

The domain name, and nothing else. The free Pages plan is ample for a site this size, and GitHub Actions does not bill public repositories or small volumes. The real cost is elsewhere: it is in the rules written at the start, which take a day and save weeks of rework.