Architecture
Twenty-seven entries on how Kolofon reached its present shape: from the three conditions set before the first line of code, through the decisions that had to be undone, to where it is heading.
Kolofon · 7 August 2026 · provisional

01 · Initial assumptions
Three conditions set before the first line of code existed. Everything that came afterwards is a consequence of them. This series describes how Kolofon arrived at its present shape — from the first deployment, through every decision that turned out well or had to be undone, to where it is heading. We are writing it because architecture described only at the end always looks as though it was thought through from the start. Ours was not. It began with three conditions, set before the first line of code existed. First: the address belongs to the author. Not a platform subdomain, not a profile, not an account somebody can suspend. Your own domain, with one person holding the keys. Second: the data belongs to the author. Texts as files in their repository, comments and ratings in their database. Leaving should mean copying a directory, not filing an export request with a support desk. Third, and most practical: zero fixed cost at zero traffic. A literary project does not earn from day one, and frequently never earns at all. An architecture that requires a monthly subscription for the text to be reachable at all puts a pressure on the author that has nothing to do with writing. These three conditions look obvious until you notice how many popular solutions satisfy two out of three. A blogging platform gives you convenience and zero cost, but the address and the data are its. Your own server gives you control, but costs every month whether or not anyone is reading. A static site generator gives you b
Kolofon · 7 August 2026 · provisional

02 · The boundary: files and database
Texts in files, reader data in the database. One dividing line that survived every later rebuild. The most important decision in this project is also the simplest to describe: content lives in files, reader data lives in the database. That line has not moved once through the whole of development, although almost everything around it has changed. An entry is a JSON file in the repository. It has a title, a date, a series, an author and an array of paragraphs. Nothing more. Several things follow that cannot be bought at any price: the full change history of every text, the ability to undo an edit from months ago, a diff review before publishing, and a backup of the entire body of work on every machine where somebody once cloned the repository. Comments, ratings, view counters and the event log go to the database, because they are data the author does not write — they come into being while people read, and they have no business requiring a deployment in order to appear. The question came up once whether comments could be files too. The answer is architectural rather than aesthetic: an edge runtime has no writable file system. Not “we advise against it” — there is simply nowhere to write. That single property of the platform forces a split that would have been right anyway. The side effect turned out to be worth more than the intended effect. Since content is files and the engine merely looks for them by a fixed pattern, swapping the content directory yields a different site on
Kolofon · 7 August 2026 · provisional

03 · Why the edge
The site runs on three hundred machines at once and costs nothing. The bill for that is paid in constraints, not in money. The engine runs at the edge — the code does not live on one server but is replicated across hundreds of locations and executes in whichever is nearest the reader. For a site whose readers sit on three continents, that is the difference between an immediate answer and a noticeable wait. The billing model is such that, at the traffic a literary project typically sees, the bill is zero. Not “small” — zero. That satisfies the third condition from the first entry in a way no server rented by the month ever will. The bill is paid in constraints, and they deserve an honest listing, because they shaped the rest of the architecture. There is no writable file system. There are no processes living between requests, so there are no background jobs in the classical sense. There is a CPU time limit per request. There is a limit on the size of the code bundle. Each of those constraints returns in later entries in this series as the cause of a specific decision. No file system forced the content–database split. No background processes forced notification delivery to ride on requests that arrive anyway. The bundle size limit forced pagination, because sending an entire archive in one response stops working at a sufficiently large archive. It is worth saying plainly, because it sounds like a drawback and is an advantage: the platform's constraints acted like a reviewer who does not wave lazy sol
Kolofon · 7 August 2026 · provisional

