← Overview

SBBy · · 5 min read

How d12r.io is built: Storyblok, Astro, Cloudflare, and one slow font

A personal site with a CMS behind it, no server in front of it, and a speed pass that started with our old arch enemy of the web: the fonts

TL;DR The setup behind this site: Storyblok for content, Astro for a static build, Cloudflare for builds and hosting. Plus what made tiny pages slow, and what fixed it.

Before writing this I ran Lighthouse on my own site, on a simulated phone. The scores were fine: 89 to 94 out of 100. The reason they weren't 100 was more interesting. These pages are tiny, 73 KiB for the start page and 219 KiB for my longest article, and they still took 2.5 to 3 seconds to show their first line of text.

The culprit was the font. So this is a tour of how d12r.io is built, and of what it took to make it fast.

The setup in one picture

Three pieces do the work: Storyblok holds the content, Astro turns it into plain HTML, Cloudflare builds and serves it. Click around: every box explains itself, and the flows walk through what happens when I publish, preview, change code, or when you load a page.

How d12r.io works

Click a box for details, or pick a flow and step through it.

All flows as text

Publish an article

  1. Me, in Storyblok: I write the article and press Publish.
  2. Storyblok: The published version is now served by the delivery API.
  3. Webhook: Storyblok calls the Cloudflare deploy hook.
  4. Cloudflare Workers Builds: Astro fetches every published story and builds the whole site as static files.
  5. Cloudflare: The new files go live, usually a minute or two after publishing.
  6. You, reading: You get the new page.

Preview a draft

  1. Me, in Storyblok: I open a story in the visual editor.
  2. Local preview: The editor loads my local preview server in an iframe.
  3. Storyblok: The preview server reads the draft with the preview token.
  4. Local preview: It renders the draft with the same code as the live site.
  5. Me, in Storyblok: I click any block to edit it; saving reloads the preview.

Change the code

  1. Claude Code: Claude Code makes the change on a branch and commits it locally.
  2. GitHub: When I say so, it goes to main.
  3. Cloudflare Workers Builds: The same build runs as for publishing.
  4. Cloudflare: Live in about a minute.

Edit with Content Agents

  1. Claude Code: I ask Claude Code to draft an article, or to update many stories at once.
  2. Storyblok MCP: It works through the Storyblok MCP server and the Management API, scoped to this space.
  3. Storyblok: The changes land as drafts. It checks I haven't edited a story since its last write, and never publishes.
  4. Me, in Storyblok: I review in the visual editor and publish myself.

Load a page

  1. You, reading: Your browser asks for a page.
  2. Cloudflare: Static HTML with the CSS inside, the fonts from the same domain.
  3. You, reading: The browser picks the image size it needs.
  4. Storyblok: The image service sends it as WebP.
  5. You, reading: Hover a link and the next page is already loading.

Get found

  1. Search and AI: A search engine or AI crawler visits.
  2. Cloudflare: It finds schema.org data, a sitemap, RSS, /llms.txt and every article as Markdown.
Everything above is the real setup of this site, as of November 2026.

Storyblok: the content, and a bit more

Every article is a story in Storyblok, and so are the start page text and the footer, so I can edit them on their own. An article is built from a handful of blocks: sections, rich text with sidenotes, figures, tables, callouts, hints, and code blocks that use a code editor field I built for exactly this.

I write in the visual editor. It shows my local preview server in an iframe, so I see the real page while I type, and clicking any block jumps to its fields. The preview reads drafts with a preview token that stays on my machine; the live site only ever sees published content.

Images go through Storyblok's image service, which resizes them on request. That also turned out to be half of the speed fix.

Astro: everything happens at build time

Astro builds the whole site as static files. There is no server rendering per request and no database, and readers download no framework. The only JavaScript on a page is a few small inline scripts: the copy buttons, the easter eggs, and the diagram above.

Everything that can be done once is done in the build:

  • Code highlighting with Shiki, so code blocks arrive already coloured.
  • A sharing image for every article, drawn with Satori and resvg.
  • The sitemap, an RSS feed, schema.org data on every page, /llms.txt, and every article as Markdown for AI tools.
  • Minified HTML, and any JavaScript no page references is dropped.

Cloudflare: builds and hosting

