markdoc-expert

Markdoc expert with deep knowledge of the Markdown-based authoring framework developed by Stripe. Covers syntax, tags, nodes, attributes, variables, functions, partials, config objects, React/HTML rendering, and TypeScript integration. Use proactively for any Markdoc development, custom tag creation, schema definition, or documentation site architecture.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "markdoc-expert" with this command: npx skills add x0y14/argent-skills/x0y14-argent-skills-markdoc-expert

Markdoc Expert

Expert guide for building documentation sites and content experiences using Markdoc, the powerful Markdown-based authoring framework developed by Stripe.

When to Use

  • Creating documentation sites with Markdoc
  • Defining custom tags and nodes
  • Configuring schema validation and attributes
  • Working with variables and functions
  • Setting up React or HTML rendering
  • Integrating Markdoc with TypeScript
  • Building reusable content with partials
  • Customizing the transformation pipeline

Overview

Markdoc is a Markdown superset based on the CommonMark specification, designed to power Stripe's public documentation. It processes content in three stages:

  1. Parse: Transform raw string into an Abstract Syntax Tree (AST)
  2. Transform: Process AST with config object to create a renderable tree
  3. Render: Generate final output (React, HTML, or custom format)

Installation and Setup

Basic Installation

npm install @markdoc/markdoc
# or
yarn add @markdoc/markdoc

With React

npm install @markdoc/markdoc react @types/react

TypeScript Configuration

Minimal tsconfig.json required:

{
  "compilerOptions": {
    "moduleResolution": "node",
    "target": "esnext",
    "esModuleInterop": true
  }
}

Basic Usage

import Markdoc from '@markdoc/markdoc';
import React from 'react';

const doc = `
# Welcome to Markdoc

{% callout type="note" %}
This is a custom callout tag.
{% /callout %}
`;

// Parse: string -> AST
const ast = Markdoc.parse(doc);

// Transform: AST -> Renderable tree
const content = Markdoc.transform(ast, config);

// Render: Renderable tree -> React elements
const rendered = Markdoc.renderers.react(content, React, { components });

Syntax Reference

Nodes (Markdown Elements)

Nodes are elements inherited from Markdown:

NodeDescriptionAttributes
documentRoot documentfrontmatter
headingHeaders (h1-h6)level
paragraphText blocks-
fenceCode blockscontent, language, process
imageImagessrc, alt, title
linkHyperlinkshref, title
listOrdered/unordered listsordered
itemList items-
blockquoteQuote blocks-
strongBold text-
emItalic text-
codeInline code-
hrHorizontal rule-
tableTables-

Tags (Custom Extensions)

Tags use {% %} syntax for custom functionality:

{% tagname attribute="value" %}
Content body
{% /tagname %}

Self-closing tags:

{% image src="/logo.svg" /%}

Annotations

Add attributes to nodes using annotations:

# Heading {% #custom-id .custom-class %}

![alt text](/image.png) {% width=500 %}

Variables

Reference variables with $ prefix:

Hello, {% $user.name %}!

{% if $flags.feature_enabled %}
New feature content here.
{% /if %}

Functions

Call functions in tags and attributes:

{% if equals($user.role, "admin") %}
Admin content
{% /if %}

{% if and($isLoggedIn, $hasPermission) %}
Protected content
{% /if %}

Debug: {% debug($variable) %}

Comments

Enable comments in tokenizer:

const tokenizer = new Markdoc.Tokenizer({ allowComments: true });
<!-- This is a comment -->

Built-in Tags

if/else - Conditional Rendering

{% if $user.loggedIn %}
Welcome back, {% $user.name %}!
{% else /%}
Please log in.
{% /if %}

Note: Markdoc treats only undefined, null, and false as falsy values (unlike JavaScript).

table - Rich Content Tables

{% table %}
* Heading 1
* Heading 2
---
* Row 1, Cell 1
* Row 1, Cell 2
---
* Row 2, Cell 1
* Row 2, Cell 2
{% /table %}

