Tutorial: Updating an Existing Node Type

This tutorial walks you through adding a new field to an existing node type in the Template Builder. Adding a field is a smaller change than creating a whole node type, but it still touches three files and they must all stay in sync.

We will add a letter spacing field to the Excerpt node so editors can control the space between characters in post excerpts.

Follow every step in order.


Before You Start

Why three files?

Every node type in this system spans three separate layers:

Layer File Purpose
Dialog definition nodeTypes.js Declares which fields appear in the settings dialog
PHP compiler class-sanilwb-template-compiler.php Reads the saved field values and writes them into the compiled CSS that visitors see
JS preview buildNodeStyles.js Reads the same field values and generates CSS on the fly so the canvas iframe shows a live preview while editing

When you add a field, all three layers must be updated. If you only update the dialog, the field will appear in the UI but will have no effect on the output. If you only update the compiler, the frontend will render correctly but the canvas preview will be wrong.

Responsive fields vs. non-responsive fields

Setting responsive: true on a field means the editor stores three separate values:

  • letter-spacing — the desktop value
  • letter-spacing__tablet — the tablet override
  • letter-spacing__mobile — the mobile override

When responsive: false, only one value is stored. Use responsive: true for any visual property that a designer might want to adjust per device (font size, colour, spacing). Use responsive: false for content choices that don't change by device (e.g. a toggle for whether to show an icon).


Step 1 — Add the field to the dialog in nodeTypes.js

Open admin/assets/js/src/template-builder/config/nodeTypes.js.

Search for excerpt: inside the NODE_TYPES object to jump to it. Inside it, find dialogTabs. The excerpt has a content tab and a style tab. We are adding a visual field, so we add to the style tab.

The style tab's fields array currently looks like this (abbreviated):

{
    id: 'style',
    label: 'Style',
    fields: [
        { type: 'section', label: 'Typography', showDeviceToggle: true },
        { name: 'excerpt-color', type: 'color', ... },
        // buildFontFields() expands to font-size, font-family, font-weight, font-style fields
        ...buildFontFields( 'excerpt' ),
        { name: 'excerpt-text-align', type: 'select', ... },

        ...FLEX_ITEM_FIELDS,   // <-- flex child controls always go last
    ],
},

Add the letter-spacing field inside the Typography section, right after excerpt-text-align and before the ...FLEX_ITEM_FIELDS spread. The FLEX_ITEM_FIELDS block should always stay last in the list so the flex controls are grouped together at the bottom.

{
    // letter-spacing: how many pixels of space to add between each character.
    // We use 'number' type with a 'px' unit label on the right of the input.
    // responsive: true stores a separate value per device breakpoint.
    name: 'excerpt-letter-spacing',
    type: 'number',
    label: 'Letter Spacing',
    responsive: true,
    unit: 'px',
},

After your change, the fields array should look like:

fields: [
    { type: 'section', label: 'Typography', showDeviceToggle: true },
    { name: 'excerpt-color', type: 'color', label: 'Text Color', responsive: true },
    // buildFontFields() expands to: excerpt-font-size, excerpt-font-family, excerpt-font-weight, excerpt-font-style
    ...buildFontFields( 'excerpt' ),
    { name: 'excerpt-text-align', type: 'select', label: 'Text Align', responsive: true, options: TEXT_ALIGN_OPTIONS },
    {
        name: 'excerpt-letter-spacing',
        type: 'number',
        label: 'Letter Spacing',
        responsive: true,
        unit: 'px',
    },
    ...FLEX_ITEM_FIELDS,
],

Do you need a default value?

If the field should start with a non-empty value when a new node is created, add it to defaultValues inside the excerpt node config. Letter spacing has no natural default (no spacing = empty), so we leave defaultValues unchanged. If a field truly needs a starting value (e.g. a button text that should say "Read More" on creation), add it there.


Step 2 — Read the field in the PHP compiler

Open includes/class-sanilwb-template-compiler.php.

Search for function build_node_css to jump to the method. Inside it, find the if ( $type === 'excerpt' ) block. It currently ends after the text-align declaration:

