Home / What YAML gives technical docs that XML and Markdown can’t

What YAML gives technical docs that XML and Markdown can’t

View as Markdown
On this page

Core thesis: For small-to-medium structured reference data that needs to live in Git and render across multiple formats, YAML functions as an ideal single source of truth—decoupling pure data from presentation without the runtime overhead of a database or the verbosity of XML.

  1. 1. Structured YAML source
  2. 2. Static site / build generator
  3. 3. Multiple derived outputs (tables, lists, APIs)
  4. 4. Clean, reviewable Git diffs
  5. 5. Schema validation & consistency

YAML is a lightweight, human-readable data format that simplifies configuration files and data exchange. Its clarity, flexibility, and efficiency make it increasingly popular among developers, often outperforming XML, Markdown, and some database solutions in modern documentation pipelines.

The core architectural argument rests on three points:

  • The problem: When structured reference data is mixed with presentation markup, keeping catalogs, tables, and specifications updated becomes an error-prone maintenance bottleneck.
  • The proposal: Store reference facts in plain YAML files under full Git version control, completely decoupled from layout.
  • The payoff: Render that single source into tables, lists, headless APIs, and UI widgets while preserving atomic Git diffs and branch-based reviews.

Storing content in plain files rather than a database keeps your source code accessible to standard text-processing tools, makes peer reviews transparent, and fits directly into CI/CD build steps.


Imaginary scenario: choosing the right engine oil

Imagine you’re an engine oil manufacturer. Every day, customers ask you which oil is best for their engines. Some have one-cylinder engines, others have multi-cylinder setups. Price, viscosity, and compatibility all matter, but helping them make the right choice isn’t just about knowing your products. It’s about how you store and present that information.

To anchor this architectural choice, we will follow a single continuous dataset throughout this article: an engine-oil catalog tracking six core attributes:

Oil Data Model
├── type            # Category / designation: Primary oil, Secondary oil
├── brand (name)    # Product brand identifier: A1X, B2Z
├── usage           # Target engine: One-cylinder engines, Two-cylinder engines
├── price           # Unit price (numeric float): 15.00, 17.00
├── cylinders       # Engine cylinders (integer): 1, 2
└── viscosity_grade # SAE rating: 0W-20, 5W-30

At first, managing this catalog looks simple enough. You create a quick reference table:

Oil TypeBrandUsePriceViscosity Grade
Primary oilA1XOne-cylinder engines150W-20
Secondary oilB2ZTwo-cylinder engines175W-30

Looks neat, right? But as your product line grows, so does the complexity. Adding new oils, updating prices, or including extra metadata like cylinder count or warranty quickly turns into a maintenance problem.

  1. 1. Small dataset (looks easy)
  2. 2. Product catalog expands
  3. 3. Extra metadata fields added
  4. 4. Multi-format requirements emerge
  5. 5. Exponential maintenance cost
Bottles of Pennzoil motor oil on a store shelf.
Motor oil catalog: managing products, viscosities, and prices across multiple documentation formats.

The XML temptation

Some teams turn to DITA reference XML, thinking formal structure will solve the problem:

DITA-style reference XML XML
<reference id="oil-types">
  <title>Oil types</title>
  <shortdesc>You will find below the recommended oil types.</shortdesc>
  <refbody>
    <section>
      <title>Primary oil</title>
      <ul>
        <li>Brand: A1X</li>
        <li>Use: One-cylinder engines</li>
        <li>Price: 15</li>
        <li>Viscosity grade: 0W-20</li>
      </ul>
    </section>
    <section>
      <title>Secondary oil</title>
      <ul>
        <li>Brand: B2Z</li>
        <li>Use: Two-cylinder engines</li>
        <li>Price: 17</li>
        <li>Viscosity grade: 5W-30</li>
      </ul>
    </section>
  </refbody>
</reference>

What XML provides: Explicit document structure, semantic tagging, and formal DTD/XSD validation.

What becomes painful in this specific use case: The moment prices change, new products are added, or you want to track extra attributes, the XML becomes cumbersome:

