Features
MarkdownLive Preview
Live Preview opens your headless frontend for an item (draft or published) from the admin item form. It is not Preview as role (RBAC JSON).
Configure
- Collection (edit drawer) → Live Preview URL, or
- Settings → Content (
/settings/project/content) → Live Preview URL (default) as fallback when the collection field is empty.
Template must be http(s)://… and may include tokens:
| Token | Value |
|---|---|
{{id}} | Item id |
{{slug}} / {{collection}} | Collection slug |
{{locale}} | Optional content locale (omitted from query when empty) |
{{version}} | draft or published |
{{token}} | Short-lived HMAC preview token (~15 min) |
Example:
https://www.example.com/preview/{{collection}}/{{id}}?version={{version}}&token={{token}}
Admin UX
On an existing item, the toolbar Live Preview button (external-link icon) calls GET /collections/{collection}/items/{item}/live-preview-url?version=draft|published (requires can-show-collections) and opens the expanded URL in a new tab.
Requires a configured template (collection or project default). Version follows the Draft/Published workspace when content versioning is on.
Frontend contract
Your site should:
- Read
token(and optionalversion/locale) from the query GET {EXTERNA_API_URL}/api/v1/preview?token=…- Render
data(see response shapes below)
Auth with origin allowlist
The preview endpoint sits behind the Public API origin gate:
- No allowlist (Settings → API → Allowed origins is empty): no key required.
- With an allowlist: browser fetches from an allowlisted origin work without a key; server-side fetches (SSR, API routes, curl) have no
Originheader and must sendAuthorization: Bearer <API key>(any active key). See Client types — Origin allowlist.
Do not put API keys in the preview URL template — keep {{token}} in the URL; add the Bearer key to server fetch headers only when an allowlist is set.
Draft vs published response shape
Published version (or no draft): same shape as a Public CMS item (Public CMS API). Translatable fields are flattened to the resolved locale; ?include= expands files and relations.
Draft version (versioned collection with a draft): returns raw draft_data without flattening or expansion:
datais keyed by field name- Translatable fields are per-locale maps (
{ "en": "…", "it": "…" }); thelocaleclaim andmeta.localedo not flatten them - No file or relation expansion; no
includesupport - No extra item attributes (status, published_at, etc.)
Frontend authors must normalize draft data themselves: pick the locale from the per-field map, then resolve file/relation ids through the Public CMS API.
Locale resolution
The admin toolbar button sends version but no locale ({{locale}} expands to empty and is dropped from the URL). The preview API resolves the locale like any Public CMS request:
- Token claim
locale(if set) - Request query
?locale=(frontend can add this) - First
Accept-Languagelanguage (if enabled) - Project
default_content_locale APP_LOCALE(if enabled)- First enabled locale
An explicit ?locale= that is not enabled → 422. meta.locale in the response shows the resolved locale.
For published responses, this flattens translatable fields (value fallback: requested → fallback_content_locales → APP_FALLBACK_LOCALE if enabled → null; see Locale fallback). For draft responses, translatable fields stay as per-locale maps; the frontend should pick the locale using the same order or pass ?locale= on the preview API call.
curl -sS "http://externa-core.test/api/v1/preview?token=TOKEN" \
-H 'Accept: application/json'
# optional: -H "Authorization: Bearer ek_YOUR_SECRET"
Published response (or no draft)
{
"data": {
"id": 1,
"collection_id": 3,
"slug": "my-item",
"data": {
"title": "Hello World",
"body": "<p>Content</p>"
},
"status": "published",
"published_at": "2026-09-24T10:00:00Z",
"created_at": "2026-09-20T08:00:00Z",
"updated_at": "2026-09-24T09:00:00Z"
},
"meta": {
"preview": true,
"version": "published",
"locale": "en",
"expires_at": "2026-09-24T12:00:00Z"
}
}
Draft response (versioned collection, has draft)
{
"data": {
"id": 1,
"collection_id": 3,
"data": {
"title": { "en": "Draft Title", "it": "Titolo Bozza" },
"cover_image": 42,
"related_items": [10, 15]
},
"created_at": "2026-09-20T08:00:00Z",
"updated_at": "2026-09-24T11:30:00Z"
},
"meta": {
"preview": true,
"version": "draft",
"locale": "en",
"expires_at": "2026-09-24T12:00:00Z"
}
}
Errors
- 422
token query parameter is required.— missing?token= - 403
Invalid or expired preview token.— token malformed, expired, or bad HMAC - 403
Origin required or API key.— origin allowlist is set; server fetch needs Bearer key - 403
Origin not allowed.— browserOriginnot in allowlist - 422
The locale :locale is not enabled for this project.— explicit?locale=(or token claim) is not enabled - 404
Preview target not found.— item/collection deleted or id mismatch
Security
Tokens are HMAC-signed with APP_KEY and expire quickly. Anyone with a live token can read that item’s preview payload until expiry — treat preview URLs like temporary share links. Rotate APP_KEY only with full awareness of invalidated tokens.
Point-in-time preview (as_of)
From externa-core 1.4.0 (#222), a preview token can carry an optional as_of claim to show the item as it will look at a future moment, taking scheduled publish_at / unpublish_at into account.
- Admin: a Preview at… date-time picker next to Live Preview calls
GET /collections/{collection}/items/{item}/live-preview-url?version=…&as_of=<ISO 8601>. - Bound:
as_ofmay be at most 90 days ahead; an unparseable or later value returns 422 on the URL endpoint. - Token only:
as_oflives inside the signed token. The URL template is unchanged and needs no new placeholder.
When GET /api/v1/preview?token=… sees an as_of claim, it resolves the target item at that moment:
Situation at as_of | Response |
|---|---|
unpublish_at is on or before as_of | 404 Preview target not found. |
Item has a draft and publish_at is on or before as_of | Draft values (meta.source: "draft_overlay"), published_at = as_of |
| Item is already published | Current published values (meta.source: "published") |
| Never published and no due publish | 404 |
meta then also carries as_of (UTC, Z) and source; without the claim, meta.as_of is null.
{
"data": {
"id": 1,
"collection_id": 3,
"data": { "title": "Launch post" },
"published_at": "2026-12-01T09:00:00+00:00"
},
"meta": {
"preview": true,
"version": "published",
"locale": "en",
"expires_at": "2026-11-03T12:00:00Z",
"as_of": "2026-12-01T09:00:00Z",
"source": "draft_overlay"
}
}
Scope: only the token's target item is simulated. Related items and ?include= hydration show their current state, and the draft overlay returns flat draft_data without relation hydration.
vs Preview as role
| Live Preview | Preview as role | |
|---|---|---|
| Purpose | Visual frontend | RBAC field visibility (JSON) |
| Secret | Signed {{token}} | Admin session |
| Audience | Editors + frontend | Admins checking permissions |
Next starter
Wire ?token= into externa-next-starter or your app; see Headless starter.