Template Builder — Developer Reference

The Template Builder is a full-screen React + Zustand visual editor for creating reusable post-card layout templates and titlebars. It is intentionally layout-only: no WP_Query, no data fetching, no business logic. Its output is a JSON node tree (schema) that the PHP compiler converts into a self-contained .php template file at wp-content/sanilwb-templates/template-{id}.php.

The two builder types share the same UI and store:

Builder type What it builds Used by
template Post-card layout (image, title, excerpt, date, etc.) Posts widget in the Page Builder
titlebar Title bar shown above a carousel Title Bar widget in the Page Builder

Architecture Overview

TemplateBuilder.jsx          ← root component, seeds store, renders UI
├── BuilderTopbar            ← shared topbar shell
│   ├── Name input           ← edits store.name
│   ├── Settings dropdown    ← category picker + PreviewPostPicker
│   └── SaveButton           ← save state machine, AJAX POST
├── NodeTree / HistoryPanel  ← left sidebar: only one visible at a time (activePanel state)
│   ├── AddNodeMenu          ← "+" button to add a node (NodeTree)
│   ├── NodeItem (×N)        ← each node row: clicking opens the settings panel
│   └── HistoryPanel         ← undo/redo timeline viewer, toggled via the header History button
├── CanvasPreview            ← iframe canvas, live-updates via store subscription
│   └── DevicePreviewControls ← desktop / tablet / mobile toggle
└── SettingsSidebar          ← right-side panel (shared shell)
    └── NodeSettingsDialog   ← form content for the selected node, auto-applies every field change

All layout state lives in useTemplateBuilderStore (Zustand). CanvasPreview subscribes to the store and rebuilds the iframe HTML whenever nodes change. No props are passed between the tree and the canvas — both import the same store module.

The node settings panel has no Save/Cancel buttons — every field edit auto-applies (debounced) straight into the store, and the canvas updates live. Every mutating store action also pushes an undo/redo history entry; see History / Undo-Redo below.


Entry Point and Data Bootstrap

File Purpose
admin/assets/js/src/template-builder/index.jsx React app entry — mounts <TemplateBuilder />
admin/templates/template-builder/editor-page.php PHP template — renders the editor page and injects window.TemplateBuilderData
admin/class-sanilwb-admin-template-builder.php Enqueues dist/template-builder.js, builds the data object

window.TemplateBuilderData shape

PHP injects this object before the JS bundle runs. initFromWindowData() reads it on mount.

{
  templateId:        number,   // 0 for a new template
  nonce:             string,   // wp_create_nonce('sanilwb_templates')
  schema:            array,    // saved node tree ([] for new templates)
  name:              string,   // template display name
  category:          string,   // template category slug (e.g. 'default')
  templateCategories: string[], // all available category slugs
  builderType:       string,   // 'template' or 'titlebar'
  imageSizes:        object,   // wp_get_registered_image_subsizes() output
  nextIndex:         number,   // used to auto-name new templates ("Template 3")
  previewPost:       object,   // { title, excerpt, author, avatar, date_ago, date_np, topic, section_name }
  pluginUrl:         string,   // plugin base URL (for sample image src in canvas)
}

On mount, initFromWindowData() parses the schema, seeds nodes[], and sets savedSnapshot so the dirty-tracking can detect changes.


Data Structure

The Template Builder stores layout as a hierarchical node tree. Container nodes (div) can have children; all other node types are leaves.

nodes[]
  └── node
        ├── id         (auto-incremented integer, ephemeral)
        ├── type       ('div' | 'heading' | 'image' | 'excerpt' | 'date' | 'author' | 'button' | 'topic' | 'carousel_nav')
        ├── children[] (populated only for div nodes; empty array for leaf nodes)
        └── ...fields  (flat key-value pairs for all style and content fields)

PHP schema format

buildSavePayload() serializes the tree to a JSON string for the AJAX save. The PHP side stores this in wp_sanilwb_templates.schema_json. The format strips children from leaves and ensures all values are strings:

[{
  "type": "div",
  "id": "101",
  "direction": "column",
  "children": [
    { "type": "image", "id": "102", "image-size": "square", "link": "1" },
    { "type": "heading", "id": "103", "link": "1" },
    { "type": "excerpt", "id": "104", "lines": "2" }
  ]
}]

The supports field