IssueDescription
Hardcoded ValuesEvery data point is embedded in XML. Updates require manual changes across every topic, which is error-prone.
Mixing Data and Presentation<ul> and <li> combine field names and values, making automated sorting or aggregation difficult.
Poor ScalabilityAdding oils or metadata requires repeating verbose boilerplate XML structures.
Lack of Unique IdentifiersSections are distinguished by titles only, risking breakage in workflows if names change.
Limited ReusabilityCopying sections across documents increases the risk of drift and inconsistencies.
Ambiguous Values<li>Price: 15</li> lacks units or currency formatting.
No Validation for Consistent StructureMissing fields reduce data quality over time without custom Schematron rules.

Hardcoded XML works for tiny static lists, but it quickly becomes brittle as catalog content grows.


Markdown tables: simple but limiting

Markdown tables are immediately readable in raw text:

| Oil Type      | Brand | Use                  | Price | Viscosity Grade |
| ------------- | ----- | -------------------- | ----- | --------------- |
| Primary oil   | A1X   | One-cylinder engines | 15    | 0W-20           |
| Secondary oil | B2Z   | Two-cylinder engines | 17    | 5W-30           |

What Markdown provides: Fast authoring, high human readability in raw text, and universal static-site generator support.

What breaks down when data scales: Behind the tidy appearance, embedded tables carry hidden maintainability problems:

IssueDescription
Hardcoded DataManual updates are required for every price or product change.
Lack of Semantic StructureField names and values are visual table cells, not machine-readable key-value pairs.
Poor ScalabilityAdding new oils or metadata requires manually refactoring every pipe and separator.
No Unique IdentifiersRows are identified only by “Oil Type,” making programmatic referencing unreliable.
AmbiguitiesValues like Price: 15 lack units or types, causing downstream ambiguity.
Limited ReusabilityTables cannot be reused across multiple documents without manual copy-pasting.
The limits of Markdown tables

Markdown was designed so that the source should be almost as human-readable as the output, whether rendered as HTML, PDF, or another format. But tables are an exception: they introduce several authoring challenges.

Long lines quickly become difficult to read and edit as text editors wrap them, making it hard to distinguish one row from another. The visual benefit of tables for readers (having columns neatly aligned on vertical pipes) turns into an authoring liability:

| Oil Type | Brand | Use  | Price | Viscosity Grade |
| - | - | - | - | - |
| Primary oil | A1X | One-cylinder engines | 15 | 0W-20 |
| Secondary oil | B2Z | Two-cylinder engines | 17 | 5W-30 |

Markdown source table

Some text editors automatically realign table columns when you edit a cell, but this triggers a full table refactor. The result is a noisy Git diff where Git flags entire lines as changed even though only a few whitespace characters moved. Conversely, if you avoid reformatting and keep column widths ragged, the raw table becomes frustrating for humans to parse.

Markdown is excellent for prose, but as your structured reference data grows, you need an architecture that decouples content from layout.


Databases: powerful, but a poor fit for source content

Databases are excellent at what they’re built for: structured storage, rich queries, integrity constraints, and concurrent access at scale. The question isn’t whether they’re capable: it’s whether they fit documentation source code.

Where Databases Excel

Query engine & live transactions

Arbitrary SQL queries, relational joins, ACID transactions, concurrent multi-user writes, and scaling to millions of dynamic records that update independently of site builds.

Where Docs-as-Code Diverges

Source-content workflow & Git review

Branching, peer review in pull requests, offline text editing, commit history, zero runtime infrastructure dependencies, and static deployment alongside application code.

For content you want to version, review, and build into a static site, a database pulls in the opposite direction. It introduces a live service to run, migrate, back up, and secure. Most importantly, the content lives outside Git: you lose readable diffs, branch-and-PR review, and editing offline in your text editor. The data is no longer plain text you can grep, refactor with sed, or roll back with a commit.

That is the real trade-off: not query performance, but where your content lives and how you change it.


YAML: readable, structured, and scalable

This is where YAML shines. It is human-readable, hierarchical, and structured, making it ideal for reference documentation that needs to scale under version control.

  1. 1. Structured YAML source (facts only)
  2. 2. Typed component layer (Astro / script)
  3. 3. Multi-format outputs (tables, lists, APIs)

Here is the canonical data source (oil-types.yaml):

