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 four forms, and the difference matters:

FormExampleBecomes
Double-quoted valuecontrols="true"the string "true", coerced to the boolean true
Single-quoted valuetitle='Product tour'the string Product tour, taken as written
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. 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.

ParameterRequired?Type / accepted valuesDefaultNotes
videoIdYesstring—Becomes src="https://www.youtube.com/embed/<videoId>"
titleNostringYouTube video playerThe iframe’s title, its accessible name
widthNostring of pixelsnoneEmitted as a width attribute
heightNostring of pixelsnoneEmitted as a height attribute
allowFullscreenNobooleantrueEmitted as the bare allowfullscreen attribute when true
autoplayNobooleanfalseAdds autoplay=1 to the player URL and autoplay; to the iframe’s allow list
controlsNobooleantruefalse adds controls=0 to the player URL
loopNobooleanfalsetrue adds loop=1
mutedNobooleanfalsetrue adds mute=1
startTimeNowhole secondsnoneAdds start=
endTimeNowhole secondsnoneAdds end=
showInfoNobooleantruefalse adds showinfo=0
relNobooleantruefalse adds rel=0
modestbrandingNobooleanfalsetrue adds modestbranding=1
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>. 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
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. 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
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
autosaveNobooleannonePassed to the form partial; kept by an editor save
redirectNostringnonePassed to the form partial; kept by an editor save
any other parameterNostring or boolean—Passed through verbatim to the form partial, but dropped by an editor save
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", 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:

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-graphite, fa-jelly, fa-mosaic, fa-notdog, fa-pixel, fa-slab, fa-thumbprint, fa-utility, fa-vellum, fa-whiteboardThe 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.

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

ESC