Moving DITA to docs as code without losing reuse
Our reference documentation for Google's data center products lives inside the vast, air-conditioned cathedral of DITA XML. Our developer guides, meanwhile, live in casual Markdown, parked right next to the code. Naturally, management wants them unified into one sleek "docs as code" workflow. Converting the raw text is trivial—you simply run DITA Open Toolkit (DITA-OT), and with a single command, it squirts out miles of Markdown1. Job done, you might think. Crack open the gin.
This is a portfolio post, written as an internal knowledge-sharing post for Google's data center products and services documentation team. Google has not engaged, sponsored or endorsed this work. The TPU examples are adapted from Google Cloud documentation under CC BY 4.0 and are modified19. Numbers marked "projected" are modelled. The assumptions are listed in Table 1.
Except it isn't. Because beneath that sunny exterior, the converter has secretly committed structural vandalism. It has taken all the clever single-sourcing and reuse features that technical writers rely on to stay sane, pulverized them into dust, and replaced them with thousands of dumb, hardcoded copies. Technical writers need modular reuse. Software developers just want to edit the documentation in the same pull request without installing Java. A migration agent called MANIFEST bridges that divide. And at the end, we shall inspect the cold, hard balance sheet to see what it is actually worth to the enterprise.
On docs as code
Google has been preaching the gospel of "docs as code" for years. As outlined in Software Engineering at Google, technical documentation should be treated with the exact same rigor as code: checked into source control, given clear owners, tracked like bugs, and put through code review2. Their internal g3doc framework deliberately parked Markdown right beside C++ source trees, on the blindingly obvious premise that developers might actually edit files that are already sitting in front of them2.
Because if you stash documentation somewhere else, it dies. When Google pulled the plug on its old internal corporate wiki, they discovered that an astounding 90% of the articles had received precisely zero views and zero updates in months—it wasn't a knowledge base; it was a digital graveyard2. Conversely, when Riona MacNamara analyzed commit habits, she found developers updated the README roughly a third of the time when pushing changes, purely because it shared the same directory3.
And proper documentation actually pays enormous dividends. The metrics below explain why companies pour millions into these migrations instead of spending it on track days.
On plain conversion
DITA is famously baroque, but it possesses three ingenious reuse mechanisms that keep technical writers from having a collective breakdown:
- Keys. You define a specification once in the master map, and topics refer to the key rather than the raw number. Change it once, and the whole fleet updates.
- Content references (conrefs). You write an intricate safety warning once, and every topic borrows it by reference like a library book.
- Conditional profiling. You tag paragraphs for specific audiences, and a build profile includes or excludes them at will.
Below is a slice of real-world TPU7x documentation written in standard DITA XML, with specifications drawn directly from Google's hardware reference6. Click through the tabs to view the map, the topic, the shared snippet, and the build filter.
<map> <title>Cloud TPU generations</title> <!-- Each value is stored once, as a key --> <keydef keys="tpu7x-chips-per-pod"> <topicmeta> <keywords><keyword>9,216</keyword></keywords> </topicmeta> </keydef> <keydef keys="tpu7x-hbm-per-chip"> <topicmeta> <keywords><keyword>192 GiB</keyword></keywords> </topicmeta> </keydef> <topicref href="tpu7x-specs.dita"/> <topicref href="slice-planning.dita"/></map>
<reference id="tpu7x-specs"> <title>TPU7x specifications</title> <refbody> <properties> <property> <proptype>Chips per pod</proptype> <propvalue><keyword keyref="tpu7x-chips-per-pod"/></propvalue> </property> <property> <proptype>HBM per chip</proptype> <propvalue><keyword keyref="tpu7x-hbm-per-chip"/></propvalue> </property> </properties> <section id="slice-shapes"> <title>Slice shapes</title> <note conref="shared.dita#shared/cube-note"/> <p audience="operator">Each TPU7x VM has 4 chips, 224 vCPUs and 960 GB of RAM.</p> </section> </refbody></reference>
<topic id="shared"> <title>Shared notes</title> <body> <!-- Written once, reused in every TPU7x topic --> <note id="cube-note">Slices larger than 64 chips are made up of one or more 4x4x4 cubes of chips.</note> </body></topic>
<val> <!-- Operator build: show capacity details --> <prop att="audience" val="operator" action="include"/> <prop att="audience" val="developer" action="exclude"/></val>
Figure 3DITA source for the TPU7x specifications topic. The topic uses two keys, one conref and one paragraph for operators only.
Now, what happens if you feed this to a standard converter? DITA-OT runs its usual pre-processing routines, bakes all the keys into static text, copies the shared notes inline, discards excluded audiences, and flattens the lot into plain Markdown7. On screen, the Markdown looks completely harmless. But underneath, every single reuse mechanism has been sledgehammered into oblivion. In the GitHub flavor, it even mangles definition lists into bullet points and deletes anchor IDs without leaving a note on the windscreen1. It doesn't throw a warning; it just smiles and hands you ruined architecture.
# TPU7x specifications| Property | Value || ------------- | ------- || Chips per pod | 9,216 || HBM per chip | 192 GiB |## Slice shapes> **Note:** Slices larger than 64 chips are made up> of one or more 4x4x4 cubes of chips.Each TPU7x VM has 4 chips, 224 vCPUs and 960 GB of RAM.
(tpu7x-specs)=# TPU7x specifications| Property | Value || ------------- | ------------------------- || Chips per pod | {{ tpu7x_chips_per_pod }} || HBM per chip | {{ tpu7x_hbm_per_chip }} |(slice-shapes)=## Slice shapes```{include} _includes/cube-note.md``````{only} operatorEach TPU7x VM has 4 chips, 224 vCPUs and 960 GB of RAM.```
Figure 4The same topic after migration, simplified. Red lines are typed copies of shared content. Green lines still point to one source.
On writers and reuse
For technical writers, content reuse isn't an academic theory; it is the only reason three people can document a product built by four thousand engineers without weeping in the stairwell. When IXIASOFT audited their enterprise DITA repositories, they found that an astonishing 68% of topics were reused across releases, climbing to over 78% for procedural task topics8.
When you flatten reuse, a single tweak suddenly metastasizes into an avalanche of tedious copy-paste edits. If your shared hardware values change 300 times a year, and each value sits in five different topics, an elegant key-based system handles that in 300 edits. Flatten it into dumb Markdown, and you now have 1,500 individual edits to juggle. That is 150 hours of a skilled writer's life thrown onto the bonfire instead of 30, purely to do the work of a Commodore 64.
MANIFEST saves us from this horror. Instead of flattening, it maps keys into MyST Markdown substitutions, transforms conrefs into neat include files, and wraps conditional text in Sphinx only directives9, 10. Everything stays centralized in a tidy Python configuration file:
# conf.py: substitutions generated by MANIFEST from keys.ditamapmyst_enable_extensions = ["substitution"]myst_substitutions = { "tpu7x_chips_per_pod": "9,216", "tpu7x_hbm_per_chip": "192 GiB",}
Figure 7Key values become MyST substitutions. To build the operator version of the site, run sphinx-build -t operator.
The review experience is transformed. Previously, verifying a migration meant a team of exhausted humans manually proofreading 5,000 files like Victorian monks. MANIFEST programmatically verifies every converted file against its XML origin, validates the anchors, and opens a passing PR automatically. Writers spend their time reviewing only the tricky outliers.
On developers and pull requests
Software developers spend half their lives hunting for lost documentation. In the 2024 Stack Overflow survey, 61% of professional developers admitted spending more than thirty minutes every day wandering around internal wikis like lost sheep, while 53% noted that waiting on basic technical clarity regularly derails their train of thought11.
Under the old regime, an engineer who modifies a hardware timeout files a Jira ticket pleading with the technical writing department to update the XML. The ticket sits in a queue, the code ships, and the manual is instantly obsolete. Post-migration, the documentation is sitting right there in Git. The engineer modifies the doc in the exact same branch, previews the output locally, and lets automated CI lint the changes.
And the linting is brutally simple. If an enthusiastic engineer attempts to type a raw integer where a substitution key belongs, the bot intercepts the PR and tells them precisely which key to drop in instead. Architectural discipline is preserved without human nagging.
MANIFEST check · pull request #2481tpu7x-specs.md PASS includes 1/1 · substitutions 2/2 · IDs 2/2slice-planning.md FAIL line 42: "9,216" is the value of key tpu7x_chips_per_pod. Fix: replace it with {{ tpu7x_chips_per_pod }}.links PASS 38 checked · 0 broken
Figure 11An example check comment on a pull request. The engineer sees the file, the line and the fix.
On MANIFEST
Under the bonnet, MANIFEST is assembled on Google’s open-source Agent Development Kit and runs as a stateless Cloud Run job—one map execution per run12. Gemini 3.8 Flash acts as the orchestrator, invoking tools via standard API function calling13, 14.
Crucially, the actual conversion tools are plain, deterministic Python scripts. Gemini does not get to guess whether XML conforms to an AST; the Python scripts verify it with absolute precision. If a topic fails conversion, Gemini attempts to remediate it twice. If it still fails, it doesn't sweep the wreckage under the carpet; it opens an issue for a human writer. When a writer approves a manual fix, the rule is saved to the pipeline, making subsequent runs smarter.
| Tool | What it does | Serves |
|---|---|---|
inventory_map | Audits elements, keys, conrefs, and conditions across the DITA map before anyone touches a spanner. | Writers |
convert_map | Executes DITA-OT with safe token placeholders protecting keys from being irreversibly flattened. | Both |
restore_reuse | Swaps placeholders for native MyST substitutions, includes, and target anchors. | Writers |
check_topic | Mechanically audits the AST, conditional renderings, and cross-references against the original XML. | Both |
check_links | Runs lychee across the rendered pages to ensure zero 404s and validates redirects20. | Developers |
Note that MANIFEST doesn't attempt to automatically untangle deeply bizarre DITA specializations or exotic conref-push patterns. It flags those as human review items, which is where they belong.
On the organisation
Now, let's talk about the money. The projections below are drawn from official Bureau of Labor Statistics data for May 2025, using the assumptions laid out in Table 1. They omit corporate perks and overhead, making these estimates conservative to a fault.
Figure 13Year-one projections for a documentation set of 5,000 topics. Table 2 shows how each number adds up.
| Input | Value | Basis |
|---|---|---|
| Documentation set | 5,000 topics | Assumption |
| Writer cost | $43.46 an hour ($90,390 a year) | BLS, May 202517 |
| Engineer cost | $65.38 an hour ($135,980 a year ÷ 2,080 hours) | BLS, May 202518 |
| Convert one topic by hand | 45 minutes | Assumption |
| Read and repair one converted topic | 25 minutes | Assumption |
| Topics MANIFEST sends to a writer | 15%, 30 minutes each | Assumption tested in Figure 15 |
| Approve one passing topic | 2 minutes | Assumption |
| Changes to reused values | 300 a year, 5 uses each, 6 minutes per edit | Assumption |
| Doc change requests | 60 engineers × 1.5 a month | Assumption |
| Gemini 3.8 Flash price | $0.75 input, $3.75 output per million tokens | Gemini API pricing, until 31 December 202615 |
| Cloud Run free tier | 180,000 vCPU-seconds a month | Google Cloud Free Tier16 |
Table 1Inputs to the projections. Every assumption can be replaced by a measured value after the first migration run.
On migration effort
Migrating a repository of 5,000 topics completely by hand requires 3,750 hours of pure misery. Running DITA-OT and then forcing writers to manually proofread every single page burns about 2,083 hours. With MANIFEST handling the heavy lifting, human review drops to roughly 517 hours.
And even if our assumptions are wildly pessimistic—even if writers have to intervene on a catastrophic 40% of all files—MANIFEST still takes only 1,100 hours. That is still half the time required to read the conversion manually.
On developer requests
Consider the daily operational grind: 60 developers raising 1.5 doc updates a month equals 1,080 ticket interactions every year. Handling that via ticketing burns roughly an hour of combined staff time per change. Moving that into the same GitHub PR drops writer overhead down to ten minutes of peer review.
On the numbers
| Area | Before | After | Saved | Value |
|---|---|---|---|---|
| Migration of 5,000 topics, one time | 2,083 writer hours (read every page) | 517 writer hours | 1,567 h | $68,082 |
| Changes to reused values, yearly | 150 writer hours | 30 writer hours | 120 h | $5,215 |
| Doc change requests, yearly | 360 engineer + 720 writer hours | 270 engineer + 180 writer hours | 90 h + 540 h | $29,350 |
| Year one total | 2,227 writer h 90 engineer h | $102,647 |
Table 2Projected year-one savings. Compared with converting by hand, the migration saving rises to 3,233 hours ($140,510).
And what does this wizardry cost in compute? Practically nothing. Processing 5,000 topics burns roughly 100,000 vCPU-seconds, which fits comfortably inside Cloud Run’s monthly free tier allowance of 180,00016. Gemini is summoned only when a topic fails its checks. If 750 topics fail twice, the total API bill comes to about seventeen dollars15. Seventeen dollars. You can barely buy two mediocre sandwiches at an airport for that. For enterprise proprietary data, the commercial paid tier is mandatory to avoid feeding your internal IP into public model training, but financially it is a rounding error15.
On balance
Writers and developers traditionally want diametrically opposed things from documentation tooling. Technical writers want sophisticated modular reuse so they don't have to maintain thousands of duplicate files. Software developers just want clean Markdown they can edit in VS Code without having to ask permission from an XML council. A plain automated conversion simply hands developers what they want while burning the technical writers' house down.
MANIFEST gives both tribes what they need without sacrificing architectural integrity. The LLM handles the edge cases, deterministic code linters enforce the quality gates, and technical writers remain firmly in the driver's seat. The full architectural design, operational stories, and sample simulators are published in the MANIFEST design document.
References
- DITA Open Toolkit 4.3, "Generating Markdown output." dita-ot.org/4.3/topics/dita2markdown
- Titus Winters, Tom Manshreck and Hyrum Wright, Software Engineering at Google, chapter 10, "Documentation." abseil.io/resources/swe-book/html/ch10.html
- Tom Johnson, "Transforming documentation processes: Riona MacNamara's docs-as-code work at Google," I'd Rather Be Writing, 15 July 2016. idratherbewriting.com
- The Register, "DORA DevOps report 2021," 23 September 2021, quoting the 2021 Accelerate State of DevOps report. theregister.com · dora.dev/research/2021
- InfoQ, "GitHub Octoverse 2021," November 2021. infoq.com/news/2021/11/github-octoverse-2021
- Google Cloud, "TPU7x (Ironwood)," last updated 13 October 2026. docs.cloud.google.com/tpu/docs/tpu7x
- DITA Open Toolkit, "Debug and filter" and related pre-processing modules. dita-ot.org/dev/reference/preprocess-debugfilter
- Keith Schengili-Roberts, "Reuse Metrics," IXIASOFT, CCMS Link 2019. ditawriter.com (PDF)
- MyST-Parser, "Syntax extensions: Substitutions." myst-parser.readthedocs.io
- Sphinx
onlydirective: including content based on tags. devopstutodoc.readthedocs.io - Stack Overflow, "2024 Developer Survey: Professional developers." survey.stackoverflow.co/2024/professional-developers
- Google, Agent Development Kit documentation. github.com/google/adk-docs
- Google AI for Developers, "Gemini models." ai.google.dev/gemini-api/docs/models
- Google AI for Developers, "Function calling with the Gemini API." ai.google.dev/gemini-api/docs/function-calling
- Google AI for Developers, "Gemini Developer API pricing." ai.google.dev/gemini-api/docs/pricing
- Google Cloud, "Free Tier." cloud.google.com/free
- U.S. Bureau of Labor Statistics, "Technical Writers," Occupational Outlook Handbook (May 2025 wage data). bls.gov
- U.S. Bureau of Labor Statistics, "Software Developers," Occupational Outlook Handbook (May 2025 wage data). bls.gov
- Google Developers, "Site Policies." developers.google.com/site-policies
- lychee link checker. github.com/lycheeverse/lychee