id: oil-types
title: Oil types
shortdesc: Recommended oil types
properties:
  headers:
    type: Type
    value: Brand
    usage: Use
  rows:
    - type: Primary oil
      value: A1X
      usage: One-cylinder engines
    - type: Secondary oil
      value: B2Z
      usage: Two-cylinder engines

Rendered in Markdown or HTML via a simple build script, it produces a clean presentation table:

Oil typeOil brandUse
Primary oilA1XOne-cylinder engines
Secondary oilB2ZTwo-cylinder engines

Benefits of YAML as a source of truth

FeatureDetails
Separation of Data and PresentationPure data lives in YAML; styling and markup live in reusable templates or components.
Structured and PredictableConsistent schemas reduce human error and simplify automated processing.
Easy to ExtendAdd new oil records or metadata attributes without altering existing layouts.
Supports AutomationStatic site generators, build scripts, and CI runners consume YAML natively.
Unique IdentifiersTop-level id keys enable unambiguous cross-referencing across topics and datasets.
Readable and MaintainableSelf-documenting key-value pairs are easier to inspect than embedded XML tags or pipe tables.
Scalable for DatasetsWorks cleanly for 5 or 500 rows while keeping diffs concise and validation automated.

YAML versatility: one source, multiple output formats

The central payoff of structured data is versatility. The same dataset can be rendered in multiple representations—lists, summary cards, data tables, or headless APIs—without changing a single character in the source file.

YAML Output Formats Flow Diagram

Single YAML Source
oil-types.yaml

Simple List View
<ToolsList />

Responsive 2-Column Table
<ToolsTable />

Sortable 4-Column Table
<ToolsTableFourCols />

Headless JSON API
/api/oil-types.json

DITA Reference Topic
oil-types.dita

Figure 1 — Single-source dataset representation: one YAML source rendered simultaneously across lists, cards, tables, and API outputs.

Below are three live representations generated from the exact same oil-types.yaml dataset:

Output 1: Compact list for quick scanning

For user guides or mobile-friendly overviews where a wide table is unnecessary, the data renders as a clean bulleted hierarchy:

Example: Display your data as a simple list

Oil types

You will find below the recommended oil types.

  • Primary oil
    • Brand: A1X
    • Usage: One-cylinder engines
    • Viscosity grade: 0W-20
    • Price: $15.00
  • Secondary oil
    • Brand: B2Z
    • Usage: Two-cylinder engines
    • Viscosity grade: 5W-30
    • Price: $17.00

Output 2: Responsive two-column summary

For summary reference pages, a two-column layout pairs each brand with its technical properties. On desktop, this renders as a table; on mobile viewports, it collapses into individual labeled rows inside cards:

Example: Display your data as a styled two-column table

Oil types

You will find below the recommended oil types.

Brand Details
A1X
Type: Primary oil
Price: $15.00
Cylinders: 1
Viscosity Grade: 0W-20
B2Z
Type: Secondary oil
Price: $17.00
Cylinders: 2
Viscosity Grade: 5W-30

Output 3: Dynamic, sortable four-column table

For comprehensive engineering specifications, the same YAML file feeds an interactive table featuring typed sorting (numeric pricing, integer cylinder counts, alphabetical text) and formatted currency labels:

Example: Display your data as a dynamic HTML table
Type ▲▼ Brand ▲▼ Cylinders ▲▼ Viscosity grade ▲▼ Price ▲▼
Primary oil A1X One-cylinder engines 0W-20 $15.00
Secondary oil B2Z Two-cylinder engines 5W-30 $17.00

Notice the key architectural takeaway: all three outputs derive from a single file. If the price of Primary oil changes to $16.50, you update that single value in oil-types.yaml. The list, the two-column summary, the four-column table, and the API payload all update in lockstep during the next build.


Easier diffs and cleaner version control

One of the most practical benefits of this architecture is version control hygiene. When data and presentation are mixed, even trivial changes produce noisy Git diffs that make code reviews slow and error-prone.

  1. 1. Clean rendered table
  2. 2. Edit single column in source
  3. 3. Editor realigns column whitespace
  4. 4. Git flags every row as modified
  5. 5. Code review noise obscures real diff

Evidence 1: Reordering columns in an embedded Markdown table

When you remove or reorder a column in a Markdown table, Git compares the file line by line. Every single row appears modified because the column delimiters moved:

