Header & Footer

Header & Footer is the admin UI for assigning and configuring folder-based presets. It lives at Header & Footer in the WordPress admin menu — this page was called "Site Layout" until that name was reclaimed by the new Site Layout CPT feature (visual, Page-Builder-authored templates for single/page/archive/404/search). The rename was label-only: the slug (sanilwb-site-layout), all sanilwb_*_preset options, asset handles, and AJAX action names are unchanged.

Current scope: header and footer are exclusively preset-based going forward — that will not change. Single/page/archive/404/search used to be tabs on this same page, but that folder-preset system has been fully removed for those five types — they're now handled exclusively by the Site Layout CPT, with the theme's own hard-coded templates as the fallback when nothing is assigned. See Relationship to the Site Layout CPT.


Tabs

Tab Theme hook Renderer class Has settings?
Header sanilwb_render_header SANILWB_SL_Renderer Yes — slot groups
Footer sanilwb_render_footer SANILWB_SL_Renderer Yes — slot groups

How Rendering Works

The theme fires a WordPress action hook at the right point inside header.php / footer.php:

do_action( 'sanilwb_render_header' );   // header.php
do_action( 'sanilwb_render_footer' );   // footer.php

SANILWB_SL_Renderer listens to these two hooks and always goes straight to the preset flow — there is no Site Layout CPT check for header/footer (see Relationship to the Site Layout CPT for why). Single/page/archive/404/search no longer fire a do_action at all — the theme's own template files (single.php, page.php, archive.php, 404.php, search.php) call SANILWB_Site_Layout_Renderer::render_for_type() directly instead. See Site Layout CPT → Rendering Bridge.

When the preset flow runs, the renderer:

  1. Reads the active preset slug from the WordPress option sanilwb_{type}_preset.
  2. Resolves the full preset config — file path, CSS URL, slots — via SANILWB_SL_Presets::get().
  3. Calls SANILWB_SL_Presets::get_active_settings() to get the merged slot values (manifest defaults merged with whatever the user has saved to the DB).
  4. Calls SANILWB_SL_Presets::build_common() to get site-wide data (logo, nav menus, widget areas).
  5. Calls require on the preset's index.php, injecting $settings and $common as local variables.

If no preset slug is saved yet, rendering is skipped and the theme's default output is used instead.

The $settings variable

Every preset template receives $settings as an associative array. Each key matches a slot id from manifest.json. For responsive slots, additional keys with __tablet and __mobile suffixes hold the breakpoint-specific overrides.

$settings['nav_bg']           // desktop value
$settings['nav_bg__tablet']   // tablet override
$settings['nav_bg__mobile']   // mobile override

The values are already merged: manifest defaults fill in anything the user has not explicitly saved. The template never needs to check for missing keys.

The $common variable

$common is injected alongside $settings and contains site-wide data that would be expensive or redundant to call directly from the template. It is built once per request and cached statically.

Key Type Description
logo_html string Ready-to-echo <img> tag if a custom logo is set, otherwise <span> with the site name
logo_url string Raw URL of the custom logo image, or empty string
site_name string get_bloginfo('name')
nav_menus array [ ['id' => int, 'name' => string], ... ] for all registered nav menus
widget_areas array [ ['id' => string, 'name' => string], ... ] for all registered sidebars

Preset Auto-Discovery

Presets are discovered automatically by SANILWB_Preset_Scanner::scan(). It scans public/presets/{type}/ for immediate subdirectories that contain a valid manifest.json. The folder name becomes the preset slug. No registration is required — dropping a new folder in the right place is enough. header and footer are the only preset types left under public/presets/.

The active preset slug for each type is stored in the WordPress option sanilwb_{type}_preset.

If the user visits the Header & Footer admin page and no preset has been saved yet for the current tab, the plugin automatically activates the first discovered preset so the frontend always renders something.


Preset Folder Structure

Every preset lives in public/presets/{type}/{slug}/. The required and optional files are:

public/presets/
  header/
    default/
      manifest.json          ← required: label, description, slot_groups
      index.php              ← required: the rendered HTML template
      style.scss             ← compiled to style.css by npm run build:css
      style.css              ← enqueued on every frontend page
      script.js              ← optional: enqueued in footer if present
      hooks.php              ← optional: register nav menus, widget areas, filters

For footer presets the structure is identical. This structure is now specific to header and footer — single/page/archive/404/search no longer have a preset folder at all; see Site Layout CPT for how those five types render instead.

manifest.json

Defines the preset's label, description, and all configurable settings (slot groups).

