feat(builder): send + restore site head code in save/load

Extend the save payload with head_code + design so the backend can
inject SiteDesign.headCode into published pages, and restore design
tokens on load() so the editor reflects the last-saved state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 18:22:38 -07:00
co-authored by Claude Opus 4.8
parent e892ee0e53
commit 92841e3f35
2 changed files with 56 additions and 2 deletions
+25 -2
View File
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { useEditor } from '@craftjs/core';
import { useEditorConfig } from '../state/EditorConfigContext';
import { usePages } from '../state/PageContext';
import { useSiteDesign, SiteDesign } from '../state/SiteDesignContext';
import { exportBodyHtml } from '../utils/html-export';
import { PageData } from '../types';
@@ -18,6 +19,10 @@ export interface BuildSavePayloadInput {
isEditingHeader: boolean;
/** True when the live canvas is showing the footer zone (activePageId === '__footer__'). */
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,
isEditingHeader,
isEditingFooter,
headCode,
design,
} = input;
const isPageActive = !isEditingHeader && !isEditingFooter;
@@ -173,6 +180,8 @@ export function buildSavePayload(input: BuildSavePayloadInput) {
header_craft_state: headerCraftState,
footer_craft_state: footerCraftState,
pages_craft_state: pagesGrapesjs,
head_code: headCode,
design,
};
}
@@ -191,6 +200,7 @@ export function useWhpApi() {
setPagesCraftState,
setActivePageIdDirect,
} = usePages();
const { design, updateDesign } = useSiteDesign();
const save = useCallback(async () => {
if (!isWHP || !whpConfig) return null;
@@ -210,6 +220,8 @@ export function useWhpApi() {
activePageId,
isEditingHeader,
isEditingFooter,
headCode: design.headCode,
design,
});
const resp = await fetch(`${whpConfig.apiUrl}?action=save`, {
@@ -221,7 +233,7 @@ export function useWhpApi() {
body: JSON.stringify(payload),
});
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 () => {
if (!isWHP || !whpConfig) return null;
@@ -255,6 +267,17 @@ export function useWhpApi() {
if (data.success && 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
if (proj.header_craft_state) {
setHeaderCraftState(typeof proj.header_craft_state === 'string'
@@ -300,7 +323,7 @@ export function useWhpApi() {
}
}
return data;
}, [isWHP, whpConfig, actions, setHeaderCraftState, setFooterCraftState, setPagesCraftState, setActivePageIdDirect, isEditingHeader, isEditingFooter]);
}, [isWHP, whpConfig, actions, setHeaderCraftState, setFooterCraftState, setPagesCraftState, setActivePageIdDirect, isEditingHeader, isEditingFooter, updateDesign]);
const uploadAsset = useCallback(
async (file: File) => {