Why this website has no Content Management System (CMS)

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 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 graphic design

The whole system, with the reason attached to every rule, is public: our design system 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.

What the build derives from one markdown file One panel on the left, marked edited by hand and highlighted: a single markdown file at content slash en slash services slash seo dot md, holding a title, a description and prose. An arrow leads to build.py, and from there a bracket fans out to eight panels: the HTML page with theme and layout applied; navigation, sub-navigation and the breadcrumb trail; the canonical address, the Finnish twin and hreflang; the sitemap entry and any redirect the move needs; structured data in JSON-LD for search engines; the social sharing card drawn from the page figure; the plain-text twin that machines read; and critical styles inlined with the rest minified. EDITED BY HAND One markdown file content/en/services/seo.md A title, a description, prose. build.py The HTML page,theme and layout applied Navigation, sub-navigationand the breadcrumb trail The canonical address, theFinnish twin, hreflang The sitemap entry and anyredirect the move needs Structured data in JSON-LDfor search engines The social sharing card,drawn from the page figure The plain-text twin thatmachines read Critical styles inlined, therest minified Open full size
One file is edited by hand. Everything else on the page, the markup, the navigation, the search metadata, the sharing card, the plain-text twin and the styles, is derived from it on every build.

Derived on every run:

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 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 a request to a live page, in five stages A left-to-right chain of five panels. One: somebody asks, in plain language, from notes, an email, a call or a rough idea. Two: an agent writes markdown on a git branch, in all languages. Three: the whole site builds, then the validation script runs over it. Four, highlighted as the gate: a person merges, having read it in all languages first, and nothing else can. Five: it goes live into a fresh release directory, and one symlink moves. A bracket under the first two stages reads: as long as the thinking takes. A bracket under the last three reads: about a minute of machine time, plus one person reading. Somebody asks In plain language.Notes, an email, acall, a rough idea. An agent writes Markdown, on agit branch. Alllanguages. It builds The whole site, thenthe validationscript over it. THE GATE A person merges Reads it in alllanguages first.Nothing else can. It goes live A fresh releasedirectory, then onesymlink moves. As long as the thinking takes About a minute of machine time, plus one person reading Open full size
Five stages from a request to a live page: somebody asks for a change in plain language, an agent writes the markdown on a git version management branch, the build and the validation script run over it, a person merges the changes, and the deploy swaps a symlink on the web server. Everything from the third stage onwards is machine work, and the merge is the one point where a person has to say yes.

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 build checks a page signature Three inputs on the left feed one check: privacy-policy.md, the source exactly as signed; privacy-policy.md.asc, the detached signature; and signing slash signers.txt, the pinned fingerprints. They converge on a panel reading the build checks, before it renders anything, which then branches two ways. Upward, highlighted: a good signature from a trusted key, where the page carries a badge naming the signer and serves the exact signed bytes so a reader can check it themselves. Downward: anything else, an unpinned key or a file edited after it was signed, where the build stops and there is no route to a badge. privacy-policy.md the source, exactly as signed privacy-policy.md.asc the detached signature signing/signers.txt the pinned fingerprints The build checks before it renders anything Good signature, trusted key The page carries a badge naming thesigner, and serves the exact signed bytesso a reader can check it themselves. Anything else An unpinned key, or a file edited afterit was signed: the build stops. There isno route to a badge from here. Open full size
The build checks the source file against its detached signature and the pinned list of trusted fingerprints. A good signature from a trusted key produces the badge and the exact signed bytes; an unpinned key, or a file edited after signing, stops the build instead and gives our agent an error message.

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:

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.

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.

Written by Claude (Opus 5), fact-checked by

#web #automation #ai #seo

This document was digitally signed by Niklas Rantanen on 30 August 2026.