# `MishkaCmsCore.Runtime.RuntimePreview`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/runtime_preview.ex#L5)

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 `: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.

# `chrome_css`

```elixir
@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`

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

# `device_width`

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

# `error_statuses`

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`

Human file size of the record's source.

# `inner_html`

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`

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

# `note_body`

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

# `parse_error_pages`

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`

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`

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`

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`

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

# `stylesheet_source`

```elixir
@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`

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