When saving, collectSupports(nodes) walks the tree and collects all leaf node types present (e.g. "image,heading,excerpt,date"). This comma-separated string is saved to the supports column in wp_sanilwb_templates. The Page Builder reads it to show or hide conditional display toggles in the Posts widget dialog (e.g. "Show Image" is only shown when the selected template supports_image).

Responsive field naming

Fields marked responsive: true in the node type config are stored with device suffixes:

Stored key Device
fieldName Desktop (base)
fieldName__tablet Tablet override
fieldName__mobile Mobile override

When reading, the device cascade is: mobile → __mobile__tablet → base; tablet → __tablet → base; desktop → base.


Store — useTemplateBuilderStore

File: admin/assets/js/src/template-builder/stores/useTemplateBuilderStore.js

Single source of truth for all editor state.

State

Key Type Purpose
nodes Node[] The live node tree
selectedNodeId number\|null Currently selected node (highlighted in tree)
hoveredNodeId number\|null Node the mouse is over in the tree
name string Template display name
category string Template category slug
templateCategories string[] Available category options
builderType string 'template' or 'titlebar'
templateId number DB id (0 for unsaved)
savedSnapshot string\|null JSON string of nodes at last save — null for a brand-new unsaved template
savedName string Template name at last save
savedCategory string Category at last save
isDirty boolean True when nodes/name/category differ from last save
previewPost object Sample post data used by the canvas for placeholder content
imageSizes object WordPress registered image sizes — populates the image-size picker
styleClipboard object\|null { nodeType, values } — style values copied from a node, ready to paste
past Array Undo stack: [{ id, label, nodes }], oldest first
future Array Redo stack: [{ id, label, nodes }], oldest first

Actions

Init and serialization

Action Description
initFromWindowData() Reads window.TemplateBuilderData and seeds all store state on mount
serializeToJson() Returns the node tree serialized to a JSON string (used for dirty-check comparison)
buildSavePayload() Returns { id, name, category, builder_type, supports, schema } — the AJAX POST body
markAsSaved(newTemplateId?) Called after a successful save. Updates savedSnapshot, savedName, savedCategory, isDirty=false. Updates templateId if this was a brand-new template.

Node CRUD

Every action below that mutates nodes also pushes an undo/redo history entry — see History / Undo-Redo.

Action Description
addNode(type) Creates a new node of the given type and inserts it based on the current selection: nothing selected → root; div selected → last child of that div; leaf selected → sibling after the leaf. Sets selectedNodeId to the new node. Snapshots immediately ('Add Node').
removeNode(id) Removes a node and all its descendants from the tree. Snapshots immediately ('Remove Node').
moveNode(id, parentId, newIndex) Moves a node to a new position — used by the DnD drag-end handler. Snapshots immediately ('Move Node') once the move target is confirmed valid.
updateNode(id, fields) Merges new field values into an existing node — called automatically (debounced) by NodeSettingsDialog's auto-apply as the user edits fields. Uses a debounced snapshot ('Edit Node') so a whole editing burst collapses into one undo entry.
setNodes(nodes) Replaces the entire node tree — called once per completed drag by useSortableTree's drag-end handler (not fired mid-drag). Snapshots immediately ('Move Node').

Style clipboard

Action Description
copyNodeStyles(nodeId) Copies all style-tab field values from the node into styleClipboard. Uses getStyleFieldNames() to identify which keys belong to the style tab so content fields are never included. Does not mutate nodes, so no history entry is pushed.
pasteNodeStyles(nodeId) Applies styleClipboard.values onto the target node. Does nothing if the clipboard is empty or the node types don't match. Snapshots immediately ('Paste Style') once those guards pass.

Selection

Action Description
selectNode(id) Sets selectedNodeId
setHoveredNodeId(id) Sets hoveredNodeId

UI state

Action Description
setTemplateName(name) Updates name and marks isDirty: true. Not part of the undo/redo history — only nodes is tracked.
setCategory(category) Updates category and marks isDirty: true. Not part of the undo/redo history.
setPreviewPost(previewPost) Replaces the sample post data used by the canvas

History

See History / Undo-Redo for the full behavior. Every action that mutates nodes calls snapshot(label) (or the debounced variant) first to push the current tree onto the undo stack.