Cloudflare Workers Builds builds the site from the main branch on GitHub, and serves the result as static assets from its network. Two things trigger a build: a push to main, and a webhook from Storyblok whenever a story is published, unpublished or deleted. Publishing an article is all it takes; the new version is live a minute or two later.

Code changes happen on a branch, mostly written by Claude Code on my machine. A push to main is a deploy, so that only happens when I say so. More on that way of working in how I run a small team of agents.

What made it slow

Page (mobile)ScoreFirst textTotal size
Start page942.5 s73 KiB
Overview912.8 s86 KiB
Longest article893.0 s219 KiB

Lighthouse 12, simulated mobile, before the changes.

Fonts. The site loaded Barlow from Google Fonts. That's a stylesheet from one domain, then four or five font files from another, and the browser waits for the stylesheet before it paints anything. Lighthouse put the cost at about 1.7 seconds on every page.

Images. Figures were PNGs at up to 1,800 pixels wide, sent at the same size to a phone as to a big screen. On the longest article Lighthouse estimated 108 KiB to save.

The server was never the problem: the start page answered in 20 ms.

What I changed

  • Fonts from my own domain, in one file. Barlow has a variable version in its own repository: one file for every weight from Light to Bold. Its weight scale doesn't match the one CSS uses, so a small script rewrites it, and the build keeps only the characters the site actually uses. About 20 KB instead of four or five files of 22 KB each, preloaded, from the same domain, nothing that blocks the first paint.
  • Images in the size you need. Every figure now offers a few widths as WebP, and the browser picks one. The timeline diagram in the midnight article went from 226 KB as a PNG to 59 KB as WebP at the same width. Width and height are set, so nothing jumps while images load.
  • Long caching. Files whose names change with their content, and the fonts, are cached for a year.
  • The next page, before you click. Hovering a link starts loading it in the background, using the browser's speculation rules.

Content for people, and for AI

More and more people meet a page through a search result or an AI answer before they ever open it. So the same build that makes the pages also describes them for machines, from the same content in Storyblok, with nothing to maintain by hand:

  • schema.org data on every page. The start page says who I am, my role at Storyblok and where else to find me. Every article says what it is, who wrote it and when, and how it fits into the site.
  • `/llms.txt`. A short description of the site and a list of every article with a one-line summary, the way AI tools like to read it.
  • Every article as Markdown. Add .md to an article's address, like this one, and you get the clean text: sidenotes as footnotes, the diagram above as a list.
  • A TL;DR and a byline on every article. The teaser from Storyblok doubles as the summary under the title, and every article says who wrote it, the two things an answer engine quotes first.
  • Crawlers are welcome. Search engines and AI crawlers can read everything. I'd rather be quoted correctly than not at all.

None of this replaces the page itself. It just makes sure that when a machine retells it, it has the right facts to work with.

After

Page (mobile)ScoreFirst textTotal size
Start page94 → 100 → 1002.5 → 1.0 → 0.9 s73 → 100 → 31 KiB
Overview91 → 99 → 1002.8 → 1.5 → 0.9 s86 → 121 → 29 KiB
Longest article89 → 99 → 1003.0 → 1.6 → 1.1 s219 → 157 → 66 KiB

Lighthouse 12, simulated mobile, same three pages: before, after the first pass, and now.

First text now shows up in about a second instead of 2.5 to 3, and all three pages score 100.

It took two passes. The first one moved the fonts to my own domain and made the pages faster, but two of them bigger: five separate font files of 22 KB each, where Google's had been 15 KB. Switching to Barlow's variable version fixed that. One file of about 20 KB now covers every weight, and every page is less than half its original size.

Lighthouse's accessibility score went to 100 on all three pages along the way: bigger tap targets in the footer, enough contrast everywhere, an underlined byline, and properly labelled sidenote toggles.

Good to know

  • Storyblok's image service caches every resized URL. Replace an image file in place and the old version keeps showing at sizes already requested. Upload it under a new file name instead.
  • Field plugin names are global in Storyblok. My code editor field went live as d12r-code-editor because code-editor was taken.
  • With the publish webhook, nobody needs to "trigger a deploy". It's super easy to set up in Cloudflare and Storyblok.