MishkaCmsCore.Runtime.RuntimePreview (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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 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 :template to HTML.
  • Layout — render :template with placeholder inner_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's root_layout_template, compiled with the same Phoenix.HTML.Engine the 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/4 appends chrome_css/0 to the <style> it already writes;
  • MishkaCmsWeb.DraftPreviewLive renders chrome_css/0 into its own NONCED <style>, which is what the enforced PUBLIC policy needs;
  • note_body/2 additionally carries a <style> of its own, because a note can end up inside an author's root_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

chrome_css()

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

details(record, resource)

Overview detail rows ({label, value}) for record of resource. Resource-specific fields (section, priority, layout) appear only when present.

device_width(device)

Pixel width for a device key ("desktop" / "tablet" / "mobile").

error_statuses(record)

The statuses present in an ErrorPage record's error_pages collection (in order) — HTTP codes, and :offline for the page an offline site shows.

file_size(record)

Human file size of the record's source.

inner_html(record, resource, status \\ nil, nonce \\ nil, site \\ nil, request \\ [])

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.

layout_inner_content(arg1)

What a layout is previewed around: its example, or a placeholder.

note_body(message, nonce)

The "Preview unavailable" note for message, with its own <style> marked with nonce.

parse_error_pages(text)

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.

render_document(record, resource, nonce \\ nil)

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_error_document(record, status, nonce \\ nil)

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_example(source, site \\ "Global", nonce \\ nil)

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

source(record)

The record's raw source string for the Code tab.

stylesheet_source(site_id \\ nil)

@spec stylesheet_source(String.t() | nil) :: String.t()

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.

theme_source(site_id \\ nil)

@spec theme_source(String.t() | nil) :: String.t()

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.