Infrastructure Part 1: Twenty hours to cutover. Zero lines of code by hand.
On July 7 at 22:48, this site did not exist. Twenty hours later, the WordPress install that used to serve tokentek.ai was gone from the server. The files were deleted, the database dropped. The static site you are reading right now had taken its place. We didn't even have to touch the DNS records. The domain already pointed to the same server, so the whole thing boiled down to swapping out the contents of the web root.
Eleven days later, the git log had grown to 128 commits. 93 of them carry the line Co-Authored-By: Claude Fable 5.
This is the story of that migration, read out of our own git history. But the interesting question isn't really whether an AI can build a website today. The interesting question is what a website has to look like for an AI to be able to work on it safely.
- commits in eleven days
- 128
- lines of code written by hand
- 0
- from first commit to WordPress gone
- 20 h
- lines of bash in the deploy script
- 73
01
Why WordPress had to go
WordPress did its job. Pages were served quickly from the cache, backups ran, and the admin panel was exactly as usual. Nothing was broken. The problem was how everything was stored: the site's entire state was locked in a MySQL database, hidden behind an interface built for a human with a computer mouse.
Sure, an AI can click its way through an admin panel. But it cannot hold the whole site in view at once. It cannot spit out a diff that shows exactly what a change means before it is saved. And it absolutely cannot propose a change that a human reviews in peace before it goes live. Everything you take for granted in a code repo disappears into a database behind a login screen.
There were other annoyances. Every time we needed a custom component, it became a battle against the theme, rather than building with it. The site only existed in English, and building Swedish search visibility requires every page to exist in Swedish too. In an admin panel, that means double the manual labor and texts that slowly but surely drift apart over time.
But ultimately, the database was the dealbreaker.
The goal of the migration boiled down to a single sentence: the site's entire state must live in text files. When everything lives in files, an agent can read all of it and change anything. Every change becomes a clear diff that a human can approve before it is published. The migration was therefore less about leaving WordPress, and more about finding a format where an AI can actually do its job.
02
Everything is text, everything is validated
If you look in the repo today, you won't find any finished web pages. Only source material. There are 25 markdown files of content, 46 component files, and about 10,000 lines of code. The content is split into four strictly typed collections — pages, jobs, posts, and cases — and each collection has a schema that is rigorously validated on every build.
We built the validation in three layers. If someone misspells a metadata field, the schema stops the build. If a body text is missing a required section, the parser trips and lists exactly which sections are missing. And if an image reference points to the wrong place, the image registry aborts with a list of the image keys that actually exist.
The result is that a content error never reaches production. It crashes the build right on the laptop or in the CI environment, and sends out an error message formulated for whoever caused it — regardless of whether it was one of us or an AI model.
03
Two languages, same filename
The bilingualism, which was a constant stumbling block in WordPress, we solved with a simple naming convention. Swedish lives at the root of the file system, English sits under /en/, and the pages are tied together by having exactly the same filename in both languages.
The build process finds these pairs and generates the hreflang links — the markup that tells Google which pages belong together — and the sitemap mirrors the same structure. To search engines, the Swedish and English look like one and the same site.
Parts of this rely on types. The text snippets in the interface live in two dictionaries, where the English dictionary is typed against the Swedish ones' keys. If someone forgets a translation, it becomes a type error and the build stops. We handle the rest via a simple working rule that the AI model was taught: when we change a Swedish page, the English one must be updated in the same commit. Everything is reviewed as a single diff.
04
73 lines of bash
Our entire deployment is built on 73 lines of bash, a small script that pings search engines, and two lines in crontab.
The publishing itself runs on pull, not push. Every fifteen minutes, the server goes out to GitHub, fetches the code, and compares it with the version currently live. If it finds something new, it kicks off a build. Only when the build shines green are the new files copied over to the web root.
We skipped webhooks entirely. A webhook means an open port sitting and listening outward that has to be defended. A server fetching data on its own initiative exposes nothing. The downside, of course, is that publishing can take up to fifteen minutes. We offset that by pinging IndexNow as soon as the build finishes, so search engines catch wind of the update immediately instead of waiting for the next crawl.
GitHub, by the way, also builds the site on every push — and then throws the entire result away. It looks like a waste of resources, but that is the whole point: we separate verification from publication. GitHub has zero contact with the server and holds no secrets that can leak. The server, in turn, accesses GitHub via a key that only has read permissions.
Those 73 lines of bash are largely about error handling. The comments in the code explain why:
# Compare against the last rev that actually DEPLOYED (stamped only after
# a successful rsync), not HEAD — otherwise a failed build leaves HEAD at
# the new rev and every following non-forced run considers it deployed
# and skips the retry.
The trick is that the script stamps a version as deployed only when the files are safely on disk. If you miss that, a broken build looks like it's already live, and the server stops trying to rebuild it. With the stamp in place, a temporary error heals itself on the next attempt fifteen minutes later.
The hardest lesson was about file permissions. At one point, a manual run via SSH accidentally created files the web server lacked permission to read, and suddenly the whole site answered with 403 Forbidden. Today, the script locks down the permissions on its own on every run. That's how the code grew — every piece of error handling is a scar from something that went wrong in reality.
05
A build that prefers to cancel itself
On the front page sits a news section that fetches its data from my sister site techbyjohan when the site is built. If you look in the code, you'll find a comment that could easily be interpreted as laziness:
// Deliberately no fallback: if the feed is broken the build fails and the
// previously published version stays live.
We deliberately didn't build in a fallback. If the RSS feed goes down, the whole build stops. Since the server waits for a green build before publishing anything, this means the old, working version stays untouched. That is much better than the site silently uploading an empty news section and publishing it to the world.
And since the source code doesn't change just because a new newsletter goes out, we have a scheduled forced build every morning at 05:30. The site is, in other words, dynamic one minute a day, and static the rest of the time.
06
What is not there
The security of this architecture is primarily about what is missing.
There is no database to hack. No PHP process to sink. No plugins requiring security updates. No admin panel standing open to the web, and no API keys stored with GitHub. The published directory contains zero JavaScript files. The few scripts that exist, like the counting animation in the site's stats tiles, are hardcoded directly into the HTML pages and only run if the visitor's browser allows it. The content comes through regardless.
Our old WordPress site was admittedly fast — the cache did its job. But a cache only hides complexity, it doesn't remove it. Beneath the surface, PHP, MySQL, and the entire login machinery were always there, demanding maintenance and standing ready every time a visitor caused a cache miss. The only thing facing the internet today is a web server serving static files. That is the entire attack surface. And the backup? It consists of the git history, plus a Duplicator archive from the WordPress days that we kept just in case.
07
Who did what?
The code is almost exclusively written by Claude Fable 5, while OpenAI's GPT-5.6 Sol was used for review. My role was to write the prompts, read through every diff, test the result on real devices, and make the actual decisions. The AI migrated the site and built the infrastructure, but it didn't write the texts — those are our own.
The aesthetics are also the result of human decisions. The models were given a clear direction inspired by Dieter Rams and Apple. The very first design draft, which drew inspiration from various AI startups, went straight into the trash because it felt too childish. The headers were compared to Apple's and got the verdict "Ours are a mess", which kicked off a proper cleanup pass across the entire site. We fed the models with references and concepts until the design sat right where it should.
But if you look in the history, the most common human input is actually neither the prompt nor the code. It's the cut. A whole section on the front page was removed because "we already mention what we do under 01, maybe that's enough?". The news cards were trimmed of unnecessary labels. A search keyword was sacrificed because the copy became too stilted: "it looks crazy, I'd rather skip that hit". And when the AI generated a series of technical illustrations, we cut all but one. "Remove the rest, that one is good and strong enough." Models like to add things. The human role is often to subtract.
When we got stuck, reality made the final call. On July 11, we spent about fifteen commits trying to get the right color behind Safari's toolbar on the iPhone. Everything was tested against a physical phone, and we finally gave up trying to outsmart the browser. The decision became instead: "you know what let's simplify. let's remove the line".
Dark mode was born in much the same way. One evening when I was testing the site, I found three bugs — the logo and some buttons lost color. It turned out the browser extension Dark Reader had tried to redraw the site on its own initiative. The first thought was to block the extension, but the answer became instead "I want dark mode". A day later, the site had a real, built-in dark theme — partly because Dark Reader's improvised color palette actually looked really good if you just cleaned up the mistakes. The models built the mechanics; the human decided what should stay.
The git log is an answer key to what this collaboration looks like in practice. During the most intense day, we made 36 commits. Five of them happened within the same minute, and three changes were rolled back entirely. It's short loops, small diffs, and a workflow where you dare to make mistakes because it's so fast to rewind.
The same method, on your processes
This site is just our own infrastructure, but the method we used is exactly the same as the one we offer our customers: give the AI a technical frame it understands, build in hard stops so a broken proposal never reaches production, and let people make the decisions. We have previously written about five questions that decide if a process is fit for AI agents. A static markdown site with automated builds checks all five. If you have a business process that meets the same requirements, it's time we talk.