From e1d381dc8a161b6262425a0001dbd8cd7311c198 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 12 Jul 2026 13:29:02 -0700 Subject: [PATCH] fix(builder): Preview includes site headCode and is \$-safe (D8) Two bugs in TopBar's Preview handler: 1. exportToHtml() was called without a headCode option, so Preview silently ignored SiteDesign.headCode (custom code the user set via the Code modal). Pull `design.headCode` from useSiteDesign() and pass it through, matching exportToHtml's ExportOptions.headCode signature. 2. html.replace(bodyMatch[1], composedBody) used the string-replacer overload of String.prototype.replace, which treats `$&`, `$1`, `$$`, etc. in the replacement string as special patterns -- any user content containing a literal `$` sequence would corrupt the preview HTML. Switch to a function replacer (`.replace(bodyMatch[1], () => composedBody)`) so the composed body is inserted literally. Verified by reading + `npm run build`; no existing TopBar test harness to extend (dynamic import + window.open/Blob in the click handler). Co-Authored-By: Claude Opus 4.8 (1M context) --- craft/src/panels/topbar/TopBar.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/craft/src/panels/topbar/TopBar.tsx b/craft/src/panels/topbar/TopBar.tsx index fedcbc9..31c4994 100644 --- a/craft/src/panels/topbar/TopBar.tsx +++ b/craft/src/panels/topbar/TopBar.tsx @@ -3,6 +3,7 @@ import { useEditor } from '@craftjs/core'; import { useEditorConfig } from '../../state/EditorConfigContext'; import { useWhpApi } from '../../hooks/useWhpApi'; import { usePages } from '../../state/PageContext'; +import { useSiteDesign } from '../../state/SiteDesignContext'; import { DeviceMode } from '../../types'; import { TemplateModal } from './TemplateModal'; import { HeadCodeModal } from './HeadCodeModal'; @@ -22,6 +23,7 @@ export const TopBar: React.FC = ({ device, onDeviceChange }) => { })); const { save, publish, load } = useWhpApi(); const { headerPage, footerPage } = usePages(); + const { design } = useSiteDesign(); const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle'); const [publishStatus, setPublishStatus] = useState<'idle' | 'publishing' | 'published' | 'error'>('idle'); @@ -189,13 +191,17 @@ export const TopBar: React.FC = ({ device, onDeviceChange }) => { const result = exportToHtml(serialized, { title: whpConfig?.siteName || 'Preview', includeFonts: true, + headCode: design.headCode, }); - // Replace the body in the full document with our composed version + // Replace the body in the full document with our composed version. + // Use a function replacer -- a plain string replacer treats + // `$&`/`$1`/`$$` sequences in user content as replacement + // patterns, silently corrupting the output. let html = result.html; const bodyMatch = html.match(/]*>([\s\S]*)<\/body>/i); if (bodyMatch) { - html = html.replace(bodyMatch[1], composedBody); + html = html.replace(bodyMatch[1], () => composedBody); } // Make proxy URLs absolute so they work from the blob: context