if ( $type === 'excerpt' ) {
    // ... line-clamp code above ...
    // ... colour, font-size, font-weight, font-family, font-style above ...
    $text_align = (string) ( $node['excerpt-text-align'] ?? '' );
    if ( $text_align !== '' && in_array( $text_align, [ 'left', 'center', 'right' ], true ) ) {
        $style .= 'text-align:' . $text_align . ';';
    }
}                 // <-- add your new line before this closing brace

Add the letter-spacing handling inside the if block, right after the text-align lines:

// Letter spacing — stored in pixels, e.g. '2'. Cast to float to support decimal values.
// preg_replace strips anything that's not a digit or decimal point to prevent CSS injection.
$letter_spacing_raw = preg_replace( '/[^0-9.]/', '', (string) ( $node['excerpt-letter-spacing'] ?? '' ) );
if ( $letter_spacing_raw !== '' ) {
    $letter_spacing = (float) $letter_spacing_raw;
    if ( $letter_spacing > 0 ) {
        $style .= 'letter-spacing:' . $letter_spacing . 'px;';
    }
}

Why preg_replace instead of (int) here?

Letter spacing can be a decimal value (e.g. 0.5px, 1.5px). Casting to int would strip the decimal part and 1.5 would become 1. We use preg_replace to allow digits and the dot, then cast to float. For whole-number-only fields (like font size) (int) is simpler and sufficient.

What about responsive values?

The compiler handles responsive values with media queries in collect_css_rules(), not here. The build_node_css() method only generates the base (desktop) CSS declaration. The responsive cascade (tablet, mobile overrides via media queries) is handled by the generic responsive loop inside collect_css_rules() — you don't need to add anything there for a standard field.


Step 3 — Read the field in the JS preview

Open admin/assets/js/src/template-builder/utils/buildNodeStyles.js.

Search for if ( type === 'excerpt' ) inside walkNodeTree() to jump to the right block. It currently ends after the text-align line and the buildAdvancedTabCss() call:

if ( type === 'excerpt' ) {
    // ... line-clamp above ...
    // buildTypographyCss() handles excerpt-color, excerpt-font-size, excerpt-font-weight,
    // excerpt-font-family, excerpt-font-style in one call.
    const typographyCss = buildTypographyCss( node, 'excerpt' );
    if ( typographyCss ) {
        cssDeclarations += typographyCss + ';';
    }
    const textAlign = getBreakpointValue( node, 'excerpt-text-align' );
    if ( textAlign ) {
        cssDeclarations += `text-align:${ sanitizeValue( textAlign ) };`;
    }
    const advancedTabCss = buildAdvancedTabCss( node );
    if ( advancedTabCss ) {
        cssDeclarations += advancedTabCss + ';';
    }
}       // <-- add your new lines before the buildAdvancedTabCss call

Add the letter-spacing lines before the buildAdvancedTabCss() call so all type-specific fields are grouped together:

// Letter spacing — getBreakpointValue() handles the device cascade automatically:
// reading 'excerpt-letter-spacing__mobile', then '__tablet', then base
// depending on which device is currently selected in the preview toggle.
const letterSpacing = getBreakpointValue( node, 'excerpt-letter-spacing' );
if ( letterSpacing !== '' ) {
    // parseFloat allows decimal values (e.g. 0.5, 1.5).
    const letterSpacingFloat = parseFloat( letterSpacing );
    if ( ! isNaN( letterSpacingFloat ) && letterSpacingFloat > 0 ) {
        cssDeclarations += `letter-spacing:${ letterSpacingFloat }px;`;
    }
}

After your change, the end of the excerpt block should look like this:

if ( type === 'excerpt' ) {
    // ... line-clamp above ...

    // buildTypographyCss() handles color, font-size, font-weight, font-family, font-style.
    const typographyCss = buildTypographyCss( node, 'excerpt' );
    if ( typographyCss ) {
        cssDeclarations += typographyCss + ';';
    }

    const textAlign = getBreakpointValue( node, 'excerpt-text-align' );
    if ( textAlign ) {
        cssDeclarations += `text-align:${ sanitizeValue( textAlign ) };`;
    }

    const letterSpacing = getBreakpointValue( node, 'excerpt-letter-spacing' );
    if ( letterSpacing !== '' ) {
        const letterSpacingFloat = parseFloat( letterSpacing );
        if ( ! isNaN( letterSpacingFloat ) && letterSpacingFloat > 0 ) {
            cssDeclarations += `letter-spacing:${ letterSpacingFloat }px;`;
        }
    }

    const advancedTabCss = buildAdvancedTabCss( node );
    if ( advancedTabCss ) {
        cssDeclarations += advancedTabCss + ';';
    }
}

