# How a post is built

The path from a Ghost draft to rendered HTML, and the four things that happen to your markup on the way.

Posts are written in Ghost. Ghost is the editor and the database; it never serves
a reader. At build time this site pulls `post.html` — Ghost's own rendering of
the Koenig editor — and runs it through one transform before it reaches a page.

That transform is where everything on the rest of these pages happens.

## The pipeline

`transformPostDirectives(html, context)` in
`src/features/posts/server/directives/index.ts` runs four passes, in this order.

**1. Embed enrichment.** Bare `<iframe>` embeds and shortcodes are rewritten
into this site's own components: Apple Music iframes become the listening card,
YouTube iframes become the click-to-load facade, `[mood:123]` becomes a mood
embed. This pass only runs for rich output targets — see below.

**2. Masking.** Everything inside `<code>`, `<pre>`, `<script>` and `<style>` is
lifted out and replaced with a private-use-area token. Nothing in the following
passes can see it, so a fenced code block showing `[!poem]` stays a code block.
The tokens are put back at the very end, and if a directive somehow mangled one
the transform throws rather than shipping broken markup.

**3. Block and meta directives.** Any paragraph whose entire content is a
`[!name key=value]` marker is matched. A *block* directive replaces the paragraph
with rendered HTML; a *meta* directive is removed from the body and its
attributes handed to the page instead.

**4. Inline directives.** These get the whole document rather than one paragraph,
because what they match is not a marker. `poem` reshapes blockquotes, `footnotes`
rewrites `[^label]` references and collects the definitions into a list at the
foot of the post.

Anything still looking like `[!something]` after all that is reported as an
unknown directive.

## Output targets

The same post is rendered for more than one destination, and a YouTube player is
useless in an RSS reader. Every directive is handed the target and decides what
to emit.

| Target | Used for | Embeds |
| --- | --- | --- |
| `web` | The `/blog/[slug]` page | Full interactive components |
| `preview` | `/dev/blog/[id]` draft preview | Full interactive components |
| `rss` | `/blog/rss.xml` | Plain links, absolute URLs |
| `agent-markdown` | `Accept: text/markdown` responses | Plain links, absolute URLs |
| `og` | Open Graph image text | Text only |
| `excerpt` | List and card summaries | Text only |

`web` and `preview` are the *rich* targets. Everywhere else, a `[!music]`
directive degrades to "Listen on Apple Music" and a footnote reference becomes an
inline parenthetical rather than a superscript pointing at an anchor that the
consumer cannot follow.

## Warnings, not failures

A malformed directive does not break the build. It is skipped, the marker is
dropped, and a warning is printed:

```
[blog-directive:invalid-directive-attributes] Invalid "mood" directive in post "my-post": attribute "id" must be a positive integer.
```

Watch the build output when you publish. The codes you will see are
`unknown-directive`, `invalid-directive-attributes`, `invalid-directive-content`,
and the four footnote codes.

There is exactly one exception. An unrecognised model in `[!authors]` throws and
stops the build, because a typo there would silently drop an authorship credit
off a published post — which is the one failure mode worth being loud about.
