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>
50 lines
2.8 KiB
TypeScript
50 lines
2.8 KiB
TypeScript
import type { SerializedTreeNode } from '../types/sitesmith';
|
|
import { TemplateComponent } from './definitions';
|
|
|
|
/**
|
|
* Converts a template's `TemplateComponent` tree (the plain
|
|
* `{ type, props, children? }` shape authored in `templates/definitions.ts`)
|
|
* into a `SerializedTreeNode` (the shape `craft-tree.ts`'s `buildNodeTree` /
|
|
* `sanitizeAiTree` / `flattenTreeForCraft` pipeline already knows how to
|
|
* materialize into a real Craft.js `NodeTree`).
|
|
*
|
|
* Bug this fixes: `TemplateModal`'s `addTemplateComponents` used to call
|
|
* `React.createElement(Component, comp.props)` with no children argument,
|
|
* which silently dropped every nested `children` array authored in
|
|
* definitions.ts -- e.g. `makeHeader()`'s `Container > Logo + Menu`,
|
|
* `makeFooterContent()`'s `Container > TextBlock...`, or a page `Section`
|
|
* wrapping a `Heading`/`TextBlock`/`ButtonLink`. The resulting Craft.js node
|
|
* ended up with `nodes: []`, so the header/footer zone preview
|
|
* (`ZonePreview` in `editor/Canvas.tsx`, via `exportBodyHtml`) -- and the
|
|
* same-path published HTML (`handlePublish` composes header/footer HTML
|
|
* from the same serialized state) -- rendered as an empty strip instead of
|
|
* the real nav/footer/section content.
|
|
*
|
|
* A follow-up attempt fixed that by building a real nested React element
|
|
* tree (`React.createElement(Component, props, ...children)`) and handing
|
|
* it to `query.parseReactElement(...).toNodeTree()`. That approach breaks
|
|
* for any component with an internal SHELL_INNER-style linked canvas
|
|
* (`Section`, `BackgroundSection`, `FormContainer` -- see `SHELL_INNER` in
|
|
* `craft-tree.ts`) or linked columns (`ColumnLayout`): `parseReactElement`
|
|
* statically walks the JSX tree and has no way to know that e.g. `Section`
|
|
* routes its `children` into a nested `<Element id="section-inner" canvas>`
|
|
* at render time, not directly under itself. Feeding it raw nested children
|
|
* produces a tree where Craft.js has to auto-materialize that linked node on
|
|
* first render -- which stores the wrong `type` and crashes with "component
|
|
* type (undefined) does not exist in the resolver" the moment the section
|
|
* renders (reproduced live: loading the SaaS Landing template crashed the
|
|
* whole editor on its "Trusted by 10,000+ development teams" `Section`).
|
|
*
|
|
* Converting to `SerializedTreeNode` and reusing `buildNodeTree` sidesteps
|
|
* both bugs at once: it's the same tree-building pipeline already used (and
|
|
* tested) for AI-generated content in `apply-ai-response.ts`, which already
|
|
* knows how to route content through SHELL_INNER / linked columns correctly.
|
|
*/
|
|
export function templateComponentToTreeNode(comp: TemplateComponent): SerializedTreeNode {
|
|
return {
|
|
type: { resolvedName: comp.type },
|
|
props: { ...comp.props },
|
|
nodes: (comp.children || []).map(templateComponentToTreeNode),
|
|
};
|
|
}
|