The content folder of this blog holds 2,838 files today: 1,419 Turkish posts and an equal number of English twins. The first content commit is dated 31 March 2026, the last one yesterday. An archive written in seven and a half months — not bad.

What I was curious about wasn't the size. I wanted to know how many times those 2,838 files had been edited after they were written. Writing is a one-off job; what about everything that comes after?

I counted: 9,721 edits. Divided across the 2,838 files standing today, that's 3.4 edits per file — edits to files that were later deleted are inside that total too, so the average errs on the generous side.

The number that actually stung came after the division. Of those 9,721 edits, only 998 — that is 10.3% — changed a sentence in a post. The other 8,723 never reached the prose at all: a field added, a field deleted, a value flipped from false to true. I thought I had spent seven and a half months writing; the history says I spent most of that time fixing the labels on files.

I'd have liked to pin this on a bot. I can't: three quarters of those edits came from my own hands.

Count first, claim second

Every number here comes from one snapshot: origin/main = 325c5012, the morning of 15 November 2026, with the -- src/content/blog pathspec. That pathspec matters: run the same count with -- 'src/content/blog/*.mdx' and you get 3,001 additions and 0 renames. If you're going to repeat this in your own repository, know which path you counted along.

Git's --diff-filter option separates changes by type: in the words of the documentation, "Added (A)", "Modified (M)", "Deleted (D)" and "Renamed (R)". Since I wanted to tell adding a file apart from editing one, this is exactly the cut I needed.

Event .mdx count
Added (A) 2,999
Modified (M) 9,721
Deleted (D) 163
Renamed (R) 2

There are 2,999 add events against 2,981 unique file paths. The 18 extra events belong to 16 files: fifteen were added twice and one four times (deleted, then brought back). To arrive at today's count: 2,999 − 163 + 2 = 2,838. It balances exactly, which tells me the count isn't leaking.

Let me state the method's two limits up front, because in this kind of counting the error usually hides in the definition. First, what I'm counting is not lines but events: if a commit touches ten lines in one file, that's one edit. Second, git detects renames at 50% similarity by default, and the documentation doesn't say which letter reports a file that was renamed and modified — so in theory R events could mask M counts. In my case the R total is two, so that ambiguity cannot move any number here.

I did not decide "frontmatter or prose" by reading commit messages, because trusting my own messages would be optimism rather than measurement. My first attempt was wrong too. A fixed threshold — "frontmatter ends within the first twenty-two lines" — looked safe enough, but in this repository the frontmatter ends anywhere between line 14 and line 32, and in 46% of the files it runs past line twenty-two.

The right way is this: for every edit, pull the file as it was before and after the commit with git cat-file, cut the frontmatter off, and compare what's left. That gives three buckets:

Where did the edit land? Events Share
Frontmatter only 8,025 82.6%
Below it, but only an import line, a component tag or a blank line 698 7.2%
An actual line of prose 998 10.3%

I keep the middle bucket separate on purpose: adding a <Mermaid> component to a post changes the body of the file without improving a single sentence. Folding the two together would be counting in my own favour.

What the edits were about

I also sorted the same 9,721 events by subject. I'm keeping this table separate from the count above, because this one is judgement rather than measurement: I put each commit in a bucket by its subject line and left what I couldn't classify under "other".

Subject of the edit Events Share
Social sharing markers 4,211 43.3%
Dead platform fields (mastodon, hashnode) 2,081 21.4%
Cover/image schema 1,618 16.6%
Other 796 8.2%
De-fab: removing fabricated content 641 6.6%
i18n / EN structure 282 2.9%
Frontmatter schema 92 0.9%

I came to this table with an expectation: I assumed I had been improving posts over time. Fixing a sentence, updating a measurement, refreshing a link. Of the 998 edits that changed a line of prose, 624 — 62.5% — come from a single cleanup campaign; the rest is scattered.

There's one more thing. Of the 1,392 commits that contain edits, just eight produced 56.0% of all of them — 5,442 events. The other 1,384 commits add up to 4,279. My archive's history consists of eight sweeps and the 1,384 small touches between them.