partial - Content Reuse

{% partial file="header.md" /%}

{% partial file="snippet.md" variables={sdk: "Ruby", version: 3} /%}

Built-in Functions

FunctionDescriptionExample
equalsStrict equality checkequals($a, $b)
andLogical ANDand($a, $b)
orLogical ORor($a, $b)
notLogical NOTnot($value)
defaultFallback for undefineddefault($value, "fallback")
debugJSON serialize valuedebug($variable)

Config Object

The config object controls transformation behavior:

import type { Config } from '@markdoc/markdoc';

const config: Config = {
  nodes: {
    // Custom node definitions
    heading: customHeadingSchema,
    fence: customFenceSchema,
  },
  tags: {
    // Custom tag definitions
    callout: calloutSchema,
    tabs: tabsSchema,
  },
  variables: {
    // Runtime variables
    user: { name: 'John', role: 'admin' },
    flags: { newFeature: true },
  },
  functions: {
    // Custom functions
    includes: includesFunction,
    uppercase: uppercaseFunction,
  },
  partials: {
    // Partial content mapping
    'header.md': headerAst,
  },
};

Creating Custom Tags

Tag Schema Definition

import type { Schema, Tag } from '@markdoc/markdoc';

export const callout: Schema = {
  render: 'Callout',
  children: ['paragraph', 'tag', 'list'],
  attributes: {
    type: {
      type: String,
      default: 'note',
      matches: ['note', 'warning', 'error', 'success'],
      description: 'The type of callout',
    },
    title: {
      type: String,
      required: false,
      description: 'Optional title for the callout',
    },
  },
  selfClosing: false,
  description: 'Display a callout/admonition block',
};

Schema Options Reference

OptionTypeDescription
renderstringComponent name to render
childrenstring[]Allowed child node types
attributesRecordAttribute definitions
slotsRecordNamed slot definitions
selfClosingbooleanAllow self-closing syntax
inlinebooleanInline element
transformfunctionCustom transform logic
validatefunctionCustom validation logic

Attribute Options

interface SchemaAttribute {
  type?: ValidationType | ValidationType[];  // String, Number, Boolean, Array, Object
  render?: boolean | string;                  // How to render attribute
  default?: any;                              // Default value
  required?: boolean;                         // Is required
  matches?: RegExp | string[];                // Validation pattern
  validate?(value, config, name): ValidationError[];  // Custom validation
  errorLevel?: 'debug' | 'info' | 'warning' | 'error' | 'critical';
  description?: string;                       // Documentation
}

Complete Custom Tag Example

// tags/callout.ts
import type { Schema, Node, Config, RenderableTreeNodes } from '@markdoc/markdoc';
import { Tag } from '@markdoc/markdoc';

export const callout: Schema = {
  render: 'Callout',
  children: ['paragraph', 'tag', 'list', 'item'],
  attributes: {
    type: {
      type: String,
      default: 'note',
      matches: ['note', 'warning', 'error', 'success'],
      errorLevel: 'error',
    },
    title: {
      type: String,
      required: false,
    },
  },
  transform(node: Node, config: Config): RenderableTreeNodes {
    const attributes = node.transformAttributes(config);
    const children = node.transformChildren(config);

    return new Tag(
      this.render,
      {
        ...attributes,
        className: `callout callout-${attributes.type}`,
      },
      children
    );
  },
};

// React component
interface CalloutProps {
  type: 'note' | 'warning' | 'error' | 'success';
  title?: string;
  className?: string;
  children: React.ReactNode;
}

export function Callout({ type, title, className, children }: CalloutProps) {
  const icons = {
    note: 'info',
    warning: 'alert-triangle',
    error: 'x-circle',
    success: 'check-circle',
  };

  return (
    <div className={className}>
      <span className="callout-icon">{icons[type]}</span>
      {title && <div className="callout-title">{title}</div>}
      <div className="callout-content">{children}</div>
    </div>
  );
}

Creating Custom Nodes

