/** * Emit a Sitecore layout XML string from a structural `Layout` shape. * * Sitecore stores layout as a single XML string in the `__Renderings` * (shared) or `__Final Renderings` (versioned) field on any item that * holds a layout — pages, partial designs, page designs. This emitter * is the single seam that turns recipe-level structural layout into * that wire form. The output is deterministic given the same inputs: * placement UIDs derive from the parent item ID + placeholder + index + * componentHandle, so re-pushes produce identical XML. * * Wire format: * * * * * ... * * * * The placeholder attribute is `ph` (`s:ph` in delta form) — Sitecore's * layout engine reads exactly that name and silently ignores anything * else. An earlier iteration wrote `placeh`, which Sitecore stored * verbatim but never bound to a placeholder, so pushed pages rendered * empty until a first save in Pages rewrote the XML. * * Recipe → wire mapping: * * componentHandle → id (renderingId(handle), curly-uppercase) * placeholder key (dict key) → ph * datasourceRef.kind=shared → ds (contentItemId(handle), curly-uppercase) * datasourceRef.kind=scoped → REJECTED for partial/page designs * (no host page to resolve against) * datasourceRef.kind=none → no ds attribute * variant → par (FieldNames=, URL-encoded) * params → par (key=value pairs, URL-encoded) * (parentItemId, placeholder, index, componentHandle) → uid (uuidv5) */ export interface ComponentPlacementInput { componentHandle: string; variant?: string; params?: Record; datasourceRef?: { kind: "shared"; handle: string; } | { kind: "scoped"; slot: string; } | { kind: "none"; }; /** * Nested placements (schema-legal on any placement). The emitter * only consumes FLAT layouts — the page, partial-design, and * page-design compilers flatten the tree into concrete * dynamic-placeholder keys before emission * (`compile/flatten-layout.ts`). A placement reaching this emitter * with children still attached comes from a layout context without a * flattening pass (a content-item version layout, a page-template * standard-values layout). Rejected loudly rather than silently * dropped. */ placeholders?: Record; } export interface LayoutInput { placeholders: Record; } export interface LayoutEmitContext { /** * The Sitecore item ID this layout will live on (partial-design or * page-design item). Seeds deterministic placement UIDs. */ parentItemId: string; /** Sitecore device GUID. Caller passes `DEFAULT_DEVICE_ID` from sitecore-templates.ts. */ deviceId: string; /** Maps a component recipe handle to its rendering item GUID. */ renderingIdFor: (handle: string) => string; /** Maps a content-item recipe handle to its content item GUID. */ contentItemIdFor: (handle: string) => string; /** * Whether `kind: "scoped"` datasource refs are permitted. False for * partial-design and page-design layouts (they don't have a host page * to resolve scoped refs against); `PageRecipe` sets it true. */ allowScoped: boolean; /** * Resolver for `kind: "scoped"` datasource refs — maps a placement's * `slot` to the GUID of the page-local datasource item the caller * materialises at `/Data/`; the scoped `` element then * carries `ds="{resolvedGuid}"`. * * When `allowScoped` is true but this is absent, a scoped ref emits * the page-relative `ds="local:/Data/"` form — the wire form * XM Cloud Pages itself writes for page-local datasources (resolved * against the context item at render time, and re-pointed for free * when a page is copied). `PageRecipe` layouts use this form; the * GUID resolver remains for callers that need absolute references. */ scopedDatasourceIdFor?: (slot: string) => string; /** * Wire form for the emitted XML. * * - `"canonical"` (default) — ``. What our recipe inputs naturally describe. * Page Design items round-trip this byte-for-byte. * * - `"delta"` — the SXA delta wire form: ``, merged over the base layout (a page's template * standard values; a partial design's inherited layout) by * Sitecore's XmlDeltas at read time. This is the form XM Cloud * Pages writes to `__Final Renderings` on every author save, so * emitting it directly means the first push round-trips and * converges in one cycle. Page Design layouts must NOT use this * mode (they preserve canonical and would diverge). */ mode?: "canonical" | "delta"; /** * Whether delta mode emits the `` device-attributes * directive as the first child of ``. Defaults to true — the form * the SXA Partial Design pipeline round-trips. Page `__Final * Renderings` deltas must pass false: operator-verified Pages-authored * deltas never carry the directive, and the page inherits its * `l="{JSON layout}"` pointer from the template's standard values. * Ignored in canonical mode. */ deltaDeviceDirective?: boolean; /** * Whether delta mode emits per-placement position anchors * (`p:before="*"` / `p:after="…"`). Defaults to true — the SXA * Partial Design pipeline's form, also what page `__Final * Renderings` deltas carry. Page `__Renderings` (SHARED layout, * `layoutScope: "shared"`) must pass false: operator-verified * Pages-authored shared layouts carry anchor-less `` elements * (placement order is document order), plus a root-level * `` directive — pass `deltaSharedForm` to emit * that too. Anchored deltas against the (typically empty) template * standard-values base fail to place. Ignored in canonical mode. */ deltaAnchors?: boolean; /** * Emit the `` root directive as the first child of * the delta root — the form XM Cloud Pages writes for the SHARED * `__Renderings` delta. Defaults to false. Ignored in canonical mode. */ deltaSharedForm?: boolean; /** * Optional resolver mapping (componentHandle, variantName) to the * headless Variant Definition item's refKey GUID. When it returns a * GUID, the placement's `FieldNames` rendering parameter carries the * curly-braced GUID — the form XM Cloud Pages writes, which its * variant picker displays as the selection and the layout service * resolves back to the variant item's NAME for the front end's * export lookup. When absent (or when it returns undefined — e.g. * the component recipe doesn't declare the variant, so no Variant * Definition item exists to reference), `FieldNames` falls back to * the raw variant name, which the layout service passes through * unresolved and the front end matches by name directly. */ variantRefFor?: (componentHandle: string, variantName: string) => string | undefined; /** * Optional per-param wire-value mapper. Called for every rendering * parameter (except the reserved `DynamicPlaceholderId` and * `FieldNames`) with the placement's componentHandle, the param name, * and the recipe-level raw value; a returned string REPLACES the raw * value in the emitted `par` blob. `PageRecipe` uses this to store * enum-backed Droplink values as enum-value item GUIDs and checkbox * values as `1`/`""` — the forms XM Cloud Pages' properties panel * reads back. Returning undefined keeps the raw value. */ paramValueFor?: (componentHandle: string, paramName: string, rawValue: string) => string | undefined; /** * SXA JSON Layout definition GUID. When set, the device element * carries an `l="{layoutId}"` attribute — `` — and `emitLayoutXml` emits the wrapper shell * even when there are zero placements (a page-template `__Standard * Values` layout: device + JSON-layout pointer, no renderings). Used * only in `"canonical"` mode; `"delta"` ignores it. When unset, the * device element has no `l` attribute (partial / page-design layouts). */ layoutId?: string; } /** * Emit the layout XML for a `Layout` block. Returns an empty string * when the layout has no placements — caller decides whether to write * an empty `__Renderings` field or skip it. * * Mode selection (see `LayoutEmitContext.mode` JSDoc): * - `"canonical"` (default) — what page-design items round-trip cleanly. * - `"delta"` — SXA partial-design wire form so first push converges. * * Throws when a `kind: "scoped"` datasourceRef appears and * `ctx.allowScoped` is false — layout contexts without an item to host * a `Data` folder (content-item version layouts, page-template * standard-values layouts) reject scoped refs. */ export declare function emitLayoutXml(layout: LayoutInput, ctx: LayoutEmitContext): string;