
# 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)` |

```sh
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
  `--local` they never see a local document, and a reference to one comes as unresolved.
- Frontmatter is flattened: `id`, `type`, `status`, `title`, `aliases`, `tags`, `created`,
  `updated` and one `rel_<kind>` list per relation. Docusaurus pages also get `id`,
  `specfold_id`, `sidebar_label` and `sidebar_position`. GitHub wiki pages have no frontmatter.
- `--banner` starts 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`:

```json
{"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.
