Builds the sandboxed-iframe document and the Overview details for the runtime
preview modal (MishkaCmsCoreWeb.Live.Admin.Components.RuntimePreviewModal),
for any module-backed runtime resource.
render_document/3 renders a record to a standalone HTML document (doctype + a
<head> that inlines the record's site CSS bundle from
MishkaCmsCore.Runtime.States.AssetsState, so the preview looks like production)
and returns {document_html, note} where note is nil on a clean render or a
short explanation otherwise. The document is bound to a sandboxed <iframe srcdoc>,
never with allow-same-origin.
A COMPONENT generates its own CSS; everything else links a bundle
A page, a layout and an error page each belong to one site, so that site's compiled bundle is exactly right for them. A component belongs to no site — it is installed once and used by whoever uses it — so no bundle is right, and picking the largest one meant a component previewed correctly only if some site already rendered it. Backwards, for a browser whose job is showing an administrator the components they have NOT used yet.
So a Component's document carries the same in-browser Tailwind the page builder's canvas runs,
plus the @theme blocks its tokens come from, and generates its rules from its own markup. Its
frame is the one that gets allow-scripts — never with allow-same-origin — and the engine is
inlined rather than linked, because an opaque origin cannot fetch a subresource. Nothing is
compiled on the server and no site's stylesheet changes, which is the point: a component earns a
place in a site's CSS by being used in a page, not by being looked at.
Renderability differs by resource:
- Page / Component — render the
:templateto HTML. - Layout — render
:templatewith placeholderinner_content(layouts wrap other content, so this is an approximation). - ErrorPage — render a chosen status's
:template(and its optional per-status:layout) wrapped in the record'sroot_layout_template, compiled with the samePhoenix.HTML.Enginethe runtime serves it with, so the preview matches production.
Anything that needs live request data degrades to a styled note; the Code tab still shows the real source.
The chrome styles itself with a <style>, never a style attribute
A Content-Security-Policy has no nonce to offer an attribute, so the only way to close
style-src-attr 'unsafe-inline' is for the attribute not to exist. A Tailwind class is no help
here either: the srcdoc document is served no stylesheet but the bundle inlined into its own
<head>, and the inner_html/4 fragments land in MishkaCmsWeb.DraftPreviewLive, whose only
stylesheet is the per-draft bundle compiled from the draft's own template. A class written in
THIS module is seen by no Tailwind build.
So the chrome — the unavailable-preview note, the layout placeholder, the stacked examples, the
short hints — is written as mishka-preview-* classes whose rules travel with them:
document/4appendschrome_css/0to the<style>it already writes;MishkaCmsWeb.DraftPreviewLiverenderschrome_css/0into its own NONCED<style>, which is what the enforced PUBLIC policy needs;note_body/2additionally carries a<style>of its own, because a note can end up inside an author'sroot_layout_template— or be the whole document, when that layout is what failed to compile — where neither of the other two homes exists.
Every <style> written here takes the CSP nonce
A srcdoc document inherits the EMBEDDER's policy, and style-src-elem grants 'self' plus a
nonce but not 'unsafe-inline' — see MishkaCmsWeb.Runtime.Plugs.SecurityHeaders. Without a
nonce the preview's whole stylesheet is refused the moment the ADMIN audience moves from
:report_only to :enforce, and the modal shows an unstyled document rather than an error.
So the nonce is the last argument of every entry point — render_document/3,
render_error_document/3, render_example/3, inner_html/4 — and is carried down to each
<style> on the way, note_body/2 included: a degraded fragment's note lands in
MishkaCmsWeb.DraftPreviewLive, where the PUBLIC policy is already enforced.
MishkaCmsCoreWeb.Live.Admin.Dashboard.Runtime.PreviewHelpers reads socket.assigns[:csp_nonce]
for the modal and that LiveView its own; both values come from the session rather than the
request, which is what lets a re-render keep them — MishkaCmsCore.Runtime.CspNonce says why.
The argument is optional and defaults to nil, which emits NO attribute — never the string
"nil", which names nothing and would block the very stylesheet it was meant to allow. Its value
is HTML-escaped like the title beside it, so a quote cannot close the attribute; in practice it
is Base.encode64/1 output, whose alphabet escaping leaves untouched.
Summary
Functions
The rules behind the mishka-preview-* classes this module's chrome is written in.
Overview detail rows ({label, value}) for record of resource. Resource-specific
fields (section, priority, layout) appear only when present.
Pixel width for a device key ("desktop" / "tablet" / "mobile").
The statuses present in an ErrorPage record's error_pages collection (in order) — HTTP codes,
and :offline for the page an offline site shows.
Human file size of the record's source.
Render record's template to just the inner body HTML (no doctype/head), for embedding inside the
real LiveView preview (MishkaCmsWeb.DraftPreviewLive) — where the per-site JS bundle and the
temporary draft CSS are linked by the layout. Returns the HTML string (or a styled note).
What a layout is previewed around: its example, or a placeholder.
The "Preview unavailable" note for message, with its own <style> marked with nonce.
Parse the Code-tab's serialized error pages (<!-- 404 -->-delimited blocks, <!-- offline -->
for the offline page, as produced by source/1) back into the error_pages collection
([%{status, template}]). Used when saving an ErrorPageDraft from the editor.
Assemble the preview document for record of resource, marking its <style> with nonce.
Render ONE error code's COMPLETE page exactly like the runtime serves it: the status's :template
(then its optional per-status :layout) wrapped in the ErrorPage's root_layout_template, or the
default document when none is set. Compiled with Phoenix.HTML.Engine — the same engine
MishkaCmsCore.Runtime.Compilers.ErrorCompiler uses — so a server-rendered root layout's
attribute interpolation (content="<%= ... %>") renders instead of raising. Returns {html, note}.
Render one of a Component's examples strings (a self-contained <.component … site="Global" …/>
invocation) to a standalone document for the Examples tab. Uses the same TagEngine render + inlined
site CSS as the Component preview, so <.component> resolves to the real compiled module.
Returns {document_html, note}.
The record's raw source string for the Code tab.
The full CSS of the stylesheet rows for site_id and the global ones.
The @theme blocks a browser-side Tailwind generator needs, for site_id and the global rows.
Functions
@spec chrome_css() :: String.t()
The rules behind the mishka-preview-* classes this module's chrome is written in.
A host renders these into a <style> element it owns, so the chrome needs no style attribute.
document/4 does it for the sandboxed preview document; MishkaCmsWeb.DraftPreviewLive does it
for the shareable /_preview/:resource/:token page, where the element takes the CSP nonce.
Overview detail rows ({label, value}) for record of resource. Resource-specific
fields (section, priority, layout) appear only when present.
Pixel width for a device key ("desktop" / "tablet" / "mobile").
The statuses present in an ErrorPage record's error_pages collection (in order) — HTTP codes,
and :offline for the page an offline site shows.
Human file size of the record's source.
Render record's template to just the inner body HTML (no doctype/head), for embedding inside the
real LiveView preview (MishkaCmsWeb.DraftPreviewLive) — where the per-site JS bundle and the
temporary draft CSS are linked by the layout. Returns the HTML string (or a styled note).
For an ErrorPage, status selects which code (404/500/… or :offline) to render; nil falls back
to the status in request's :query_params, then to the first entry.
A fragment that degrades to a note carries a <style> of its own, so nonce reaches that too.
For a Page, request is the address the preview was opened at: :path is handed to the template
as @page.path and :query_params as @query_params, so a page that links to itself —
base={@page.path} with params={@query_params} — pages inside the preview instead of leaving
it. Without them the template sees the record's own path and an empty query.
What a layout is previewed around: its example, or a placeholder.
The "Preview unavailable" note for message, with its own <style> marked with nonce.
Parse the Code-tab's serialized error pages (<!-- 404 -->-delimited blocks, <!-- offline -->
for the offline page, as produced by source/1) back into the error_pages collection
([%{status, template}]). Used when saving an ErrorPageDraft from the editor.
Assemble the preview document for record of resource, marking its <style> with nonce.
Returns {document_html, note} — note is nil on a clean render, otherwise a
short message (the document still renders, showing the message/example inside).
Render ONE error code's COMPLETE page exactly like the runtime serves it: the status's :template
(then its optional per-status :layout) wrapped in the ErrorPage's root_layout_template, or the
default document when none is set. Compiled with Phoenix.HTML.Engine — the same engine
MishkaCmsCore.Runtime.Compilers.ErrorCompiler uses — so a server-rendered root layout's
attribute interpolation (content="<%= ... %>") renders instead of raising. Returns {html, note}.
Render one of a Component's examples strings (a self-contained <.component … site="Global" …/>
invocation) to a standalone document for the Examples tab. Uses the same TagEngine render + inlined
site CSS as the Component preview, so <.component> resolves to the real compiled module.
Returns {document_html, note}.
The record's raw source string for the Code tab.
The full CSS of the stylesheet rows for site_id and the global ones.
BOTH HALVES ARE NEEDED, and only together. A kit's @theme block is mostly ALIASES —
@theme { --color-base-border-light: var(--base-border-light); }— and the variable it points at is declared outside that block, in a plain :root rule further up
the same row. Ship the block alone and every alias dead-ends: the generator emits
border-color: var(--color-base-border-light), the chain resolves to nothing, and the browser
falls back to black on a component whose border is grey.
So a preview document carries this whole thing in an ordinary <style> — where the browser
applies the :root rules and ignores the @theme at-rule it does not know — and the extracted
blocks separately as type="text/tailwindcss", which is the only part the generator reads.
The canvas needs no equivalent: it links the site's COMPILED bundle, where Tailwind has already
turned all of this into resolved :root variables.
The @theme blocks a browser-side Tailwind generator needs, for site_id and the global rows.
Only the theme travels: a kit's tokens (--color-primary-…) are what turn bg-primary into a
colour and the generator cannot know them, while the rest of a stylesheet is already in whatever
compiled bundle the document links — sending it twice would have it fight itself in the cascade.
Shared with MishkaCmsCoreWeb.Live.Admin.Builder.CanvasLive, which asks the same question of the
same engine.