Files
site-builder/craft/scripts/e2e-preview-assets-entry.ts
T
shadowdaoandClaude Opus 5.5 8ff0e01c45 preview: inline uploaded assets as data: URLs so they render in the sandboxed Preview
Preview (and version-history Preview) renders the page in an
<iframe sandbox srcdoc> without allow-same-origin, i.e. an opaque origin.
Every site-builder asset response carries Cross-Origin-Resource-Policy:
same-site (whp libs/SiteBuilderAssetServe.php), so each upload requested
from that frame is cross-site and blocked (ERR_BLOCKED_BY_RESPONSE.NotSameSite,
naturalWidth 0). The canvas was fine; Preview images were broken for everyone.

CORP and the sandbox stay as they are. Before the HTML is handed to the
sandboxed frame, the editor (panel origin, same-origin to the proxy) fetches
this site's proxy assets and swaps each URL for a data: URL in the HTML text
(utils/preview-assets.ts): img src/srcset, poster, video src, <link> icon,
inline-style and <style> url(), &quot;/&amp; forms, both proxy URL forms.
<a href>, <meta content>, other sites, other origins are left alone. Per-asset
(8 MiB), total (40 MiB), count (200) and timeout (15 s) caps; any per-asset
failure keeps the original URL. Save/Publish/export are untouched.

blob: was measured and rejected: in Chromium and Firefox a blob: URL made by
the editor window or by the Preview tab does not load inside the null-origin
frame; data: does.

Tests: vitest for the collector/rewriter; new real-browser suite
test:browser:preview-assets (real HTTP stub with the backend's CORP header,
Preview opened by a click through the real openers) reproduces the block with
the old opener and passes with the new one in Chromium and Firefox, measures
blob vs data, and checks the real backend publish holds only assets/<file>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185ukXgk6UunFCKyXqLe2Cq
2026-11-02 20:29:36 -08:00

48 lines
3.1 KiB
TypeScript

// Bundled (esbuild, node) by scripts/e2e-preview-assets.mjs: a version-history snapshot whose header, body and
// design carry uploads exactly as the AssetPicker stores them (the same-origin panel proxy URL), turned into the
// Preview document by the REAL buildSnapshotPreviewHtml (the same export + absolute-URL step as TopBar's Preview),
// plus the published body for the backend publish check.
import { editorProxyUrl } from '../src/sitesmith/editor-asset-urls';
import { buildSnapshotPreviewHtml, type Snapshot } from '../src/utils/history-restore';
import { exportBodyHtml } from '../src/utils/html-export';
import { flattenTreeForCraft } from '../src/utils/craft-tree';
import { inlinePreviewAssets, collectPreviewAssetUrls } from '../src/utils/preview-assets';
export const SITE_ID = 7;
export const FILES = ['1700000000_aa11bb22_hero.png', '1700000001_bb22cc33_logo+v2.png', 'my photo.png', 'café.png', 'poster.png', 'fav.png'];
export const OTHER_SITE_FILE = 'other-site.png';
const U = (i: number) => editorProxyUrl(SITE_ID, FILES[i]);
const n = (type: string, props: Record<string, unknown>, nodes?: any[]) => ({ type: { resolvedName: type }, props, ...(nodes ? { nodes } : {}) });
const state = (tree: any) => JSON.stringify(flattenTreeForCraft(tree).nodes);
const header = state(n('Container', { node_id: 'ROOT', tag: 'div' }, [
n('Navbar', { node_id: 'nav', logoType: 'image', logoImage: U(1), logoUrl: '/', links: [{ text: 'Home', href: '/' }] }),
]));
const page = state(n('Container', { node_id: 'ROOT', tag: 'div' }, [
n('BackgroundSection', { node_id: 'bg', bgImage: U(0), overlayOpacity: 0.2 }, [n('Heading', { node_id: 'h', text: 'Hello', level: 'h1' })]),
n('ImageBlock', { node_id: 'i1', src: U(0), alt: 'plain' }),
n('ImageBlock', { node_id: 'i2', src: U(1), alt: 'plus' }),
n('ImageBlock', { node_id: 'i3', src: U(2), alt: 'space' }),
n('ImageBlock', { node_id: 'i4', src: U(3), alt: 'unicode' }),
n('Container', { node_id: 'c', tag: 'div', style: { minHeight: '60px', backgroundImage: `url(${U(0)})` } }),
n('HtmlBlock', { node_id: 'html', code:
`<img id="rich" src="${U(0)}" alt="rich"><img id="srcset" alt="srcset" src="${U(2)}" srcset="${U(0)} 1x, ${U(3)} 2x">`
+ `<video id="vid" poster="${U(4)}" width="40" height="30"></video>`
+ `<a id="lnk" href="${U(0)}">full size</a>`
+ `<img id="other" alt="other" src="${editorProxyUrl(8, OTHER_SITE_FILE)}">` }),
n('ImageBlock', { node_id: 'i5', src: editorProxyUrl(SITE_ID, 'does-not-exist.png'), alt: 'missing' }),
]));
export const SNAPSHOT: Snapshot = {
pages_craft_state: [{ id: 'p1', name: 'Home', slug: 'index', craftState: page }],
header_craft_state: header,
footer_craft_state: null,
design: { favicon: U(5) },
} as unknown as Snapshot;
/** The Preview document as origin/main hands it to the sandboxed tab (proxy URLs made absolute on `origin`). */
export function previewHtml(origin: string): string { return buildSnapshotPreviewHtml(SNAPSHOT, 0, origin); }
/** The page body Save/Publish sends (no Preview step involved). */
export function publishBody(): string { return exportBodyHtml(header).html + exportBodyHtml(page).html; }
export { inlinePreviewAssets, collectPreviewAssetUrls };