Git diff: reordering columns in a Markdown table Diff
diff --git a/src/content/blog/scalable-maintainable-technical-docs-with-yaml.mdx b/src/content/blog/scalable-maintainable-technical-docs-with-yaml.mdx
index 61dadd8..0f70057 100644
--- a/src/content/blog/scalable-maintainable-technical-docs-with-yaml.mdx
+++ b/src/content/blog/scalable-maintainable-technical-docs-with-yaml.mdx
@@ -26,10 +26,10 @@ Imagine you’re an engine oil manufacturer. Every day, customers ask you which

   At first, it might seem simple. You could create a quick reference table:

-| Oil Type      | Brand | Use                  | Price | Viscosity Grade |
-| ------------- | ----- | -------------------- | ----- | --------------- |
-| Primary oil   | A1X   | One-cylinder engines | 15    | 0W-20           |
-| Secondary oil | B2Z   | Two-cylinder engines | 17    | 5W-30           |
+| Oil Type      | Use                  | Price | Viscosity Grade |
+|---------------|----------------------|-------|-----------------|
+| Primary oil   | One-cylinder engines | 15    | 0W-20           |
+| Secondary oil | Two-cylinder engines | 17    | 5W-30           |

Notice: Even though the actual product data was untouched, four lines were flagged as deleted and replaced. Reviewers cannot quickly tell if a price or viscosity was altered.

Tip: Mitigate table diff issues with third-party tools

To partially improve the readability of table diffs, use tools like GitHub Desktop or git diff --word-diff, or configure a custom diff driver in .gitattributes.

GitHub Desktop screenshot showing word-level diff highlights.
GitHub Desktop highlighting word deletions in green, though Git still records entire line modifications under the hood.

These tools help human reviewers spot changes, but Git internally still treats the entire line as modified.

Evidence 2: Removing keys from the YAML source

By contrast, when tables are generated from YAML, removing an unused field is an atomic, legible change:

Git diff: removing keys from YAML source Diff
diff --git a/src/data/oil-types.yaml b/src/data/oil-types.yaml
index 11eef57..87c04df 100644
--- a/src/data/oil-types.yaml
+++ b/src/data/oil-types.yaml
@@ -5,24 +5,20 @@ shortdesc: You will find below the recommended oil types.
 properties:
   headers:
     type: Type
-    name: Brand
     usage: Use
   row_schema:
     type: str
-    name: str
     price: float
     cylinders: int
     viscosity_grade: str
   rows:
     - type: Primary oil
-      name: A1X
       price: 15.0
       cylinders: 1
       viscosity_grade: 0W-20
     - type: Secondary oil
-      name: B2Z
       price: 17.0
       cylinders: 2
       viscosity_grade: 5W-30

Notice: Only the exact lines containing name: Brand and the corresponding product values are removed. Every other property line remains untouched, making peer reviews instantaneous.

Evidence 3: Updating table presentation in the component script

An even cleaner docs-as-code pattern is adjusting the rendering component rather than the data. If you want to hide a column across all documentation pages, you update the Astro component:

Git diff: adjusting the table rendering script Diff
diff --git a/src/components/table.astro b/src/components/table.astro
index c05af99..d594203 100644
--- a/src/components/table.astro
+++ b/src/components/table.astro
@@ -3,7 +3,6 @@ import data from "../data/oil-types.yaml";
 type OilRow = {
   type: string;
-  name: string;
   usage: string;
   viscosity_grade: string;
   price: number;
@@ -34,7 +33,6 @@ const wordsToNumber: Record<string, number> = Object.fromEntries(
     <thead>
       <tr>
         <th data-key="type" data-type="text">Type <span class="arrow">▲▼</span></th>
-        <th data-key="name" data-type="text">Brand <span class="arrow">▲▼</span></th>
         <th data-key="cylinders" data-type="cylinders">Cylinders <span class="arrow">▲▼</span></th>
         <th data-key="viscosity_grade" data-type="text">Viscosity grade <span class="arrow">▲▼</span></th>
         <th data-key="price" data-type="number">Price <span class="arrow">▲▼</span></th>
@@ -44,7 +42,6 @@ const wordsToNumber: Record<string, number> = Object.fromEntries(
       {rows.map((row) => (
         <tr>
           <td data-label="Type">{row.type}</td>
-          <td data-label="Brand">{row.name}</td>
           <td data-label="Cylinders">{numberToWords(row.cylinders)}-cylinder engines</td>
           <td data-label="Viscosity grade">{row.viscosity_grade}</td>
           <td data-label="Price">${row.price.toFixed(2)}</td>

Notice: A single 4-line change in the rendering component updates every table across your entire documentation site without touching a single record in oil-types.yaml.


Growing with your YAML: The maturity model

What begins as a simple reference file can mature into a complete documentation asset pipeline:

  1. 1. Flat YAML lookup file
  2. 2. Multi-channel reuse (DITA, API, UI)
  3. 3. Strong typing & constraints
  4. 4. Automated schema validation
  5. 5. Reliable CI/CD build enforcement

Our oil-types.yaml file scales through four stages:

  1. Structured source: The YAML file stores pure data facts once.
  2. Multi-channel distribution: The file generates DITA reference topics, OpenAPI JSON endpoints, and dynamic web UI components.
  3. Strong typing: Enforcing numeric types for prices (float) and cylinder counts (int) prevents formatting errors.
  4. Schema validation: A central JSON Schema or Zod validator in your CI pipeline guarantees that missing fields or malformed records fail the build before reaching production.

Why a structured source is better than embedded tables

Tables are an effective way to present structured data in a familiar, scannable layout. However, authoring user-facing tables directly in Markdown source files ties data to presentation.

A stronger alternative is to extract data at build time from a structured source, such as a YAML single source of truth. This approach allows you to render the same information in multiple ways, each tailored to the target medium and the specific needs of your audience: whether that’s a Markdown table in documentation, a JSON payload for an API, or a dynamic HTML component in a UI. See how Astro exposes YAML data through a live API for a working example of this distribution pattern.

By decoupling data from presentation, you gain maintainability, consistency, and flexibility as your content grows.


Where YAML wins, and where it doesn’t

The title of this post is deliberately bold, so it is essential to scope the architectural claim honestly. YAML outperforms the alternatives for one specific job: small-to-medium structured reference data that builds into a static site or feeds an API under Git.

Stretch it past that job and the comparison flips, because the alternatives are not strawmen; each is strong where YAML is weak:

YAML Single Source

Best fit: Reference data under Git

Small-to-medium structured reference catalogs, product specs, configuration files, and multi-format generated outputs that require clean Git diffs and branch reviews.

Markdown

Best fit: Narrative & prose

Conceptual explanations, tutorials, guides, and thought leadership where content is paragraph-driven and adding a formal schema would introduce needless friction.

XML / DITA

Best fit: Enterprise structural enforcement

Large-scale documentation teams requiring strict structural enforcement, controlled vocabularies, content specialization, and formal schema validation across multi-division publications.

Relational Database

Best fit: High-scale querying & transactions

Datasets exceeding tens of thousands of records, arbitrary SQL queries, relational joins across tables, concurrent writes, and real-time updates that occur outside static site builds.

YAML also has real trade-offs and sharp edges that a fair architectural evaluation must acknowledge:

  • Indentation sensitivity: A missing space can silently restructure an entire object hierarchy.
  • Boolean parsing traps: In older YAML 1.1 parsers, strings like yes, no, on, and off can be coerced into booleans unless strictly quoted.
  • No built-in schema: Unlike XML (which has native XSD/DTD validation), YAML requires an external schema validator (such as JSON Schema or Zod) to guarantee data integrity.
  • Scale limits: Parsing a 50,000-row YAML file during build time is inefficient; flat files lack relational indexes and JOIN capabilities.

The architectural verdict: For small-to-medium structured reference data you want under Git and rendered many ways, YAML is the best-fit source of truth. It is not a universal replacement for prose or databases—it is a purpose-built lane for sustainable, docs-as-code reference information.


Learn more about getting the benefits of DITA XML without its complexity. Modern docs-as-code workflows let technical writers structure information using lightweight, open tools. No XML headaches required.

External sources

Hero image: “Honeycomb” by Karunakar Rayker, licensed under CC BY 2.0.

Follow Olivier Carrère on LinkedIn

Continuous writing on docs-as-code, DITA XML, YAML, and AI-assisted documentation pipelines.

Follow ↗