04 · A database without migrations
A new instance receives an empty database and furnishes it on its own. The onboarding that does not exist is the best onboarding there is. The database is SQLite running at the edge. It holds five kinds of data: view counters, ratings, comments, the event log and — recently — reader accounts with their sessions. The first tables were created by hand, pasting statements into a console in a dashboard. That worked for exactly as long as there was one instance. The moment a second one became a prospect, the approach stopped being acceptable: you cannot sell a tool whose setup begins with pasting SQL. The solution is lazy schema creation. On the first write the engine checks whether the tables exist and creates the missing ones. The statements are written so that running them repeatedly breaks nothing. There is no migration system, no schema version and no command to remember. You connect an empty database and it furnishes itself. There is one trap in this, which we only discovered while extending the schema with accounts. Every statement ran in one loop, inside one error handler. It was enough for one of them to fail — because an index already existed in a slightly different form, say — and the loop broke and silently skipped everything below it. The database looked ready while being half ready. Now every statement has its own error handling. It is one of those fixes that take a sentence to describe and separate a tool that works from a tool that works for everybody.
Kolofon · 7 August 2026 · provisional

05 · Content as data
There is no editor and no dashboard. An entry is a file and a new series is a new directory. The drawbacks are real and worth naming. The engine has no admin dashboard. No editor, no entry wizard, no publish button. An entry is a file, a series is a directory, and the series metadata — name, description, tagline, ordering — sits in a separate file inside that directory. As a result, adding a new series is an operation on one directory rather than a rebuild of the site. The engine finds series by itself, scanning the content directory against a fixed pattern. There is no list of series anywhere that would need updating — which means there is no list anyone can forget. A series' behaviour is data too. Whether it renders as a gallery, how the invitation to rate is phrased beneath a text, what a screen reader announces — all of it sits in the series file. Previously it lived in the code as comparisons against particular names, and it was one of those solutions that work perfectly until the day a second site appears. The drawbacks of this approach are real and there is no point hiding them. A non-technical person will not add an entry unaided. There is no live preview. There is no uploading photographs from a browser. This tool assumes somebody on the other side who can operate a repository — or somebody doing it on their behalf. We chose that drawback deliberately, because an admin dashboard is a promise to maintain an interface for years, and early deployments are meant to teach us something. A signup form teach
Kolofon · 7 August 2026 · provisional

06 · Series and the stream
An essay, a captioned photograph, an entry with a film and a record of a dream are the same object with different fields filled in. The site divides content into series, but it also has one shared stream in which everything lines up chronologically. That distinction turned out to matter more than we assumed: a series is a declaration of form, and the stream is a way of reading. The regular reader enters through the stream. The reader from a search engine lands in a series. Underneath there is one entry type. There is no separate type for photographs and another for essays. There is one object with optional fields: an image, a film identifier, images woven between paragraphs, a marker for an event distinct from the publication date. The fields that are filled in decide how the entry renders. That decision was taken when the first photographic series arrived, and for a while it looked like taking the easy way out. In practice it rescued the next three series. When the idea of entries with film came up, no new type was needed — one field sufficed. When entries with images inside the text arrived, one array arrived. When the publication date had to be separated from the date of the event, one field arrived. If each of those had been a separate entry type, each would have required its own route, its own template and its own branch everywhere entries are enumerated. Instead every series travels one route and one template. The cost is that the entry type has a good many optional fields and the template a good many
Kolofon · 7 August 2026 · provisional

07 · Publishing ahead of time
A hundred and twenty-one entries written in advance, revealing themselves day by day. The whole mechanism is one date comparison. One of the most useful mechanisms in the entire engine is perhaps ten lines long. An entry dated in the future is not visible. When the date passes, the entry appears by itself — on the site, in the RSS feed and in notifications. In practice this means an author can write a hundred and twenty-one entries in a month and release them over six months, one a day. The site looks as though it is tended daily, while the work was done in batches, when there was time and appetite for it. For a project run alongside other commitments that is not a convenience, it is a condition of existence. The same mechanism incidentally solved a problem we had not anticipated: how to hide an entry without deleting it. The answer is banal — set the date to the year 2099. The entry stays in the repository with its whole history but appears nowhere. The original date goes into the commit message, so restoring it is one edit. A field was added that separates the displayed date from the date in the RSS feed. It came out of a specific collision: when you publish an archive backwards, entries carry genuine dates from months ago, but in the feed they need to be new — otherwise the automation redistributing them concludes there is nothing new. Trivial mechanics, large consequences. The date stopped being metadata describing an entry and became machinery controlling its visibility.
Kolofon · 7 August 2026 · provisional