{
    "label": "Default",
    "description": "The default header layout.",
    "slot_groups": [
        {
            "label": "Main Menu",
            "slots": [
                {
                    "id": "nav_bg",
                    "type": "color",
                    "label": "Background",
                    "default": "var(--color-primary)",
                    "breakpoints": ["desktop"]
                }
            ]
        }
    ],
    "demo_sidebars": {
        "sanilwb_header_sidebar": [
            { "type": "block", "content": "<!-- wp:paragraph --><p>Sidebar content</p><!-- /wp:paragraph -->" }
        ]
    }
}

The breakpoints key controls visibility in the Site Layout screen. A slot is only shown to the user when the currently active device (desktop, tablet, or mobile) appears in that slot's breakpoints array. More importantly, if the breakpoints key is missing entirely, is set to an empty array, or contains values other than "desktop", "tablet", or "mobile", the slot is treated as invalid and is permanently hidden — it will never appear in the settings panel under any device. SANILWB_SL_Presets::get_all() filters these out during discovery, so they also never reach the frontend $settings array.

See Manifest Reference for the complete manifest.json format including all slot types and breakpoint options.

index.php

The rendered PHP template. It receives $settings (merged slot values) and $common (site-wide data) as local variables. The template should never call get_option(), wp_get_nav_menus(), or similar WP functions directly for data that $common already provides.

For responsive slot values, use the get_bp_var() helper to get an array keyed by desktop, tablet, mobile. Then emit a <style> block with media queries to apply the per-breakpoint values as CSS custom properties:

$nav_bg = get_bp_var($settings, 'nav_bg');
// Returns: ['desktop' => '#1a1a1a', 'tablet' => '#1a1a1a', 'mobile' => '#1a1a1a']

Check SANILWB_SL_Renderer::is_preview() inside the template to detect when the template is being rendered inside the admin preview iframe. Use this to show placeholder UI for empty widget areas instead of silently rendering nothing:

if ( is_active_sidebar('sanilwb_header_sidebar') ) {
    dynamic_sidebar('sanilwb_header_sidebar');
} elseif ( SANILWB_SL_Renderer::is_preview() ) {
    echo '<div class="sanilwb-sidebar-placeholder">Header Sidebar — Add Widgets</div>';
}

style.scss / style.css

Each preset manages its own stylesheet. npm run build:css compiles all style.scss files in all preset subdirectories in place. The compiled style.css is enqueued automatically on every frontend page by the renderer when the preset is active.

If a preset also has a script.js file in its folder, that script is enqueued in the footer automatically alongside the CSS.

hooks.php

Optional. Auto-loaded by the main plugin orchestrator very early in the request lifecycle — before after_setup_theme and widgets_init have fired. Use this file to register nav menus, widget areas, and any filters that this preset needs. All registrations must be wrapped in add_action() calls.

// Register nav menus for this preset.
add_action( 'after_setup_theme', function () {
    register_nav_menus( [
        'main_menu' => 'Main Menu',
    ] );
} );

// Register widget areas for this preset.
add_action( 'widgets_init', function () {
    register_sidebar( [
        'id'   => 'sanilwb_header_sidebar',
        'name' => 'Sanil WB Header Sidebar',
        // ...
    ] );
} );

The auto-loader scans all preset type directories (header, footer) and includes hooks.php from every preset folder that has one. No central registration is needed.


Preview System

Header and footer use an isolated preview endpoint. When the user clicks "Preview Changes", the React app builds a URL like:

/?sanilwb_sl_preview=default&sanilwb_sl_type=header&nav_bg=%231a1a1a&...

The plugin intercepts this request at template_redirect, verifies the user has edit_pages capability, then outputs a minimal HTML page containing only wp_head(), the preset's rendered HTML, and wp_footer(). The current (unsaved) slot values are passed as GET params and overlaid on the manifest defaults before rendering — so the preview always reflects the current panel state without needing a save first.

The admin bar is hidden inside the iframe via a direct show_admin_bar(false) call inside maybe_render_preview() itself (not a separate hook). Note: SANILWB_SL_Renderer::maybe_hide_admin_bar(), registered on init, checks for ?sanilwb_content_preview=1 — that param belonged to the now-removed content-tab preview flow and is never set by the header/footer preview URL, so that method no longer fires in practice. It's harmless leftover code, not something header/footer preview depends on.


Demo Content Import

Presets can ship default widget content via demo_sidebars in manifest.json. The "Import Demo Content" button in the admin panel triggers a form POST to admin-post.php?action=sanilwb_sl_import_demo. The handler in SANILWB_Admin_SiteLayout reads demo_sidebars, creates block or text widget instances, and writes them to the matching sidebar. Existing sidebar content is replaced entirely.

Widget types currently supported in demo_sidebars:

Type How it is stored
block widget_block option — uses Gutenberg block markup in content key
text widget_text option — uses title and text keys

Settings Persistence

