← Projects
Part 1 · MANIFEST · DITA · Docs as code · 10 min read

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.

KEYS.DITAMAPtpu7x-chips-per-pod= 9,216Plain conversionkeys resolved to textMANIFESTkeys kept as substitutionstyped four times9,2169,2169,2169,216conf.py · tpu7x_chips_per_pod{{key}}{{key}}{{key}}{{key}}a change means four edits,and one missed copy4 topics · 1 source value · 1 editone edit, everywhere
Fig. 1The same key after two kinds of migration. The value 9,216 is the number of chips in a TPU7x pod6.

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.

1 in 3pushes that also updated the README when docs lived in the repositoryMacNamara, Google3
2.4×more likely to see better delivery and operational performance with higher-quality docsDORA 20214
50%productivity boost from good, reliable and up-to-date documentationGitHub Octoverse 20215
Fig. 2Three findings on documentation that lives with the code. All three are measured results from the cited studies.
2016 · 2021

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.

DITA-OT 4.3

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.

keys.ditamapeach value stored once, as a key
<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>
tpu7x-specs.ditatopic using two keys, a conref and a profiled paragraph
<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>
shared.ditawritten once, reused everywhere
<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>
operator.ditavalthe operator build filter
<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-specs.md · plain conversionreuse lost
# 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.md · MANIFESTreuse kept
(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.

Finding · Google Cloud TPU7x pageLook at what happens in the wild: on the live Cloud TPU7x documentation, the comparison table quotes the HBM capacity as 192 GiB, while the very next paragraph calls it 192 GB. In the bandwidth section, it lists 7,380 GBps in one place and "about 7.37 TB/s" in another6. This isn't because Google's writers are careless; it's because when you type the same specification into five different files by hand, human beings will inevitably make a pig's breakfast of it. A key stores the truth once.
IXIASOFT · 2015 to 2016

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.

0%25%50%75%100%Task topics78.3%Reference topics69.7%All topics68.2%Concept topics58.1%procedures reuse most
Fig. 5Share of topics reused from one release to the next in IXIASOFT's own DITA documentation (2015 to 2016)8. Measured data.
REUSE KEPT · 1 EDITEdit 1 valuetopictopictopictopictopicREUSE FLATTENED · 5 EDITSediteditediteditmissed?every copy is another edit, and a chance to miss oneWRITER HOURS A YEAR · PROJECTEDFlattened150 hKey kept30 h050100150300 value changes a year, 5 uses each, 6 minutes an edit
Fig. 6The reuse multiplier. Each flattened copy adds an edit and a chance to miss one.

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.pygenerated from keys.ditamap
# 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.

TODAYWITH MANIFESTConvert the mapRead every pageall topicsRebuild reuseby handPublisha person reads every pageAgent converts and checksevery topic, with a reportWriter approvespassing PRsWriter handlesissues onlyPublishthe review moves fromevery page to the exceptions
Fig. 8The writer's workflow before and after. The review effort moves from every page to the exceptions.
0%10%20%30%40%Under 15 min9.3%15 to 30 min27.0%30 to 60 min37.9%60 to 120 min18.3%Over 120 min7.6%the three red rowsare the 61% over 30 minutes
Fig. 9Time developers spend searching for answers each day, Stack Overflow Developer Survey 202411. Measured data. Blue bars are more than 30 minutes; the survey reports them together as 61%.
Stack Overflow · 2024

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.

Code and doc changePull requestone reviewChecksbuild · keys · linksWriter reviewstyle and accuracyMerge and publishfailed check:a comment withfile, line, fix
Fig. 10The developer's flow after migration. Code and docs travel in one pull request.

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.

pull request #2481 · check commentexample
MANIFEST check · pull request #2481​tpu7x-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.

Technical writermigrate tpu.ditamapDITA sourceGit, unchangedMANIFEST AGENT · ADK · GEMINI 3.8 FLASH · CLOUD RUN JOBinventory_mapconvert_maprestore_reusecheck_topiccheck_linksfix (Gemini)calls DITA-OT 4.4 · Sphinx + MyST · report to Cloud StorageTools give the same result every time.Gemini runs only after a failed check.passfailPull requestpassing topics · writer approvesReview issueafter 2 failed fixes
Fig. 12MANIFEST architecture. The DITA source and DITA-OT stay as they are. The agent adds the checks and the report.
ADK · Cloud Run

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.

ToolWhat it doesServes
inventory_mapAudits elements, keys, conrefs, and conditions across the DITA map before anyone touches a spanner.Writers
convert_mapExecutes DITA-OT with safe token placeholders protecting keys from being irreversibly flattened.Both
restore_reuseSwaps placeholders for native MyST substitutions, includes, and target anchors.Writers
check_topicMechanically audits the AST, conditional renderings, and cross-references against the original XML.Both
check_linksRuns 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.

BLS · May 2025

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.

2,227 hwriter hours returned in year one, about one full-time writer Projected
$102,647value of writer and engineer time saved in year one Projected
$17Gemini cost for 5,000 topics at paid rates Projected15

Figure 13Year-one projections for a documentation set of 5,000 topics. Table 2 shows how each number adds up.

InputValueBasis
Documentation set5,000 topicsAssumption
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 hand45 minutesAssumption
Read and repair one converted topic25 minutesAssumption
Topics MANIFEST sends to a writer15%, 30 minutes eachAssumption tested in Figure 15
Approve one passing topic2 minutesAssumption
Changes to reused values300 a year, 5 uses each, 6 minutes per editAssumption
Doc change requests60 engineers × 1.5 a monthAssumption
Gemini 3.8 Flash price$0.75 input, $3.75 output per million tokensGemini API pricing, until 31 December 202615
Cloud Run free tier180,000 vCPU-seconds a monthGoogle Cloud Free Tier16

Table 1Inputs to the projections. Every assumption can be replaced by a measured value after the first migration run.

5,000 topics

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.

01,0002,0003,0004,000Convert by hand3,750 h$162,963DITA-OT, read every page2,083 h$90,535MANIFEST517 h$22,453
Fig. 14Projected writer hours to migrate 5,000 topics, with cost at $43.46 an hour17. Converting by hand costs $162,963. Dashed outlines mark projected values.
01,0002,0003,0004,0000%10%20%30%40%share of topics MANIFEST sends to a writerconvert by hand: 3,750 hread every page: 2,083 hassumed 15%: 517 h40%: 1,100 h
Fig. 15Sensitivity of the MANIFEST estimate to the share of topics that need a writer. Projected.

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.

1,080 requests a year

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.

Engineer hoursWriter hours04008001,200Ticket to docs team360 h720 h1,080 h$54,824Same pull request270 h180 h450 h$25,473
Fig. 16Projected yearly hours for 1,080 doc change requests, priced at $65.38 an hour for engineers18 and $43.46 for writers17.
Year one

On the numbers

AreaBeforeAfterSavedValue
Migration of 5,000 topics, one time2,083 writer hours (read every page)517 writer hours1,567 h$68,082
Changes to reused values, yearly150 writer hours30 writer hours120 h$5,215
Doc change requests, yearly360 engineer + 720 writer hours270 engineer + 180 writer hours90 h + 540 h$29,350
Year one total2,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.

Summary

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.

Siddharth
Sources

References

  1. DITA Open Toolkit 4.3, "Generating Markdown output." dita-ot.org/4.3/topics/dita2markdown
  2. Titus Winters, Tom Manshreck and Hyrum Wright, Software Engineering at Google, chapter 10, "Documentation." abseil.io/resources/swe-book/html/ch10.html
  3. Tom Johnson, "Transforming documentation processes: Riona MacNamara's docs-as-code work at Google," I'd Rather Be Writing, 15 July 2016. idratherbewriting.com
  4. The Register, "DORA DevOps report 2021," 23 September 2021, quoting the 2021 Accelerate State of DevOps report. theregister.com · dora.dev/research/2021
  5. InfoQ, "GitHub Octoverse 2021," November 2021. infoq.com/news/2021/11/github-octoverse-2021
  6. Google Cloud, "TPU7x (Ironwood)," last updated 13 October 2026. docs.cloud.google.com/tpu/docs/tpu7x
  7. DITA Open Toolkit, "Debug and filter" and related pre-processing modules. dita-ot.org/dev/reference/preprocess-debugfilter
  8. Keith Schengili-Roberts, "Reuse Metrics," IXIASOFT, CCMS Link 2019. ditawriter.com (PDF)
  9. MyST-Parser, "Syntax extensions: Substitutions." myst-parser.readthedocs.io
  10. Sphinx only directive: including content based on tags. devopstutodoc.readthedocs.io
  11. Stack Overflow, "2024 Developer Survey: Professional developers." survey.stackoverflow.co/2024/professional-developers
  12. Google, Agent Development Kit documentation. github.com/google/adk-docs
  13. Google AI for Developers, "Gemini models." ai.google.dev/gemini-api/docs/models
  14. Google AI for Developers, "Function calling with the Gemini API." ai.google.dev/gemini-api/docs/function-calling
  15. Google AI for Developers, "Gemini Developer API pricing." ai.google.dev/gemini-api/docs/pricing
  16. Google Cloud, "Free Tier." cloud.google.com/free
  17. U.S. Bureau of Labor Statistics, "Technical Writers," Occupational Outlook Handbook (May 2025 wage data). bls.gov
  18. U.S. Bureau of Labor Statistics, "Software Developers," Occupational Outlook Handbook (May 2025 wage data). bls.gov
  19. Google Developers, "Site Policies." developers.google.com/site-policies
  20. lychee link checker. github.com/lycheeverse/lychee