08 · Ratings and views
An internal scale of 0–10, because half a star has to be a whole number. An author's provisional rating stands until readers vote. Under every entry there is a star rating, and on the listings a legible badge with the average. It looks like the simplest element on the whole site and it was one of the trickier ones. First decision: an internal scale from zero to ten, where one point is half a star. It follows that half a star has to be a whole number if the average is to be computed without rounding errors. It is the kind of decision you either take at the start or pay for by rewriting the data later. Second: one rating per browser, but changeable any number of times. That means the server must be able not only to add a vote but to correct a previous one — so on a change the old value is sent along and the server subtracts it from the sum. Without that, changing a rating would inflate the vote counter indefinitely. Third, and the most interesting in its effects: the stars on the listings should refresh the instant a vote is cast, with no page reload. This is handled by shared state in the browser with simple notification — the rating component announces a change and every badge on the page picks it up. A field was also added for a provisional rating from the author. A freshly published entry with no votes would display zero stars, which looks like a verdict but is an absence of data. Now, until the first reader votes, the author's rating is shown, clearly marked as provisional.
Kolofon · 7 August 2026 · provisional

09 · Comments and anti-spam
A trap for bots, a rate limit and external verification. And the email field is optional, because a mandatory one deters more than spam does. Comments arrived early and from the start without sign-in — the reader supplies a name and a body. The anti-spam layer grew in stages, because designing it up front would have been guesswork. First a trap for bots: a hidden form field a human never sees and an automated client fills in. Alongside it, length limits and a server-side rate limit — one comment per entry within a short window. Only when that stopped being enough did external bot verification arrive. Moderation has been in the schema since day one, although it went unused for a long time. A comment carries an approval flag defaulting to yes. Switching the site to pre-moderation is a change to one default value, with no table rebuild and no data migration. This is a pattern that recurs repeatedly in this engine: prepare a place for a decision you are not yet making. The email field went through a full cycle. First it was absent — less friction, less personal data. Then it became mandatory. In the end it is optional, serves to notify about a reply, and is never displayed publicly. A mandatory address deterred more readers than it stopped spam. Recently reader accounts arrived, and with them a comment policy switch: everyone, accounts only, or nobody. What matters is what the switch means in practice — choosing “accounts only” does not remove the anti-spam gate, it moves it from the comment to the act of creating an acc
Kolofon · 7 August 2026 · provisional

10 · The event log
Our own analytics instead of a third-party tracking script. After ninety days addresses are masked, but the records stay. The site has no third-party analytics script. Instead it has its own event log in its own database: an entry view, a rating, a comment, a comment stopped by bot verification, a click out to an external link, a film played, a share. The reason is simple and follows directly from the assumptions in the first entry. An analytics script means handing data about your readers to a third party in exchange for charts. On a site that promises the author their data is theirs, that would be a promise with an asterisk. With each event we record the time, the IP address, the country recognised by the edge network, the browser, the language and where the reader came from. The last of those turned out to be the most instructive and the most technically deceptive: the first version recorded the site's own address as the source, because the request header carried the previous subpage rather than the actual entry from outside. That information had to be passed from the browser instead. The privacy question was settled unambiguously and against the first version. Originally, records older than ninety days were to be deleted. The rule now reads: we delete nothing, and after ninety days we mask the address. An IPv4 address loses its last octet, an IPv6 address is truncated to a prefix. The event stays, the historical statistics stay, the ability to tie it to a person disappears. That change of dire
Kolofon · 7 August 2026 · provisional

