Exporting to Markdown
Tip
spec export markdown --target <target> --out <dir>writes every committed document as plain Markdown, with[[references]]turned into the target’s own links.
Targets
| Target | Layout | Links |
|---|---|---|
obsidian |
<owner>/<type>/<slug>.md |
[[file#Heading\|title]] |
mkdocs |
<owner>/<type>/<slug>.md and index.md |
relative .md links; headings keep {#anchor} (enable attr_list) |
docusaurus |
<owner>/<type>/<slug>.md and index.md |
relative .md links; headings keep {#anchor} |
github-wiki |
flat <slug>.md, Home.md, _Sidebar.md |
[title](page#heading) |
spec export markdown --target obsidian --out vault
spec export markdown --target mkdocs --out docs/specs --banner
What goes in
- Committed documents of every status. Superseded and rejected documents carry a status line.
- Documents in
.spec.local/only with--local. The same holds for exporter plugins: without--localthey never see a local document, and a reference to one comes as unresolved. - Frontmatter is flattened:
id,type,status,title,aliases,tags,created,updatedand onerel_<kind>list per relation. Docusaurus pages also getid,specfold_id,sidebar_labelandsidebar_position. GitHub wiki pages have no frontmatter. --bannerstarts each page with a note naming its source file.
References that lead nowhere
A Markdown link whose target is a document id, [the plan](plan:root/20261030020644), is
rewritten like a reference when it resolves and left as written when it does not; spec check
warns about both and suggests [[id|text]].
A reference that does not resolve, names a remote, or names a document left out of the export
is written as plain text and listed in the output. --fail-on unresolved writes nothing and
exits 1 instead.
Secrets check and public guards
Before anything is written or a plugin is called, the built-in secrets check and every guard
with public = true read what is leaving: the built-in tree, or the source files of the
documents a plugin receives. The secrets check runs even when no guard is configured, and also
reads the exported documents’ text attachments kept in the repository (up to [files]
scan_max_bytes, binary files skipped). An
error-level hit writes nothing and exits 1; warn-level hits are listed. --no-guard skips the
check and the guards. Matched values are never printed.
The output directory
--out must be missing, empty, or an earlier export. Each export writes
.specfold-export.json, listing its files; the next export removes those files and keeps
everything else (an .obsidian/ folder, for example). Specfold refuses a directory that holds
the repository or sits inside .spec, .spec.local or .git. Replacing an earlier export, or
sending documents past a public guard, previews first and writes only with --yes.
Files are named after the slug. When two documents would get the same file name (compared
without case), documents are taken in id order: the first keeps payment-rollout.md, the next
gets payment-rollout-2.md, then -3. The names stay the same from one export to the next
until a document with that slug is added, removed or renamed; spec mv <id> --slug <new> gives
one a name of its own.
Exporter plugins
markdown is reserved for the built-in exporter. A plugin that wants more than the compact
payload lists it in describe:
{"protocol": "specfold.plugin.v1", "name": "site", "wants": ["body", "links"],
"provides": {"exporters": ["html"]}}
export.run then also sends, per document, hash (the source_rev hash), body (the text
after the frontmatter) and links: each reference with its byte span in that body, raw,
target_id, anchor, remote, resolved_path, title, status, and reason when it does not
resolve. Bundle chapters come as chapter_bodies. Once the bodies pass --max-bytes, the rest
are sent as body_path for the plugin to read itself. Plugins that ask for nothing get the same
payload as before.