Embeds and Icons

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:

FormExampleBecomes
Quoted valuecontrols="true"the string "true", coerced to the boolean true
Bare valuewidth=800the string 800 — no spaces allowed
Bare keycontrolsthe 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".

ParameterRequired?Type / accepted valuesDefaultNotes
videoIdYesstring—Becomes src="https://www.youtube.com/embed/<videoId>"
widthNostring of pixelsnoneEmitted as a width attribute
heightNostring of pixelsnoneEmitted as a height attribute
allowFullscreenNobooleantrueEmitted as the bare allowfullscreen attribute when true
autoplayNobooleanfalseAdds autoplay; to the iframe’s allow list — it permits autoplay rather than starting it
controlsNobooleantrueAccepted and validated, but not emitted into the player URL
loopNobooleanfalseAccepted, not emitted
mutedNobooleanfalseAccepted, not emitted
thumbnailTimestampNonumber of secondsnoneAccepted, not emitted
data-idNostringnoneWritten by the editor; the block’s stable address
The Edit YouTube Video dialog with the video ID, title and size fields filled in and the playback toggles visible

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" %}
ParameterRequired?Type / accepted valuesDefaultNotes
videoIdYesstring—The Cloudflare Stream video id
assetIdYesstring—The Leed asset id; emitted as data-assetid
widthNostring of pixelsnoneAlso widens the generated poster image
heightNostring of pixelsnoneAlso heightens the generated poster image
autoplayNobooleanfalseAdded to the player URL, and adds autoplay; to the allow list
controlsNobooleantrueAdded to the player URL
loopNobooleanfalseAdded to the player URL
mutedNobooleanfalseAdded to the player URL
allowFullscreenNobooleantrueEmitted as the bare allowfullscreen attribute when true
thumbnailTimestampNonumber of secondsnoneBuilds a poster= query parameter pointing at that frame
posterNostringnoneAccepted and validated, but not emitted — the poster frame comes from thumbnailTimestamp
data-idNostringnoneWritten 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.

The Cloudflare video block settings with the poster and thumbnail-timestamp control and the playback toggles, on a real Stream asset

Audio

{% audio path="/audio/interview.mp3" assetId="aaa-111" controls %}
ParameterRequired?Type / accepted valuesDefaultNotes
pathYesstring—Becomes the <source src>
assetIdYesstring—Emitted as data-assetid
controlsNobooleantrueEmitted as the bare controls attribute when true
autoplayNobooleanfalseEmitted as the bare autoplay attribute when true
loopNobooleanfalseEmitted as the bare loop attribute when true
data-idNostringnoneWritten 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.

The audio block settings with the controls and autoplay toggles and the linked asset name

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" %}
ParameterRequired?Type / accepted valuesDefaultNotes
srcYesstring—The URL to embed
titleNostringnoneThe accessible name for the frame
widthNostring of pixelsnone
heightNostring of pixelsnone
sandboxNospace-separated tokens, so quote itnonee.g. sandbox="allow-scripts allow-same-origin"
loadingNolazy or eagernone
referrerPolicyNostringnoneEmitted lower-cased as referrerpolicy
allowFullScreenNobooleannoneNote the capital S; emitted as the bare allowfullscreen attribute
data-idNostringnoneWritten 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.

The iframe block settings with the src, title and size fields filled in and the sandbox, loading and referrer options visible

Forms

Embed a form you built in Leed by its id:

{% form formId="contact-form" %}
ParameterRequired?Type / accepted valuesDefaultNotes
formIdYesstring—The id of the form to render
any other parameterNostring or boolean—Passed through verbatim to the form partial
data-idNostringnoneWritten 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:

PrefixStyle
fa-solidSolid — the default and by far the most common
fa-regularRegular outline
fa-lightLight
fa-thinThin
fa-brandsBrand logos (GitHub, Linux, Apple, …)
fa-duotoneDuotone; combine with a weight, e.g. fa-duotone fa-solid
fa-sharpSharp; combine with a weight, e.g. fa-sharp fa-regular
fa-sharp-duotoneSharp duotone; combine with a weight
fa-chisel, fa-etch, fa-jelly, fa-notdog, fa-slab, fa-thumbprint, fa-whiteboardThe 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.

The icon picker with search results, a selected icon, and the size and color controls

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:

AuthoredAfter 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.

ESC