11 · An app instead of an app
An installable icon on a phone, offline reading and notifications — with no store, no review, and no second repository. At some point the question of a phone app came up. The answer is: there is one, it just is not in a store. The site is a progressive web app — it installs from the browser, gets an icon on the home screen, opens full screen and works with no signal. The arithmetic here is one-sided to the point of being suspect. A native app is a separate project in a different technology, fees to two stores, a review on every update, and a second body of code to maintain for years. A progressive web app is a few files added to a site that already exists: an app manifest, a background script and notification handling. For a project run by one or two people that is not a compromise, it is the right choice. Three things it unlocks beyond an ordinary site: installation with no store, reading with no signal, and notifications about a new text. The third is the one that genuinely brings a reader back — an RSS feed does not. There is one thing a progressive web app will not give: presence in a store as a discovery channel. For a site that builds its reach through links and search anyway, that loss is notional. Every file in this layer — the app manifest, the background script, the offline page — is now generated at build time from the instance configuration. Previously they were three copies of the same data, with the name and colours hard-coded separately into each.
Kolofon · 7 August 2026 · provisional

12 · Seven versions of one file
The background script went through seven versions in one session. Each fixed a problem that only became visible once the previous one was fixed. The script running in the browser's background is the most treacherous element in the whole stack. It went through seven versions, mostly within a single session, and each fixed a problem that only became visible once the previous one was fixed. The final division of strategies is this. Page navigation: network first, and cache only when there is no network. That way an online reader always sees fresh content and an offline one gets the last known version. Built assets and images: cache first, because their names contain a hash of their contents — a new version means a new name, so an old copy is never stale. That second rule is more important than it looks. The entire correctness of the mechanism rests on the file name changing together with its contents. If assets had fixed names, a cache-first strategy would serve old code indefinitely. The hardest part turned out not to be offline mode itself but moving between subpages without a reload when there is no network. The application then fetches code fragments on demand, and those fragments were not always in the cache. A hard reload worked, an in-place transition did not. The solution — and we say this plainly, because it is a workaround rather than a removal of the cause — is an automatic page reload in the error handler, guarded against looping. For the reader the effect is right. For us it is a debt recorded in a code commen
Kolofon · 7 August 2026 · provisional

13 · Offline without a server
For reading to work without signal, we had to stop asking the server for things that were already in the code bundle. Offline mode forced a distinction that turned out to be clarifying for the whole architecture: what is content and what is data. Originally the entry listings were fetched by a call to the server. Convenient and correct — as long as there is a network. Without one the call does not go through and the page has nothing to display, even though the content is physically present in the browser's memory, because it is compiled into the code bundle. The solution was to replace server calls with a local read wherever content is concerned. Entries are files compiled into the application, so the browser already has them and does not need to ask. The server stays necessary only for things that by nature live on its side: counters, ratings, comments. That boundary coincides exactly with the files–database boundary from the second entry. This is not a coincidence. The split we introduced because there is no file system at the edge turned out to be precisely the split that correct offline behaviour requires. Two entirely different forces pointed at the same line — which usually means the line is in the right place. There is a performance side effect. Since the listings require no conversation with the server, moving between subpages is instant when online too.
Kolofon · 7 August 2026 · provisional

14 · Notifications
Payload encryption, signing keys, and delivery attached to traffic that arrives anyway — because there is nowhere to run a daemon. Notifications are the most formally demanding part of the engine, because the standard permits no shortcuts. The payload has to be encrypted with the particular recipient's key, the request signed with the site's key pair, and all of it conformant with a specification that does not forgive being one byte out. Subscriptions live in the instance's database. The public key is public by nature, since it goes to the browser; the private key exists only as a secret on the platform side and is never in the repository. The most interesting thing here is a decision forced by the constraint from the third entry: there are no processes living in the background, so there is nothing to wake at dawn and check whether a new entry exists. The solution is to attach that check to traffic that arrives anyway. While handling a request, the engine checks — rate limited, so as not to do it constantly — whether an entry has appeared that nobody was notified about. This has a consequence that deserves an honest name: a notification for an entry published at dawn goes out on the first request after that hour, not on the hour. On a site with any traffic the difference is minutes. On a site with no traffic the notification waits for the first reader — which is logical, because there is nobody to deliver it to. The alternative would be an external scheduled job. We chose the solution with no dependency, b
Kolofon · 7 August 2026 · provisional

