How FolioPress works: one page, every language
Most websites speak one language. The moment a business needs a second one, things get awkward: a translation plugin bolted on top, a second copy of the site to keep in sync, or a pricier plan just to say "Olá" next to "Hello".
FolioPress started from the opposite idea: a page isn't a page with translations attached - it is all of its languages at once. The title, the address, the menu, the tags, what Google sees: everything exists in every language you choose, from the first minute.
It all started with the wish to replace a custom-built CMS that had multiple languages bolted into it. After searching for alternatives I found out they all failed miserably in integrating multiple language content in a simple-to-use website builder.
This is a look behind the curtain: how the whole thing fits together, the trade-offs behind it, and the things that broke along the way.
The bird's-eye view
A visit travels left to right. The control tower never sits in that path. It only builds, moves and looks after the sites.
A visit to a FolioPress site makes three hops. Cloudflare catches it near the visitor and filters out junk. A front door: Traefik, which also takes care of the HTTPS certificates - looks at the address and finds the right site. The site itself answers, either from a shared building, where many small sites live side by side, or from a house of its own when it gets busy. Off to the side, a control tower handles everything a visitor never sees: sign-ups, billing, web addresses, updates and health checks, with a small caretaker on each server that starts and stops the sites.
All of it is Python, in three layers I wrote: a web framework (pystarter, built on FastAPI), the content management system on top (foliopress), and the control tower (Forestry). Payments go through Polar, which acts as the seller of record, so European VAT is their headache rather than mine.
Every page, every language
Each language gets its own readable address and, if you like, its own domain.
When a Lisbon guesthouse writes its "About" page, it writes it once per language, and each version gets an address that reads naturally in that language. Under the hood, a translation is a row next to the original, not a copy of the page: titles, addresses, menus, tags and search descriptions all live per language. Visitors land on their language automatically, and Google is told exactly which page is which (through hreflang tags and a sitemap per language), so the Portuguese version shows up for Portuguese searches. Want the German site on its own .de domain? That works too. And when you'd rather not start from a blank page, an AI first draft of each translation is one click away, you review it, you decide.
Each site keeps all of this in its own small database file (SQLite). There's no database server to run, the nightly off-site backup is a simple file copy, and a whole site is one folder that can be moved, restored or deleted in one go.
Small sites share, busy sites move out
Most websites are quiet most of the time. Giving each one its own machinery would be like renting a whole house for someone who visits twice a year. So small sites share a building - one process, strictly separate data - and the platform watches each site's traffic. When one gets busy, it moves into a house of its own. When it quiets down for a couple of weeks, it moves back.
Sharing a building only works if the walls are solid. When a request arrives, the shared process looks at the address, then switches to that site's own database and settings just for that request (using Python's contextvars), so one site never sees another's data. Each site's container also has its own private network with an outbound firewall. And because owners can upload and edit their own themes, those templates run in a sandbox with limits on time, output and memory.
What counts as "busy" took a few tries. A single newsletter, or one crawler having a burst, shouldn't make a site look popular. So the score is the site's typical day over the past week (the median), plus how busy its busy hours are (the 95th-percentile hour), which ignores the handful of wildest hours.
Moving day, without closing the shop
Every update, and every move between the shared building and a house of its own, happens while the site keeps serving visitors. Engineers call it a blue/green deploy: a new copy of the site starts next to the old one, and only when it's healthy does the front door start sending people to it. Two details make it truly seamless. The caretaker doesn't assume the front door has switched over, it checks the front door's own numbers until it has. And the old copy is told to "drain" first: it stops accepting new visitors, finishes whatever it was doing, and only then switches off.
Things that broke
Nothing teaches you about a system like watching it fail. A few favourites:
The time I took everything down myself. To keep one slow page from holding up everyone else, I moved page rendering onto a pool of worker threads. Then, to check that site moves had no hiccups, I sent a flood of test traffic at the live sites. They froze for nine minutes and stayed frozen after the traffic stopped. Pages were each holding one database connection while asking for a second, and with dozens of threads and a pool of fifteen connections, every thread ended up waiting for another one. Meanwhile the health check, which never touched the database, cheerfully reported that everything was fine.
The fix: database pools that can't deadlock, a limiter that turns an overload into a quick "try again" instead of a hang (with a fair share for each site, so one busy site can't starve the others), a health check that notices a stuck process, a watchdog that restarts it and a firm rule about where load tests happen.
The 45-second hiccup. Most site moves were seamless, but one kind wasn't: if the move changed the site's routing rules, the old and new copies briefly described the same route in two different ways. The front door, unable to decide which to believe, served neither for about 45 seconds. Every other part of the move was correct, which is exactly why it took a while to spot.
A tiny site at 100% CPU. A small shop with a few hundred visitors a day kept maxing out its processor. The culprit wasn't customers, it was automated scanners firing more than a hundred requests in ten seconds, looking for files like /.env or /wp-login.php that a FolioPress site never has. Each one got a beautiful, fully designed "page not found": about 70 ms of work, a hundred times over.
The fix: those requests now get a nine-byte "Not found" before any real work happens, and Cloudflare turns most of them away before they arrive.
"Server not found" for a site that was up. A brand-new site's web address is created during setup. If anything looked the name up a moment too early, internet providers remembered "this name doesn't exist" for up to thirty minutes, even though the site was working fine. The fix was a catch-all address, so every name under foliopress.io always exists; names without a site get a friendly "there's no site here" page.
Neighbours. Hosting many sites side by side raises questions a single website never has to answer: what one site's code can do to another, and what a site can claim that isn't its own. A security review before opening sign-ups turned up a few of these. All of them are fixed: stricter cookies, proof of ownership for custom domains, and every site mounted in its own separate folder, so no site can reach another's files.
What I'd tell someone starting out
- Boring, proven pieces go a long way. SQLite, a single framework, plain containers. Most of the hard problems were in the seams between them.
- Measure before believing. The traffic score, the outage and the CPU spikes all looked like something else at first.
- Design for the quiet majority. Most sites are small. Making small and cheap is what makes the whole thing possible.
What's next
Social publishing, mermaid diagrams, but mostly waiting for user feedback to shape the future.
If you have a site that needs to speak more than one language, give it a try at foliopress.io — the first 30 days are free. And if you're the technical type, I'd love to hear how you would have built it.