In 2-10: Build the Web — Back to HTML, CSS, and JavaScript
we settled the shape: content as text, the frame in HTML and CSS, and Python
baking the two together. Here we ship that baked html/ outward.
There are two homes for it: your own machine from 2-02, or Cloudflare Pages. The procedure is the same either way, so you can start on one and move to the other later.
Decide the shape of publishing first
This chapter settles six things. When you hand this chapter to an AI, these decisions are what you are handing over — not code.
- What is published is the baked HTML (
html/) — generating it was the previous chapter's job - No dynamic server — the moving parts gather on one FastAPI
- One of two homes — your own machine (Caddy) or Cloudflare Pages. The
html/that goes up is the same either way - HTTPS is automatic — Caddy takes it, or Cloudflare adds it
- Build, verify, and deploy stay separate — what you verified and what goes live are the same bytes
- Public and internal are separated by name — anything carrying secrets stays behind the gate (2-05)
Choose between two homes
[cols="1,2,2"]
| Your own machine (2-02 + Caddy) | Cloudflare Pages | |
|---|---|---|
| The machine | ||
| Yours, the one box from 2-02 | ||
| Borrowed; no machine to look after | ||
| Certificate | ||
| Caddy takes it from Let's Encrypt and renews it | ||
| Cloudflare adds it automatically | ||
| Delivery | ||
| As wide as your own line | ||
| From a global CDN | ||
| Moving parts | ||
| Straight to FastAPI on the same machine | ||
| To the API behind the gate on your own machine, from outside | ||
| Suits | ||
| Internal use, publishing at a size you can see, and keeping the IoT intake on the same box | ||
| Heavy traffic, and not wanting to think about the line or the machine |
If you are unsure, start on your own machine. The box from 2-02 is already
standing, and all you add is a few lines of Caddy. If traffic grows and the line
starts to matter, put the same html/ on Cloudflare Pages and you have moved
the same day.
Put it on your own machine
Stand Caddy in front of the Debian box from 2-02. Caddy is in Debian 13's apt and runs under systemd (2-02). It takes ports 80 and 443, fetches the certificate, and renews it. The configuration is this much.
example.com {
root * /srv/html
file_server
handle /api/* {
reverse_proxy localhost:8000
}
}
The public web (static files) and the moving parts (FastAPI) now sit under one domain. Nothing crosses to a second origin, so the browser needs no permission settings.
Three things to decide.
- Where the published files live —
/srv/html. Send thehtml/you baked locally to that path - Internal tools get their own hostname —
intra.example.comor similar, behind the gate (2-05). The same Caddy is fine; the name is what separates them - How the outside reaches you — a fixed IP or DDNS for the name, and only 80 and 443 open
Put it on Cloudflare Pages
To publish while holding no machine, use Cloudflare Pages. Upload the files and they are served from a global CDN, with HTTPS added automatically.
Leave delivery and defense to Cloudflare, and keep the source and the build in your own hands. The published HTML holds no secrets, so this part is safe to hand over.
Uploading needs no npm and no Node — one script hitting Cloudflare's API is
enough. That script is published as
aiseed-dev/cf-publish (on PyPI,
uv tool install cf-publish): one line, and
only the files that changed go up.
Separate build, verify, and deploy
Whichever home you chose, the procedure is the same. Do not fold it into one command.
uv run python tools/build.py # 1. bake
uv run python -m http.server --directory html 8000 # 2. look at it locally
rsync -a --delete html/ [email protected]:/srv/html/ # 3a. to your own machine
cf-publish html --project <name> # 3b. to Cloudflare Pages
Do not use auto-rebuild, and do not use a command that combines "build and deploy." The point is to *keep the HTML you verified identical to the HTML that goes live*. Where there are moving parts, the "verify" step runs the four pre-launch checks of 2-12. Cloudflare Pages has a preview branch ahead of production, so you can look before pointing the production URL at it. On your own machine, adding a second hostname on the same box does the same thing.
Connect the domain
- Your own machine — point the A record at the machine's IP. The moment the name resolves, Caddy goes and fetches the certificate
- Cloudflare Pages — add it as a custom domain. If your DNS is on Cloudflare, the record is added automatically and the certificate comes with it
Leave mail (MX, SPF) untouched. You are changing where the web is served, and nothing else. If an old server is running, keep it running while you verify the new side, and point the name only once you have.
Wire the moving parts with FastAPI
A contact form, a booking intake, a job search — that is about the extent of what moves. Gather it on one FastAPI.
- The intake — FastAPI (2-12, where booking is written too)
- Storage — the DB from 2-03
- Notification — the mail from 2-08
- Identity — the gate (2-05). Contact and booking alike are accepted only after the gate's one-time code (a number sent by mail) has confirmed the address
People from outside confirm their mail address first, too. A reply needs an address, and a confirmed address brings neither typos nor fake submissions. No password and no account creation — one number to type. With that, every moving part sits behind the gate.
How you wire it depends on the home. On your own machine, Caddy sends /api/*
to FastAPI on the same box. On Cloudflare Pages, the public side reaches the
FastAPI on your own machine from outside. Either way, only people whose identity
the gate has confirmed get through.
Separate the public side from behind the gate
The public site can live in either home. It is static and holds no secrets. The internal tools stood up in this part — gate, documents, code, mail, meetings — are handled separately. They carry secrets and raw data, so they stay behind the gate.
- The public site (static, no secrets) — your own machine or Cloudflare Pages
- The internal tools (auth, business data) — behind the gate
A borrowed window can be swapped at any time. The public site's substance is a
pile of static files and the sources in your own hands. Send the same html/ to
another home tomorrow, and the move is complete. The window may be borrowed, but
the exit is always open.
How to check you are done
This chapter is done when these five hold.
- The published site opens on your own domain and the browser shows the lock mark
- The page you looked at on port 8000 locally and the production page look the same
- Before pointing production, the same page opens and can be checked at another URL
- Sending the contact form after typing the number that arrived by mail produces a notification in the 2-08 mail and a row in the 2-03 DB
- Mail still arrives as before, because MX was never touched
curl -sI https://example.com | head -1 # the published page answers
curl -sI https://example.com/api/health # the moving parts answer under the same name
What the human holds
Values the human supplies
- The domain name to publish on
- The choice of home (your own machine, or Cloudflare Pages)
- For your own machine — the machine's IP and the destination path (
/srv/html) - For Cloudflare Pages — the account and API token, and the project name
- The address that receives contact notifications (the 2-08 mail)
Actions the AI states before performing
- Pointing the production domain at a new home
- Replacing the whole production
html/(anything involvingrsync --delete) - Editing the Caddy configuration to add or remove a public hostname
- Shutting down the old server
- Changing the MX or SPF records in DNS (this chapter does not touch them)
Versions checked, and when
- Caddy 2.6 (Debian 13 package, automatic HTTPS), Cloudflare Pages,
cf-publish0.3 (aiseed-dev/cf-publish, PyPI),rsync, Python'shttp.server - This procedure was written on 2026-09-21 and reviewed on 2026-10-06
- If a version has moved, have the AI confirm the official procedure before proceeding
Summary
Ship the baked HTML to one of two homes.
- Two homes — your own machine from 2-02 (Caddy), or Cloudflare Pages. Same
html/ - One procedure — build, verify, deploy, kept separate; ship exactly what you verified
- HTTPS is automatic — Caddy takes it, or Cloudflare adds it
- One FastAPI for the moving parts — storage in 2-03, notification in 2-08; people from outside pass the gate's one-time code before they get through
- Public and internal stay apart — anything carrying secrets stays behind the gate
Next, we expose the core systems' logic as an API (FastAPI) so every app can use it.