I opened all eight. Not one of them improves a single sentence:

Date Files What it did
27 Apr 337 Converted embedded SVG covers to PNG
7 May 457 Migrated covers to a standard template
9 May 934 Flipped twitter/bluesky to true (and linkedin in 490 files)
9 May 480 Wrote separate cover paths for EN
10 May 946 Added mastodon, hashnode, devto fields
16 May 1,042 Deleted the mastodon field
16 May 212 Removed duplicate <figure> and import lines from EN posts
18 May 1,034 Deleted the hashnode field

Three cover decisions, four social-field decisions, and one markup cleanup on the EN side. All mine, all after the fact.

Counting commits and counting edits tell different stories

The first row of the subject table — 43.3% — is the bill for a single architectural choice. The frontmatter of a post carries a block like this; the field list has changed over time, and fewer than half the files in the archive carry all six today:

socialMedia: twitter: false # has it gone to X yet?  linkedin: false # ...to LinkedIn?  bluesky: false # ...to Bluesky?  devto: false # ...to dev.to?  burncpu: false # ...to burncpu.com?  hashnode: false # ...to Hashnode? 

These fields don't mean "should this be shared" but "has this been shared". When a post goes to X, a script comes back and writes twitter: true. The idea is simple and, at first glance, tidy: let the record of where a post went live next to the post, so nothing else has to be consulted.

Here's the consequence. A share is an event; a file is a work. The moment I wrote the event inside the work, every share became a commit. Since 9 May, a commit author named Social Bot has pushed 1,236 commits into the content folder — an average of 6.5 commits a day over 189 days. I counted this five weeks ago too; the bot stood at 1,131 commits that day, and it's at 1,236 now.

But the unit of this post is the edit, not the commit. Switch units and the picture inverts:

Author Commits with edits Edit events
Me (across four email addresses) 119 7,292 (75.0%)
Social Bot 1,235 2,330 (24.0%)
Other bots (FAQ, Editorial, Content) 38 99 (1.0%)

The bot has ten times my commits and a third of my edits (2,330 against 7,292). It touches one or two files each time; I make a decision once in a while and open a thousand files. Two units, two stories, both true — and if I don't say which one I picked, I'm misleading the reader. My first draft of this post opened with "the biggest editor of my archive is a bot"; counting the authors caught the claim on my own methodology warning.

Diagram

There is a point where I have to defend my own choice, because it wasn't entirely thoughtless. Keeping share state in the file bought me something: git history gave me a free audit trail of when each share happened. That's exactly why I can produce the bot numbers in this post. An UPDATE in a database would have left no trace; a line in a file did. Still, I think the trade is clear: if I want an audit trail, I keep one deliberately, rather than making every file in the archive writable on a daily basis.

A field that never entered the schema still sits in 1,225 files

The 21.4% held by dead platform fields is the second bill for the same choice — and the part that taught me the real lesson.

On 10 May I wrote mastodon: true into 946 files. At 12:30 on 16 May I deleted that field from 1,042 files. The reason was mundane: my Mastodon account was suspended, and the information the field carried became meaningless.

My first version of this story opened with "on 16 May I removed the field from the schema". The schema says otherwise: the mastodon lines deleted from src/content.config.ts that day belonged not to the blog but to the short-video collection I had opened a day earlier. The blog's socialMedia schema recognised exactly three fields at the time — twitter, linkedin, bluesky. So mastodon had gone into the frontmatter of 946 posts without ever being defined.

Then I counted today.

