The problem: a wrong attribute value shouldn't ship silently
Lesson 9's callout tag restricts type to "info" | "warning" | "error" via matches. But nothing stops someone writing {% callout type="urgent" %} in a doc, and transform() alone won't catch it, it just renders whatever attribute value it's given. Markdoc separates checking a document from rendering it: Markdoc.validate(ast, config) runs the schema checks without producing any output, exactly the kind of check you'd wire into CI before a docs deploy.
The code, piece by piece
const validErrors = Markdoc.validate(validAst, config);Markdoc.validate takes the same ast + config shape transform does, but returns an array of structured error objects instead of a render tree, an empty array means the document passed every check the tag schemas describe.
for (const error of invalidErrors) { console.log(error.error.id, error.error.level, error.error.message);}Each validation error carries .error.id (a stable, matchable code like "attribute-value-invalid"), .error.level ("error" vs. lower severities Markdoc also supports, like warnings), and .error.message (human-readable). The matches list on callout's type attribute (from Lesson 9's fixtures/config-tags.js) is exactly what produces this specific error when the value isn't "info", "warning", or "error".
Checkpoint
Markdoc.validate(ast, config): runs the same tag/attribute schemastransformuses, but only to check, no render tree is produced, an empty array means the document is valid.- Errors are structured, not just strings:
.error.id,.error.level,.error.message, meant to be matched programmatically (e.g. failing a CI check on anylevel: "error"entry), not just printed. - Validation and rendering are independent:
transform()doesn't validate anything on its own, an invalid document still renders,validate()is a separate, explicit step you choose to run.
If anything here still feels unclear, ask before moving to Lesson 11.