Markdoc
Write content with Markdoc — variables, conditionals, typed tags, and custom functions evaluated at build time.
Markdoc
Markdoc is a Markdown-based authoring format that adds variables, conditionals, loops, functions, and typed tags — all evaluated at build time. In Docyard, .mdoc files are processed before the normal remark pipeline runs: Markdoc transforms the document into Markdown with MDC component syntax, then citations, cross-references, and math are handled exactly as they are in .md and .qmd files.
File extensions
| Extension | Notes |
|---|---|
.mdoc | Recommended short form; recognized by Markdoc editor plugins. |
.markdoc | Explicit Markdoc extension used in some toolchains. |
.markdoc.md | Useful when your editor requires a .md suffix for Markdown features. |
Plain .md files are never processed by Markdoc.
Variables
Every frontmatter key is automatically available as a $-prefixed Markdoc variable.
---
title: My Page
version: 2
draft: false
---
# {% $title %}
Version {% $version %} — draft: {% $draft %}
Docyard also injects $docyard.version, populated from the first entry of the DOCYARD_VERSIONS environment variable (an empty string when the variable is not set):
Built with docyard {% $docyard.version %}.
Conditionals
Use {% if %}, {% elsif %}, and {% else %} to control which content appears at build time. The chosen branch is the only content present in the rendered HTML — discarded branches produce no output and no hidden elements.
{% if $draft %}
This page is a draft and will not ship.
{% elsif equals($docyard.version, "") %}
No version information is available.
{% else %}
Published with docyard {% $docyard.version %}.
{% /if %}
Conditionals are resolved when the site is built. A branch whose condition evaluates to false is completely absent from the rendered HTML — it does not rely on JavaScript or hidden elements.
Loops
Iterate over an array variable with {% for %}:
{% for item in $items %}
- {% $item.name %}
{% /for %}
The loop variable is scoped to the loop body. $items must be declared in markdoc.config.ts under variables.
Built-in functions
Docyard ships a standard set of functions usable in tag attributes, conditionals, and inline expressions (Markdoc itself has no built-in function library — these are registered by Docyard's Markdoc config).
| Function | Description |
|---|---|
upper(str) | Convert a string to uppercase |
lower(str) | Convert a string to lowercase |
trim(str) | Strip leading and trailing whitespace |
contains(arr, val) | True if an array or string contains val |
equals(a, b) | Strict equality check |
default(val, fallback) | Return val unless it is undefined |
and(a, b) | Logical AND |
or(a, b) | Logical OR |
not(val) | Logical NOT |
concat(a, b) | Concatenate two strings |
plus(a, b) | Add two numbers |
minus(a, b) | Subtract two numbers |
multiply(a, b) | Multiply two numbers |
divide(a, b) | Divide two numbers |
mod(a, b) | Remainder after division |
length(val) | Length of a string or array |
Uppercase title: {% upper($title) %}
Description length: {% length($description) %} characters
Fallback value: {% default($subtitle, "Untitled") %}
Built-in tags
callout
Renders a DocyardCallout admonition block.
| Attribute | Type | Default | Values |
|---|---|---|---|
type | string | note | note, tip, warning, caution, important |
title | string | — | Custom heading text |
collapse | string | — | Any non-empty value makes the callout collapsible |
{% callout type="tip" title="Quick tip" %}
Content goes here. You can use **Markdown** inside a callout.
{% /callout %}
Rendered output for each type:
A note callout — for supplementary information.
A tip callout with a custom title.
A warning callout.
Caution
A caution callout that starts collapsed. Any non-empty value for collapse enables this behavior.
An important callout.
figure
Renders a DocyardFigure component. This tag is self-closing — use /%} to close it.
| Attribute | Type | Required | Description |
|---|---|---|---|
src | string | Yes | Image URL or path |
alt | string | No | Alternative text for accessibility |
caption | string | No | Caption displayed below the image |
id | string | No | Cross-reference id (e.g. fig-my-image) |
{% figure src="/image.svg" alt="Placeholder" caption="A sample figure." id="fig-sample" /%}
See @fig-sample for the rendered output.
See Figure 1 for the rendered output above.
table
Renders a DocyardTable component wrapping a Markdown pipe table. Write the pipe table directly inside the tag body.
| Attribute | Type | Required | Description |
|---|---|---|---|
caption | string | No | Caption displayed with the table |
id | string | No | Cross-reference id (e.g. tbl-my-table) |
{% table caption="Browser support matrix." id="tbl-browsers" %}
| Browser | ES2022 |
| ------- | ------ |
| Chrome | Yes |
| Firefox | Yes |
{% /table %}
See @tbl-browsers for the matrix.
See Table 1 for the rendered output above.
steps / step
Renders a DocyardSteps container with DocyardStep children, used for numbered procedural instructions.
{% step %} attributes:
| Attribute | Type | Required | Description |
|---|---|---|---|
title | string | No | Step heading |
{% steps %}
{% step title="Install" %}
Add the dependency to your project.
{% /step %}
{% step title="Configure" %}
Edit your configuration file.
{% /step %}
{% /steps %}
Add the dependency
Install @markdoc/markdoc in your site's package.json.
Write a .mdoc file
Create a file with a .mdoc extension in your content directory and use Markdoc syntax.
Build
Run the normal docyard build. Markdoc files are processed automatically before the remark pipeline.
equation
Renders a DocyardEquation component from a LaTeX source string. This tag is self-closing — use /%} to close it.
| Attribute | Type | Required | Description |
|---|---|---|---|
src | string | Yes | LaTeX expression for the display equation |
id | string | No | Cross-reference id (e.g. eq-my-equation) |
{% equation src="E = mc^2" id="eq-einstein" /%}
See @eq-einstein for mass-energy equivalence.
See Equation 1 for the rendered output above.
Crossrefs and citations
Cross-references (@fig-, @tbl-, @eq-) and citations (@key, [@a; @b]) are handled by Docyard's remark pipeline, which runs after the Markdoc transform. Use them in any .mdoc file exactly as you would in .md or .qmd — Markdoc passes @-references through as plain text and remark resolves them normally.
{% figure src="/diagram.svg" caption="Architecture overview." id="fig-arch" /%}
As shown in @fig-arch, the pipeline is straightforward.
User config
Create markdoc.config.ts in your site's root directory to add custom tags, site-wide variables, and custom functions. The file is loaded automatically at build time. User-defined entries take precedence over built-in Docyard tags of the same name.
import type { MarkdocUserConfig } from "@docyard/theme/remark/markdoc";
export default {
tags: {
badge: {
render: "Badge",
selfClosing: true,
attributes: {
label: { type: String, required: true },
color: { type: String, default: "blue" },
},
},
},
variables: {
site: {
name: "My Docs",
repo: "https://github.com/example/my-docs",
},
},
functions: {
acronym: {
parameters: { value: { type: String } },
transform(params) {
return String(params["value"] ?? "")
.split(/\s+/)
.map((w) => w[0])
.join("")
.toUpperCase();
},
},
},
} satisfies MarkdocUserConfig;
Custom tags with a render name (e.g. Badge) must be registered as global Vue components in your site app. Docyard does not auto-register user tag renderers.
Use the custom tag in any .mdoc file:
{% badge label="New" color="green" /%}