The problem: an Ast isn't renderable yet

Lesson 2's Ast describes the document's structure (this is a heading, that's a list), but it's a Markdoc-specific shape, not something an HTML renderer or React can consume directly, and it hasn't resolved anything a config would supply, no variable values, no custom tags. transform() is the stage that does both: it walks the Ast and produces a render tree, a plain, renderer-agnostic tree of Markdoc.Tag objects and raw strings.

The code, piece by piece

const renderTree = Markdoc.transform(ast, {});

transform takes the Ast plus a config object. Passing {} here means no custom tags, variables, or functions are registered, transform still runs completely, built-in Markdown constructs (headings, paragraphs, lists, bold text) resolve to their default Tag equivalents on their own, no config entry needed for heading -> h1 or list -> ul, that mapping is Markdoc's built-in default behavior (the same one Lesson 14 later shows how to override).

function describeRenderNode(node, depth = 0) {
if (typeof node === "string") { ... }
console.log(`<${node.name}>`, node.attributes);
for (const child of node.children) { ... }
}

Render tree nodes are Markdoc.Tag instances with .name, .attributes, .children, note the field is .name here, not .type like an Ast node, that's one of several small shape differences between the two trees. Render tree children can also just be plain strings (raw text), Ast nodes never appear as bare strings the way render tree children do.

Ast vs. render tree, side by side

  • Produced by: Ast comes from Markdoc.parse(source); the render tree comes from Markdoc.transform(ast, config).
  • Node identity field: Ast nodes carry .type (e.g. "heading"); render tree nodes carry .name (e.g. "h1").
  • Needs a config? Ast: no. Render tree: yes, even {} counts.
  • Can contain raw strings as children? Ast: no, always nodes. Render tree: yes.
  • What consumes it: Ast feeds transform()/validate(); the render tree feeds a renderer, renderers.html or renderers.react.

Checkpoint

  • Markdoc.transform(ast, config): the middle pipeline stage, Ast + config -> render tree. This is where tags, variables, and functions actually get resolved (starting Lesson 7).
  • Render tree shape: Markdoc.Tag objects with .name / .attributes / .children, plus raw strings as children, distinct from the Ast's .type / .attributes / .children shape.
  • Config isn't optional in spirit, even if it's optional in code: an empty {} still produces a full render tree, because built-in Markdown constructs have default Tag mappings baked in.

If anything here still feels unclear, ask before moving to Lesson 4.