← Notes

Paweł · 10 September 2026

Engine and instance — where to draw the line

Each new site ended the same way: with the second one it turned out that eighty per cent of the code was shared, only scattered across two repositories. Kolofon began as an attempt to do it properly.

There is one rule: the engine knows nothing about the brand, the authors or the content it renders. It receives everything as data, through six clearly named seams.

1. site.config.ts     brand, addresses, public keys, database identifiers
2. entries glob       content/posts/<series>/*.json
3. authors.ts         author attribution and typography
4. redirects.ts       address history (301)
5. pages/* + routes   the instance's own pages
6. public/*           icons, logo, OG image
Six places where the instance tells the engine who it is

Everything outside that list belongs to the engine and is pulled into the instance by synchronisation. The instance does not edit those files — and if it does anyway, the change disappears on the next run.

Why not an npm package

The natural reflex: since it is a library, publish it as a package and pull it in as a dependency. I did not, and I wrote the reason down in the documentation so that I would not come back to it myself in six months.

There are two reasons, and both are debt, not a virtue. First, the module reading the entries scans them with a glob on a relative path — in the dependency directory such a glob points into nothing. Second, the engine imports instance content in a dozen or so places, so the dependency runs both ways: an outside user has nothing to import.

Until both are paid off, vendoring applies — copying the engine's files into the instance. It sounds primitive, and it is. But it is written down, it has a mechanism, and it is known when it will end — which is more than can be said of most technical debt.

A key above language

The site has a Polish and an English version. The first version tied them together with an address map: a dictionary that gave the English address for a Polish one. It fell apart at the first change of a title.

The solution was a key. Every piece of content gets an identifier independent of language, and the address stays in the language of the content.

{ "key": "dz-042", "slug": "noc-w-schronisku",   "lang": "pl" }
{ "key": "dz-042", "slug": "a-night-in-the-hut", "lang": "en" }
The same material in two versions

Moving between versions goes through a route by key, not by address. Changing a title on one side cannot break the link, because the link does not know about titles.

This led to a second rule that turned out to matter more: the boundary runs along what a thing concerns. Comments, ratings and poll votes concern the MATERIAL, so they are shared by both versions. Views, the address and the interface strings concern the PAGE, so they are separate. This cannot be settled once and for all — you have to ask with every new feature.