Customizing Built-in Nodes

import type { Schema, Node, Config } from '@markdoc/markdoc';
import { Tag } from '@markdoc/markdoc';

// Custom heading with auto-generated IDs
export const heading: Schema = {
  children: ['inline'],
  attributes: {
    id: { type: String },
    level: { type: Number, required: true },
  },
  transform(node: Node, config: Config) {
    const attributes = node.transformAttributes(config);
    const children = node.transformChildren(config);

    // Generate ID from text content
    const id = attributes.id || generateSlug(children);

    return new Tag(
      `h${attributes.level}`,
      { id, className: 'heading' },
      children
    );
  },
};

function generateSlug(children: any[]): string {
  return children
    .filter((child) => typeof child === 'string')
    .join('')
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/[^\w-]/g, '');
}

Custom Fence (Code Block)

export const fence: Schema = {
  render: 'CodeBlock',
  attributes: {
    content: { type: String, render: false },
    language: { type: String },
    process: { type: Boolean, default: true },
    title: { type: String },
    highlight: { type: String },
  },
  transform(node: Node, config: Config) {
    const attributes = node.transformAttributes(config);

    // Parse highlight ranges (e.g., "1-3,5,7-9")
    const highlightLines = parseHighlight(attributes.highlight);

    return new Tag(this.render, {
      ...attributes,
      highlightLines,
    });
  },
};

Creating Custom Functions

import type { ConfigFunction } from '@markdoc/markdoc';

// Check if array includes value
export const includes: ConfigFunction = {
  parameters: {
    0: { type: Array, required: true },
    1: { required: true },
  },
  transform(parameters) {
    const [array, value] = Object.values(parameters);
    return Array.isArray(array) ? array.includes(value) : false;
  },
};

// Convert to uppercase
export const uppercase: ConfigFunction = {
  parameters: {
    0: { type: String, required: true },
  },
  transform(parameters) {
    const value = parameters[0];
    return typeof value === 'string' ? value.toUpperCase() : value;
  },
};

// Format date
export const formatDate: ConfigFunction = {
  parameters: {
    0: { type: String, required: true },
    1: { type: String, default: 'en-US' },
  },
  transform(parameters) {
    const [dateStr, locale] = Object.values(parameters);
    try {
      return new Date(dateStr).toLocaleDateString(locale);
    } catch {
      return dateStr;
    }
  },
};

// Register in config
const config = {
  functions: {
    includes,
    uppercase,
    formatDate,
  },
};

Rendering

React Rendering

import Markdoc from '@markdoc/markdoc';
import React from 'react';
import { Callout } from './components/Callout';
import { CodeBlock } from './components/CodeBlock';
import { Tabs, Tab } from './components/Tabs';

const components = {
  Callout,
  CodeBlock,
  Tabs,
  Tab,
};

function DocumentRenderer({ content }: { content: string }) {
  const ast = Markdoc.parse(content);
  const errors = Markdoc.validate(ast, config);

  if (errors.length > 0) {
    console.error('Validation errors:', errors);
  }

  const transformed = Markdoc.transform(ast, config);
  return Markdoc.renderers.react(transformed, React, { components });
}

HTML Rendering

import Markdoc from '@markdoc/markdoc';

function renderToHTML(content: string): string {
  const ast = Markdoc.parse(content);
  const transformed = Markdoc.transform(ast, config);
  return Markdoc.renderers.html(transformed);
}

Custom Renderer

function customRenderer(node: RenderableTreeNode): string {
  if (typeof node === 'string') {
    return escapeHtml(node);
  }

  if (node === null || typeof node !== 'object') {
    return String(node);
  }

  const { name, attributes, children } = node;
  const attrs = formatAttributes(attributes);
  const content = children.map(customRenderer).join('');

  if (selfClosingTags.has(name)) {
    return `<${name}${attrs} />`;
  }

  return `<${name}${attrs}>${content}</${name}>`;
}

Validation

Built-in Validation

import Markdoc from '@markdoc/markdoc';