Keep the buildAdvancedTabCss() call last inside each type block. It adds margin and padding from the Advanced tab, and those should always come after the type-specific declarations.


Step 4 — Build the JavaScript

Run the build from the plugin root:

# Production build (use before committing or deploying)
npm run build

# Or use watch mode while actively making changes (rebuilds on every save)
npm run start

Checklist before you test

  • [ ] Field added to the excerpt style tab in nodeTypes.js
  • [ ] Field read and emitted in the if ( $type === 'excerpt' ) block in build_node_css() in the compiler
  • [ ] Field read and emitted in the if ( type === 'excerpt' ) block in walkNodeTree() in buildNodeStyles.js
  • [ ] npm run build completed without errors

Testing it

  1. Open any template in the Template Builder that has (or add) an Excerpt node.
  2. Click the pencil/settings icon on the Excerpt node to open its dialog.
  3. Go to the Style tab. You should see a "Letter Spacing" input in the Typography section.
  4. Type 2 in the input and watch the canvas — the excerpt text should spread out.
  5. Switch to the tablet preview toggle and set a different value. Save. Open a post on the frontend and resize the browser window to confirm the responsive value applies correctly.

Troubleshooting

The field doesn't appear in the dialog

Check that the field object has name, type, and label. A missing name will cause the field to be silently skipped. Also confirm the field is inside the fields array of the correct tab object.

The canvas preview doesn't respond when I type a value

You may have a JS error in buildNodeStyles.js. Open the browser DevTools console (or right-click inside the canvas iframe and choose Inspect) and look for errors. Also confirm you ran npm run build after editing the file.

The frontend ignores the value even though the dialog shows it

The PHP compiler block is missing or reading the wrong key. Double-check that $node['excerpt-letter-spacing'] exactly matches the field name you declared in nodeTypes.js. Keys are case-sensitive and must include the full hyphenated name.

Responsive values (tablet/mobile) don't apply on the frontend

This is handled automatically by the generic responsive loop in collect_css_rules() inside the compiler. That loop reads the __tablet and __mobile suffixed keys for all fields and wraps them in the appropriate @media query. You don't need to add anything for standard responsive fields — if it's not working, check that the field is marked responsive: true in nodeTypes.js (not responsive: false).


Section order matters for the default-open behavior

Sections in node settings dialogs open and collapse when clicked. The first section on each tab opens by default; all other sections start collapsed. This is handled automatically — FormFields passes defaultOpen={true} only to the first FormSection it renders.

This means the field you add will be immediately visible if it lands in the first section of its tab, and collapsed (but accessible) if it lands in any section below that. Place high-priority or frequently-used fields in the first section of the tab, and secondary or advanced fields in later sections.


What if I need to change an existing field, not add a new one?

Changing a field label or options list

Edit only nodeTypes.js. The label and options are purely UI — they don't affect stored values or compiled output. No PHP or JS changes needed.

Renaming a field's name

This is a breaking change for saved templates. Any template saved with the old field name will have the value stored under the old key. After renaming, those templates will lose the value.

If you need to rename, either: - Write a one-time migration script that reads the old key, writes the new key, and clears the old one - Or support both keys in the PHP and JS by reading $node['new-key'] ?? $node['old-key'] ?? ''

Removing a field entirely

  1. Remove the field from dialogTabs in nodeTypes.js.
  2. Remove the corresponding CSS output line from the compiler's build_node_css().
  3. Remove the corresponding line from buildNodeStyles.js.
  4. Existing saved values for that field in the database are harmless — they'll be ignored.

Changing a field from non-responsive to responsive

This is also a breaking change: existing saved templates store only a single value under the base key. After the change, the system will still read the base key for desktop, so existing templates won't break — they'll just lack tablet/mobile overrides (which will fall back to the base value anyway). This is generally safe.