feat(preview): consume rustycms live-preview drafts
Deploy / verify (push) Successful in 45s
Deploy / deploy (push) Successful in 1m1s

`?preview_draft=<sid>` on any page/post route forwards the signed
session id to the CMS, which renders the in-memory draft instead of
the persisted entry. Layout listens for `rustycms:reload` postMessages
from the admin iframe parent and re-runs `invalidateAll()` to pick up
fresh draft state. CSP `frame-ancestors` is set per-request when
`PUBLIC_PREVIEW_PARENT_ORIGIN` is configured so the admin iframe can
embed the response.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-05-01 09:24:05 +02:00
parent 3bc7273e71
commit 00d42ebb42
5 changed files with 59 additions and 7 deletions
+18 -1
View File
@@ -49,7 +49,10 @@ export const handle: Handle = async ({ event, resolve }) => {
},
});
const isPreview = event.url.searchParams.has('preview');
const isPreview =
event.url.searchParams.has('preview') ||
event.url.searchParams.has('preview_draft');
const isPreviewDraft = event.url.searchParams.has('preview_draft');
if (
event.request.method === 'GET' &&
@@ -73,5 +76,19 @@ export const handle: Handle = async ({ event, resolve }) => {
);
}
// Live-preview: allow the admin iframe to embed this response. The admin
// origin is configured via PUBLIC_PREVIEW_PARENT_ORIGIN; when unset we fall
// back to wildcard (dev only). Outside live-preview we leave existing
// headers alone so production hardening (X-Frame-Options / CSP from Caddy)
// stays intact.
if (isPreviewDraft) {
const parent = (process.env.PUBLIC_PREVIEW_PARENT_ORIGIN ?? '').trim();
response.headers.set(
'content-security-policy',
`frame-ancestors 'self' ${parent || '*'}`,
);
response.headers.delete('x-frame-options');
}
return response;
};