Zo werkt FolioPress: één pagina, elke taal
De meeste websites spreken één taal. Zodra een bedrijf een tweede nodig heeft, wordt het lastig: een vertaalplugin die erbovenop wordt geschroefd, een tweede kopie van de site die je synchroon moet houden, of een duurder abonnement alleen om “Hello” naast “Hallo” te zetten.
FolioPress begon vanuit het tegenovergestelde idee: een pagina is geen pagina met vertalingen eraan vastgeplakt – ze is al haar talen tegelijk. De titel, het adres, het menu, de tags, wat Google ziet: alles bestaat in elke taal die je kiest, vanaf de eerste minuut.
Het begon allemaal met de wens om een zelfgebouwd CMS te vervangen waar achteraf meerdere talen in waren geschroefd. Toen ik naar alternatieven zocht, ontdekte ik dat ze allemaal jammerlijk faalden in het integreren van meertalige content in een eenvoudig te gebruiken websitebouwer.
Dit is een kijkje achter de schermen: hoe het geheel in elkaar zit, de afwegingen erachter en de dingen die onderweg stukgingen.
In vogelvlucht
Een bezoek reist van links naar rechts. De verkeerstoren zit nooit op dat pad. Die bouwt, verhuist en verzorgt alleen de sites.
Een bezoek aan een FolioPress-site maakt drie sprongen. Cloudflare vangt het op dicht bij de bezoeker en filtert de rommel eruit. Een voordeur – Traefik, dat ook voor de HTTPS-certificaten zorgt – kijkt naar het adres en vindt de juiste site. De site zelf antwoordt, ofwel vanuit een gedeeld gebouw, waar veel kleine sites naast elkaar wonen, ofwel vanuit een eigen huis als het er druk wordt. Aan de zijlijn regelt een verkeerstoren alles wat een bezoeker nooit ziet: aanmeldingen, facturatie, webadressen, updates en gezondheidschecks, met een kleine conciërge op elke server die de sites start en stopt.
Het is allemaal Python, in drie lagen die ik zelf heb geschreven: een webframework (pystarter, gebouwd op FastAPI), het contentmanagementsysteem daarbovenop (foliopress) en de verkeerstoren (Forestry). Betalingen lopen via Polar, dat optreedt als officiële verkoper (de zogeheten merchant of record), dus de Europese btw is hun hoofdpijn en niet de mijne.
Elke pagina, elke taal
Elke taal krijgt een eigen, leesbaar adres en, als je wilt, een eigen domein.
Als een guesthouse in Lissabon zijn pagina “Over ons” schrijft, schrijft het die één keer per taal, en elke versie krijgt een adres dat in die taal natuurlijk leest. Onder de motorkap is een vertaling een record naast het origineel, geen kopie van de pagina: titels, adressen, menu's, tags en beschrijvingen voor zoekmachines bestaan allemaal per taal. Bezoekers komen automatisch in hun eigen taal terecht, en Google krijgt precies te horen welke pagina welke is (via hreflang-tags en een sitemap per taal), zodat de Portugese versie verschijnt bij Portugese zoekopdrachten. Wil je de Duitse site op een eigen .de-domein? Kan ook. En als je liever niet met een leeg vel begint, staat er met één klik een eerste AI-versie van elke vertaling klaar: jij kijkt hem na, jij beslist.
Elke site bewaart dit allemaal in een eigen klein databasebestand (SQLite). Er is geen databaseserver om te beheren, de nachtelijke externe back-up is een simpele bestandskopie, en een hele site is één map die je in één keer kunt verplaatsen, herstellen of verwijderen.
Kleine sites delen, drukke sites verhuizen
De meeste websites zijn het grootste deel van de tijd rustig. Elke site een eigen machinerie geven zou zijn alsof je een heel huis huurt voor iemand die twee keer per jaar langskomt. Dus delen kleine sites een gebouw – één proces, strikt gescheiden data – en houdt het platform het verkeer van elke site in de gaten. Wordt het ergens druk, dan verhuist die site naar een eigen huis. Wordt het een paar weken rustig, dan verhuist hij terug.
Een gebouw delen werkt alleen als de muren stevig zijn. Als er een verzoek binnenkomt, kijkt het gedeelde proces naar het adres en schakelt dan alleen voor dat verzoek over op de eigen database en instellingen van die site (met Pythons contextvars), zodat een site nooit de data van een andere ziet. De container van elke site heeft bovendien een eigen privénetwerk met een firewall voor uitgaand verkeer. En omdat eigenaren hun eigen thema's kunnen uploaden en bewerken, draaien die templates in een sandbox met limieten op tijd, uitvoer en geheugen.
Wat als “druk” telt, vergde een paar pogingen. Eén nieuwsbrief, of een crawler die even losgaat, mag een site niet populair laten lijken. Daarom is de score de typische dag van de site in de afgelopen week (de mediaan), plus hoe druk de drukke uren zijn (het uur op het 95e percentiel), waardoor het handjevol wildste uren buiten beschouwing blijft.
Verhuisdag, zonder de winkel te sluiten
Elke update, en elke verhuizing tussen het gedeelde gebouw en een eigen huis, gebeurt terwijl de site gewoon bezoekers blijft bedienen. Engineers noemen dat een blue/green-deploy: een nieuwe kopie van de site start naast de oude, en pas als die gezond is, begint de voordeur er mensen naartoe te sturen. Twee details maken het echt naadloos. De conciërge gaat er niet van uit dat de voordeur is omgeschakeld, maar controleert de cijfers van de voordeur zelf tot dat zo is. En de oude kopie krijgt eerst de opdracht om “leeg te lopen”: ze neemt geen nieuwe bezoekers meer aan, maakt af waar ze mee bezig was en schakelt pas dan uit.
Wat er stukging
Niets leert je zoveel over een systeem als het zien falen. Een paar favorieten:
De keer dat ik alles zelf platlegde. Om te voorkomen dat één trage pagina alle andere ophield, verplaatste ik het renderen van pagina's naar een pool van worker-threads. Om daarna te controleren of site-verhuizingen zonder haperingen verliepen, stuurde ik een stortvloed aan testverkeer op de live sites af. Ze bevroren negen minuten lang en bleven bevroren nadat het verkeer was gestopt. Elke pagina hield één databaseverbinding vast terwijl ze om een tweede vroeg, en met tientallen threads en een pool van vijftien verbindingen wachtte uiteindelijk elke thread op een andere. Ondertussen meldde de gezondheidscheck, die de database nooit aanraakte, opgewekt dat alles prima was.
De oplossing: databasepools die niet in een deadlock kunnen raken, een limiter die een overbelasting omzet in een snel “probeer het opnieuw” in plaats van vastlopen (met een eerlijk deel voor elke site, zodat één drukke site de rest niet kan uithongeren), een gezondheidscheck die een vastgelopen proces opmerkt, een watchdog die het herstart en een vaste regel over waar loadtests plaatsvinden.
De hapering van 45 seconden. De meeste site-verhuizingen verliepen naadloos, maar één soort niet: als de verhuizing de routeringsregels van de site veranderde, beschreven de oude en de nieuwe kopie heel even dezelfde route op twee verschillende manieren. De voordeur kon niet kiezen wie ze moest geloven en bediende ongeveer 45 seconden lang geen van beide. Al het andere aan de verhuizing klopte, en juist daarom duurde het een tijdje voordat het opviel.
Een piepkleine site op 100% CPU. Een kleine webwinkel met een paar honderd bezoekers per dag trok steeds zijn processor helemaal vol. De boosdoeners waren niet de klanten, maar geautomatiseerde scanners die meer dan honderd verzoeken in tien seconden afvuurden, op zoek naar bestanden als /.env of /wp-login.php die een FolioPress-site nooit heeft. Elk verzoek kreeg een prachtige, volledig vormgegeven “pagina niet gevonden”: zo'n 70 ms werk, honderd keer achter elkaar.
De oplossing: die verzoeken krijgen nu een “Not found” van negen bytes voordat er echt werk wordt gedaan, en Cloudflare houdt de meeste al tegen voordat ze aankomen.
“Server niet gevonden” voor een site die gewoon draaide. Het webadres van een gloednieuwe site wordt tijdens de installatie aangemaakt. Als iets de naam een moment te vroeg opzocht, onthielden internetproviders tot dertig minuten lang “deze naam bestaat niet”, ook al werkte de site prima. De oplossing was een wildcardadres, zodat elke naam onder foliopress.io altijd bestaat; namen zonder site krijgen een vriendelijke pagina met “hier staat geen site”.
Buren. Veel sites naast elkaar hosten roept vragen op die een losse website nooit hoeft te beantwoorden: wat de code van de ene site met een andere kan doen, en wat een site kan claimen dat niet van haar is. Een beveiligingsreview vóór de opening van de aanmeldingen bracht een paar van zulke problemen aan het licht. Ze zijn allemaal opgelost: strengere cookies, bewijs van eigendom voor eigen domeinen en elke site in een eigen, aparte map gemount, zodat geen enkele site bij de bestanden van een andere kan.
Wat ik iemand zou vertellen die net begint
- Saaie, beproefde onderdelen brengen je ver. SQLite, één framework, gewone containers. De meeste lastige problemen zaten in de naden ertussen.
- Eerst meten, dan geloven. De verkeersscore, de storing en de CPU-pieken leken in eerste instantie allemaal iets anders.
- Ontwerp voor de stille meerderheid. De meeste sites zijn klein. Klein goedkoop houden is wat het geheel mogelijk maakt.
Wat er nog komt
Publiceren op sociale media, mermaid-diagrammen, maar vooral wachten op feedback van gebruikers om de toekomst vorm te geven.
Heb je een site die meer dan één taal moet spreken? Probeer FolioPress dan op foliopress.io – de eerste 30 dagen zijn gratis. En als je zelf technisch bent: ik hoor graag hoe jij het zou hebben gebouwd.