Get AI summaries of any video or article — Sign up free
Ryan Paul - Two years of Markdoc: what we’ve learned about balancing developer and author experience thumbnail

Ryan Paul - Two years of Markdoc: what we’ve learned about balancing developer and author experience

Write the Docs·
6 min read

Based on Write the Docs's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.

TL;DR

Stripe’s documentation scaling failures came from mixing code and content in templates, which increased security risk, maintenance cost, and debugging difficulty.

Briefing

Markdoc is Stripe’s solution to a scaling problem in documentation: when engineers can freely embed code inside content, documentation becomes harder to test, harder to debug, and increasingly inconsistent as more teams contribute. The fix is to keep the familiarity and readability of Markdown while adding a constrained, declarative extension layer that enforces “rails” for interactivity and page logic—so developers can innovate without turning every doc page into a mini application.

Stripe’s documentation platform is built around three experiences: user experience, developer experience, and authoring experience. On the user side, Stripe tailors docs to the reader—such as including the user’s own API test key in code samples and conditionally showing content based on location or enabled features. On the developer side, internal engineers can extend documentation using familiar technologies like React and Stripe’s design system patterns, while maintaining consistency and reliability. The authoring experience is where the biggest tension appears: technical writers need productivity and low friction, while engineers want to add ambitious capabilities that often increase complexity for writers.

Before Markdoc, Stripe’s legacy system mixed content and Ruby logic directly in templates. That approach offered flexibility, but it created a long list of operational downsides: security and technical risk from code embedded in pages, high maintenance burden, and a high barrier to entry for contributors. Even small content edits could trigger the same heavy engineering-style review and CI pipeline used for code changes. Debugging was also painful—runtime errors could surface as generic 500s with cryptic stack traces, forcing time-consuming bisecting to find the broken block. Page-specific logic and styling further undermined system-wide consistency, because one-off hacks spread across thousands of pages.

At scale, Stripe concluded that “best practices” alone weren’t enough; the platform needed to be prescriptive by default. That meant limiting open-ended styling and customization (for example, avoiding unrestricted CSS escape hatches) and providing standardized conventions for page logic and custom UI. Markdoc was designed to do exactly that: it extends Markdown with a small set of composable primitives—annotations for metadata, tags with attributes (and optional nested content), and variables for interpolation—while deliberately excluding features that tend to reintroduce complexity. Markdoc omits looping and variable assignment to discourage procedural content generation and mutable state inside documents, keeping rendering order predictable and reducing bug classes.

Technically, Markdoc is implemented in JavaScript on top of a Markdown parsing library. It tokenizes Markdown, converts it into Markdoc’s own data structure, validates it against a tag schema, and renders either to HTML or to React. Stripe uses a dynamic React renderer that builds a virtual DOM tree on the client side, while a static renderer can transpile content into JavaScript for module-like use cases. Custom tags are defined declaratively via a JavaScript schema, enabling validation and editor feedback (including a VS Code extension at Stripe).

Released as open source under the MIT license, Markdoc is positioned as a drop-in component rather than a full static-site generator: it provides the parser, renderer, and validator so teams can integrate it with their existing stack. Stripe reports adoption beyond its own documentation, including small projects, and ongoing work on tooling like a language service and browser-based editing. The central lesson is that balancing developer and author experience at scale requires constraining extensibility—turning documentation content into analyzable data without sacrificing Markdown’s simplicity.

Cornell Notes

Stripe’s documentation scaling problem came from letting content pages behave like open-ended applications—code embedded in templates made docs harder to test, debug, and keep consistent as more teams contributed. Markdoc keeps Markdown’s readability but adds a constrained, declarative extension system for tags, annotations, and variables, with validation driven by tag schemas. It deliberately leaves out looping and variable assignment to prevent mutable state and procedural content generation inside documents. Markdoc parses content into an analyzable structure and renders it to HTML or React, enabling interactivity while enforcing “rails” for styling and page logic. Released as open source under the MIT license, it’s designed as a component that integrates with existing stacks rather than a full site generator.

Why did Stripe’s legacy template approach (Ruby logic inside doc pages) become a liability as documentation grew?

Embedding Ruby logic directly in templates created multiple failure modes: code interspersed with content raised technical and security concerns; maintenance became difficult; and contributors faced a high barrier because even trivial content edits required the same code review and CI pipeline as engineering changes. Debugging was also unreliable—runtime errors could surface as generic 500s with cryptic stack traces, forcing engineers to bisect pages to locate missing block delimiters (like an unclosed end keyword). Finally, page-specific logic and styling encouraged one-off hacks that made system-wide consistency hard to guarantee across thousands of pages.

