fix(builder): render template header/footer nested content in zone preview + export

TemplateModal's addTemplateComponents() built each template component via
React.createElement(Component, comp.props) without ever passing
comp.children, silently dropping every nested children array authored in
templates/definitions.ts (header/footer Container > Logo/Menu/TextBlock,
page Section > Heading/TextBlock/ButtonLink). The resulting Craft.js node had
nodes: [], so the header/footer zone preview (ZonePreview -> exportBodyHtml)
rendered as an empty strip, and published output was affected the same way.

Fix converts each TemplateComponent to a SerializedTreeNode and reuses
craft-tree.ts's buildNodeTree (sanitize -> flatten -> materialize) -- the
same tested tree pipeline already used for AI-generated content -- instead
of hand-rolling a React-element tree, since a naive nested-children fix via
parseReactElement crashes any component with an internal SHELL_INNER linked
canvas (Section/BackgroundSection/FormContainer) or linked columns
(ColumnLayout). Also fixes two latent bugs in buildNodeTree itself, only
surfaced by exercising it against a real Craft.js editor for the first time:
data.type must be the actual resolved component reference (not a string or
{resolvedName} object) for correct rendering, and the synthesized SHELL_INNER
node needs data.name set for actions.addNodeTree's own validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 06:19:26 -07:00
parent 8aeadefa88
commit da558fd52d
6 changed files with 386 additions and 67 deletions
+18 -14
View File
@@ -8,9 +8,10 @@ import {
TemplateComponent,
TemplateCategory,
} from '../../templates';
import { componentResolver } from '../../components/resolver';
import { clickableProps } from '../../utils/a11y';
import { Modal } from '../../ui/Modal';
import { buildNodeTree } from '../../utils/craft-tree';
import { templateComponentToTreeNode } from '../../templates/apply-template';
// ---------------------------------------------------------------------------
// Types
@@ -77,28 +78,31 @@ export const TemplateModal: React.FC<TemplateModalProps> = ({ open, onClose }) =
return allTemplates.filter((t) => t.category === activeTab);
}, [activeTab]);
// Resolve a TemplateComponent type name to its React component
const resolverMap = componentResolver as Record<string, React.ComponentType<any>>;
/**
* Add all components from a template definition onto the current (empty) canvas ROOT.
* Uses Craft.js parseReactElement + addNodeTree which correctly builds valid node structures.
* Converts each `TemplateComponent` (the plain `{type, props, children?}`
* shape templates author) to a `SerializedTreeNode` and runs it through
* `buildNodeTree` (sanitize -> flatten -> materialize), the same tree pipeline
* already used for AI-generated content. This recurses into nested
* `children` (e.g. a header/footer `Container` wrapping `Logo`/`Menu`, or a
* page `Section` wrapping a `Heading`) AND correctly routes them through a
* component's SHELL_INNER linked canvas / linked columns where needed --
* see `templates/apply-template.ts` for the two bugs this fixes (dropped
* children, and a naive `parseReactElement` tree crashing `Section`/
* `BackgroundSection`/`FormContainer`/`ColumnLayout`).
*/
const addTemplateComponents = useCallback(
(components: TemplateComponent[]) => {
for (const comp of components) {
const Component = resolverMap[comp.type];
if (!Component) {
console.warn(`Template references unknown component type: ${comp.type}`);
continue;
try {
const tree = buildNodeTree(query, templateComponentToTreeNode(comp));
actions.addNodeTree(tree, 'ROOT');
} catch (e) {
console.warn(`Failed to build template component tree for type "${comp.type}":`, e);
}
const element = React.createElement(Component, comp.props);
const tree = query.parseReactElement(element).toNodeTree();
actions.addNodeTree(tree, 'ROOT');
}
},
[query, actions, resolverMap],
[query, actions],
);
/**