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 four forms, and the difference matters:
| Form | Example | Becomes |
|---|---|---|
| Double-quoted value | controls="true" | the string "true", coerced to the boolean true |
| Single-quoted value | title='Product tour' | the string Product tour, taken as written |
| 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. In a double-quoted value ", &, { and } stand for ", &, { and }, which is how the editor writes a value holding a quote or a brace. A key is matched whole, so data-src is never read as src.
Each embed type validates its parameters. When validation fails — a required parameter missing, a value of the wrong shape, or a parameter list that cannot be read at all, such as an unterminated quote — 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. A shortcode whose type is not one of the six on this page is not an embed at all and is left on the page as literal text.
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, titled with your title parameter or, when there is none, YouTube video player.
| Parameter | Required? | Type / accepted values | Default | Notes |
|---|---|---|---|---|
videoId | Yes | string | — | Becomes src="https://www.youtube.com/embed/<videoId>" |
title | No | string | YouTube video player | The iframe’s title, its accessible name |
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=1 to the player URL and autoplay; to the iframe’s allow list |
controls | No | boolean | true | false adds controls=0 to the player URL |
loop | No | boolean | false | true adds loop=1 |
muted | No | boolean | false | true adds mute=1 |
startTime | No | whole seconds | none | Adds start= |
endTime | No | whole seconds | none | Adds end= |
showInfo | No | boolean | true | false adds showinfo=0 |
rel | No | boolean | true | false adds rel=0 |
modestbranding | No | boolean | false | true adds modestbranding=1 |
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>. A javascript:, vbscript:, data: or file: path is refused: the site renders an empty src, and the editor drops the path on import and on paste |
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. A javascript:, vbscript:, data: or file: URL is refused: the site renders an empty src, the editor drops it on import and on paste, and the iframe dialog will not accept one |
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 |
autosave | No | boolean | none | Passed to the form partial; kept by an editor save |
redirect | No | string | none | Passed to the form partial; kept by an editor save |
| any other parameter | No | string or boolean | — | Passed through verbatim to the form partial, but dropped by an editor save |
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", with its backslashes unchanged and a trailing backslash dropped so it cannot escape the closing quote.
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. Sites ship Font Awesome Pro 7.3.1, and the families in that 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-graphite, fa-jelly, fa-mosaic, fa-notdog, fa-pixel, fa-slab, fa-thumbprint, fa-utility, fa-vellum, fa-whiteboard | The newer families, each with its own weight requirements and variants (fa-jelly-fill, fa-jelly-duo, fa-notdog-duo, fa-slab-press, fa-slab-duo, fa-slab-press-duo, fa-utility-duo, fa-utility-fill) |
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.
Animation is a class too. Besides the long-standing fa-spin, fa-spin-pulse, fa-beat, fa-beat-fade, fa-bounce, fa-fade, fa-flip and fa-shake, the 7.3.1 build adds fa-flip-360, fa-spin-snap (with fa-spin-snap-4 and fa-spin-snap-8), fa-buzz, fa-wag, fa-float, fa-swing and fa-jello. The upgrade also retimed four existing animations — fa-fade and fa-beat-fade now ease in and out, fa-flip runs for 1.5s rather than 1s, and fa-shake runs for 0.75s rather than 1s — so an animated icon may move a little differently than it did before.
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" theme="dark" %} | {% form formId="f" autosave data-id="…" %} — autosave and redirect are kept; any other parameter is gone |
{% cloudflare videoId="v" assetId="a" width="800" thumbnailTimestamp="31.1" %} | {% cloudflare videoId="v" assetId="a" thumbnailTimestamp="31.1" data-id="…" %} — thumbnailTimestamp is kept, width and height are not |
{% youtube videoId="v" width="800" controls="true" title="Tour" %} | {% youtube videoId="v" title="Tour" width="800" data-id="…" %} — an option left at its default disappears |
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.