Get AI summaries of any video or article — Sign up free
Ryan Paul - Documentation as an application: enabling interactive content tailored to the user thumbnail

Ryan Paul - Documentation as an application: enabling interactive content tailored to the user

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 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?

Contextual awareness means documentation content changes based on information known about the viewer or their account. Stripe uses geographic location to highlight payment methods and APIs relevant to a region. It also checks which features are enabled in a user’s account to selectively hide or show content. In checklists, it can read account settings to proactively toggle steps—for example, automatically checking a box when two-step authentication is enabled. Stripe has also experimented with reordering navigation to surface items a specific user is more likely to care about.

Why did Stripe’s legacy docs architecture become a bottleneck for updates and contributions?

The legacy site was a monolithic Ruby app with Sinatra routing and Herb templates. Content pages mixed UI and page logic through Ruby helper functions and one-off Ruby code embedded in templates. That made scaling difficult: every content change required a developer-style workflow (preview servers, pull requests, code review, CI, deploy), raising friction for non-engineers and reducing self-serve contributions. It also caused fragmentation—cross-page changes were brittle because interactions with many page-specific Ruby snippets were hard to predict and test.

How does Markdoc replace “docs as code” with “docs as data”?

Markdoc keeps page logic declarative inside a markdown-derived format. It adds three primitives: annotations (attributes on markdown blocks), tags (named constructs with attributes and nested content), and variables (externally provided values). Conditionals and flow control are expressed as Markdoc tags (e.g., an `if`-style tag), which makes them first-class nodes in a Markdoc AST. With a tag schema, Markdoc validates structure and attributes, enabling static analysis and transformations—such as finding variable usage or validating links—without executing arbitrary page code.

How does Markdoc integrate with React while reducing breakage risk?

Markdoc tags map to React components through a schema that defines how tag attributes become component props. That insulation means underlying React components can be refactored as long as the props produced by tag attributes still match the schema. Markdoc also renders standard markdown nodes (paragraphs, headings, ordered lists) as React components, and it supports variable resolution both at build/server time and in the browser for interactive behaviors driven by React state.

What role do Ruby “contexts” play, and why are they structured around JSON-serializable primitives?

Stripe still uses Ruby for certain backend logic that depends on Stripe-internal APIs, such as reading a user’s account settings to tailor checklists. Instead of scattering one-off Ruby snippets across pages, Stripe groups this logic into topically organized “contexts” (e.g., billing, checkout). Each context returns a hash of primitive types that can be serialized into JSON. Pages declare which context they need via YAML front matter, and the renderer passes those variables into Markdoc, creating a clean interface that improves testability and keeps page logic declarative.

What trade-off exists between static site rendering and per-user runtime customization?

Markdoc variables can be resolved on the server or client, but pre-rendering for static sites limits runtime behavior. Static builds can interpolate variables only at build time, not dynamically per user at runtime. If personalization depends on user-specific data (like tailoring documentation to a merchant’s account), the page must be rendered on the server at request time rather than fully pre-built.

Review Questions

  1. How does Markdoc’s AST and tag schema enable safer documentation tooling compared with embedding arbitrary Ruby logic in templates?
  2. What kinds of user-specific features can be implemented with build-time vs runtime variable resolution, and what determines the difference?
  3. Why does mapping Markdoc tags to React components via a schema reduce refactor breakage, and what still could cause incompatibilities?

Key Points

  1. 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. 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. 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. 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. 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. 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. 7

    Static pre-rendering supports build-time interpolation, but true per-user runtime customization requires server-side rendering for request-specific data.

Highlights

Stripe’s biggest documentation shift is structural: moving from Ruby-in-templates to a declarative Markdoc format that can be statically analyzed and rendered consistently.
Interactive personalization isn’t just UI polish—checklists can read account settings to auto-toggle steps like two-step authentication.
Markdoc’s small set of primitives (annotations, tags, variables) is designed to keep markdown readable while still expressing conditionals, flow control, and UI controls declaratively.
React integration is handled through schema-based tag-to-component mappings, aiming to prevent content breakage during component refactors.
The platform keeps some Ruby logic, but funnels it through “contexts” that return JSON-serializable primitives to avoid one-off page code sprawl.

Topics

  • Documentation Personalization
  • Markdoc Authoring
  • React Rendering
  • Docs as Data
  • Contextual Awareness

Mentioned

  • Stripe
  • Ryan Paul
  • API
  • CI
  • AST
  • JSON
  • YAML
  • CSAT