the preview is the real page, proxied
The panel drew its own header and footer in React. It looked plausible and it was wrong — the site's header has a logo hanging past a rule and a nav in the content column — and worse, it was a second implementation of the layout, so it would have gone on being wrong differently every time either side moved. So the admin embeds the site's own `/layout` route instead: the real shell with no page in it. A designer changing the header in code changes what the admin shows, with no admin change at all. That is the bridge. Proxied through Officer rather than linked, because everything this plugin knows about where the site lives is loopback — 127.0.0.1:<devPort> and nothing else. There is no public hostname stored anywhere and adding one would be a second fact about the same thing, kept in step by hand. The request goes the way every other request here goes, server-side, and the browser talks only to Officer. Same origin, which is also what keeps the door open: the day the page builder wants to draw handles on top of the real page, it can. A cross-origin iframe never could. The rewrite is narrow and its limits are the interesting part. The site writes root-relative URLs and those resolve against Officer's root inside the iframe; a <base> tag does not help, because it moves relative URLs and leaves root-relative ones exactly where they were wrong. So href, src, srcset and CSS url() are moved under the prefix — and `//` is excluded deliberately, because that names another host and prefixing one would turn it into a path on this server. Only HTML is rewritten. Stylesheets, fonts and images are returned as they came: rewriting bytes we do not understand is how a proxy corrupts a font. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -150,61 +150,30 @@ export const LayoutPanel = () => {
|
||||
{saved && <p className="text-xs text-success">Saved</p>}
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
|
||||
{/* ── The sketch ───────────────────────────────────────────────────────────────────────── */}
|
||||
{/*
|
||||
The REAL page, embedded — not a drawing of it.
|
||||
|
||||
It was a React sketch: a centred row of labels, a body, a band at the bottom. It looked
|
||||
plausible and it was wrong, because the site's header has a logo hanging past a rule and a nav
|
||||
that sits in the content column. Worse, it was a second implementation of the layout, so it
|
||||
would have gone on being wrong differently every time either side changed.
|
||||
|
||||
This is `/layout` on the development server — the site's own shell with no page in it — proxied
|
||||
through Officer so it is same origin. See the preview route in api/router.ts.
|
||||
*/}
|
||||
<section className="flex flex-col gap-2">
|
||||
<div
|
||||
className="overflow-hidden rounded-lg border border-border"
|
||||
style={{ backgroundColor: bg, color: fg }}
|
||||
>
|
||||
{/* The menu, in the order it will appear, in the face chosen for it. */}
|
||||
<div
|
||||
className="flex flex-wrap items-center justify-center gap-x-5 gap-y-1 border-b px-4 py-3 text-[11px]"
|
||||
style={{ borderColor: fg, fontFamily: draft.font_sub ?? undefined }}
|
||||
>
|
||||
{/* The menu when there is one, the site's own pages when there is not — the same fallback
|
||||
the page itself applies, so the sketch never shows a menu the site would not draw. */}
|
||||
{(menu.length ? menu : draft.pages.map((page) => ({ label: page.label, href: page.path }))).map(
|
||||
(item, index) => (
|
||||
<span key={index}>{item.label}</span>
|
||||
),
|
||||
)}
|
||||
{!menu.length && !draft.pages.length && <span className="opacity-40">No menu yet</span>}
|
||||
</div>
|
||||
|
||||
{/* The body. Deliberately empty: what goes here is the page, and the page is what this
|
||||
section is NOT about. Its only job is to give the two bands something between them. */}
|
||||
<div className="px-4 py-10 text-center" style={{ fontFamily: draft.font_title ?? undefined }}>
|
||||
<p className="text-lg">The page</p>
|
||||
<p className="mt-1 text-[11px] opacity-50" style={{ fontFamily: draft.font_body ?? undefined }}>
|
||||
Whatever is built for it goes here
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* The footer, with the columns as they will fall. */}
|
||||
<div className="border-t px-4 py-4" style={{ borderColor: fg }}>
|
||||
{columns.length ? (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
{columns.map((column, index) => (
|
||||
<div key={index}>
|
||||
{column.title && (
|
||||
<p className="mb-1 text-[11px]" style={{ fontFamily: draft.font_sub ?? undefined }}>
|
||||
{column.title}
|
||||
</p>
|
||||
)}
|
||||
{column.links.map((link, i) => (
|
||||
<p key={i} className="text-[11px] opacity-70">
|
||||
{link.label}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[11px] opacity-40">No footer columns yet</p>
|
||||
)}
|
||||
{draft.footer_note && <p className="mt-3 text-[10px] opacity-60">{draft.footer_note}</p>}
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-lg border border-border bg-white">
|
||||
<iframe
|
||||
src="/fofos-admin/preview/layout"
|
||||
title="The site's layout"
|
||||
className="h-[420px] w-full border-0"
|
||||
/* Nothing is typed into it and nothing is submitted from it. It is a window. */
|
||||
sandbox="allow-same-origin allow-scripts"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
The development server, live. Save to see a change here.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* ── Menu ─────────────────────────────────────────────────────────────────────────────── */}
|
||||
|
||||
Reference in New Issue
Block a user