What does “prescriptive rails” mean in Stripe’s documentation platform design?

“Prescriptive rails” means the platform constrains how extensibility is done so consistency and maintainability survive at scale. Stripe argues that relying on contributors to follow best practices isn’t enough when hundreds of people touch thousands of pages. The platform therefore limits open-ended customization (e.g., unrestricted CSS escape hatches) and instead provides standardized conventions for implementing page logic and custom styling. Markdoc is the mechanism for enforcing those constraints while still letting developers add interactivity.

How does Markdoc extend Markdown without turning documents back into full programming environments?

Markdoc adds a small number of composable primitives: annotations attach key-value metadata to Markdown blocks; tags accept attributes and can optionally enclose other content; and variables allow interpolation inside content. Everything else—like conditionals and UI composition—is expressed declaratively through Markdoc tags. Crucially, Markdoc omits looping and variable assignment, discouraging procedural content generation and mutable state inside documents. That keeps rendering order deterministic and reduces bug classes tied to execution flow.

What role does validation play in Markdoc’s authoring experience?

Validation is driven by a JavaScript schema that defines each custom tag’s attributes, allowed children, and the output target (HTML tag or React component). If a writer uses an invalid attribute value or an undefined attribute, the validator blocks publishing the change. Stripe’s VS Code extension uses the same schema to highlight errors inline while typing, providing immediate feedback and reducing the likelihood of broken pages reaching production.

How does Markdoc integrate with React while keeping the document format technology-agnostic?

Markdoc content is agnostic about the rendering technology: the document format is parsed into an internal data structure, and rendering is handled separately. Markdoc supports rendering to HTML or React, and custom tags can map directly to React components. Stripe uses a dynamic React renderer that builds a React virtual DOM tree on the client side from the processed document structure. A static renderer can transpile Markdoc content into JavaScript for module-like or static-site workflows.

Why did Stripe choose Markdown as the base format for Markdoc?

Markdown’s ecosystem and familiarity create strong network effects: many tools already understand it, and both engineers and technical writers can read and write it easily. Stripe also emphasizes Markdown’s simplicity—its narrow scope and readability—arguing it’s hard to improve Markdown without sacrificing that ethos. Markdoc therefore extends Markdown with minimal surface area rather than replacing it with a templating language.

Review Questions

  1. What specific debugging and maintenance problems arise when code is mixed directly into documentation templates, and how do those problems scale with contributor count?
  2. Which Markdoc features are intentionally excluded (and why), and how do those exclusions reduce classes of runtime bugs?
  3. How does Markdoc’s schema-driven validation change the authoring workflow compared with free-form templating?

Key Points

  1. 1

    Stripe’s documentation scaling failures came from mixing code and content in templates, which increased security risk, maintenance cost, and debugging difficulty.

  2. 2

    A documentation platform needs three coordinated experiences—user, developer, and authoring—with authoring experience treated as a first-class concern.

  3. 3

    Consistency at scale requires prescriptive constraints (“rails”), not just best-practice guidance for contributors.

  4. 4

    Markdoc extends Markdown with a small set of declarative primitives (annotations, tags, variables) while excluding looping and variable assignment to avoid mutable state and procedural generation.

  5. 5

    Markdoc parses content into an analyzable structure and validates it against tag schemas, enabling editor feedback and blocking invalid publishes.

  6. 6

    Markdoc renders to HTML or React and keeps the document format decoupled from the rendering technology, supporting multiple output strategies.

  7. 7

    Markdoc is open source under the MIT license and is designed as an embeddable component (parser/renderer/validator) rather than a full static-site generator.

Highlights

Stripe’s legacy approach embedded Ruby logic inside doc pages, leading to cryptic runtime failures (like 500s) and time-consuming bisecting to find broken blocks.
Markdoc enforces consistency by making extensibility declarative and schema-validated, while deliberately omitting looping and variable assignment to keep documents stateless.
Markdoc’s React integration uses a dynamic renderer that converts processed document structures into a React virtual DOM tree on the client side.
Markdoc is released as an MIT-licensed open source component that teams can integrate with their existing stacks instead of adopting a monolithic site generator.

Topics

  • Documentation Scaling
  • Authoring Experience
  • Markdoc
  • Declarative Content
  • React Rendering

Mentioned

  • Ryan Paul