15 · Images
A photograph goes in once and comes out in three forms. Everything computed at preparation time, nothing at read time. Photographic series forced a separate image processing pipeline, and one that runs outside the site — because rescaling a photograph on every request would waste CPU time, which at the edge comes in a measured portion. A photograph goes in once and comes out in three forms. The body variant: longer edge around two thousand pixels, compression chosen so that no difference is visible on screen. The share card: the aspect ratio social networks require and a hard weight limit, with quality stepped down until the file fits. The file name is always the entry's identifier, so there is nowhere any record of what belongs to what. The share card is the element whose absence is only visible from outside. Without one, a link looks like bare text on a social network and simply does not get clicked. It is the part of the infrastructure that serves not present readers but future ones. The app icons are generated too, rather than drawn by hand — from a monogram and colours supplied as arguments. This came out of the fact that a full icon set for a progressive web app is several sizes in two variants, and a single missing one breaks installation: the background script fetches the whole list at once and the first missing file aborts the operation. The more general conclusion: anything that can be computed while preparing content should be computed then. At the edge, CPU time is a resource rather
Kolofon · 7 August 2026 · provisional

16 · Pagination and capacity
A response of constant size regardless of the archive. Previously the entry list grew along with the author's body of work. Up to a point, the entry listings fetched every entry. With a dozen that is elegant simplicity. With a hundred and twenty it is a noticeable response weight. With a thousand it would stop working. Pagination went in on the server, not in the browser. The distinction matters, because the browser version looks identical and is easier, but it downloads everything first and then pretends it did not. It solves the impression, not the problem. The effect is a response of constant size regardless of how many entries the site has. That lifted the only constraint that grew along with the author's body of work — and that changes the character of the tool. A site that starts to slow down after three years of writing is a tool for one season. We did a capacity calculation while we were at it, because we would rather know the ceiling than discover it. With the response size limit lifted, the bottleneck became the size of the code bundle — and entries are compiled into it. That works out to an order of a few thousand entries on the free plan, and several times that on the paid one, which costs about as much as a coffee a month. For an author writing an entry a day, a few thousand entries is years of work. The ceiling exists, but it is high and we know exactly where it is — and that is the only form in which a ceiling stops being a problem.
Kolofon · 7 August 2026 · provisional

17 · Bucketing
The limit we hit was not in the engine or in the hosting. It was in the number of files in one directory. This entry is about a limit nobody looks for until they trip over it. Entries lay flat in the series directory. A dozen files — fine. A hundred and twenty — still fine, though the directory becomes unreadable. Several thousand — and trouble starts, not in the engine and not in the hosting, but in the tools around the repository. The solution is banal: entries split into subdirectories by year of publication. The change affects only the file layout, because the engine scans the content directory by pattern rather than by a list. It is worth pausing on the fact that scanning by pattern turned out to be an advantage we had not considered when choosing it. If a list of files existed anywhere and needed updating, this change would have been a rewrite of that list. Since none exists, it was a move of some files. A more general observation: bottlenecks in systems like this rarely sit where you look for them. We looked in response size and CPU time limits. It turned up in the number of files in a directory — a layer nobody thinks of as part of the architecture. Hence a rule we now try to apply: for anything that grows linearly with the number of entries, ask what happens at ten thousand. Not in order to optimise immediately — in order to know where the wall is.
Kolofon · 7 August 2026 · provisional