$ grep -c "^ mastodon:" src/content/blog/*/*.mdx | grep -v ":0" | wc -l 1225 

The field sits in 43% of the archive today — 1,223 false and 2 true. Their creation dates explain why:

Month the file was added Carries mastodon today
2026-05 255
2026-06 504
2026-07 400
2026-08 66

The template that generates posts kept writing the field I had deleted in May, right through the end of August. The deletion didn't win; the template did. Nothing objected either: the field wasn't in the schema, so it tripped no validation — and since 29 June the blog isn't even an Astro collection, with content served from content.db, so no gate inspects the extra frontmatter fields at all today.

I fell for the same illusion once more. On 18 May, while hashnode was being removed from 1,034 files, the same commit also cleaned up 24 mastodon: false lines. My first reading was "the sweep two days earlier missed 24 files" — a neat lesson about thousand-file operations never landing in one pass. Then I checked when those 24 files were born: all twenty-four were created after the sweep. The sweep missed nothing; what was missed, again, was the template.

That's the correct lesson: a field is owned by the pipeline that writes it, not by the archive that holds it. Close the gate first, then sweep; do it the other way round and every sweep breeds the next one. Eleven days ago, counting how nine of 3,580 commits followed the rule I had written into a document, I ended up somewhere similar: there a written rule was racing its own template, here what races is where the state lives — and this time the loser's bill gets multiplied by the file count. hashnode is back in 1,593 files today and devto sits in 2,827 — same cause. Producing 1,391 tags and leaving 931 of them on a single post each was the same class of mistake: I fixed the output instead of the gate on the production side.

I made the cover decision at 340 files

The cover/image line — 16.6% — shows best how the cost grows with the size of the archive.

On 27 April I converted covers from SVG to PNG, because RSS readers and social cards wouldn't render embedded SVG. I walked into a measurement trap here too: I asked "how many files were in the archive that day" and took the end-of-day count. The right denominator is the sweep's own tree:

Sweep .mdx in the commit's tree Touched Ratio
27 Apr, SVG → PNG 340 337 99.1%
7 May, cover template 467 457 97.9%
16 May, mastodon 1,058 1,042 98.5%

With end-of-day numbers all three came out at "97%"; against their own trees they climb towards 99%. The direction doesn't change, but the pattern gets sharper: a schema decision means touching nearly the whole archive as it stands at that moment.

That tree holds 2,838 files today. A decision that touched 340 files in April touches 2,838 now — 8.3 times more in six and a half months. The hundred posts I write next will nudge that multiplier further. This is the measured version of "decide early", and I did the opposite: I produced fast and fixed the schema on the way.

The wave that touched the prose

The de-fab line — 641 events — is the largest piece of work in this archive that genuinely changed the text. At the end of June, across six commits, I cleaned fabricated numbers, outputs and anecdotes out of AI-generated posts; 455 distinct files were touched.

One detail: all 455 of them are Turkish. The commits' own tree held 1,802 files, but half of those were English twins and the cleanup never reached them. (I took that tree count from the commits themselves rather than from the end of the day — so as not to fall into the trap from the previous section a second time.) I learned that afterwards, while counting the 474 figures that stayed behind in the English twins of the 455 posts I had cleaned — so this line doesn't say "I cleaned the archive", it says "I cleaned the Turkish half of it".

I don't bring it up as an achievement either. A ban on fabrication should have been a rule from the start; applying it retroactively to an archive of 901 Turkish posts in June is proof the rule arrived late. The post where I counted my own fixes turned up something similar: some of those fixes weren't failures but habits. This one is the same kind of thing — 641 edits, the bill for a policy gap.

This work has a name

Google's SRE book calls this sort of work toil and names six characteristics: manual, repetitive, automatable, tactical, "devoid of enduring value" and — the one that cuts closest for me — "O(n) with service growth", meaning it grows linearly as the service grows.

My 8,723 edits that never reached the prose satisfy all six — "manual" included, because three quarters of that work came from my own hands, not from a script; my commit count merely looks small because I touch a thousand files at a time. They satisfy the last one all too well: when the archive doubles, the cost of every schema decision doubles with it.

The book's target is for toil to stay under half of an SRE's time, and Google's own surveys measured an average of 33%. My 89.7% is not in the same unit as those figures: theirs is a share of time, mine a share of edit events. I'm writing it down for the sense of scale, not as a comparison.

Saying "I automated it" doesn't make the cost disappear either; it moves it. I checked this while writing: the deploy workflow's push trigger explicitly excludes the src/content/** path, so those bot commits don't each kick off a publish run — that was my first guess and it was wrong. The cost sits elsewhere: my content folder never goes quiet. Every branch I prepare a post on sits on top of a main branch sliding underneath it, and every measurement turns into a snapshot that is already a few minutes stale. The numbers in this post had to be nailed to a commit for the same reason.

Today's price list

How widespread each field is across the archive today is the price tag on every decision I'm postponing:

Field Files containing it
tags, readingTime, coverImageAlt 2,838
socialMedia block 2,831
devto 2,827
hashnode 1,593
translatedFrom 1,419
burncpu 1,349
mastodon (not in the schema) 1,225

The decision framework I've distilled comes down to three questions:

  1. Is this field part of the work, or part of what happened to it? What I wrote (title, text, sources) stays in the file. What happens to the post afterwards (shared, viewed, liked) doesn't. One question is enough to separate them: can this value change without me touching the file? If yes, it doesn't belong in the file.
  2. How many files is this decision worth? Before I propose a schema change, I count today's files. 2,838 is too large a number to leave something to "we'll handle it later".
  3. Who writes the field? Deleting it from the schema is not deleting it from the archive. Every cleanup done without closing the production path produces the raw material for the next cleanup — my 1,225 files are the proof.

Astro's content collections documentation describes the schema like this: with Zod it will "validate every file's data within a collection", and for a file that violates it, "Astro will provide a helpful error". So a schema is not a promise but a gate: the moment you add a new required field, the entire archive has to pass through it. The documentation doesn't describe a migration path for that; that part is left to you.

My mistake was never using that gate at all. None of the sharing fields were defined in the blog's schema: it recognised three fields while I wrote six into the frontmatter, and the extras passed silently. A field that doesn't go through the gate doesn't report back when the gate changes — that's the short explanation for the dead field in 1,225 files.

Finding somewhere to move it is harder than moving it

Everything up to here pointed at one recommendation: take the state out of the file and put it in the database. Since 29 June, posts have been served from content.db at runtime rather than at build time, so that looked like the obvious destination.

Opening the code showed me half my own advice doesn't work. content.db isn't an independent ledger but a copy derived from the MDX: the prebuild step runs migrate-content.mjs, which fills the social_media column straight out of the frontmatter. The runtime connection is opened readonly: true, so the site cannot write to that ledger.

The script has run incrementally since 29 June: a file whose content hash hasn't changed isn't re-rendered, and its row is carried over from the previous database. So a "shared" value I wrote by hand wouldn't vanish at the next build — it would vanish the first time that post's MDX changed. In practice it comes to the same thing: the source of truth is still the file, and the database is its echo.

That makes the job bigger than I thought. Deleting the socialMedia block from 2,831 files is the easy half. The hard half is giving share state its own writable ledger that the build doesn't reset, pointing the sharing script at it, and then inverting ownership. That isn't an evening's work — and it's the honest reason I keep postponing it, rather than "I didn't have time".

The lesson I'm keeping

Writing 1,419 posts in seven and a half months is a story about speed. But that speed turned the archive itself into a cost base: every schema decision I make today gets multiplied by 2,838 files, and tomorrow by a bigger number.

Output speed is easy to measure, which is why we all measure it. Measuring maintenance debt means looking backwards, and the past is uncomfortable: nine out of every ten edits never touch a sentence. Through one architectural choice, I turned the upkeep of what I produce as a writer into a daily chore for myself.

Four of my claims collapsed under measurement while I was preparing this post: that the sweep had missed 24 files, that a bot was the biggest editor of my archive (true only when counting commits), that I had removed the field from the schema, and that a fixed line threshold could tell prose edits from label edits. All four sounded good. That's the price of measuring your own history — the numbers may not like the story you're telling. The benefit sits in the same place: a dead field sitting in 1,225 files only becomes visible once you bother to count it.

Official Sources