The problem
Staff at TTMI use an in-house ERP across 50 stores, and they needed a help portal that explains how to do everyday tasks in modules like HR and operations. The existing help content lived in very large hand-maintained source files. They were hard to merge and impossible for a non-engineer to review.
The harder part was how the content got written. To describe a screen correctly, you have to read the real ERP frontend and backend code. That means every draft starts out full of internal details, such as API paths, backend module names and branch names, that must never reach a staff-facing page. On top of that, AI coding agents drafted most of the content, and AI-written instructions drift from the real UI. They invent screens, keep stale button labels and get required fields wrong.
So I had three goals. Bad content must not ship. Internal details must not ship. And the portal itself must never become a runtime dependency of the ERP or a place where data can leak.
What I did
A typed content contract that fails at build time
I designed each ERP module as a self-contained content package that follows a typed contract, plus one central registry the UI reads from. The registry validates every package when the build imports it. A malformed package stops the build.
The trade-off is that nothing degrades gracefully. I accepted that because a help article that is silently wrong is worse than a deploy that fails loudly.
Markdown with a strict build-time parser
I replaced the hand-maintained catalogs with Markdown files and structured frontmatter, read by a small, dependency-free parser that runs at build time and enforces the schema. Non-engineers can now review an article as plain text in a normal diff.
The cost is that the whole catalog ships in the bundle with no lazy loading. For a bounded internal catalog, zero runtime parsing and hard schema checks were worth it.
Separate public and internal content, enforced by the build
Every article has two parts: a staff-facing guide, and an internal technical reference that the AI agent used for grounding. The internal reference is never bundled. A build guard rejects any public guide that contains internal technical markers. Integrity assertions stop articles from silently disappearing and stop drafts from going live.
Authors do hit build failures for phrasing that leaks. That friction is intended.
A verification record for AI-produced content
I treated AI output like an untrusted dependency. For each batch of features, the record pins the exact app and backend versions, restores a scoped local database snapshot, and replays the real workflow through the real UI with synthetic role accounts. It then checks the saved state over HTTP instead of trusting what the screen shows, checks every image asset, and ends with an explicit “not verified” list. A written screenshot standard bans AI-redrawn UI.
This is slower than reading a diff, and it is openly incomplete. Writing down the gaps was cheaper than overstating coverage.
No network, no backend
The portal makes no API calls and has no CMS, analytics or login. Content and the search index are bundled at build time, and search state lives in the URL. Search ignores Vietnamese diacritics, using Unicode decomposition plus an explicit fallback for the one letter that decomposition does not handle, so staff can type without accents and still match text inside individual steps.
The trade-off is no live content and no fuzzy search. For a read-only internal catalog that was acceptable.
Result
The portal is deployed and used company-wide as an internal product. A second ERP module shipped as an additive content package; the only change in shared code was a registry entry. The hand-maintained catalogs are fully replaced by the validated Markdown pipeline, and internal technical references stay out of the shipped bundle by construction.
The verification process also caught one of its own mistakes: a re-run found an earlier wrong conclusion and corrected it before publication. I am not claiming adoption, defect-rate or time-saved numbers, because none were measured.
What I’d do differently
The project has no automated tests or CI of its own. The build gates carry the policy, but the gates themselves are not tested, so I would add tests for the parser and the leak guard first. I would also set a size budget for the bundle early, before the catalog grows enough to make the no-lazy-loading choice expensive.