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:
@@ -1,6 +1,7 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { buildSavePayload } from './useWhpApi';
|
||||
import { PageData } from '../types';
|
||||
import { DEFAULT_SITE_DESIGN } from '../state/SiteDesignContext';
|
||||
|
||||
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' };
|
||||
@@ -19,6 +20,8 @@ describe('buildSavePayload', () => {
|
||||
activePageId: '__header__',
|
||||
isEditingHeader: true,
|
||||
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.
|
||||
@@ -52,6 +55,8 @@ describe('buildSavePayload', () => {
|
||||
activePageId: '__footer__',
|
||||
isEditingHeader: false,
|
||||
isEditingFooter: true,
|
||||
headCode: DEFAULT_SITE_DESIGN.headCode,
|
||||
design: DEFAULT_SITE_DESIGN,
|
||||
});
|
||||
|
||||
expect(payload.footer_craft_state).toBe('LIVE_FOOTER');
|
||||
@@ -75,6 +80,8 @@ describe('buildSavePayload', () => {
|
||||
activePageId: 'home',
|
||||
isEditingHeader: false,
|
||||
isEditingFooter: false,
|
||||
headCode: DEFAULT_SITE_DESIGN.headCode,
|
||||
design: DEFAULT_SITE_DESIGN,
|
||||
});
|
||||
|
||||
// Live canvas goes to the active page and top-level slots.
|
||||
@@ -105,6 +112,8 @@ describe('buildSavePayload', () => {
|
||||
activePageId: 'home',
|
||||
isEditingHeader: 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),
|
||||
@@ -116,4 +125,26 @@ describe('buildSavePayload', () => {
|
||||
// The other page is untouched.
|
||||
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">');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user