When the user clicks Save in the admin panel, the React app sends an AJAX request (sanilwb_save_sl_preset action) with:

  • preset — the active preset slug
  • type — the current tab type
  • settings — a JSON object of slot values (only the keys that differ from defaults need to be included, but the JS sends all of them)

The AJAX handler saves the slug to sanilwb_{type}_preset and the settings object to sanilwb_{type}_preset_settings as a JSON string. On the next page load, get_active_settings() decodes this JSON and merges it over the manifest defaults.

The "Reset to Defaults" button posts to admin-post.php?action=sanilwb_sl_preset_reset. The handler writes an empty {} to sanilwb_{type}_preset_settings, which causes get_active_settings() to return only manifest defaults on the next load.


CSS / JS Enqueue

On every frontend page load, the renderer enqueues the active preset's compiled style.css using the handle sanilwb-{type}-preset. If the preset folder also contains a script.js, it is enqueued in the footer using the handle sanilwb-{type}-preset-js. Both are versioned using sanilwb_asset_cache_buster().


Relationship to the Site Layout CPT

The Site Layout CPT fully replaced the single/page/archive/404/search tabs that used to live on this page — that migration is complete, not in progress. Those five types no longer have a folder-preset system, an admin tab here, or a plugin class mediating their rendering:

  • The theme's own template files (single.php, page.php, archive.php, 404.php, search.php) call SANILWB_Site_Layout_Renderer::render_for_type( $type ) directly.
  • If a Site Layout CPT entry is assigned (option sanilwb_layout_assignment_{type}, published post), it renders and the theme template does nothing further.
  • If nothing is assigned, the theme includes its own hard-coded default template (partials/site-layout-defaults/{type}.php) — there is no plugin-managed fallback anymore.

Header and footer are unaffected and always will be. SANILWB_SL_Renderer has no Site Layout CPT check — those two tabs stay preset-only permanently, per Post Type Registration: SANILWB_CPT_Site_Layout::ASSIGNABLE_TYPES deliberately excludes header/footer.


Key Files

File Purpose
includes/class-sanilwb-preset-scanner.php Shared directory scanner — finds all preset folders with a valid manifest
includes/class-sanilwb-sl-presets.php Auto-discovery, settings resolution, $common builder, font list
includes/class-sanilwb-sl-renderer.php Header/footer renderer + isolated preview endpoint
includes/class-sanilwb-site-layout-renderer.php Rendering bridge for the Site Layout CPT — see Site Layout CPT
admin/class-sanilwb-admin-site-layout.php Admin page asset enqueue, window.SanilWbSiteLayout data injection, reset + demo import handlers
admin/assets/js/src/site-layout/ React app — preset picker, slot editor, preview iframe, device bar
admin/templates/site-layout/site-layout-manager.php PHP mount point for the React app
public/presets/ Header and footer preset folders only

Window Data Object

The admin page injects window.SanilWbSiteLayout before the React bundle loads. This is the single source of truth for all preset and settings data on the client side.

Key Type Description
currentTab string Active tab: 'header' or 'footer'
allPresets object All discovered presets for the current tab, keyed by slug. Each has id, label, description, slot_groups, slots, css_url
savedPreset string Slug of the currently active preset
savedSettings object Merged slot values (defaults + saved DB values) for the active preset
navMenus array [{id, name}] — all registered WordPress nav menus
sidebarOptions array [{id, name}] — all registered WordPress widget areas
fontList array Available Google Fonts — same list as the theme options font picker
previewBaseUrl string Base URL for the preview iframe — always home_url('/') for header/footer
previewItems array Always [] — header/footer have no per-item preview selector
nonce string sanilwb_templates nonce — used for the save AJAX call
ajaxUrl string WordPress admin-ajax.php URL
adminUrl string WordPress admin root URL
themeColors object Colors from Theme Options — available as var(--color-*) defaults in slot pickers
resetNonce string sanilwb_sl_preset_reset nonce
adminPostUrl string WordPress admin-post.php URL
importDemoNonce string sanilwb_sl_import_demo nonce
adminNotice object|null One-time notice shown after a redirect: {type, message} or null

React App Architecture

File: admin/assets/js/src/site-layout/SiteLayout.jsx

SiteLayout             ← root: reads window.SanilWbSiteLayout, resolves active preset, computes initialValues
└── DeviceProvider     ← seeds the device-aware field buffer; provides getValue/setValue/serialize to all descendants
    └── PresetPanel    ← all interactive logic: save, preview, reset, isDirty tracking, beforeunload guard
        └── SlotRenderer (per slot)  ← checks device visibility, maps slot.type to field component

