Generating Content with Directives
How to use catalog and include directives to generate and embed content in Markdown files.
mdsmith can generate content inside your Markdown
files. Two directives handle this: <?catalog?> for
building file indexes and <?include?> for embedding
content from other files. Both regenerate their body
on mdsmith fix and flag stale content on
mdsmith check.
# Building a file index
Use <?catalog?> when you need a list of files with
metadata — for example, a table of plans, a docs
index, or a rule directory. For the agent-facing
docs index, see
Progressive Disclosure for AI Agents
.
# Minimal catalog
The simplest form lists all matching files as a bullet list:
<?catalog
glob: "docs/**/*.md"
?>
- [cli.md](design/cli.md)
- [index.md](development/index.md)
<?/catalog?>Without row, header, or footer, the directive
outputs - [<basename>](<path>) per file, sorted by
path.
# Custom template
Add row (and optionally header, footer) to
control the output format. Use {field} placeholders
to pull front matter values from matched files:
<?catalog
glob: "plan/*.md"
sort: id
header: |
| ID | Title |
|----|-------|
row: "| {id} | [{title}]({filename}) |"
?>
| 74 | [Directive guide](plan/74_directive-guide.md) |
| 75 | [Single-brace placeholders](plan/75_single-brace-placeholders.md) |
<?/catalog?># Project a list-typed field into a row
{field} placeholders reach scalar front-matter
values only. To project a list — multiple authors
per file, multiple peer-linter mappings on a rule
README, multiple tags — switch the directive from
row: to row-expr:. The value is a CUE
expression. Every identifier-safe key in the
matched file’s front matter binds at top-level
scope. The expression must return a string.
<?catalog
glob: "internal/rules/MDS*/README.md"
where: 'category: "heading"'
sort: id
row-expr: |
"| \(id) | " +
strings.Join(
[for m in markdownlint {
"\(m.id) \([if m.default {"✅"}, if !m.default {"⚪"}][0]) \(m.name)"
}],
", "
) + " |"
?>
<?/catalog?>The strings standard-library package is
preimported. CUE has no infix ternary; pick
between two values with the
[if cond {a}, if !cond {b}][0] list-comprehension
idiom. row and row-expr are mutually exclusive
on one directive. columns: width constraints
apply to row only.
# Multiple glob patterns
glob accepts a YAML list to collect files from
several directories:
<?catalog
glob:
- "docs/**/*.md"
- "plan/*.md"
sort: path
row: "- [{title}]({filename})"
?># Excluding files
Prefix a glob pattern with ! to exclude matching
files. Exclusion patterns use the same glob syntax as
include patterns:
<?catalog
glob:
- "**/*.md"
- "!drafts/**"
- "!internal/notes.md"
row: "- [{title}]({filename})"
?>At least one non-negated (include) pattern is required. Excludes and includes are collected into separate lists — a file matching any exclude pattern is filtered out regardless of where the pattern appears in the list.
# Gitignore filtering
By default, files matched by .gitignore rules are
excluded from catalog results. To include gitignored
files, set gitignore: "false":
<?catalog
glob: "**/*.md"
gitignore: "false"
?>#
Filtering with where
The where parameter narrows the matched set by a CUE
expression
evaluated against each file’s parsed front matter. The
expression grammar is the same one
mdsmith list query
accepts, so a CLI query expression drops in unchanged:
<?catalog
glob: "internal/rules/MDS*/README.md"
where: 'nature: "directive"'
row: "- [{name}]({filename})"
?>
<?/catalog?>The filter runs after globbing and front matter parsing, but before sort and render. Failure modes:
- An invalid CUE expression triggers an MDS019 diagnostic on the directive’s opening line.
- A file whose front matter is missing the referenced field is excluded (no diagnostic).
- A field whose value does not satisfy the constraint (wrong type or wrong value) is excluded.
# Sorting
Format: [-][numeric:]KEY. A - prefix means
descending. Built-in keys: path, filename. Any
other key is looked up in front matter. Missing
values sort as empty string.
The optional numeric: prefix opts a field into
integer comparison. Use it for ID-shaped fields
where mixed 2-digit and 3-digit values would
otherwise collate lexicographically (100 before
52):
<?catalog
glob: "plan/*.md"
sort: numeric:id
row: "| {id} | [{title}]({filename}) |"
?>If any matched file’s value fails to parse as an
integer, the directive falls back to string
compare for all entries — no error is raised, so a
field that is sometimes numeric stays usable.
-numeric:id reverses the order.
# What happens when no files match
If empty is defined, its text is used. Otherwise
zero lines appear between the markers.
For full parameter reference, see MDS019 catalog .
# Embedding file content
Use <?include?> when you want to embed the content
of another file — for example, sharing a development
guide across README and docs, or including a config
file as a code block.
# Basic include
<?include
file: DEVELOPMENT.md
strip-frontmatter: "true"
?>
Build and test reference for contributors.
<?/include?>By default, YAML front matter is stripped. Set
strip-frontmatter: "false" to keep it.
# Code fence wrapping
Include a non-Markdown file wrapped in a fenced code block:
<?include
file: config.yml
wrap: yaml
?>
```yaml
key: value
```
<?/include?># Heading-level adjustment
heading-level takes "absolute" or an integer 1-6.
Use "absolute" when including under an existing heading,
to shift included headings so they nest correctly:
## Project
<?include
file: DEVELOPMENT.md
heading-level: "absolute"
?>
### Build
Steps here.
<?/include?>Without this parameter, included headings keep their original level, which may break heading hierarchy.
Use an integer to pin the shallowest included heading to
that level, whatever the source or parent — handy at the
document root, where "absolute" is a no-op:
<?include
file: features.md
heading-level: "2"
?>
## Pinned To Level 2
<?/include?>heading-level is mutually exclusive with heading-offset.
# Heading-offset adjustment
To shift every included heading by a fixed amount, use
heading-offset with a signed integer from -6 to 6:
<?include
file: features.md
heading-offset: "1"
?>
## Was An H1 In The Source
<?/include?>A positive value demotes headings; a negative value
promotes them. Unlike heading-level: "absolute", the
shift does not depend on a preceding heading, so it also
works at the document root — handy when a file’s visual
title is a logo or image rather than an H1.
heading-offset cannot combine with heading-level or
extract: — those parameters are mutually exclusive with
it.
# Link rewriting
Relative links in included content are automatically rewritten to resolve from the including file’s directory. Absolute URLs and protocol links are not modified.
# Cycle detection
Include chains are tracked. Circular includes and chains deeper than 10 levels are rejected:
cyclic include: a.md -> b.md -> a.md# Include a typed value
When the target file is kind-typed, extract: walks
the same JSON projection
mdsmith extract
produces and splices a single leaf. The value is a
dotted path: frontmatter.title reaches the title
field; tagline.text reaches the paragraph under
the ## Tagline heading; a section whose body is a
fenced code block is reached through its .code key.
A paragraph section. The target’s
## Tagline heading projects to {"tagline": {"text": "..."}}, so the path tagline.text lands
on the prose:
<?include
file: docs/brand/messaging.md
extract: tagline.text
?>
Markdown, fast.
<?/include?>A fenced code block. A section whose body is a
single fenced code block projects its contents under
a code key. Given a file setup.md whose
## Setup section holds an install command, the
projection is {"setup": {"code": "..."}}, and
setup.code splices the code body verbatim:
<?include
file: setup.md
extract: setup.code
?>
go install github.com/jeduden/mdsmith/cmd/mdsmith@latest
<?/include?>A frontmatter scalar. Every kind-typed file’s
projection carries a top-level frontmatter object
beside the body sections. The path is
frontmatter.<key>:
<?include
file: docs/brand/messaging.md
extract: frontmatter.title
?>
mdsmith product messaging
<?/include?>Single-content-key shortcut. A leaf object that
carries exactly one of text, code, inline,
items, or rows resolves to the inner value, so
extract: tagline and extract: tagline.text
splice the same content. An inline value is a
typed span list rather than a scalar, so it cannot
be spliced yet and surfaces as a lint error.
Multi-key wrappers (the JSON projection of a
section with several content entries) are ambiguous
and surface as a lint error with the available keys
listed.
Restrictions. extract: is scalar-only for now —
combining it with strip-frontmatter: or
heading-level: is rejected. A target file with no
resolved kind is rejected too: without a schema there
is no projection to walk. A target whose body fails
required-structure surfaces the same diagnostic
mdsmith check would raise on the target, anchored
to the include directive’s call site.
For full parameter reference, see MDS021 include .
# Placement rules
Both directives are only recognized at document root (parent must be the Document node). Maximum indent is 3 spaces.
Directives are not recognized inside list items, blockquotes, tables, fenced code blocks, or HTML blocks.
4-space indent footgun: 4 or more leading spaces turns the line into a code block. The directive is silently ignored with no error. Always use 0-3 spaces.
# Nesting
Same-type nesting is supported. When an included file
contains its own generated sections (include, catalog,
etc.), the inner markers are treated as literal content
of the outer directive. FindMarkerPairs pairs only the
outermost markers of each directive type; inner markers
are skipped. Cross-type directives between markers may
appear but are overwritten by the outer generator on
fix.
# Placeholder syntax
{field} is the only placeholder syntax. It works in
row templates and schema headings.
{filename}— relative path from the marker file.{title},{summary}— looked up in front matter.- Missing field — empty string.
- Case mismatch — “did you mean?” hint.
- Non-string scalar — formatted to string. Composite values (maps, slices) — empty string.
- Literal
{— write{{. Literal}— write}}.
CUE paths provide nested access for structured front matter values.