18 · Separating the engine
Open-heart surgery on a running site, spread across four stages with a green build after each. Separating the engine from the content was the hardest operation in the project's history, because it was performed on a site that was running and had readers. The method: one question at every file — does this code know the name of the site it was written for. A surprising number of files knew. The site name in the page description, in the RSS feed, in the app manifest. Series names in comparisons deciding how things look. Author pen names in signature rules. Old addresses in redirects. Colours in static files that cannot read configuration. It went in four stages, each with a green build at the end. First: one instance configuration file as the single source of brand and keys. Second: author signature rules and address history moved into data. Third: eight static pages moved into content, with the routes reduced to thin adapters. Fourth, and most important: inverting the dependency so that the engine stopped reaching for content and started being handed it. Up to the fourth stage you could say the engine was separated, and it would have been true about the directory structure rather than about the dependencies. As long as the engine imports content by path, a second instance is not a matter of configuration but a matter of branching the code — and a branch starts drifting from the original on day one. The test of that separation was brutally simple: stand up a second instance
Kolofon · 7 August 2026 · provisional

19 · Vendoring instead of a package
An instance does not install the engine — it pulls its files in according to a list. The drawbacks are obvious, the advantages less so. Kolofon is not a package you install with a command. An instance pulls its files in, according to a list of paths called the manifest, and builds them locally. This is a solution that every guide files under “do not do this”, so it is worth saying why we do. Reason one: an instance has to be self-sufficient. The instance repository contains everything needed to build the site, without reaching for a private package registry. If everything on our side disappears tomorrow, the instance still builds and deploys. Reason two: an instance can read the engine. Not as a minified bundle but as legible source, with comments explaining the decisions. For a tool sold as “your data, your code”, that is not a detail. The drawback is obvious: updating is an operation rather than bumping a version number. Hence the manifest gained a check mode, which before synchronising verifies that nothing on the engine's side has been modified within the instance. Without it, synchronisation silently overwrites somebody else's changes — and instances are sometimes edited with tools other than ours. The manifest itself recently received an important fix: it is on its own list, so it synchronises together with the engine. Previously an instance could be left with an old list of paths and quietly miss files added in a newer version. The history of that fix, side effects included, is described in the Changelog
Kolofon · 7 August 2026 · provisional

20 · Where this is going
An author's dashboard, drafts in the database, publishing from the server. And one thing we deliberately do not do. To close, what does not exist yet — with the caveat that these are directions, not promises with dates. Direction one and most important: an author's dashboard. Today an entry is a file and publishing is an operation on a repository. Eventually the author writes a draft that lands in the database, and publishing is a task on the server side. One architectural decision here is already settled: the application will never receive a key to the repository. A key in a browser is a leaked key. The server publishes, on the author's behalf, with a key the author never sees. What is interesting is that such an arrangement is cleaner than the present one, not merely more convenient. Drafts live in the database before they become files. An author can write today, release in a month and revise along the way — without touching a file once. Direction two: reader accounts. The schema, sessions and sign-in through an external provider are already standing; the interface is missing. What matters here is what we deliberately do not build — profiles, following, activity notifications. An account is meant to give a durable identity under a comment and nothing beyond that. Every further social feature is a promise to moderate it for years. Direction three is the first deployments that are not ours. Until then, everything we write about scaling and about what people need is a hypothesis — sometimes a
Kolofon · 9 August 2026 · provisional

21 · Language versions without a hub
Two language versions have to find each other. Everything that comes to mind first — an address map or a central directory — is a structure somebody has to remember to update. A language version here is a separate instance: its own subdomain, its own worker, its own database. Not one site with a switcher but two sites that know about each other. The reason is the same one that makes the address belong to the author — the English version has to be able to live, fail or disappear independently of the Polish one. That leaves the question of how they are to recognise one another. The language switcher has to build the address of the same text next door, and the slug is in the language of the content, so `zalozenia-wyjsciowe` and `initial-assumptions` have nothing in common as strings. The first answer that comes to mind is a map: a file stating what corresponds to what. Rejected. A map is a structure somebody has to remember to update, and it will be forgotten precisely when somebody changes a slug — that is, at the moment it was the only reason for it to exist. The second answer is more ambitious and worse: a central directory. One site knows every language and every mapping, the instances ask it and need not know about one another. It sounds like order. It is an intermediary — exactly the one this engine was built against. A site that has to ask somebody in order to assemble its own menu has a platform above it, even if the engine's own author runs that platform. Two practical consequences follow as well: when the hub is down the switcher fails everywh
Kolofon · 9 August 2026 · provisional

