If you maintain a docs folder that keeps drifting β missing index entries, stale translations, categories that no longer match β this is for you.
** NENE2** is my small PHP framework for building AI-readable business APIs.
Repository:
https://github.com/hideyukiMORI/NENE2
Over time it grew a large set of how-to guides. Today the English directory holds 261 guides, and every one of them is mirrored into five more languages: Japanese, French, Chinese, German, and Brazilian Portuguese.
That is 261 guides across 6 locales β 1,566 files in all.
Maintained by one person.
The only way this stays sane is to stop treating docs as prose and start treating them as data.
This article is about the machinery that makes that possible.
What you'll learn:
If you have ever kept a docs folder, you know how index pages rot.
You add a guide. You forget to add it to the index. Someone else renames a file. The category list drifts. A translation goes missing and nobody notices for months.
None of this is dramatic. It is just slow decay.
At six guides it does not matter. At 261 Γ 6 it is fatal.
So the rule I settled on is simple:
No index is ever edited by hand. Every index is regenerated from the files on disk.
Each English guide opens with a YAML frontmatter block:
---
title: "How-to: A/B Testing Framework"
category: product
tags: [ab-testing, experimentation, state-machine, analytics]
difficulty: advanced
related: [feature-flags, feature-flag-api]
---
That block is the source of truth for everything the index knows about the guide:
title
β display namecategory
β one of seven fixed bucketstags
β lowercase kebab-case labelsdifficulty
β beginner, intermediate, or advancedrelated
β slugs of sibling guidesft
β an optional field-trial referenceThe seven categories are fixed in code:
getting-started
auth
security
database
api-design
infrastructure
product
The guide body follows the frontmatter. The frontmatter is not decoration β it is structured data that a generator reads.
A single script, tools/build-howto-index.php
, rebuilds every index. It is wired to composer howto:index
.
For English, it walks every guide, parses the frontmatter, and groups guides into the seven categories. It writes two things:
docs/howto/README.md
by-tag.md
The category loop is the whole idea in a few lines:
foreach (guideFiles($dir) as $file) {
$slug = basename($file, '.md');
$fm = readFrontmatter($file);
if ($fm === null || !isset($fm['category'])) {
$missing[] = basename($file);
continue;
}
$byCategory[(string) $fm['category']][] = [
'slug' => $slug,
'title' => $fm['title'] ?? $slug,
'difficulty' => $fm['difficulty'] ?? '',
'tags' => $fm['tags'] ?? [],
];
}
Guides that have no frontmatter, or no category
, are not silently dropped. They are collected into $missing
and printed as a warning. Skipping something is a visible event, not a quiet gap.
Here is a decision that surprises people: the translated guides have no frontmatter at all.
A Japanese guide starts straight with its H1:
There is no category:
line in the translation. Duplicating the taxonomy into six languages would mean six chances to drift out of sync.
So the generator treats locales differently. For every non-English locale it builds a flat, alphabetical index using the first H1 it finds in each file:
function howtoTitle(string $path): string
{
foreach (preg_split('/\r?\n/', file_get_contents($path)) as $line) {
if (preg_match('/^#\s+(.+?)\s*$/', $line, $m) === 1) {
return $m[1];
}
}
return basename($path, '.md');
}
The taxonomy lives in exactly one place β the English frontmatter. Translations only need to exist and have a heading. That keeps the source of truth singular.
Each README has a hand-written part I want to keep: a curated "I want toβ¦" finder table.
So the generator does not overwrite the whole file. It only replaces the content between two markers:
<!-- AUTO-INDEX:START (generated by `composer howto:index` β do not edit by hand) -->
...regenerated content...
<!-- AUTO-INDEX:END -->
Everything before the start marker and after the end marker is preserved verbatim. The injection is idempotent: run it twice and you get byte-identical output. That property is the reason the whole thing can be enforced in CI.
The generator is only useful if a stale index cannot be merged. That guarantee lives in the CI pipeline:
- name: Verify howto index is up to date
run: |
composer howto:index
git diff --exit-code docs/howto/README.md 'docs/*/howto/README.md' docs/howto/by-tag.md
- name: Validate howto frontmatter
run: composer howto:frontmatter -- --require-all
The first step regenerates every index and then runs git diff --exit-code
. If regenerating produced any change, the working tree is dirty, the diff is non-empty, and the build fails.
In other words: if you added a guide but did not regenerate the index, CI notices β because it regenerates the index for you and sees that your committed version does not match.
The second step is a separate validator, tools/validate-howto-frontmatter.php
. It checks every English guide against the schema: required fields present, category
and difficulty
from the allowed sets, tags lowercase kebab-case, related
pointing at guides that actually exist. The --require-all
flag means an English guide with no frontmatter also fails the build.
Between the two steps, drift cannot accumulate silently. A missing index entry, a typo in a category, a broken related
link, or a guide with no metadata all turn into a red check.
This was not built in one shot. The git history shows a deliberate rollout:
That last step is the one that flipped frontmatter from "nice to have" to "the build fails without it." The guide count has grown from 256 to 261 since then, and the machinery absorbed the growth without any manual index edits.
Adding a guide is not one file. It is one English guide plus five translations, and the index must stay complete in all six locales.
In practice the translations land in batches β the commit history has entries like "translate N untranslated guides into all five locales." This is an AI-assisted solo project, so drafting six language variants of a guide is exactly the kind of work that gets delegated to a model and then reviewed.
But the honest part is this: the quality gate is not "an AI wrote it." The gate is the same mechanical check for everyone. A translation either exists and produces an index row, or the locale index differs from what CI regenerates and the build goes red. The generator does not care who or what produced the file.
Three things held up at scale:
None of this is clever. It is boring on purpose.
Boring is what lets one person keep 261 guides alive in six languages without the docs quietly rotting underneath them.
ββ By Hideyuki Mori (Ayane International) β I build back-office systems for small businesses at published, fixed prices.
π hideyuki-mori.com