Site builder: ship head code to published pages + fix 6 adversarial-review Minors #4

Merged
jknapp merged 4 commits from headcode-and-minors into main 2026-07-13 01:37:13 +00:00
2 changed files with 56 additions and 2 deletions
Showing only changes of commit 92841e3f35 - Show all commits
+31
View File
@@ -1,6 +1,7 @@
import { describe, test, expect } from 'vitest'; import { describe, test, expect } from 'vitest';
import { buildSavePayload } from './useWhpApi'; import { buildSavePayload } from './useWhpApi';
import { PageData } from '../types'; import { PageData } from '../types';
import { DEFAULT_SITE_DESIGN } from '../state/SiteDesignContext';
const pageA: PageData = { id: 'home', name: 'Home', slug: 'index', craftState: 'STORED_HOME' }; const pageA: PageData = { id: 'home', name: 'Home', slug: 'index', craftState: 'STORED_HOME' };
const pageB: PageData = { id: 'page_2', name: 'About', slug: 'about', craftState: 'STORED_ABOUT' }; const pageB: PageData = { id: 'page_2', name: 'About', slug: 'about', craftState: 'STORED_ABOUT' };
@@ -19,6 +20,8 @@ describe('buildSavePayload', () => {
activePageId: '__header__', activePageId: '__header__',
isEditingHeader: true, isEditingHeader: true,
isEditingFooter: false, isEditingFooter: false,
headCode: DEFAULT_SITE_DESIGN.headCode,
design: DEFAULT_SITE_DESIGN,
}); });
// The fresh live canvas must be reflected in header_craft_state, not the stale stored one. // The fresh live canvas must be reflected in header_craft_state, not the stale stored one.
@@ -52,6 +55,8 @@ describe('buildSavePayload', () => {
activePageId: '__footer__', activePageId: '__footer__',
isEditingHeader: false, isEditingHeader: false,
isEditingFooter: true, isEditingFooter: true,
headCode: DEFAULT_SITE_DESIGN.headCode,
design: DEFAULT_SITE_DESIGN,
}); });
expect(payload.footer_craft_state).toBe('LIVE_FOOTER'); expect(payload.footer_craft_state).toBe('LIVE_FOOTER');
@@ -75,6 +80,8 @@ describe('buildSavePayload', () => {
activePageId: 'home', activePageId: 'home',
isEditingHeader: false, isEditingHeader: false,
isEditingFooter: false, isEditingFooter: false,
headCode: DEFAULT_SITE_DESIGN.headCode,
design: DEFAULT_SITE_DESIGN,
}); });
// Live canvas goes to the active page and top-level slots. // Live canvas goes to the active page and top-level slots.
@@ -105,6 +112,8 @@ describe('buildSavePayload', () => {
activePageId: 'home', activePageId: 'home',
isEditingHeader: false, isEditingHeader: false,
isEditingFooter: false, isEditingFooter: false,
headCode: DEFAULT_SITE_DESIGN.headCode,
design: DEFAULT_SITE_DESIGN,
}); });
// The live edit must land in pages_craft_state[0] (index.html slot), // The live edit must land in pages_craft_state[0] (index.html slot),
@@ -116,4 +125,26 @@ describe('buildSavePayload', () => {
// The other page is untouched. // The other page is untouched.
expect(payload.pages_craft_state.find((p) => p.id === 'p2')?.craftState).toBe('STORED_ABOUT_2'); expect(payload.pages_craft_state.find((p) => p.id === 'p2')?.craftState).toBe('STORED_ABOUT_2');
}); });
test('head code + design tokens are included in the save payload', () => {
const design = { ...DEFAULT_SITE_DESIGN, headCode: '<meta name="x">' };
const payload = buildSavePayload({
siteId: 1,
siteName: 'Test Site',
liveCraftState: 'LIVE_PAGE_HOME',
pages: [pageA, pageB],
headerPage,
footerPage,
activePageId: 'home',
isEditingHeader: false,
isEditingFooter: false,
headCode: '<meta name="x">',
design,
});
expect(payload.head_code).toBe('<meta name="x">');
expect(payload.design).toEqual(design);
expect(payload.design.headCode).toBe('<meta name="x">');
});
}); });
+25 -2
View File
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { useEditor } from '@craftjs/core'; import { useEditor } from '@craftjs/core';
import { useEditorConfig } from '../state/EditorConfigContext'; import { useEditorConfig } from '../state/EditorConfigContext';
import { usePages } from '../state/PageContext'; import { usePages } from '../state/PageContext';
import { useSiteDesign, SiteDesign } from '../state/SiteDesignContext';
import { exportBodyHtml } from '../utils/html-export'; import { exportBodyHtml } from '../utils/html-export';
import { PageData } from '../types'; import { PageData } from '../types';
@@ -18,6 +19,10 @@ export interface BuildSavePayloadInput {
isEditingHeader: boolean; isEditingHeader: boolean;
/** True when the live canvas is showing the footer zone (activePageId === '__footer__'). */ /** True when the live canvas is showing the footer zone (activePageId === '__footer__'). */
isEditingFooter: boolean; isEditingFooter: boolean;
/** Site-wide custom `<head>` code (also present on `design.headCode`). */
headCode: string;
/** Full site design tokens object -- lets load() restore colors/fonts/headCode. */
design: SiteDesign;
} }
/** /**
@@ -45,6 +50,8 @@ export function buildSavePayload(input: BuildSavePayloadInput) {
activePageId, activePageId,
isEditingHeader, isEditingHeader,
isEditingFooter, isEditingFooter,
headCode,
design,
} = input; } = input;
const isPageActive = !isEditingHeader && !isEditingFooter; const isPageActive = !isEditingHeader && !isEditingFooter;
@@ -173,6 +180,8 @@ export function buildSavePayload(input: BuildSavePayloadInput) {
header_craft_state: headerCraftState, header_craft_state: headerCraftState,
footer_craft_state: footerCraftState, footer_craft_state: footerCraftState,
pages_craft_state: pagesGrapesjs, pages_craft_state: pagesGrapesjs,
head_code: headCode,
design,
}; };
} }
@@ -191,6 +200,7 @@ export function useWhpApi() {
setPagesCraftState, setPagesCraftState,
setActivePageIdDirect, setActivePageIdDirect,
} = usePages(); } = usePages();
const { design, updateDesign } = useSiteDesign();
const save = useCallback(async () => { const save = useCallback(async () => {
if (!isWHP || !whpConfig) return null; if (!isWHP || !whpConfig) return null;
@@ -210,6 +220,8 @@ export function useWhpApi() {
activePageId, activePageId,
isEditingHeader, isEditingHeader,
isEditingFooter, isEditingFooter,
headCode: design.headCode,
design,
}); });
const resp = await fetch(`${whpConfig.apiUrl}?action=save`, { const resp = await fetch(`${whpConfig.apiUrl}?action=save`, {
@@ -221,7 +233,7 @@ export function useWhpApi() {
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
return resp.json(); return resp.json();
}, [isWHP, whpConfig, query, pages, activePageId, headerPage, footerPage, isEditingHeader, isEditingFooter]); }, [isWHP, whpConfig, query, pages, activePageId, headerPage, footerPage, isEditingHeader, isEditingFooter, design]);
const publish = useCallback(async () => { const publish = useCallback(async () => {
if (!isWHP || !whpConfig) return null; if (!isWHP || !whpConfig) return null;
@@ -255,6 +267,17 @@ export function useWhpApi() {
if (data.success && data.project) { if (data.success && data.project) {
const proj = data.project; const proj = data.project;
// Restore site design tokens (colors/fonts/headCode) so the editor
// reflects what was last saved. Prefer the full `design` object when
// present; fall back to just `head_code` for older project.json files
// saved before this field existed (backward-compatible: defaults for
// everything else).
if (proj.design && typeof proj.design === 'object') {
updateDesign(proj.design);
} else if (typeof proj.head_code === 'string') {
updateDesign({ headCode: proj.head_code });
}
// Restore header craft state // Restore header craft state
if (proj.header_craft_state) { if (proj.header_craft_state) {
setHeaderCraftState(typeof proj.header_craft_state === 'string' setHeaderCraftState(typeof proj.header_craft_state === 'string'
@@ -300,7 +323,7 @@ export function useWhpApi() {
} }
} }
return data; return data;
}, [isWHP, whpConfig, actions, setHeaderCraftState, setFooterCraftState, setPagesCraftState, setActivePageIdDirect, isEditingHeader, isEditingFooter]); }, [isWHP, whpConfig, actions, setHeaderCraftState, setFooterCraftState, setPagesCraftState, setActivePageIdDirect, isEditingHeader, isEditingFooter, updateDesign]);
const uploadAsset = useCallback( const uploadAsset = useCallback(
async (file: File) => { async (file: File) => {