Three sites, one small system
This site is the case study. The consulting site, the dev notes, and the photography gallery are three separate static sites, built from one repository and deployed as three independent Cloudflare Pages projects. There is no client-side framework, no database, and no content management system. The only server-side code is a single Worker that sends the contact form over SMTP.
The brief
The site had to be fast, readable, and cheap to keep running. It also had to hold three kinds of work that do not belong together: client consultancy, personal software projects, and photographs. Navigation, typography, metadata, and the footer should be defined once.
The old site was already built with Astro and a shared package, which was the right instinct. What needed to change was the amount of machinery around that idea: webfonts from a third party, a lightbox library from a CDN, scroll animations, and a JavaScript theme toggle. All of it worked, but none of it was earning its place on a page whose job is to be read.
Structure
One npm workspace contains four packages:
kit— the shared layout, header, footer, design tokens, typography, and social links;runi.se— the bilingual consulting site;dev.runi.se— projects and notes, in English;photography.runi.se— the gallery.
Each site is a separate Astro project with its own site URL, sitemap, and build output. Content in kit changes all three sites at once; anything specific to one site stays there. The deployments do not depend on each other.
Why not one shared web root
One web server can serve three hostnames from one directory by rewriting each request into a per-site subdirectory, with an exception for the shared _astro assets. It is a neat trick, and it was considered.
It was rejected for three reasons. Browser caches are partitioned by top-level site, so the shared CSS is downloaded again on each domain anyway. Canonical URLs, sitemaps, and robots.txt become per-host special cases. And a single rewrite mistake can expose one site’s pages under another domain. Sharing the source solves the maintenance problem without sharing the runtime.
Content as files
Projects and notes are Markdown files with a typed frontmatter schema. The build fails if a field is missing or a category is misspelled, which is preferable to discovering the problem in a browser. Adding a project means adding a file and rebuilding.
The English and Swedish case studies are separate files with the same filename, so the language switch always points at the corresponding page. The privacy notice is Markdown too. The photography gallery reads EXIF data once at build time and writes the camera, lens, aperture, shutter, and ISO into the static HTML, so adding a photograph means dropping a file into a folder.
Almost no JavaScript
Pages ship no framework and no client-side code. Two small scripts exist:
theme.js, a short external file in the document head that applies a stored light/dark preference before the first paint;- Cloudflare Turnstile, loaded only on the contact page;
- nothing else.
The theme control has two states, light and dark. Until a visitor chooses, the page follows the operating system preference. The choice is stored in localStorage; no cookie is set. The script is external rather than inline, which lets the Content Security Policy forbid inline scripts entirely.
The contact form posts to a Worker and redirects to a static confirmation page. It works without JavaScript, apart from the spam check.
Design
The palette is Gruvbox Material, faded, in a light and a dark variant. Typography uses the system serif for prose, the system sans for navigation and metadata, and the system mono for code. There are no webfonts to download and no layout shift while type loads.
The layout is a single measured column with thin rules instead of cards. Links are underlined. There are no gradients, shadows, hover lifts, or scroll animations. Light is the default; a machine that asks for dark gets dark.
Trade-offs
- Publishing requires a rebuild. There is no browser-based editor, which is a feature for a site this size and a limitation for anyone else who might edit it.
- There are three deploy targets rather than one.
- The contact form needs a Worker with outbound SMTP access. Everything else is static files.
- Turnstile makes the contact form dependent on JavaScript. Without it, the form would be easier to spam.
None of these has been a problem in practice. The parts that were removed — the CMS, the tracking, the third-party UI libraries — were the parts that needed maintenance.
Built with
- Astro
- TypeScript
- Cloudflare Pages
- Cloudflare Workers
- Cloudflare Turnstile