{"slug": "261-docs-6-languages-one-maintainer-frontmatter-is-the-source-of-truth", "title": "261 docs, 6 languages, one maintainer: frontmatter is the source of truth", "summary": "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.", "body_md": "If you maintain a docs folder that keeps drifting — missing index entries, stale translations, categories that no longer match — this is for you.\n\n** NENE2** is my small PHP framework for building AI-readable business APIs.\n\nRepository:\n\n[https://github.com/hideyukiMORI/NENE2](https://github.com/hideyukiMORI/NENE2)\n\nOver 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.\n\nThat is 261 guides across 6 locales — 1,566 files in all.\n\nMaintained by one person.\n\nThe only way this stays sane is to stop treating docs as prose and start treating them as data.\n\nThis article is about the machinery that makes that possible.\n\n**What you'll learn:**\n\nIf you have ever kept a docs folder, you know how index pages rot.\n\nYou 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.\n\nNone of this is dramatic. It is just slow decay.\n\nAt six guides it does not matter. At 261 × 6 it is fatal.\n\nSo the rule I settled on is simple:\n\n**No index is ever edited by hand. Every index is regenerated from the files on disk.**\n\nEach English guide opens with a YAML frontmatter block:\n\n```\n---\ntitle: \"How-to: A/B Testing Framework\"\ncategory: product\ntags: [ab-testing, experimentation, state-machine, analytics]\ndifficulty: advanced\nrelated: [feature-flags, feature-flag-api]\n---\n```\n\nThat block is the source of truth for everything the index knows about the guide:\n\n`title`\n\n— display name`category`\n\n— one of seven fixed buckets`tags`\n\n— lowercase kebab-case labels`difficulty`\n\n— beginner, intermediate, or advanced`related`\n\n— slugs of sibling guides`ft`\n\n— an optional field-trial referenceThe seven categories are fixed in code:\n\n```\ngetting-started\nauth\nsecurity\ndatabase\napi-design\ninfrastructure\nproduct\n```\n\nThe guide body follows the frontmatter. The frontmatter is not decoration — it is structured data that a generator reads.\n\nA single script, `tools/build-howto-index.php`\n\n, rebuilds every index. It is wired to `composer howto:index`\n\n.\n\nFor English, it walks every guide, parses the frontmatter, and groups guides into the seven categories. It writes two things:\n\n`docs/howto/README.md`\n\n`by-tag.md`\n\nThe category loop is the whole idea in a few lines:\n\n``` php\nforeach (guideFiles($dir) as $file) {\n    $slug = basename($file, '.md');\n    $fm   = readFrontmatter($file);\n    if ($fm === null || !isset($fm['category'])) {\n        $missing[] = basename($file);\n        continue;\n    }\n    $byCategory[(string) $fm['category']][] = [\n        'slug'       => $slug,\n        'title'      => $fm['title'] ?? $slug,\n        'difficulty' => $fm['difficulty'] ?? '',\n        'tags'       => $fm['tags'] ?? [],\n    ];\n}\n```\n\nGuides that have no frontmatter, or no `category`\n\n, are not silently dropped. They are collected into `$missing`\n\nand printed as a warning. Skipping something is a visible event, not a quiet gap.\n\nHere is a decision that surprises people: the translated guides have **no frontmatter at all**.\n\nA Japanese guide starts straight with its H1:\n\n```\n# ハウツー: A/B テストフレームワーク\n```\n\nThere is no `category:`\n\nline in the translation. Duplicating the taxonomy into six languages would mean six chances to drift out of sync.\n\nSo 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:\n\n```\nfunction howtoTitle(string $path): string\n{\n    foreach (preg_split('/\\r?\\n/', file_get_contents($path)) as $line) {\n        if (preg_match('/^#\\s+(.+?)\\s*$/', $line, $m) === 1) {\n            return $m[1];\n        }\n    }\n    return basename($path, '.md');\n}\n```\n\nThe 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.\n\nEach README has a hand-written part I want to keep: a curated \"I want to…\" finder table.\n\nSo the generator does not overwrite the whole file. It only replaces the content between two markers:\n\n```\n<!-- AUTO-INDEX:START (generated by `composer howto:index` — do not edit by hand) -->\n...regenerated content...\n<!-- AUTO-INDEX:END -->\n```\n\nEverything 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.\n\nThe generator is only useful if a stale index cannot be merged. That guarantee lives in the CI pipeline:\n\n```\n- name: Verify howto index is up to date\n  run: |\n    composer howto:index\n    git diff --exit-code docs/howto/README.md 'docs/*/howto/README.md' docs/howto/by-tag.md\n\n- name: Validate howto frontmatter\n  run: composer howto:frontmatter -- --require-all\n```\n\nThe first step regenerates every index and then runs `git diff --exit-code`\n\n. If regenerating produced any change, the working tree is dirty, the diff is non-empty, and the build fails.\n\nIn 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.\n\nThe second step is a separate validator, `tools/validate-howto-frontmatter.php`\n\n. It checks every English guide against the schema: required fields present, `category`\n\nand `difficulty`\n\nfrom the allowed sets, tags lowercase kebab-case, `related`\n\npointing at guides that actually exist. The `--require-all`\n\nflag means an English guide with **no** frontmatter also fails the build.\n\nBetween the two steps, drift cannot accumulate silently. A missing index entry, a typo in a category, a broken `related`\n\nlink, or a guide with no metadata all turn into a red check.\n\nThis was not built in one shot. The git history shows a deliberate rollout:\n\nThat 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.\n\nAdding a guide is not one file. It is one English guide plus five translations, and the index must stay complete in all six locales.\n\nIn 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.\n\nBut 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.\n\nThree things held up at scale:\n\nNone of this is clever. It is boring on purpose.\n\nBoring is what lets one person keep 261 guides alive in six languages without the docs quietly rotting underneath them.\n\n── By Hideyuki Mori (Ayane International) — I build back-office systems for small businesses at published, fixed prices.\n\n🔗 [hideyuki-mori.com](https://hideyuki-mori.com/en/?ref=devto)", "url": "https://wpnews.pro/news/261-docs-6-languages-one-maintainer-frontmatter-is-the-source-of-truth", "canonical_source": "https://dev.to/hideyukimori/261-docs-6-languages-one-maintainer-frontmatter-is-the-source-of-truth-2c8f", "published_at": "2026-07-24 16:17:54+00:00", "updated_at": "2026-07-24 16:34:04.224473+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["NENE2"], "alternates": {"html": "https://wpnews.pro/news/261-docs-6-languages-one-maintainer-frontmatter-is-the-source-of-truth", "markdown": "https://wpnews.pro/news/261-docs-6-languages-one-maintainer-frontmatter-is-the-source-of-truth.md", "text": "https://wpnews.pro/news/261-docs-6-languages-one-maintainer-frontmatter-is-the-source-of-truth.txt", "jsonld": "https://wpnews.pro/news/261-docs-6-languages-one-maintainer-frontmatter-is-the-source-of-truth.jsonld"}}