Action Description
snapshot(label) Saves the current nodes to past[] before any mutation. Clears future[]. Flushes any pending debounced edit first so it is not lost or merged into the wrong entry. Caps past[] at 50 entries.
undo() Flushes any pending debounced edit, then pops past[], pushes current nodes to future[], restores the popped tree
redo() Flushes any pending debounced edit, then pops future[], pushes current nodes to past[], restores the popped tree
jumpToHistory(targetIndex) Flushes any pending debounced edit, then computes the equivalent past/future arrays for an arbitrary jump in a single set() call (not a loop of undo()/redo() calls, which would re-render every subscriber once per step). Used by the History panel's click-to-jump feature.

Debounced edit snapshots: updateNode calls scheduleDebouncedSnapshot(label) instead of snapshot(label) directly, since NodeSettingsDialog's auto-apply calls it on every field change (debounced ~150ms). The pre-edit tree is captured once when the burst starts, and the history entry is only pushed to past[] after a 500ms pause in edits — so typing in the settings panel collapses into a single undo entry instead of one per debounced commit. A single module-scope timer is shared (only one node settings panel can be open at a time), so switching to a different node mid-edit flushes the in-progress burst as its own entry rather than merging it with the new one under the wrong label.

Internal tree helpers (not exported)

  • findNode(nodes, id) — finds a node anywhere in the tree by id, or null.
  • findParentContext(nodes, id) — returns { parent, list, index } for any node id. parent is null for root-level nodes.
  • removeNodeFromTree(nodes, id) — returns a new tree with the node and its descendants removed.
  • updateNodeInTree(nodes, id, fields) — returns a new tree with the matching node updated. Also prunes breakpoint keys that were explicitly cleared (set to '') so the device cascade falls back to the desktop base.
  • countNodesOfType(nodes, type) — used to auto-generate admin labels ("excerpt", "excerpt1", "excerpt2", …).
  • collectSupports(nodes) — collects all leaf node types present in the tree as a comma-separated string.
  • serializeNode(node) — converts one node to the PHP schema format (strips children from leaves, converts values to strings, omits empty keys).
  • loadHierarchy(schema) — converts the PHP schema format back to the internal tree format (always ensures children: [] on every node, converts id to number).

UI Components

TemplateBuilder.jsx

File: admin/assets/js/src/template-builder/TemplateBuilder.jsx

The root component. Renders:

  • BuilderTopbar — shared topbar shell. Left slot: template name <input>. Right slot: Settings dropdown + SaveButton.
  • Settings dropdown — contains a category <select> and PreviewPostPicker. Opens on clicking the gear icon.
  • Body — left sidebar (NodeTree + AddNodeMenu, or HistoryPanel when toggled), canvas (CanvasPreview + DevicePreviewControls), and a right-side SettingsSidebar wrapping NodeSettingsDialog.

editingNodeId is local state in TemplateBuilder. Clicking any NodeItem row calls openNodeSettings(nodeId), which sets it. The sidebar title is derived from the editing node's type. NodeSettingsDialog receives nodeId as a prop and renders its form content inside the sidebar.

activePanel ('components' | 'history') is also local state, controlling which panel occupies the left sidebar slot. The sidebar header has a History toggle button (sanilwb-tb-history-toggle-btn) next to the "Components" label that switches to 'history'; the HistoryPanel's own close button switches back to 'components'.

Keyboard shortcuts: TemplateBuilder attaches a keydown listener to the parent window for Ctrl+Z (undo) and Ctrl+Y / Ctrl+Shift+Z (redo), skipped when focus is in an INPUT/TEXTAREA/SELECT/contentEditable element. Because the canvas preview lives in an iframe with its own window, CanvasPreview.jsx attaches a matching listener to iframe.contentWindow once the iframe loads — see its section below.

NodeTree.jsx

File: admin/assets/js/src/template-builder/components/NodeTree.jsx

Renders the flat/hierarchical list of node items. Drag and drop is handled by useSortableTree (from shared hooks), which uses @dnd-kit/core. On drag end, moveNode(id, parentId, newIndex) is called. Nodes can be reordered within their parent but cannot be reparented via drag — that would require a separate drop target.

NodeItem.jsx

File: admin/assets/js/src/template-builder/components/NodeItem.jsx

Each row in the node tree. Shows the node's icon, resolved label (resolveNodeLabel()), and an ellipsis dropdown with:

Action Description
Copy styles Calls copyNodeStyles(nodeId)
Paste styles Calls pasteNodeStyles(nodeId) — disabled when styleClipboard is empty or type doesn't match
Delete Calls removeNode(nodeId) with a window.confirm guard

