Docs Writing
Directive syntax
The [!name key=value] grammar — how markers are matched, how attributes parse, and the three kinds of directive.
A directive is a marker written on a line of its own in the Ghost editor:
[!mood id=482 theme=dark]
Ghost wraps that line in a paragraph, and the build matches paragraphs whose entire content is a marker. That “entire” is not a technicality — it is the whole reason the syntax is safe to use in prose.
Matching
The marker must be alone in its paragraph. This is matched:
<p>[!youtube id=dQw4w9WgXcQ]</p>
None of these are:
<p>Try writing [!youtube id=dQw4w9WgXcQ] on its own line.</p>
<p><strong>[!youtube id=dQw4w9WgXcQ]</strong></p>
So you can write about the syntax in a post without triggering it, as long as the marker shares its paragraph with something. Inside a code block you do not even need that — code is masked before directives run.
In the Ghost editor, put the marker in its own paragraph with a blank line above and below it. If it renders as normal text on the published page, it was almost certainly wrapped in formatting or joined to the paragraph before it.
Names
[!name]
[!name attributes]
A name starts with a lowercase letter and continues with lowercase letters,
digits, and hyphens: [a-z][a-z0-9-]*. Matching is case-insensitive, so
[!Mood id=1] works, but write it lowercase.
An unrecognised name is left in the document and logged as unknown-directive.
That is deliberate — a silent deletion of something you typed is worse than a
visible [!moood id=1] on the page telling you what you got wrong.
Attributes
Attributes are key=value pairs separated by whitespace. Values may be bare,
double-quoted, or single-quoted:
[!authors ai=anthropic/claude-opus-4 note="drafted the migration table"]
The rules:
- Keys follow the same shape as names — lowercase, digits, hyphens.
- Bare values run to the next whitespace. Quote anything containing a space.
- A quoted value may contain the other quote character but not its own.
- A repeated key is an error, not a last-one-wins.
- An attribute the directive does not declare is an error.
key=with nothing after it is a legal empty string.
Anything that fails to parse produces invalid-directive-attributes and the
marker is dropped from the output.
The three kinds
Which kind a directive is determines when it runs and what it can do.
Block
Matched one paragraph at a time; replaces that paragraph with HTML. Blocks may be
async, because several of them fetch metadata — the YouTube title, the Apple
Music artwork. mood, music, and youtube are blocks.
Meta
Matched the same way, but produces no HTML. The paragraph is removed and the
parsed attributes are collected into result.meta under the directive name, for
the page template to use. authors is the only meta directive: the credit
belongs in the post footer, not where you happened to type it.
Because a meta marker never renders, it would otherwise leak into anything derived from the raw source — the excerpt, the plaintext, the Markdown output. Those are scrubbed of standalone meta markers separately, with code fences respected.
Inline
Handed the entire document instead of a single paragraph, because what these
match is not a marker at all. poem looks at the shape of blockquotes;
footnotes looks for [^label] anywhere in the text. Inline directives run
after all the block directives, so they see the finished document.
Reference
| Directive | Kind | Attributes |
|---|---|---|
[!mood] |
block | id, theme, density |
[!music] |
block | id |
[!youtube] |
block | id, start |
[!authors] |
meta | ai, note |
[!poem] |
inline | (modifiers, not attributes) |
[^label] |
inline | (no attributes) |
Adding one
Directives are registered in one frozen array in
src/features/posts/server/directives/index.ts:
export const postDirectiveRegistry: readonly Directive[] = Object.freeze([
poemDirective,
footnotesDirective,
moodDirective,
musicDirective,
authorsDirective,
youtubeDirective,
]);
A new one is a file in that directory exporting an object matching
BlockDirective, MetaDirective, or InlineDirective, added to the array.
Order matters only among inline directives, which run in registry order. Use
parseKeyValueAttributes and rejectUnsupportedAttributes from ./attributes
rather than writing a parser — throwing DirectiveAttributeError is what turns a
bad marker into a warning instead of a crash.