cd /news/developer-tools/261-docs-6-languages-one-maintainer-… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-72292] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

261 docs, 6 languages, one maintainer: frontmatter is the source of truth

A developer built NENE2, a PHP framework for AI-readable business APIs, and maintains 261 how-to guides in English mirrored into five more languages (1,566 files total) by treating frontmatter as the single source of truth. A script regenerates all indexes from disk, preventing drift in categories, tags, and translations. Translated guides omit frontmatter entirely, relying on the English taxonomy to keep the source of truth singular.

read6 min views1 publishedJul 24, 2026

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

── more in #developer-tools 4 stories Β· sorted by recency
── more on @nene2 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/261-docs-6-languages…] indexed:0 read:6min 2026-07-24 Β· β€”