Clicking the row itself calls both selectNode(nodeId) and onSettingsOpen(nodeId), opening the settings sidebar for that node. The ellipsis dropdown uses stopPropagation so opening the menu does not also open the settings panel.

div nodes also show a child-count indicator and a collapse toggle. The selected node is highlighted via selectedNodeId.

AddNodeMenu.jsx

File: admin/assets/js/src/template-builder/components/AddNodeMenu.jsx

Dropdown button that lists all node types available for the current builderType. Calls getAvailableNodeTypes(builderType) from nodeTypes.js to get the filtered list. Clicking an entry calls addNode(type).

NodeSettingsDialog.jsx

File: admin/assets/js/src/template-builder/components/NodeSettingsDialog.jsx

Renders the content of the settings panel for the selected node — no outer chrome. The SettingsSidebar shell in TemplateBuilder.jsx provides the header, title, and X button. NodeSettingsDialog returns only:

<DeviceProvider initialValues={savedValues} itemId={nodeId}>
    <DialogBody tabs={tabs} extraValues={extraValues} autoApply onSave={handleSave} onClose={onClose} />
</DeviceProvider>

The autoApply prop (shared Dialog.jsx behavior, also used by Page Builder's settings sidebar) means there is no Save/Cancel footer — every field change is debounced (~150ms) and calls onSave automatically. handleSave calls updateNode(nodeId, serializedValues) on every one of those debounced commits, so the store (and therefore the canvas) always reflects the panel's current values without the user clicking anything. Closing the panel is handled entirely by SettingsSidebar's own header X button — onClose never needs to be called from inside the dialog itself.

Tab config comes from getNodeDialogTabs(type, builderType, imageSizes).

builderType is passed as an extraValue (read-only, not saved) so showIf: '__builderType=template' and showIf: '__builderType=titlebar' conditions work correctly to hide/show fields depending on which builder is active.

CanvasPreview.jsx

File: admin/assets/js/src/template-builder/components/CanvasPreview.jsx

Renders an <iframe> that shows a live visual preview of the current node tree. The canvas loads ?sanilwb_preview=1 and communicates with the parent via postMessage (it does not mount a React root inside the iframe the way Page Builder's IframeCanvas.jsx does).

Live update pipeline: The component subscribes to the store. Because NodeSettingsDialog auto-applies every field change straight into nodes (debounced), the store is always the single source of truth — no buffer-overlay merging is needed here. On every change: 1. renderCanvas(nodes, opts) — converts nodes to an HTML string. 2. buildNodeStyles(nodes, device) — converts nodes to a CSS string. 3. Both strings are injected into the iframe via postMessage, debounced 100ms. If the nodes reference is unchanged since the last full render (i.e. only CSS-affecting values changed), only the CSS string is re-sent (sanilwb:update-css) instead of replacing the HTML — this avoids an innerHTML replacement flickering images on every keystroke.

Preview post: Sample post data from store.previewPost is passed to renderCanvas so placeholder content (title, excerpt, author, date, topic) reflects a real post shape.

Undo/redo keyboard shortcut inside the iframe: keydown events that originate inside the iframe fire on the iframe's own window, not the parent's — they never bubble to TemplateBuilder.jsx's listener. CanvasPreview therefore attaches its own Ctrl+Z / Ctrl+Y / Ctrl+Shift+Z listener to iframe.contentWindow inside handleIframeLoad(), calling useTemplateBuilderStore.getState().undo() / .redo() directly. It is removed on unmount via the removeKeyDownListener ref. This mirrors the parent listener's input-field guard so typing inside canvas-rendered fields is not intercepted.

DevicePreviewControls.jsx

File: admin/assets/js/src/template-builder/components/DevicePreviewControls.jsx

Desktop / tablet / mobile toggle buttons. Writes to useDeviceStore. The canvas reads the active device and applies responsive overrides when rendering styles and HTML.

Mode Canvas iframe width
desktop unconstrained
tablet 768 px
mobile 375 px

SaveButton.jsx

File: admin/assets/js/src/template-builder/components/SaveButton.jsx

Handles the entire save flow. Since NodeSettingsDialog auto-applies every field change directly into the store as it happens, there is nothing left to flush before saving — SaveButton calls buildSavePayload() directly and POSTs to window.ajaxurl with action sanilwb_template_save.

On success, calls markAsSaved(returnedId). The button is disabled when !isDirty or a save is already in flight.

State machine: idlesavingsaved (1.5 s) → idle.

PreviewPostPicker.jsx

File: admin/assets/js/src/template-builder/components/PreviewPostPicker.jsx

Lives inside the settings dropdown. Lets the user search for a real post to use as the canvas preview source. Calls sanilwb_search_posts and sanilwb_get_post_preview AJAX actions. On selection, calls setPreviewPost(postData) to update the canvas sample data.

HistoryPanel.jsx

File: admin/assets/js/src/template-builder/components/HistoryPanel.jsx

Thin wrapper that connects the shared admin/assets/js/src/shared/components/HistoryPanel.jsx presentational component to useTemplateBuilderStore's past, future, and jumpToHistory. All the timeline rendering logic (flat list, current-entry highlight, jump-index math) lives in the shared component — this file only supplies the store data and Template-Builder-specific empty-state copy. Page Builder's page-builder/components/HistoryPanel.jsx is the same kind of thin wrapper around the same shared component, so the ~90 lines of timeline logic exist in exactly one place.

Rendered in place of NodeTree in the left sidebar when activePanel === 'history' (see TemplateBuilder.jsx above). Its close button switches activePanel back to 'components'.


History / Undo-Redo

Every mutation to nodes — add, remove, move, paste-style, or an auto-applied field edit — is undoable. The implementation mirrors Page Builder's usePageBuilderStore history stack (see mkdocs/docs/features/page-builder.md) one-to-one, keyed on nodes instead of sections:

  • Store: past[] / future[] arrays of { id, label, nodes } entries, capped at 50. snapshot(label), undo(), redo(), and jumpToHistory(targetIndex) actions — see the Store Actions tables above for the exact mechanics, including the debounced-snapshot collapsing used by updateNode.
  • UI: the HistoryPanel.jsx component (above) rendered in the left sidebar, toggled via a header button next to "Components".
  • Keyboard shortcuts: Ctrl+Z (undo), Ctrl+Y / Ctrl+Shift+Z (redo) — wired in both TemplateBuilder.jsx (parent window) and CanvasPreview.jsx (the canvas iframe's own window), since keydown events originating inside the iframe never bubble to the parent. See those two components' sections above for the exact hook points.
  • What is not tracked: name and category (template metadata) are not part of the undo/redo history — only the visual nodes tree is. This matches Page Builder, where only sections is tracked.

Node Type Registry

File: admin/assets/js/src/template-builder/config/nodeTypes.js

NODE_TYPES is a plain object keyed by type string. Each entry drives the Add menu, default values on creation, dialog tabs, canvas rendering, and compiler output.

Exported functions

Function Purpose
getNodeDialogTabs(type, builderType, imageSizes?) Returns the full assembled tab config for a node's settings dialog. Deep-copies the node's dialogTabs, resolves image size options for image nodes at runtime, appends SHARED_STYLE_FIELDS into the style tab, and appends SHARED_ADVANCED_TAB last.
getNodeDefaults(type, builderType) Returns the initial field values for a new node. Uses defaultValuesTitlebar if builderType === 'titlebar' and the node type defines it; otherwise uses defaultValues.
getAvailableNodeTypes(builderType) Returns all node type configs whose builderTypes array includes the given builder type. Used by AddNodeMenu.
getStyleFieldNames(type, builderType) Returns a flat array of all style-tab field name strings for a node type (including __tablet and __mobile variants for responsive fields). Used by copyNodeStyles to know exactly which keys belong to the style tab.

Shared field groups

These constants can be spread into any node's style tab fields array:

Constant What it adds
SIZE_SECTION_FIELDS A "Size" section header (with device toggle) + Width, Height, and Aspect Ratio inputs. Spread into any leaf node's style tab to give it standard sizing controls.
ASPECT_RATIO_FIELD Standalone Aspect Ratio input-group (W and H sub-inputs, responsive). Included in SIZE_SECTION_FIELDS and also added to the div node's Size section individually.
FLEX_ITEM_FIELDS Flex Shrink, Flex Grow, Flex Basis, Align Self — for any node inside a flex container
GRADIENT_FIELDS Gradient angle, start color, end color, color stops — for any node with a background color
buildFontFields(prefix) Font Size, Font Family, Font Weight, Font Style — for any text node. Returns four field configs using the given prefix (e.g. 'heading'heading-font-size, heading-font-family, etc.)

Node types reference

Type Builder types canHaveChildren Key content fields Key style fields
div template, titlebar true direction, align, justify, flex-wrap, gap, bgcolor, box-shadow, min/max-width, min/max-height, aspect-ratio
heading template, titlebar false link (wrap in post link); source_text (titlebar only) width, height, aspect-ratio, heading-color, heading-hover-color, font fields
image template false image-size, link width, height, aspect-ratio, animate-hover, box-shadow
excerpt template false lines (line clamp: 2–5) width, height, aspect-ratio, excerpt-color, font fields, text-align
date template false format (time_ago/readable), show-icon width, height, aspect-ratio, date-color, font fields
author template false avatar, link width, height, aspect-ratio, author-color, author-hover-color, font fields
button template, titlebar false btn-text, btn-link (template only) width, height, aspect-ratio, btn-bg-color, btn-hover-bg-color, btn-color, btn-hover-color, gradient, font fields, display, text-align
topic template false topic-link width, height, aspect-ratio, topic-bg-color, topic-hover-bg-color, topic-color, topic-hover-color, gradient, font fields, display, text-align
carousel_nav titlebar only false nav-prev-text, nav-next-text width, height, aspect-ratio, nav-size, nav-font-size, nav-color, nav-hover-color, nav-bg-color, nav-hover-bg-color

Notes: - All leaf node types (every type except div) include a Size section via SIZE_SECTION_FIELDS: Width, Height, and Aspect Ratio (W × H input-group). The div node has its own Size section with the same Aspect Ratio field. - Aspect Ratio is stored as two separate fields: aspect-ratio-x (W) and aspect-ratio-y (H). Both the JS preview and the PHP compiler only apply the ratio when both values are positive integers. - For image nodes, the aspect-ratio CSS is emitted by buildNodeStyles.js (not as an inline style) so it updates live in the canvas preview without requiring a full HTML re-render. When neither custom field is set, the ratio is auto-computed from the registered WordPress image size dimensions. - heading has defaultValuesTitlebar which sets source: 'taxonomy' — in a titlebar, the heading shows the section/category name instead of the post title. - button.btn-link is hidden in titlebar builder via showIf: '__builderType=template' — titlebar buttons get their URLs from the Page Builder widget instead. - carousel_nav applies its border fields to the .sanilwb-carousel-prev and .sanilwb-carousel-next child buttons rather than its own wrapper div. - All node types include SHARED_STYLE_FIELDS (spacing/borders) and SHARED_ADVANCED_TAB (visibility + admin label + CSS class) via getNodeDialogTabs().


Canvas Rendering

renderCanvas.js

File: admin/assets/js/src/template-builder/utils/renderCanvas.js

Pure function: renderCanvas(nodes, opts) → HTML string.

Converts the hierarchical node tree into the HTML injected into the canvas iframe. Every node is rendered to its semantic HTML element with data-node-id="{id}" for CSS targeting.

  • Uses previewPost sample data (Nepali placeholder text by default) to fill in titles, excerpts, author names, dates, and topics.
  • Checks sanilwb_show_{device} flags via isHiddenForDevice(node) and adds .sanilwb-tbve-hidden to hidden nodes.
  • Empty tree renders a placeholder "Add a component to start building." message.

Per-type HTML output:

Type HTML element Notes
div <div class="sanilwb-tbve-div"> Recursively renders children inside
image <div class="sanilwb-tbve-image-wrap"> wrapping <img> Uses admin/assets/images/sample-post-image.jpg as the sample src; inline style contains only width (for initial layout); aspect-ratio is intentionally emitted as a CSS rule by buildNodeStyles.js so live-preview updates work without a full HTML re-render
heading <h3 class="sanilwb-tbvisual-heading"> Wraps in <a> if link=1; uses source_text for titlebar mode
excerpt <p class="sanilwb-tbvisual-excerpt"> Line-clamp applied inline
date <span class="sanilwb-tbvisual-date"> Shows Bootstrap icon if show-icon=1
author <span class="sanilwb-tbvisual-author"> Renders initials avatar and name
button <a> (if btn-link=post) or <span> Uses btn-text value
topic <a> or <span class="sanilwb-tbvisual-button sanilwb-tbvisual-topic">
carousel_nav <span> wrapping .sanilwb-carousel-prev + .sanilwb-carousel-next buttons

buildNodeStyles.js

File: admin/assets/js/src/template-builder/utils/buildNodeStyles.js

Pure function: buildNodeStyles(nodes, device, opts?) → CSS string.

The opts object currently accepts one key: imageSizes (the WordPress registered image sizes object). This is required for the image node to compute its default aspect-ratio when no custom ratio is set.

Generates CSS rules targeting [data-node-id="X"] for every node in the tree. Uses CSS rules rather than inline styles so :hover pseudo-classes work naturally in the canvas.

Applies the device cascade (mobile → tablet → desktop) via getBreakpointValue(node, fieldName) before emitting any property. Shared helpers:

  • buildSizeCss(node) — emits width, height, and aspect-ratio (from aspect-ratio-x/aspect-ratio-y). Called for all leaf node types except image (which has its own size logic).
  • buildTypographyCss(node, prefix) — emits color, font-size, font-weight, font-family, font-style.
  • buildLinearGradientCss(node) — emits background-image: linear-gradient(...). Requires both gradient-color-1 and gradient-color-2 to be set.
  • buildAdvancedTabCss(node) — emits margin-top, margin-bottom, padding-top/bottom, padding-left/right.
  • buildFlexItemCss(node) — emits flex-shrink, flex-grow, flex-basis, align-self.
  • buildBoxShadowCss(node) — emits box-shadow using the SHADOWS preset map (small/medium/large).
  • buildHoverLinkColorRule(selector, node, field) — emits selector a:hover { color: ... }.
  • buildHoverColorsRule(selector, node, bgField, colorField) — emits selector:hover { ... }.

image node special case: aspect-ratio is emitted as a CSS rule here (not as an inline style in renderCanvas.js). This is intentional — the CanvasPreview optimization sends CSS-only updates (sanilwb:update-css) when only field values change, so anything that must update during live typing must live in the CSS layer. When aspect-ratio-x/y are set, those values are used; otherwise the ratio is auto-computed from the registered image size dimensions.

carousel_nav special case: Border fields are redirected to the .sanilwb-carousel-prev and .sanilwb-carousel-next child selectors rather than the outer wrapper, since the wrapper is invisible and the buttons are the visible interactive elements.


PHP Compiler

SANILWB_Template_Compiler

File: includes/class-sanilwb-template-compiler.php

Compiles a node tree (schema_json) into a self-contained PHP template file on disk. The compiled file lives at:

wp-content/sanilwb-templates/template-{id}.php

The directory is protected: an .htaccess denies direct browser access, and an index.php silences directory listings.

Public API:

SANILWB_Template_Compiler::compile(int $id, string $name, array $nodes, string $type = 'template'): bool

Dispatches to compile_template() or compile_titlebar() based on $type.

What the compiled layout template does:

The compiled file receives $args (from the shortcode) and $query (a WP_Query object). It: 1. Reads grid config from $args['columns'], $args['gap_horizontal'], $args['gap_vertical']. 2. Reads wrapper style from $args['margin_top'], $args['margin_bottom'], $args['padding_vertical'], $args['padding_horizontal'], $args['bck_color'], $args['bck_border_radius']. 3. Opens a CSS grid wrapper. 4. Loops while ($query->have_posts()) and renders each post card using the compiled node HTML. 5. Calls wp_reset_postdata() after the loop.

What the compiled titlebar template does:

Receives $args only (no $query). Renders the titlebar HTML once, reading heading text from $args['heading_text'] (if provided) or falling back to the WordPress archive/category title.

CSS compilation:

compile_css(int $id, array $nodes) generates per-node CSS from the same field values and writes it to wp-content/sanilwb-templates/template-{id}.css. This CSS file is enqueued by the shortcode renderer when the template is used. It mirrors the logic in buildNodeStyles.js so the canvas preview matches compiled output.

The compiler reads breakpoint overrides from apply_breakpoint_overrides($nodes) and emits them inside @media blocks matching the compiler's breakpoint values:

Breakpoint Media query
tablet @media (max-width: 1024px)
mobile @media (max-width: 767px)

These match the canvas preview widths (tablet: 768 px, mobile: 375 px) — the compiler emits max-width: 1024px so the tablet override kicks in at the same viewport width where the canvas switches to tablet simulation.

Hover rules are breakpoint-aware too. heading-hover-color, author-hover-color, btn-hover-bg-color/btn-hover-color, topic-hover-bg-color/topic-hover-color, and nav-hover-color/nav-hover-bg-color (plus heading-color/author-color's own link-targeting fix, see below) are built by a shared build_hover_rules(array $node, string $selector): string method, called once for the desktop pass and again for each breakpoint that has any override, using a $node already resolved through apply_breakpoint_overrides(). This was previously inlined directly in collect_css_rules() and only ever read the base (desktop) hover-color key — the dialog fields have always allowed a device toggle on these (responsive: true in nodeTypes.js) and the canvas preview (buildHoverLinkColorRule()/buildHoverColorsRule() in buildNodeStyles.js) already resolved them per-device, but the compiled frontend CSS silently ignored the breakpoint override and always applied the desktop hover color everywhere. build_hover_rules() closes that gap so the compiled output now matches what the canvas always showed.

Heading/Author link color targets <a> directly, not just the parent. A color set on heading-color/author-color only reaches a wrapped link's <a> tag via CSS inheritance, and the browser's own default link color beats a merely-inherited value — so build_hover_rules() also emits selector a { color:...!important } (not just the :hover variant) whenever the node is linked, at all three breakpoints. Page Builder's Heading and Post Title widgets have the identical fix, added for the same reason — see build_typography_responsive_style() in class-sanil-website-builder-public.php.

Font sync: After compiling, SANILWB_Template_Compiler calls SANILWB_Font_Sync::update_entity_fonts() to queue any Google Fonts used in the template for download.


Save / AJAX

AJAX action: sanilwb_template_save Nonce: window.TemplateBuilderData.nonce (action: sanilwb_templates) Handler: SANILWB_Ajax::template_save()

The save payload from buildSavePayload():

{
  action:       'sanilwb_template_save',
  nonce:        window.TemplateBuilderData.nonce,
  id:           templateId,           // 0 for new templates
  name:         'Template Name',
  category:     'default',
  builder_type: 'template',           // or 'titlebar'
  supports:     'image,heading,excerpt', // comma-separated leaf node types
  schema:       '[{"type":"div",...}]', // JSON string
}

On success the handler: 1. Upserts the row in wp_sanilwb_templates via SANILWB_DB. 2. Calls SANILWB_Template_Compiler::compile(id, name, decoded_schema, builder_type). 3. Returns { success: true, data: { id: newId } }.

SaveButton then calls markAsSaved(returnedId). If templateId was 0, the returned id updates the store so subsequent saves use the correct DB id.


List Page

File: admin/templates/template-builder/list-page.php

The template list page uses SANILWB_Templates_List_Table (a WP_List_Table subclass) for the table UI. Actions available per template: Edit, Trash, Duplicate. Trashed templates can be Restored or Permanently Deleted.


Adding a New Node Type — Checklist

  1. nodeTypes.js — Add an entry to NODE_TYPES with type, label, icon (Lucide icon component), canHaveChildren, builderTypes, defaultValues, and dialogTabs. getAvailableNodeTypes() auto-discovers it — no other registration needed. For leaf nodes, spread ...SIZE_SECTION_FIELDS into the style tab fields so the node gets standard Width / Height / Aspect Ratio controls consistent with all other node types.

  2. class-sanilwb-template-compiler.php — Add a case 'your-type': in the HTML rendering switch (~line 602). Add a build_your_type_style() method and call it from the style-dispatch block (~line 401).

  3. buildNodeStyles.js — Add a if (type === 'your-type') block in walkNodeTree() to generate the JS-side CSS for the live canvas preview. Mirror the compiler's logic exactly.

  4. renderCanvas.js — Add a if (type === 'your-type') block in renderNode() to generate the HTML element the canvas will show.

  5. Test: Add the node in the builder, configure its settings, verify the canvas preview matches, save, and confirm the frontend renders correctly when the template is used in the Posts widget.

Adding a New Field to an Existing Node — Checklist

  1. nodeTypes.js — Add the field config object to the node's dialogTabs array. Add a default value to defaultValues.

  2. class-sanilwb-template-compiler.php — Read the new field in build_{type}_style() (or in the HTML case if it affects markup). The compiler reads directly from node data — it does NOT read the JS config.

  3. buildNodeStyles.js — Add JS-side CSS generation for the live canvas preview. Only add special handling here if the field can't map directly to a CSS property.