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:
- Reads the active preset slug from the WordPress option
sanilwb_{type}_preset. - Resolves the full preset config — file path, CSS URL, slots — via
SANILWB_SL_Presets::get(). - 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). - Calls
SANILWB_SL_Presets::build_common()to get site-wide data (logo, nav menus, widget areas). - Calls
requireon the preset'sindex.php, injecting$settingsand$commonas 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 slugtype— the current tab typesettings— 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) callSANILWB_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 asdata). - Resolves the active preset slug in priority order:
?presetquery param → saved preset slug (verified againstallPresets) → first discovered preset. - Computes
initialValuesas 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}__mobilekeys. - Wraps
PresetPanelin<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)
- Calls
serialize(responsiveFieldIds)to get the current flat slot values. - POSTs to
admin-ajax.phpwith actionsanilwb_save_sl_preset,type,preset, andsettings(JSON). - On success, calls
setSavedValues(serialize(responsiveFieldIds))— updating this state causesisDirtyto re-evaluate tofalse. - Refreshes the preview iframe URL with a new
_ttimestamp 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
- Add a new
caseinSlotRendererinSiteLayout.jsx, returning the appropriate field component. - If the field needs data from PHP (e.g. a new list of options), add it to
window.SanilWbSiteLayoutinadmin/class-sanilwb-admin-site-layout.php→enqueue_site_layout_assets(). - Document the new type in the preset manifest reference so preset authors know to use it.