---
title: How this website was built, and why there is no CMS
description: One markdown file per page is the only source of truth on this site. The layout, the metadata, the social cards and the signatures are all derived from it at build time.
date: 2026-08-02
author: Claude (Opus 5)
factchecker: Niklas Rantanen
tags: web, automation, ai, seo
---
# How this website was built, and why there is no CMS

::: lead
This site has no content management system. No database, no admin panel, no login. A page is a simple markdown file in a git repository, and that file is the only thing anybody edits.
:::

Everything else is produced from it when the site is built: the layout, the navigation, the search metadata, the OpenGraph image, the structured data, the language pairing, the plain-text copy that AI systems read, and the cryptographic signature at the bottom of some pages. None of it is maintained by hand. This is what content management looks like when the content is genuinely in charge, and it is a different animal from a CMS.

## The content model: one file, one page

A page is a file at `content/<language>/<path>.md`. It opens with a few lines of metadata, the title and the description that search engines and link previews use, and then the body in ordinary markdown. There is no database, no admin panel and no revision table. The revision history is the git history.

### The path is the page's identity

The file's path is the page's identity, and that single decision buys a surprising amount. Because `content/fi/services/seo.md` sits at the same path as `content/en/services/seo.md`, the builder script knows the two are translations of each other, and it can automatically generate the language switcher, the [`hreflang` annotations](https://developers.google.com/search/docs/specialty/international/localized-versions) that tell search engines about the pair, and the breadcrumb trail without anybody writing them. Files are named in English in every language for exactly this reason.

### Addresses are separate from identity

Public addresses are separate from that identity. A small map turns each English path segment into a localised one, so the same page is reachable at `/en/services/seo` and at the natural Finnish equivalent. The Finnish slugs are authored separately with words Finnish users actually use. This is then folded to a safe form for the address bar automatically removing characters like spaces and umlauts. Renaming a page's address means editing one line of that map, and the old address keeps working, because the build compiles a redirect for it.

## A component vocabulary, not a page builder

Plain markdown handles prose. For the structures a marketing site needs, cards, an accordion, a pricing block, a testimonial, the authoring format adds a small set of named containers. Each one opens with a line of colons and a name and closes with a line of colons: `::: cards` opens a card grid, every `###` heading inside it becomes one card, and `:::` ends it.

That is the whole vocabulary, around a dozen names. The build turns each one into markup with the correct roles, headings and accessibility semantics. An author never writes component HTML, and cannot get it wrong.

### The build does not know what it looks like

What the build deliberately does not know is what any of it looks like. A card grid is a promise about meaning, and the theme decides everything else. We tried Material Design first and could not get it to look the way we wanted, so the theme, the layout system and the motion are our own. The same content would render just as correctly through Bootstrap, or through whatever replaces it. Swapping the design system is a theme swap, not a migration, and that is the practical test of whether the content is really the source of truth.

### Why not simply install a CMS

The reason to work this way rather than to install a CMS is not aesthetics. A CMS puts a database between the writer and the page. The consequence is that the content stops being a file, and once content is not a file it cannot be reviewed as a difference, cannot be tested before it is live, cannot be branched, and cannot be cryptographically signed. Every capability described further down this article follows from the content being a file that a person or an agent can read, and a machine can verify.

## Content dictates the layout

The usual sequence is that somebody draws a wireframe, and the text is then written to fit the boxes. On this site the sequence is reversed. The component vocabulary was designed around the kinds of statement the content actually needed to make, and the design system was built to render those components well.

### A palette narrow enough to compose

The visual system is deliberately narrow so that it composes: nine achromatic greys and exactly one accent green, no secondary or tertiary colour roles at all. Depth comes from steps in lightness rather than from shadows, and hover states change lightness only, so nothing moves or grows under the cursor. The accent is rationed to interactive elements and to the one measured number on a screen, on the grounds that more than about two accent elements in view and the colour stops meaning "act on this". Icons are Material Symbols, injected inline as vector graphics that inherit the surrounding text colour and size, so an icon in a heading is the size of the heading without anybody specifying it.

### In-house graphical design

The whole system, with the reason attached to every rule, is published at [our design system]({{ROOT}}static/brand/design-system.html). It computes its own swatches and contrast ratios on the page, so a token that drifts makes the document visibly wrong rather than quietly stale. The reasoning behind the colour choices is worth its own article, and will get one: the palette leans on lightness rather than hue because differences in lightness survive every form of colour vision deficiency, and the ten-colour set used for charts is our own, computed out of the 2014 Material palette as a candidate pool and deliberately not derived from the brand accent, because the green candidates lost separation from the brown swatch under deuteranopia.

## What the build generates

The build is a single deterministic Python script. Given the content directory it produces the entire public site, and it regenerates all of it every time rather than trying to work out what changed. For a site of this size that takes seconds and removes a whole category of stale-output bug.

Derived on every run:

- Navigation, sub-navigation and breadcrumbs, from the file tree.
- Canonical addresses, `hreflang` pairs, and the sitemap.
- A structured-data graph in JSON-LD using [schema.org](https://schema.org/) types: the organisation, the site, the breadcrumb trail, service pages, and a machine-readable question-and-answer block generated from any accordion an author wrote.
- Social sharing images. If a page's hero declares an illustration, the build renders that same vector file into an OpenGraph card with `rsvg-convert`, applying a colour transform so one drawing works both inline on a dark panel and as a standalone card. Pages without one fall back to a sitewide card.
- Style assets: the critical subset of the stylesheet inlined into the page so the first paint does not wait on a network request, and the full stylesheet minified and loaded without blocking rendering. JavaScript minification is the next step in the same pass and is not in place yet.
- Redirect rules, compiled from a plain text list into server configuration.

A separate validation script runs after the build and refuses to let it ship if anything is off: unbalanced component fences, a link written in a non-portable form, two pages whose localised addresses would collide, or an em dash anywhere in customer-facing copy, which is a house style rule enforced mechanically rather than remembered.

## Every page also ships as plain text

Add `.md` to any address on this site and the page comes back as markdown. There is a machine-readable index at `llms.txt` as well, and the crawler rules permit AI crawlers on purpose.

The reasoning is narrow and practical. A language model sent to an HTML page spends most of its input budget on layout, navigation and styling before it reaches a sentence anybody wrote. Handing it the markdown instead means it reads the content and nothing else, which makes it more likely to quote us correctly. The same twin also happens to be the cleanest possible input for any client's own automation.

## It works with JavaScript switched off

Turn scripts off and the site still renders and still navigates. Accordions are native [`<details>` elements](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details) which are open in the markup, so their content is present for a reader and for a crawler whether any script runs or not. The same rule governs anything added later: a form on this site will post to a server rather than depend on a script to submit it. JavaScript is an enhancement layer only.

This is partly a privacy position and partly a durability one: a page whose content only appears after a script has run is a page that disappears for anyone with a strict browser, a slow connection or an aggressive corporate proxy.

## From a sentence to a live page

The publishing chain is where the content-as-a-file decision pays off.

### From an instruction to a branch

A change starts as an instruction from an user in plain text language. An agent edits the markdown on a git-branch, runs the build and runs the validation script. Sometimes a person writes a short example of the structure they want, a fragment of markup or a rule, and hands it over as a specification; the agent implements it from there. The code and the copy that end up in the repository and online are machine output, directed and inspected by a person, and every one of the commits behind this site was authored that way.

### The gate, and what happens after it

Then the gate. Merging edits to the main branch is what publishes the edit, and only a person merges. On merge, continuous integration system builds the site, runs the validation script again, copies the output into a fresh timestamped release directory on the server, and moves a symlink to point at it in one atomic step before reloading the web server. The previous releases stay on disk, so a rollback is a symlink moved back rather than a rebuild under pressure.

Once a change reaches the main branch, the build, the validation gate and the deploy take about a minute end to end, with no human touching any part of it. The slow part is not the machine. It is a person reading the result in both languages before the merge, which is the step worth slowing down for.

## Autonomy needs a receipt

An agentic publishing pipeline raises a fair question: if software can change the site, how does a reader know the words in front of them are the words a human approved?

### How the signature works

The answer is a detached cryptographic signature. When a page's source file has a valid signature beside it, made with a key whose fingerprint is on a short pinned trust list, the builder script renders a badge above the footer naming the signer and the date the signature itself carries. The trust model is closed rather than advisory: a signature made by a key that is not on the list fails the build outright, and so does a signature that no longer matches its source, which is what happens if the file is edited after signing. If the signing tools are absent from the environment, no badge appears at all. The guarantee runs one way only. A badge that is present always means a good signature from a trusted key was present during the deterministic build, and the build has no path by which it can invent one.

### Verifying it yourself

For a signed page the site serves the exact bytes that were signed, rather than the tidied plain-text twin, which means a reader can verify the page independently:

```bash
gpg --verify privacy-policy.md.asc privacy-policy.md
```

Not every page carries a signature today. The point is not blanket coverage, it is that the chain is verifiable from a browser to a fingerprint without taking our word for any link in it. That is the honest answer to autonomous publishing: not a promise that the robots behave, but a receipt anybody can check.

## Where it runs

The public edge is a Raspberry Pi 5 on our own rack, reaching the internet through an outbound tunnel rather than an open port, behind a content delivery network (CDN) that absorbs the traffic.

::: callout
**Why a company would choose to publish this way.** This article is the how. The why is a separate read: which decisions we made first, why the hardware is ours, and what we gave up to get it. [Argnode, our own first client]({{ROOT}}en/references/argnode).
:::

## Editing it without knowing any of this

The obvious objection to a repository full of markdown is that only a technical person can now update the site. It works the other way round.

### What actually happens

A non-technical colleague edits this site by asking for what they want. They supply the substance, a new page for a service they have started selling, a paragraph that is now out of date, the notes from a customer call, in whatever form they already have it. The pipeline does the rest: it writes the final copy against the documented tone of voice, sets the title and the meta description to the right length and the right intent, picks the internal links, generates the structured data, produces the other language as a transcreation rather than a translation, and raises anything in the source material that does not check out. A person reviews the result and merges it.

### What a CMS consistently misses

This is the part a content management system consistently misses. A CMS hands a business user an empty box and a hopeful field label. The fields that get skipped are always the same ones, because somebody selling industrial coatings has no reason to know what a meta description is, why its length matters, or that leaving it blank means a search engine will improvise one from whatever text it finds first. The result is a search listing assembled by accident, page by page, and it is one of the most common findings in any audit we run. The metadata here is not a field somebody forgot. It is generated from the content by something that knows what it is for.

### What is genuinely lost

What is genuinely lost is immediacy. This is a request-and-review loop, not a text box that publishes on save, and the human gate is deliberate rather than a limitation we intend to remove.

## What this stack does not do

It has no visual editor and no preview server; you build and read the output. It rebuilds the entire site on every run, which is fine at this size and would not be at ten thousand pages. Reviewing a change still means being comfortable reading a difference logs, so the review step needs someone slightly technical even though the authoring step does not. And the build has gaps we have not closed, JavaScript minification and article-level structured data among them.

## If you are choosing a stack

Three questions worth asking of any content setup, whoever builds it. Can your content be reviewed as a diff before it goes live? Can the site be rebuilt from scratch by a stranger with the repository and nothing else? And when something publishes itself, what is the receipt? A stack that answers those three well will outlive the fashion it was built in.

::: seealso
See also:
- [Argnode, our own first client]({{ROOT}}en/references/argnode)
- [Websites as a service]({{ROOT}}en/services/websites)
- [How we work]({{ROOT}}en/how-we-work)
:::
