Ryan Paul - Documentation as an application: enabling interactive content tailored to the user
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.
Stripe’s documentation strategy treats content like an application by dynamically adapting to user context (locale, account features, authentication state) and adding interactive flows such as checklists and integration builders.
Briefing
Stripe’s documentation is shifting from static, page-by-page manuals into an application-like experience that adapts to each user—while also fixing the engineering bottlenecks that made that kind of interactivity hard to build and maintain. The core idea is “contextual awareness”: content can change based on who’s viewing it (location, enabled account features, authentication status) and based on what the user is doing (progress through checklists, step-by-step integration flows). The payoff is documentation that feels tailored and interactive rather than generic, but it comes with real costs—more complexity, more engineering overhead, and a higher bar for contributors.
To make that possible, Stripe built a next-generation documentation platform that treats documentation as data instead of code. In the legacy setup, the docs site was effectively a monolithic Ruby application using Sinatra routing and Herb templates. UI elements inside content—tabs, code snippet boxes, and other interactive widgets—were implemented as Ruby helper functions, and page-specific logic often lived as one-off Ruby inside templates. That approach was powerful, but it scaled poorly: every content change followed a developer workflow (preview servers, pull requests, code review, CI, deploy), which created friction for technical writers and reduced self-serve contributions. It also fragmented logic across hundreds of pages, making cross-cutting changes brittle and hard to test.
The modernization effort pairs a React-based front end with a new authoring format called Markdoc. Markdoc is markdown-based but extended with a small set of composable primitives—annotations, tags, and variables—so page logic can be expressed declaratively rather than as arbitrary Ruby. Conditionals, flow control, partials, and UI controls are represented as first-class Markdoc constructs, which enables static analysis through an abstract syntax tree (AST) and schema validation for custom tags. That “docs as data” approach allows Markdoc documents to be parsed and serialized (for example into JSON) at build time, supports tooling across languages, and reduces the need to embed complex code throughout content.
On the rendering side, Markdoc supports multiple renderers, including plain HTML and a React renderer. Custom Markdoc tags map to React components with defined schemas, insulating the content layer from component refactors as long as props match the tag attributes. Variables can be resolved at build time/server time or in the browser, enabling interactive behaviors like a counter button driven by React state. Stripe also routes some remaining logic through Ruby services using “contexts”—topically grouped modules that return JSON-serializable primitive variables. Pages declare which context they need via YAML front matter, and the renderer consumes those variables to toggle checklist steps (e.g., auto-checking two-step authentication when enabled) or tailor payment methods and API examples.
Stripe’s approach also reflects trade-offs and constraints: static pre-rendering supports build-time data, while per-user runtime customization requires server-side rendering. Internally, Markdoc is still early in adoption and currently proprietary, though Stripe is considering open-sourcing later. Metrics are also still forming; early signals include customer satisfaction feedback and engagement patterns, alongside efforts to reduce “tab overload” by improving how users navigate and find relevant content.
Overall, the platform aims to deliver application-grade documentation—interactive, personalized, and consistent—without returning to the brittle, code-heavy authoring model that slowed updates and limited who could contribute.
Cornell Notes
Stripe is turning documentation into an application by making content dynamically adapt to each user’s context (account settings, locale, enabled features) and by adding interactive UI patterns like checklists, wizards, and integration builders. The technical shift is away from embedding one-off Ruby logic inside Herb templates and toward a declarative “docs as data” model using Markdoc. Markdoc extends markdown with annotations, tags, and variables so conditionals and UI controls are represented in a parseable AST with schema validation, enabling static analysis and safer tooling. A React front end renders Markdoc via custom tag-to-component mappings, while remaining backend logic is centralized in Ruby “contexts” that return JSON-serializable primitives. The result is richer interactivity with less brittle page-specific code, though runtime personalization still requires server-side rendering.
What does “contextual awareness” mean in Stripe’s documentation, and what are concrete examples?
Why did Stripe’s legacy docs architecture become a bottleneck for updates and contributions?
How does Markdoc replace “docs as code” with “docs as data”?
How does Markdoc integrate with React while reducing breakage risk?
What role do Ruby “contexts” play, and why are they structured around JSON-serializable primitives?
What trade-off exists between static site rendering and per-user runtime customization?
Review Questions
- How does Markdoc’s AST and tag schema enable safer documentation tooling compared with embedding arbitrary Ruby logic in templates?
- What kinds of user-specific features can be implemented with build-time vs runtime variable resolution, and what determines the difference?
- Why does mapping Markdoc tags to React components via a schema reduce refactor breakage, and what still could cause incompatibilities?
Key Points
- 1
Stripe’s documentation strategy treats content like an application by dynamically adapting to user context (locale, account features, authentication state) and adding interactive flows such as checklists and integration builders.
- 2
Legacy docs relied on monolithic Ruby (Sinatra + Herb) with page-specific Ruby logic embedded in templates, which created high friction for non-engineers and made cross-page changes brittle.
- 3
Markdoc replaces “docs as code” with “docs as data” by extending markdown using annotations, tags, and variables so logic is declarative and machine-parseable.
- 4
Markdoc’s AST and tag schemas enable static analysis, validation, and tooling (e.g., finding variable usage or checking links) without executing arbitrary page logic.
- 5
A React front end renders Markdoc through a tag-to-component mapping that uses schemas to reduce refactor breakage, while still allowing interactive client-side behaviors.
- 6
Backend personalization is centralized in Ruby “contexts” that return JSON-serializable primitives, keeping page logic clean and testable while integrating with Stripe-internal APIs.
- 7
Static pre-rendering supports build-time interpolation, but true per-user runtime customization requires server-side rendering for request-specific data.