MishkaCmsCore.Runtime.PageBuilder.CardPreview (Mishka CMS Core v0.0.1)

Copy Markdown View Source

What a palette card looks like before you drop it — the small document shown when the pointer rests on a card.

It previews the drop, not the component

The source is the card's own data-template: the exact HEEx a drag carries, presets and all. So a card whose preset says "outline, small" previews outline and small, and what the preview shows is what the page gets. Rendering some other example of the component would be a nicer picture and a worse answer.

Why this is cheap enough to fire on hover

Three measurements decided the design, against the real 171-component install:

  • the rendered fragment is 757–1,826 bytes — a whole <.component …/> expanded. That is what travels over the socket per hover;
  • rendering it costs ~2 ms warm and 0.5–1.4 s the first time a component is asked for, which is its module being compiled. So the first hover is asynchronous with a placeholder and every later one is instant — and MishkaCmsCore.Runtime.States.CardPreviewState means "later" covers the rest of the session;
  • the CSS costs nothing. The site's compiled bundle is served at /assets/css/<site>/<hash> as immutable, and the canvas frame has already downloaded it by the time anyone hovers a card. The preview links the same URL and the browser answers from cache. Inlining it instead would have put 194 KB on every hover.

Compare the honest alternative: RuntimePreview.render_example/3 builds a standalone document with its own generated CSS — 335 KB and ~1 s each, every time. That is right for a modal somebody opened on purpose and wrong for something that follows the pointer.

The admin's Content-Security-Policy reaches inside the frame

A srcdoc document inherits the embedding page's policy, and the admin's is enforced: style-src-elem 'self' 'nonce-<n>', with no 'unsafe-inline'. Two consequences, both of which fail SILENTLY — the preview simply draws unstyled, which reads as a broken component:

  • every <style> this module writes carries the page's nonce, read in the LiveView process and passed in;
  • the frame is sandbox="allow-same-origin". Under a bare sandbox="" the document has an opaque origin, 'self' matches nothing, and the linked bundle is refused. It is granted same-origin and NOT allow-scripts: with nothing able to run inside, the origin buys the frame no capability, and it is also what lets the panel read the drawn height and size itself to fit.

RuntimePreview cannot make the same trade — it runs Tailwind's browser build inside its frame, and allow-scripts together with allow-same-origin defeats the sandbox entirely — which is why that one inlines its CSS and this one links it.

The cache holds the body, never the document

Only the rendered markup is remembered. The surrounding document — which carries the stylesheet's content-hashed URL — is assembled on every read, at a cost of one string join.

That is what makes a recompile safe. A cached DOCUMENT would keep pointing at the previous bundle's hash, and an immutable URL that no longer exists fails silently: the preview draws unstyled and looks like a broken component rather than a stale cache. Holding the body alone means a new hash is picked up the moment it exists, with nothing to invalidate.

The markup itself can still go stale when a component is recompiled, which is why the cache listens for that and drops what changed.

What it does not do

It does not render a component that needs data — one whose template reads @id or another required attr raises, and the preview says so in the same words the Preview tab uses rather than showing a blank box. That is a real answer: the component needs configuring before it will draw.

See MishkaCmsCore.Runtime.RuntimePreview for the heavyweight previews.

Summary

Functions

The preview document for template, as a string ready for an iframe's srcdoc.

The width the preview is laid out at, so the caller can scale the frame to fit.

Functions

document(template, site_name, nonce \\ nil)

@spec document(String.t(), String.t() | nil, String.t() | nil) ::
  {:ok, String.t()} | {:error, String.t()}

The preview document for template, as a string ready for an iframe's srcdoc.

Returns {:ok, html}, or {:error, message} when the template will not render — the caller shows the message, because "this needs configuring first" is worth saying.

width()

@spec width() :: pos_integer()

The width the preview is laid out at, so the caller can scale the frame to fit.