Frontmatter Parameters
Format
YAML between --- markers. Must appear as the first block in the file. File encoding: UTF-8 (BOM stripped). Line endings: CRLF normalized to LF.
---
alias: my-document
title: My Document
---
Document body content...
The fields below are recognized by the CLI and server. Additional hand-authored keys are tolerated — they are left untouched on pull and simply ignored by the server on push.
That tolerance is real but it is not a guarantee about a specific key name, and the distinction matters when the CLI is upgraded. The recognized set grows; the day a key joins it, pull starts managing that key in every file that already had it. So a key is safe from pull because it is not in the table yet — not because you wrote it by hand. template is the worked example: it was an ordinary hand-authored key until the release that added the row below, and from that release on a document with no template on the server has the key removed on pull. Both halves are deliberate (see Templates below), and if you keep private metadata in frontmatter, pick names that will not collide with a product concept.
Fields
| Field | Type | Required | Default | Values |
|---|---|---|---|---|
title |
string | yes (for publish) | — | Any text |
alias |
string | yes (for publish) | — | kebab-case, unique within project |
parentAlias |
string | no | — | Alias of existing document |
parent_alias |
string | no | — | Same as parentAlias (snake_case variant) |
order |
int | no | — | 0-based position among siblings — see Order resolution below |
workflow |
string | no | published |
draft | published |
visibility |
string | no | visible |
visible | hidden |
exposure |
string | no | inherit |
inherit | private | unlisted | public |
template |
string | no | absent | Template alias from the project registry — see Templates below |
archived |
bool | no | absent | true | false — see The archive axis below |
replacedBy |
string | no | — | Alias of the successor document (same project) |
fields |
map | no | — | Custom-field values block — see Custom fields below |
If both parentAlias and parent_alias are present, parentAlias takes precedence.
Hand-authored keys other than those above are preserved on pull (see Frontmatter merge on pull below) but are ignored by the server on push.
The archive axis
archived marks a document as retired: still readable by direct link, but out of listings, out of search by default, and flagged to agents as no longer current. It is a separate axis from workflow, visibility and exposure — every combination is legal, including archived: true on a published, public, visible document. replacedBy names the successor and stands on its own: an active document may point at a future replacement without being archived.
Absent is not false
This is the one rule to get right, and it is three-valued:
| in the file | meaning on push |
|---|---|
| key absent | do not change whatever the server has |
archived: false |
un-archive |
archived: true |
archive (no cascade — see below) |
The reason is meldoc pull. A pull without --archived excludes archived documents from the stream entirely, so a document archived in the web UI keeps a local file with no archived: line. If absence meant false, the next push would un-archive it — in every repository, on the first sync after the feature shipped. Absence therefore has to mean silence, and it does.
The same rule governs replacedBy: an absent key leaves the successor alone, an explicit empty value clears it.
What is not written to frontmatter
archivedAt and archivedBy are server facts — who archived the document and when — not declarations by an author, so pull never writes them. In a file they would ride back on the first push from a repository that had sat unsynced for a month and overwrite the real archive date with the file’s.
Cascade
Archiving through the web UI or MCP cascades to descendants; archiving through a pushed file does not. In a repository every document declares itself, and a cascade applied to a tree of files would overwrite an explicit archived: false on a child that the author meant.
Changing it from the CLI
meldoc archive <alias> and meldoc unarchive <alias> change the state on the server directly and then rewrite the local file to match — see Command Reference. Archiving through those commands does cascade over descendants, because it is the same action the web UI performs; archiving through a pushed file does not, per the paragraph above. The two paths are not a contradiction: one is a command, the other is a declaration about one file.
Prune
The server refuses to delete an archived document over the CLI. Such an item comes back as skipped with reason archived, and the CLI drops it from the cache manifest so the sweep converges instead of re-sending the same delete on every run. A project can also be configured so that a CLI delete archives instead of removing; that is a server-side project setting, not a meldoc.config.yml key, precisely because a client-side setting has no effect on an un-upgraded CLI or a version pinned in somebody else’s CI. To delete an archived document from the CLI, un-archive it first.
Templates
A template is a project entity: a description, an optional body skeleton, an icon, and the set of custom fields that apply to a document using it. template: names one by alias.
---
alias: pricing-rework
title: Pricing rework
template: adr
---
Assigning a template is what makes a document’s custom fields applicable, so it is not decoration: template and fields are related, and the template is the one that decides which keys under fields: the project considers relevant to this document.
meldoc templates list and meldoc templates show <alias> browse the registry; meldoc new --template <alias> writes the key and seeds the body from the skeleton. See Command Reference.
Nothing else called “template” is one of these
The word is used four ways in this tool and only this one is a document template. Searching for “template” will otherwise give three confident wrong answers:
| Other use | What it actually is |
|---|---|
.meldoc/cache/templates/<project>.json |
the on-disk cache of this registry — the one exception, same concept |
meldoc skills templates |
skill files embedded in the binary and installed into .claude/skills/ |
meldoc init template |
the commented-out starting meldoc.config.yml |
nameTemplate in meldoc.config.yml |
a filename pattern for meldoc new |
Absent is not empty
Like archived, the key is three-valued rather than two — and it has a fourth state that looks like the third and is not:
| In the file | On the wire | Server |
|---|---|---|
| key absent | key omitted | leaves the assignment alone |
template: adr |
"adr" |
assigns adr |
template: "" |
"" |
detaches the document |
template: (bare) |
key omitted | leaves the assignment alone — plus a warning |
template: 12, true, a list, a map |
key omitted | leaves the assignment alone — plus a warning |
Absence has to mean silence for the same reason it does for archived: without that rule, the first push from any repository created before templates existed would strip the template off every document that had one assigned in the web UI.
The bare form is the trap. template: with nothing after the colon is YAML null, which is indistinguishable from an absent key by the time the value is read — so it does the one thing a user writing it never means, namely nothing. Write template: "" to detach. Both this and a non-string value are reported by push (always, including --skip-validate, and by meldoc ci) and again by meldoc validate.
An unknown alias is a warning, and the assignment survives
A template: naming an alias the project does not have does not fail the push. The file is accepted, a warning is printed, and the document’s existing template is left as it was — it does not become untyped.
Both halves are deliberate. Failing would break a whole push over one typo in one file, and a template carries no critical distinction. Silently detaching would be worse than failing: a document that had a template assigned in the web UI would lose it on the first CI run after somebody mistyped the alias, which is exactly what the absence rule above exists to prevent.
The cost is a warning nobody reads. meldoc validate reports the same thing, and in the web UI the registry’s per-template document count plus a “no template” filter find the affected documents in one query.
Offline, and the cache
meldoc validate is offline by default and stays that way. The template registry is projected to .meldoc/cache/templates/<project>.json by meldoc pull and by meldoc templates list (a config with no project alias keeps the original flat .meldoc/cache/templates.json) — and validate is strict when that cache is present and skips the check with a warning when it is not. meldoc new --template reads the same cache for the skeleton, and writes the key with an empty body rather than refusing when there is nothing to read.
Custom fields
Custom fields are project-scoped metadata defined in the Meldoc project schema (for example status, owner, tags). Their per-document values live under the fields: frontmatter namespace. The server is the source of truth for the schema; the fields: block is a local projection synced on pull and push.
---
alias: payments-service
title: Payments Service
fields:
status: in-review
owner: platform-team
reviewed: false
tags:
- billing
- backend
---
Rules:
- Keys are the stable kebab-case field keys from the project schema.
- Values are typed per the field definition: string, number, boolean, or list of strings.
- Ordering follows the field’s
orderin the project schema. Fields without an order, or with equal order, fall back to alphabetical order by key. - Empty values are omitted. An unset value, empty string, or empty list produces no key. Boolean
falseis not empty — it is written explicitly; only an absent (null) value is treated as unset.
On pull
The fields: block is rewritten from the server’s resolved values (only this block — see Frontmatter merge on pull below). If the server returns no values, the block is removed.
Applicability is not a filter here, on purpose. A document’s template decides which fields the UI shows as applicable, and a field that holds a value is shown regardless. Pull writes every non-empty value the server resolved, including one belonging to a field the current template does not cover. Frontmatter is not a UI view — it is the input to the next push — so dropping such a value would delete data on the round trip, and it would delete it precisely when someone changes a document’s template. The consequence to know: the value comes back on the next push as an explicit assignment, so a field only really goes away when it is cleared.
On push
The fields: block is sent to the server verbatim. The server maps keys onto the project’s field definitions, validates and coerces the value types, and upserts the values.
- Unknown keys (no matching project field definition) are ignored by the server — the schema is owned by the project, not the file — but reported back as non-fatal warnings printed at the end of the push. See Command Reference.
Frontmatter merge on pull
Pull does not rebuild frontmatter from scratch. Starting from the existing local frontmatter, it:
- Overwrites the server-owned keys:
title,alias,parentAlias,workflow,visibility,exposure,order(clearing those at their default value — see below),templatewhen the server supports templates, andarchived/replacedBywhen the server said anything about them. - Rewrites only the
fields:block from the server’s resolved custom-field values. - Leaves every other (hand-authored) key untouched.
For a new file there is nothing local to preserve, so the frontmatter is built from the server values alone.
template is the key that makes step 1 conditional on the server, not only on the document. When the server does not advertise the templates endpoint it reports no template for every document, so the removal branch would run over the whole repository and delete every hand-written template: in a single pull. The key is therefore not touched at all against a server that predates the feature. A newer CLI against a newer server manages it; an older CLI against either preserves it as an unrecognized key. No combination strips it.
Auto-alias derivation (first push)
When a file has no alias in frontmatter, the CLI derives one on first push:
baseNameWithoutExt(filePath)— strips.meldoc.md(or configuredext) from basename- If result is not unique across all files being pushed — prepends the parent directory name:
{dir}-{basename}
The derived alias is written to frontmatter on the file’s first successful push.
Title extraction (first push)
When a new file (not in cache) has no title in frontmatter but has an H1 heading (# …) at the start of the body:
- H1 text is extracted as
titleand added to frontmatter - H1 heading is removed from the body
- Leading blank lines after frontmatter are stripped
This transformation only applies on first push. Existing cached files are not modified.
Order resolution (push)
Priority order when determining order to send to server:
orderfield in frontmatter- Filename numeric prefix: regex
^(\d+)[\-_.]on basename (e.g.01-intro.meldoc.md→ 1, leading zeros stripped) - Cached order from previous pull/push
- Omitted — server appends to end or preserves existing position
After a push the server renumbers the children of every parent the push touched to 0 … n-1, archived ones included, keeping their relative order. So the value is a position, not a free sort key: 1, 2, 3 or 10, 20, 30 arrive as 0, 1, 2, and the files no longer match the server. Inserting a page at position k means adding 1 to every sibling at k or above in the same change. meldoc validate warns when a parent’s children are not numbered from 0 without gaps.
Default value behavior (pull)
For the server-owned keys workflow, visibility, and exposure, pull writes the value only when it differs from the default. When the server value is the default, the key is cleared from frontmatter — even if it was present locally:
workflow: published— removed (default)visibility: visible— removed (default)exposure: inherit— removed (default)archived: false— removed, never written asfalsereplacedBywith no successor — removed
Any non-default value is written. For archived there is a third case the others do not have: when the server says nothing about the axis (an older build, or a projection that does not carry it), the local key is left exactly as it is rather than cleared — clearing it would silently strip a hand-written archived: true. This applies only to these server-owned keys; hand-authored keys are preserved untouched (see Frontmatter merge on pull above).
template is the exception to the whole section: it has no default, so there is no value to compare against and nothing to omit. It is written whenever the document has a template and removed when it has none, which is what makes the file tell the truth after a round trip instead of hiding the assignment. The gate is server support rather than a default value, as described above.
Deprecated fields
Sync bookkeeping keys of the pre-alias format. Nothing reads them: the document id sent to the server comes from the cache, not from frontmatter.
| Field | Replacement |
|---|---|
id |
alias |
server_version |
stored in cache only |
last_sync |
stored in cache only |
meldoc validate fails a document that still carries one, and meldoc migrate is what removes them — it is named in validate’s own hint. Removal is that one command and no other: meldoc scan is a read, and push, pull, status, validate and most MCP tools call it, so a scan that rewrote frontmatter would be an unannounced write on every command. Pull never writes these keys back.
They are not inert for change detection, however: every frontmatter key takes part in the content hash, so removing one moves the file’s SHA256 without changing anything a push sends. meldoc migrate re-baselines the cache for that reason.
See also:
- Configuration — project configuration
- Command Reference — meldoc new, meldoc migrate