const ast = Markdoc.parse(content);
const errors = Markdoc.validate(ast, config);

errors.forEach((error) => {
  console.log(`[${error.level}] ${error.message}`);
  if (error.location) {
    console.log(`  at line ${error.location.start.line}`);
  }
});

Custom Validation

export const restrictedTag: Schema = {
  render: 'RestrictedContent',
  attributes: {
    role: {
      type: String,
      required: true,
      matches: ['admin', 'editor', 'viewer'],
    },
  },
  validate(node: Node, config: Config) {
    const errors: ValidationError[] = [];
    const role = node.attributes.role;

    if (config.validation?.environment === 'production' && role === 'admin') {
      errors.push({
        id: 'restricted-role',
        level: 'error',
        message: 'Admin-only content not allowed in production',
        location: node.location,
      });
    }

    return errors;
  },
};

Advanced Patterns

Table of Contents Generation

interface TocEntry {
  id: string;
  title: string;
  level: number;
}

function extractToc(ast: Node): TocEntry[] {
  const toc: TocEntry[] = [];

  function walk(node: Node) {
    if (node.type === 'heading') {
      const text = extractText(node);
      const id = generateSlug(text);
      toc.push({
        id,
        title: text,
        level: node.attributes.level,
      });
    }

    if (node.children) {
      node.children.forEach(walk);
    }
  }

  walk(ast);
  return toc;
}

Frontmatter Processing

import Markdoc from '@markdoc/markdoc';
import yaml from 'js-yaml';

function parseWithFrontmatter(content: string) {
  const ast = Markdoc.parse(content);
  const frontmatter = ast.attributes.frontmatter
    ? yaml.load(ast.attributes.frontmatter)
    : {};

  return {
    ast,
    frontmatter,
    content: Markdoc.transform(ast, {
      ...config,
      variables: {
        ...config.variables,
        frontmatter,
      },
    }),
  };
}

Dynamic Imports with Partials

import Markdoc from '@markdoc/markdoc';
import fs from 'fs/promises';
import path from 'path';

async function loadPartials(dir: string): Promise<Record<string, Node>> {
  const partials: Record<string, Node> = {};
  const files = await fs.readdir(dir);

  for (const file of files) {
    if (file.endsWith('.md')) {
      const content = await fs.readFile(path.join(dir, file), 'utf-8');
      partials[file] = Markdoc.parse(content);
    }
  }

  return partials;
}

// Usage
const partials = await loadPartials('./partials');
const config = {
  partials,
  // ... other config
};

Slots for Complex Layouts

export const card: Schema = {
  render: 'Card',
  children: ['paragraph', 'tag', 'list'],
  slots: {
    header: {
      render: 'CardHeader',
      required: true,
    },
    footer: {
      render: 'CardFooter',
      required: false,
    },
  },
  attributes: {
    variant: {
      type: String,
      default: 'default',
    },
  },
};

Usage in Markdoc:

{% card variant="elevated" %}
{% slot "header" %}
# Card Title
{% /slot %}

Main content goes here.

{% slot "footer" %}
Footer content
{% /slot %}
{% /card %}

Next.js Integration

App Router Setup

// app/docs/[...slug]/page.tsx
import Markdoc from '@markdoc/markdoc';
import React from 'react';
import fs from 'fs/promises';
import path from 'path';
import { config, components } from '@/markdoc/config';

interface Props {
  params: { slug: string[] };
}

export default async function DocPage({ params }: Props) {
  const filePath = path.join(process.cwd(), 'content', ...params.slug) + '.md';
  const content = await fs.readFile(filePath, 'utf-8');

  const ast = Markdoc.parse(content);
  const transformed = Markdoc.transform(ast, config);

  return (
    <article className="prose">
      {Markdoc.renderers.react(transformed, React, { components })}
    </article>
  );
}

export async function generateStaticParams() {
  // Generate static paths from content directory
  const contentDir = path.join(process.cwd(), 'content');
  const files = await getMarkdownFiles(contentDir);

  return files.map((file) => ({
    slug: file.replace('.md', '').split('/'),
  }));
}

