In the editor these are the media and embed controls — Insert Asset, YouTube Video, Iframe, Form and Icon. You pick a file or paste an id into a dialog and the block appears; this page is the format those dialogs write, and what the site build makes of it.
The embed syntax
An embed is a shortcode on a line of its own:
{% type param="value" flag %}Parameters are parsed in three forms, and the difference matters:
| Form | Example | Becomes |
|---|---|---|
| Quoted value | controls="true" | the string "true", coerced to the boolean true |
| Bare value | width=800 | the string 800 — no spaces allowed |
| Bare key | controls | the boolean true |
The strings "true" and "false" are the only values coerced to booleans; everything else stays a string until the embed’s own schema converts it.
Each embed type validates its parameters. When validation fails — a required parameter missing, a value of the wrong shape — the embed renders a short placeholder <div> naming the type (Could not render Youtube video, Could not render Audio Tag, and so on) rather than breaking the rest of the page.
Only {% icon %} may appear inline inside a sentence. The other five must stand alone on their own line.
YouTube
{% youtube videoId="dQw4w9WgXcQ" width="800" height="450" %}videoId is the part after v= in a YouTube URL. The output is a div.embedded-youtube.video-player wrapping an <iframe> with a fixed allow list and title="YouTube video player".
| Parameter | Required? | Type / accepted values | Default | Notes |
|---|---|---|---|---|
videoId | Yes | string | — | Becomes src="https://www.youtube.com/embed/<videoId>" |
width | No | string of pixels | none | Emitted as a width attribute |
height | No | string of pixels | none | Emitted as a height attribute |
allowFullscreen | No | boolean | true | Emitted as the bare allowfullscreen attribute when true |
autoplay | No | boolean | false | Adds autoplay; to the iframe’s allow list — it permits autoplay rather than starting it |
controls | No | boolean | true | Accepted and validated, but not emitted into the player URL |
loop | No | boolean | false | Accepted, not emitted |
muted | No | boolean | false | Accepted, not emitted |
thumbnailTimestamp | No | number of seconds | none | Accepted, not emitted |
data-id | No | string | none | Written by the editor; the block’s stable address |
Cloudflare video
A video uploaded to your asset library is served through Cloudflare Stream and needs two ids — the Stream video id and the Leed asset id:
{% cloudflare videoId="abc123" assetId="xyz789" controls="true" thumbnailTimestamp="31.1" %}| Parameter | Required? | Type / accepted values | Default | Notes |
|---|---|---|---|---|
videoId | Yes | string | — | The Cloudflare Stream video id |
assetId | Yes | string | — | The Leed asset id; emitted as data-assetid |
width | No | string of pixels | none | Also widens the generated poster image |
height | No | string of pixels | none | Also heightens the generated poster image |
autoplay | No | boolean | false | Added to the player URL, and adds autoplay; to the allow list |
controls | No | boolean | true | Added to the player URL |
loop | No | boolean | false | Added to the player URL |
muted | No | boolean | false | Added to the player URL |
allowFullscreen | No | boolean | true | Emitted as the bare allowfullscreen attribute when true |
thumbnailTimestamp | No | number of seconds | none | Builds a poster= query parameter pointing at that frame |
poster | No | string | none | Accepted and validated, but not emitted — the poster frame comes from thumbnailTimestamp |
data-id | No | string | none | Written by the editor |
The output is a div.embedded-cf-video.video-player around an <iframe> carrying data-assetid and data-cloudflare-video="true". That second attribute is load-bearing beyond styling: the build scans each page’s HTML and injects the Cloudflare Stream SDK only into pages that contain a Cloudflare video iframe, so a page with no video ships no video script.
Audio
{% audio path="/audio/interview.mp3" assetId="aaa-111" controls %}| Parameter | Required? | Type / accepted values | Default | Notes |
|---|---|---|---|---|
path | Yes | string | — | Becomes the <source src> |
assetId | Yes | string | — | Emitted as data-assetid |
controls | No | boolean | true | Emitted as the bare controls attribute when true |
autoplay | No | boolean | false | Emitted as the bare autoplay attribute when true |
loop | No | boolean | false | Emitted as the bare loop attribute when true |
data-id | No | string | none | Written by the editor |
The result is a real <audio> element with a <source type="audio/mpeg"> and fallback text for browsers that cannot play it — not an iframe, so it inherits your own audio styling.
Iframes
An iframe embeds any external page. src is the only way to supply the URL:
{% iframe src="https://example.com/embed" title="Embedded demo" width="800" height="600" loading="lazy" %}| Parameter | Required? | Type / accepted values | Default | Notes |
|---|---|---|---|---|
src | Yes | string | — | The URL to embed |
title | No | string | none | The accessible name for the frame |
width | No | string of pixels | none | |
height | No | string of pixels | none | |
sandbox | No | space-separated tokens, so quote it | none | e.g. sandbox="allow-scripts allow-same-origin" |
loading | No | lazy or eager | none | |
referrerPolicy | No | string | none | Emitted lower-cased as referrerpolicy |
allowFullScreen | No | boolean | none | Note the capital S; emitted as the bare allowfullscreen attribute |
data-id | No | string | none | Written by the editor |
Nothing is injected into an iframe. Only the parameters you write appear on the rendered frame, so an option you omit is genuinely absent from the markup rather than present with a default.
Forms
Embed a form you built in Leed by its id:
{% form formId="contact-form" %}| Parameter | Required? | Type / accepted values | Default | Notes |
|---|---|---|---|---|
formId | Yes | string | — | The id of the form to render |
| any other parameter | No | string or boolean | — | Passed through verbatim to the form partial |
data-id | No | string | none | Written by the editor |
A form embed is the one shortcode that does not become HTML at the markdown stage. It becomes a call to the leed/form Handlebars partial carrying every parameter you gave it, and that call is resolved later in the build, after the markdown has already been turned into HTML. A boolean parameter is passed to the partial as a bare key; everything else is passed as key="value".
That is why the parameter list is open-ended: whatever the partial understands, the embed will hand it. It is also why an unrecognized parameter is silently inert rather than an error.
Icons
An icon shortcode takes positional class names, not key="value" parameters. Whatever you write between {% icon and %} becomes the class attribute of an <i> element, verbatim:
{% icon fa-brands fa-linux %}
Click {% icon fa-solid fa-gear %} to open settings.How it renders
Click to open settings.
Icons are the one embed that works inline, which is what makes them useful in running prose and in table cells.
Prefixes
A class list is a family prefix plus the icon’s own fa- name. The families shipped with Leed’s Font Awesome build are:
| Prefix | Style |
|---|---|
fa-solid | Solid — the default and by far the most common |
fa-regular | Regular outline |
fa-light | Light |
fa-thin | Thin |
fa-brands | Brand logos (GitHub, Linux, Apple, …) |
fa-duotone | Duotone; combine with a weight, e.g. fa-duotone fa-solid |
fa-sharp | Sharp; combine with a weight, e.g. fa-sharp fa-regular |
fa-sharp-duotone | Sharp duotone; combine with a weight |
fa-chisel, fa-etch, fa-jelly, fa-notdog, fa-slab, fa-thumbprint, fa-whiteboard | The newer families, each with its own weight requirements and variants (fa-jelly-fill, fa-slab-press, and so on) |
Write the full class names. Font Awesome’s short forms — fas, far, fab and the rest — are what the CMS icon picker uses internally when it talks to Font Awesome’s API; they are not markdown syntax, and a shortcode containing fas publishes an <i class="fas …"> with no font behind it. If you omit the prefix entirely, the editor’s parser fills in fa-solid, but the published page gets exactly what you wrote, so write the prefix.
Icon names are kebab-case and always carry the fa- prefix: fa-circle-check, fa-arrow-right, fa-user. Browse fontawesome.com/iconsto find them.
Size and color
Both are class names in the same positional list.
Size is a Font Awesome size class: fa-2xs, fa-xs, fa-sm, fa-lg, fa-xl, fa-2xl, or fa-1x through fa-10x.
Color is a Tailwind text-color class, optionally paired with a dark: variant:
{% icon fa-solid fa-circle-check fa-2x text-emerald-600 dark:text-emerald-400 %}Rendered:
The text-… and dark:text-… forms are what the editor’s color control writes, and they are the only color form that survives a round trip through the editor.
What a save does to an embed
Everything on this page is written by a dialog in the CMS, and the editor re-serializes each block from the attributes it models. Anything outside that model is dropped the first time the page is saved:
| Authored | After a save |
|---|---|
{% audio path="/a.mp3" assetId="x" controls autoplay %} | {% audio path="/a.mp3" assetId="x" autoplay="true" data-id="…" %} — bare flags become explicit values, and a flag left at its default disappears |
{% form formId="f" autosave="true" %} | {% form formId="f" data-id="…" %} — extra parameters are gone |
{% cloudflare videoId="v" assetId="a" thumbnailTimestamp="31.1" %} | {% cloudflare videoId="v" assetId="a" autoplay="false" muted="false" loop="false" controls="true" allowFullscreen="true" data-id="…" %} — defaults are written out, thumbnailTimestamp is dropped |
{% icon fa-solid fa-home fa-2x %} | {% icon fa-solid fa-home 2x %} — the size class stops being a size class |
Parameter order also changes: data-id is written last on an embed and first on an ordinary block. The complete list of what a round trip changes is on Fidelity and Unsupported Syntax.
Page types can switch iframes and icons off — on blogs only
iframe and icons are two of the nine editorFormattingOptions a page type carries, and both default to off. Those nine flags gate posts-type page types only: on a documentation or api page type the toolbar shows every control, the server-side check on AI and MCP writes returns without checking anything, and Settings does not render the toggle block at all. There is no switch to find.
Forms are gated on a different axis entirely — the Form control is shown on every page type except a posts type, which is the reverse of the pattern above. YouTube, Cloudflare video and audio embeds are never gated.
What each page type allows is on What Each Page Type Lets You Format.
Where the ids come from
You will not type an assetId or a Stream videoId by hand. Both come out of the asset library when you insert the file — see Video, Audio and Document Assets for the upload side and Inserting Media and Embeds for the CMS flow that produces the shortcodes on this page. The same partials and helpers these embeds resolve to are also callable directly from a layout, which How Templates Work covers.