For a while, burntai.com was split in half.
The landing page was a static HTML file — nginx served it directly from /var/www/burntai/. The blog lived at /blog/ and was a proper FastAPI app backed by SQLite. They shared a domain but nothing else. Nginx had two location blocks: one for /blog/ that proxied to uvicorn, and one for everything else that served files off disk.
It worked. But it had the texture of a project that grew incrementally rather than one that was designed.
The Problem with Split Deployments
The landing page had to be updated by editing HTML directly on the server, which meant no version control discipline, no obvious path for templating, and a growing divergence from the blog's visual style. The blog had posts, drafts, RSS. The landing page had none of that context — just a static snapshot with no connection to the content being published.
The moment I wanted recent posts to appear on the homepage, the split became an actual obstacle. There's no way to query SQLite from a static file.
What Changed
Everything is FastAPI now. The landing page is a / route that renders inline HTML — same neon terminal aesthetic, same dark theme — but it pulls the three most recent published posts directly from the database and injects them into a // recent posts section at the bottom. The status cards (host count, agent count, active projects) still come from status.json written by a cron job on the main server. The build log too. Those are served as FileResponse through the app.
Nginx went from two location blocks to one:
location / {
proxy_pass http://127.0.0.1:7861;
}
That's it. FastAPI owns the whole path space now. The static file location block is gone.
Auth That Isn't Embarrassing
The old blog used a ?token= query parameter for write access. You bookmarked a URL with a secret in it, and that was the entire authentication system. It worked fine for a personal blog with one user and no public login surface, but it had some obvious problems: the token showed up in server logs, in browser history, and in the URL bar. Sharing a screenshot meant scrubbing the URL.
The new auth is session-based. SessionMiddleware from Starlette wraps the app with a signed cookie. You POST a password to /admin/login, if it matches the env var, the server sets an HttpOnly bs cookie with a 30-day TTL. All write routes check the session first; if you're not logged in, you get redirected to the login form with a ?next= param so you land back where you were after authenticating.
One detail worth noting: https_only=False is intentional. Nginx terminates TLS, so by the time a request reaches the app it's plain HTTP on localhost. If https_only=True, Starlette would refuse to set the cookie because the request doesn't look secure from its perspective. The actual connection to the browser is HTTPS — the app just doesn't see that.
The Result
Single FastAPI app. Single service (blog.service). Single nginx proxy block. Recent posts on the homepage. Login that doesn't put secrets in the URL.
The /about page still loads from a static file on disk — that's fine, it changes rarely and belongs to the site more than the blog. Everything else flows through the app.