Markdoc Config File

// markdoc/config.ts
import type { Config } from '@markdoc/markdoc';
import { callout } from './tags/callout';
import { tabs, tab } from './tags/tabs';
import { codeBlock } from './tags/codeBlock';
import { heading } from './nodes/heading';
import { fence } from './nodes/fence';

export const config: Config = {
  nodes: {
    heading,
    fence,
  },
  tags: {
    callout,
    tabs,
    tab,
  },
  functions: {
    // Custom functions
  },
};

export { components } from './components';

Common Patterns

Error Boundaries for Rendering

import React, { Component, ErrorBoundary } from 'react';

interface Props {
  fallback: React.ReactNode;
  children: React.ReactNode;
}

class MarkdocErrorBoundary extends Component<Props, { hasError: boolean }> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('Markdoc rendering error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

Live Preview Component

'use client';

import { useState, useMemo } from 'react';
import Markdoc from '@markdoc/markdoc';
import React from 'react';

export function MarkdocEditor({ initialContent = '' }) {
  const [content, setContent] = useState(initialContent);

  const rendered = useMemo(() => {
    try {
      const ast = Markdoc.parse(content);
      const errors = Markdoc.validate(ast, config);
      const transformed = Markdoc.transform(ast, config);

      return {
        content: Markdoc.renderers.react(transformed, React, { components }),
        errors,
      };
    } catch (error) {
      return {
        content: null,
        errors: [{ message: String(error), level: 'error' }],
      };
    }
  }, [content]);

  return (
    <div className="editor-container">
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        className="editor-input"
      />
      <div className="editor-preview">{rendered.content}</div>
      {rendered.errors.length > 0 && (
        <div className="editor-errors">
          {rendered.errors.map((err, i) => (
            <div key={i} className={`error-${err.level}`}>
              {err.message}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Best Practices

Schema Design

  1. Use descriptive names: Tag and attribute names should be self-documenting
  2. Provide defaults: Always set sensible defaults for optional attributes
  3. Validate early: Use the validate function to catch errors at parse time
  4. Document thoroughly: Use the description field for all schemas and attributes

Performance

  1. Cache parsed ASTs: Parsing is expensive, cache results when possible
  2. Use static rendering: Pre-render at build time for static content
  3. Lazy load components: Code-split large rendering components
  4. Minimize transforms: Keep transform functions lightweight

Content Organization

  1. Use partials for reuse: Extract common content into partials
  2. Leverage frontmatter: Store metadata in frontmatter, not inline
  3. Keep tags simple: Prefer composition over complex single tags
  4. Use variables for configuration: Don't hardcode values in content

TypeScript Integration

  1. Type your schemas: Use the Schema type from @markdoc/markdoc
  2. Type component props: Match component props to tag attributes
  3. Use strict mode: Enable strict TypeScript for better type safety
  4. Export types: Export types for use in consuming applications

Debugging Tips

Inspect AST

const ast = Markdoc.parse(content);
console.log(JSON.stringify(ast, null, 2));

Debug Transform Output

const transformed = Markdoc.transform(ast, config);
console.log(JSON.stringify(transformed, null, 2));

Use Debug Function

{% debug($variable) %}

Validate Schema

const errors = Markdoc.validate(ast, config);
if (errors.length > 0) {
  console.table(errors);
}

References

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Web3

helius-phantom

Build frontend Solana applications with Phantom Connect SDK and Helius infrastructure. Covers React, React Native, and browser SDK integration, transaction signing via Helius Sender, API key proxying, token gating, NFT minting, crypto payments, real-time updates, and secure frontend architecture.

Archived SourceRecently Updated
General

stripe-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
1.4K-stripe
General

react

No summary provided by upstream source.

Repository SourceNeeds Review
1.3K-lobehub
General

fix

No summary provided by upstream source.

Repository SourceNeeds Review