22 · An identity that can be erased
A setting switched off the engine signature in the footer. The HTML was clean. The engine's name and address lay in a JavaScript file next to it — because the switch operated at runtime, and by then it is too late. Kolofon has a setting that switches off the engine signature in the footer. It exists for instances published anonymously or under a pen name: a site is under no obligation to announce what it runs on, and information about the supplier is sometimes the first thread somebody pulls. The setting worked. The generated HTML contained not a single occurrence of the engine's name. The whole time that was true, a JavaScript file served from the same domain held a constant with the engine's name and address — legible, with no obfuscation, findable by anybody who opens the file. The mechanism of the failure is instructive, because there is no error in the code anywhere in it. The setting is read at runtime, from the instance configuration. The build tool has no way of establishing that the branch reading “if the signature is enabled” will never execute, so it leaves it in the output together with the text that branch renders. The code was dead. The text was alive. This is a broader principle than one case. Anything that is not to exist in the result must be settled before the result is produced. A condition checked at render time decides whether you see something — not whether something is there. Hiding presence requires an earlier moment. The fix moves the decision to build time. The instance supplies the value as a compile-time constant, the branch collapses to a literal, and the text
Kolofon · 14 August 2026 · provisional

23 · The feed as an output
An RSS feed is not the site in another format. It is the one output an author never looks at — and the one somebody else reads instead of the site. Content leaves Kolofon by three routes. As a page — rendered by a browser, where everything relative resolves against the address the reader is standing on. As a book — typeset by Typst, where everything must be settled before compilation, because paper has nowhere to reach. And as a feed — read by a program we do not know, on a device we cannot see, in a context we do not control. The third route is the easiest to neglect, because the author never travels it. The page they see after every deploy. The book they download when they publish it. The feed they never open — and even if they did, they would see valid XML and learn nothing. A fault in a feed shows up only in somebody else's hands. First rule: no relative addresses in a feed. On the page `/images/frames/2026/rock.jpg` is fine, because the browser knows what to resolve it against. Lifted out of the page and dropped into a reader, it leads nowhere. A reader given no image in the feed will fetch the page and try to extract one itself; that is exactly where it hits this problem, and it draws an error glyph. It looks like a broken photograph and it is a broken feed. Second: a feed may not assume the reader will do anything beyond reading the feed. It will not fetch the page, run a script, or unfold any abstraction. Whatever is meant to be seen must be in the file — image, text, author, date. A feed that is only a list of lin
Kolofon · 16 August 2026 · provisional

24 · Photographs outside the repository
Content divides not into text and images but into what carries language and what does not. That border runs somewhere other than you would think. Content in the repository is one of the three conditions set before the first line of code, and for many months it had no exceptions. Entries, photographs, preview cards — everything sits beside the code, everything reaches production by the same movement, everything has a history. The second language version of the first deployment showed where that condition has its limit. Instinct says the border runs between text and image: text must be translated, an image need not. That is not true. A preview card is an image, and it carries a sentence from the entry, so in English it has to be a different one. A photograph is an image and carries nothing — the same frame serves every version. The border runs along language, not along format. This has a consequence you cannot see on the day you decide it. A binary file enters version control without delta compression and stays in the history forever, including after it is replaced. A growing photographic series, multiplied by the number of languages, gives you a repository that takes longer to clone every month — and the only thing growing in it is copies of the same thing. So what carries language stays in the instance’s repository, and what does not leaves for an object store and stands there once. The language versions point at the same store. The engine serves files from it by its own route, from the instance’s address — not from the