SiteLayout (root)

  • Reads window.SanilWbSiteLayout (destructured at module level as data).
  • Resolves the active preset slug in priority order: ?preset query param → saved preset slug (verified against allPresets) → first discovered preset.
  • Computes initialValues as a flat object that seeds the field buffer:
  • If active slug matches the saved preset: uses savedSettings (DB values merged over defaults by PHP).
  • Otherwise (user is previewing a different preset): expands each slot's manifest default. Object-shaped defaults ({ desktop, tablet, mobile }) are split into {id}, {id}__tablet, {id}__mobile keys.
  • Wraps PresetPanel in <DeviceProvider initialValues={initialValues}>.

DeviceProvider / useDeviceValues

File: admin/assets/js/src/shared/hooks/useDeviceValues.jsx

A React context that provides device-aware field access to every field component and to PresetPanel.

Value Type Description
activeDevice 'desktop' \| 'tablet' \| 'mobile' Currently selected device in the device bar
setActiveDevice(device) function Changes the active device — causes SlotRenderer to re-check visibility for each slot
getValue(name) function Returns the current buffer value for the field name (resolves device suffix automatically)
setValue(name, value) function Writes a value into the buffer
serialize(responsiveFieldIds) function Returns a flat object: responsive fields produce {id}, {id}__tablet, {id}__mobile keys; non-responsive fields produce only {id}

All field components from shared/components/fields/ consume this context via useDeviceValues(). No prop-drilling is needed.

PresetPanel

The main customizer pane. Left side: preset dropdown + slot groups. Right side: preview iframe.

isDirty tracking

savedValues state holds a flat snapshot of slot values at the time of the last successful save (initialized to initialValues on mount). isDirty is memoized — on every edit it calls serialize(responsiveFieldIds) and compares every key against savedValues. Missing keys and empty strings are treated as equivalent so loading defaults never falsely marks the panel dirty.

beforeunload guard

When isDirty is true, a beforeunload handler is registered that sets e.returnValue = '' to trigger the browser's "Leave site?" dialog.

To avoid showing a redundant browser dialog on top of a custom window.confirm dialog, every action that already asks for confirmation before navigating (preset change, reset, import demo) sets isIntentionalNavRef.current = true first. The beforeunload handler checks this ref and skips if it is set.

Save flow (handleSave)

  1. Calls serialize(responsiveFieldIds) to get the current flat slot values.
  2. POSTs to admin-ajax.php with action sanilwb_save_sl_preset, type, preset, and settings (JSON).
  3. On success, calls setSavedValues(serialize(responsiveFieldIds)) — updating this state causes isDirty to re-evaluate to false.
  4. Refreshes the preview iframe URL with a new _t timestamp so the browser reloads it.

Preview flow (handlePreview)

  • Builds the isolated preview URL with all current slot values as GET params so unsaved changes are visible in the iframe without saving.
  • URL format: {previewBaseUrl}?sanilwb_sl_preview={slug}&sanilwb_sl_type={type}&{slot_key}={value}&...&_t={timestamp}

This is now the only preview path — SiteLayout.jsx no longer branches on tab type. The old single/page/archive/404/search preview code (content-tab preview URL builder, preview item dropdown, its localStorage-persisted selection) was removed along with the five dead tab links, since currentTab can now only ever be 'header' or 'footer'.

SlotRenderer

A pure function component. Receives one slot object from the manifest's slot_groups[].slots[].

First check: if slot.breakpoints exists and does not include activeDevice, returns null — the slot is hidden for the current device.

responsive is true when slot.breakpoints.length > 1. This flag is passed to the field component so it renders per-device controls.

Slot type reference

Slot type Field component Notes
color ColorField
toggle ToggleField
menu_select SelectField Options built from data.navMenus; prepends "— Select a Menu —"
sidebar_select SelectField Options built from data.sidebarOptions; prepends "— None —"
image_url ImageUrlField
select SelectField Options come from slot.options in manifest.json
layout_picker ColumnLayoutField Options come from slot.options; not responsive
shortcode TextField Placeholder: [shortcode]
font_family FontFamilyField
font_weight FontWeightField
text TextField Supports slot.description
textarea TextareaField Supports slot.description

Unrecognized slot types fall back to TextareaField.


Adding a New Preset Type — Checklist

Header and footer are the full set of preset types the PHP renderer classes support (SANILWB_SL_Renderer, SANILWB_SITE_PRESETS). Adding a new structural preset type requires changes to both the PHP renderer and the React tab bar — it is not folder-drop-only like adding a new preset within an existing type.

Adding a New Slot Type — Checklist

  1. Add a new case in SlotRenderer in SiteLayout.jsx, returning the appropriate field component.
  2. If the field needs data from PHP (e.g. a new list of options), add it to window.SanilWbSiteLayout in admin/class-sanilwb-admin-site-layout.phpenqueue_site_layout_assets().
  3. Document the new type in the preset manifest reference so preset authors know to use it.