Embeds
An embed is a marker in a Page's HTML that pulls in live content when the page renders — a widget, an image or video from Media, another page's body, a menu, a list of recent posts, or a list of entries from a collection.
Words on this page
- Page — a standalone page on your site, like About or Contact. A Page can be written as raw HTML.
- Post — a dated article, written in the rich-text editor. Posts don't hold HTML.
- HTML — the code a web page is made of: tags like
<div>and<p>. - Attribute — a
name="value"setting written inside a tag, likeclass="note"orstyle="max-width:600px". - JSON — a simple text format for settings:
{"key":"value"}. - Marker — a tag carrying a
data-embed-configattribute. Tovu swaps it for real content when the page is shown. - id and slug — two ways to name one item. The id is a long code Tovu assigns; the slug is the short readable name you pick, like
contact-form. Both are shown in the admin. - Theme — the set of files that decides how your whole site looks.
- Template — a theme file that lays out one kind of page; your Page's content is poured into it.
- Partial — a piece of a theme reused on every page, like the header menu or the footer, kept in its own file.
- Widget — a small reusable block you build once in the admin: text, social links, a list of recent entries, a menu, or a contact form.
- Wrapper — the tag that carries the marker. Some embed types keep it around the content they add; some remove it.
What an embed is
- Syntax — any element with a
data-embed-configattribute holding a JSON object with a"type"key. - Single quotes — wrap the JSON in single quotes, so its own double quotes need no escaping. A double-quoted or unquoted marker is silently ignored. A literal apostrophe inside a JSON string value would end the attribute early — write the JSON escape
'instead. - Tag names — any lowercase tag name works, including
<h1>–<h6>and a hyphenated custom element like<my-card>. - Ignored spots — a marker inside a comment,
<script>, or<style>is never matched. - Posts don't take markers — only Pages can hold raw HTML. Posts store rich text, and their embeds are separate, editor-inserted widgets and images.
<div data-embed-config='{"type":"widget","slug":"contact-form"}'></div>
Where embeds work
On this site's theme, every embed type — widget, media, content, post, menu, partial, post-previews, and collection — resolves wherever you put a marker: in a Page's HTML body, a theme template, a theme marketing page (like /pricing), the home page, a theme partial (header, footer), or the themed 404 page.
A site on a different theme tier (declarative, templated, or handlebars) is narrower: a Page's HTML body only takes widget, media, content, and post markers there — menu, partial, post-previews, and collection markers don't resolve on those tiers.
Embed types
Widget
- Keys —
idorslug(id wins if both are set); each capped at 200 characters. - Output — whatever the widget itself is: text, social links, recent entries, a menu, or a contact form. There's no separate form type — a form embeds as its contact-form widget (see Forms).
This is the Footer Resources menu widget, the same one embedded on /menus:
<div data-embed-config='{"type":"widget","slug":"sample-post-resources-menu"}'></div>
Media
- Keys —
idorslug(id wins);variant, default"public". Today, onlypublicis registered, so any other value shows the placeholder. - Output — an image renders as
<img>; a stored video renders as<video controls>. There's no audio player.
<div style="max-width:720px" preload="metadata" data-embed-config='{"type":"media","slug":"sure-lets-try-it"}'></div>
preload belongs on a video, so it moves onto the rendered <video>; style stays on the wrapper.
Manage the files themselves under Media.
Content
- Keys —
idorslug(id wins if both are set). In a theme template, a marker with neither means whatever page or post is currently rendering.headerdefaults totrue— only the literalfalsehides the title and date. - Output — for a Post, or a Page written in the editor, its title, date, and body. For an HTML page, its raw body is spliced in directly, with its own embeds resolved too (up to 5 levels deep, 50 fetches). Only published targets resolve.
<main class="page-body" data-embed-config='{"type":"content"}'></main>
Post
Shows one published Post inside a Page: its title, date, and text.
- Keys —
idorslug(id wins if both are set); the Post must be published.headeris ignored, so the title always shows. - Where to find it — the Post's slug is right in the admin editor's address bar. Its id, if you'd rather use that, is in the admin API's response for that post.
<section data-embed-config='{"type":"post","slug":"small-releases-fewer-surprises"}'></section>
That marker, live:
Small Releases, Fewer Surprises
Big releases feel productive right up until something breaks and nobody can tell which of the forty changes did it. Small releases trade that drama for boredom, which is exactly what you want on a Friday afternoon.
Our rule is simple: if a change can ship on its own, it ships on its own. One fix, one deploy, one line in the changelog. When something does go wrong, the suspect list is one item long and the rollback takes a minute instead of an afternoon.
You also hear back sooner. A feature nobody wanted is much cheaper to learn about after a week than after a quarter.
Start with your next bug fix. Ship it by itself, and notice how quiet the day stays.
post is an older name. content does the same job and can also hide the title with "header":false — use it for new pages.
Menu
- Keys —
id, looked up by the menu's slug first, then by its own id.variant: "tree"renders it nested; anything else renders it flat. The reserved iddocs-current-page-sidebarresolves to the docs sidebar menu for whatever page is rendering. - Output — flat is plain links, with the link for the page you're on marked, so screen readers and styles can pick it out. Tree is nested lists with depth and current/active classes.
<nav class="docs-nav" aria-label="Documentation" data-embed-config='{"type":"menu","id":"docs-current-page-sidebar","variant":"tree"}'></nav>
See Menus for how to build the menu itself.
Partial
A partial is a piece of the theme shared by every page — the header menu or the footer. The theme lists its partials by name. This site's theme has two: nav (the header) and footer, which also comes in a shorter minimal version.
- Keys —
id, the partial's name.variantpicks another version of it, likeminimal.currenthighlights the matching link in the partial, where the theme supports it. - Output — the partial's HTML in place of the marker.
<section data-embed-config='{"type":"partial","id":"footer","variant":"minimal"}'></section>
This is the same marker, live — the site's minimal footer, pulled in here:
Post previews
- Keys —
limit, default 6, clamped between 1 and 24. - Output — one card per published post: title, link, and date.
<section class="blog-grid" data-embed-config='{"type":"post-previews","limit":6}'>No posts yet.</section>
Collection
- Keys —
id, a collection's key (find it, with a ready-made Copy embed code button, on the Collections screen). Optional:where(an object of field/value pairs, exact match only),sort(newest,oldest,updated,title, a field name, or-field; defaultnewest),limit(default 6, up to 24),layout(cardsorlist; defaultcards),columns(default 3, up to 6, cards only),fields(which fields to show; default is every one). - Output — a card or list item per published entry: its title as plain text (entries don't have their own page yet), then the fields you asked for. An id that isn't a real, user-made collection falls back the same as an unknown field name — you never get an unfiltered list by mistake.
- Your own markup — put a
<template>inside the marker to lay out each entry yourself, with{{title}},{{date}},{{url}}(empty for now), and{{fields.<name>}}placeholders. See Collections for a live templated example.
<div data-embed-config='{"type":"collection","id":"tovu_feature","layout":"cards"}'></div>
This site's own feature list, default card layout, live:
Attributes on the marker
The element carrying the marker can hold its own class, style, or any other attribute. What happens to them depends on the type:
| Type | The wrapper element | data-embed-config itself |
|---|---|---|
| widget | Bare marker, no other attributes: the whole element is replaced, wrapper included. Add any other attribute and the wrapper is kept around the widget. | Removed |
| media | Wrapper kept. Attributes that belong on <img>/<video> (autoplay, muted, loop, controls, poster, alt, width, height, and similar) move onto the rendered tag; everything else stays on the wrapper. Your value always beats the file's own stored value. | Removed |
| content, post | Wrapper and all its attributes are kept. | Removed |
| menu, post-previews | Wrapper and all its attributes are kept; only what's inside is replaced. | Kept — ships in the rendered HTML |
| collection | Wrapper and all its attributes are kept; only what's inside is replaced. | Removed once a list renders; kept if nothing matches, as part of your fallback content |
| partial | Bare marker: dropped entirely, replaced by the partial. Add any other attribute and the wrapper is kept around the partial, the same rule as widget. | Removed |
One quirk: controls="false" turns off a video's controls — that string is never emitted as-is on the rendered tag.
Fallback content and errors
- Menu, partial, post-previews, collection — if nothing resolves (no menu, no matching posts, no matching entries, an undeclared slot), the marker and whatever's written inside it stay exactly as written. That's your fallback content.
- Widget, media, content, post — there's no fallback; the marker is replaced outright. Today, an unknown id or slug, an unpublished target, a bad
variant, or going past the 50-marker limit for these four types all render a placeholder box instead. - Bad JSON, or no
"type"— the marker is skipped and left exactly as written. Nothing checks this when you save a Page. - An unrecognized
type— also left exactly as written. - Case — keep
"type"lowercase.
Caveats
- The 50-marker limit — widget, media, content, and post markers past the 50th on one page get the placeholder, not the real content.
- Ten distinct collection configs per page — repeating the exact same collection marker counts once; past 10 different ones, the extras show their fallback content.