Create a New Preset

This tutorial walks through creating a brand-new preset from scratch. The example builds a second header preset called minimal. The same steps apply to any preset type — footer, single, page, archive, 404, or search — with minor differences noted along the way.


Step 1 — Create the Folder

Presets are discovered by folder name. Create a new subdirectory inside the matching type folder:

public/presets/header/minimal/

The folder name (minimal) becomes the preset slug. It must be lowercase, contain only letters, numbers, and hyphens, and be unique within its type.


Step 2 — Write manifest.json

Create public/presets/header/minimal/manifest.json. This file controls everything the admin UI shows for this preset.

{
    "label": "Minimal",
    "description": "A clean header with logo and a single nav menu.",
    "slot_groups": [
        {
            "label": "Header",
            "slots": [
                {
                    "id": "header_bg",
                    "type": "color",
                    "label": "Background",
                    "default": "#ffffff",
                    "breakpoints": ["desktop", "tablet", "mobile"]
                },
                {
                    "id": "header_padding",
                    "type": "text",
                    "label": "Padding (top/bottom)",
                    "default": "16px",
                    "breakpoints": ["desktop", "tablet", "mobile"]
                }
            ]
        },
        {
            "label": "Navigation",
            "slots": [
                {
                    "id": "primary_menu",
                    "type": "menu_select",
                    "label": "Menu",
                    "default": "",
                    "breakpoints": ["desktop"]
                },
                {
                    "id": "nav_link_color",
                    "type": "color",
                    "label": "Link Color",
                    "default": "var(--color-text)",
                    "breakpoints": ["desktop"]
                }
            ]
        }
    ]
}

Rules for slot_groups:

  • Every slot must have a non-empty breakpoints array. Slots without one are silently ignored.
  • Slot id values must be unique within the preset. They become the keys in $settings.
  • Use var(--color-*) defaults to pull colors from Theme Options automatically.
  • For content-type presets (single, page, archive, 404, search) that have no configurable settings, set "slot_groups": [].

See Manifest Reference for every available slot type and all breakpoint options.


Step 3 — Write index.php

Create public/presets/header/minimal/index.php. This is the PHP template that renders the actual HTML on the frontend.

The template always receives two local variables:

  • $settings — merged slot values (manifest defaults merged with whatever the user has saved).
  • $common — site-wide data: logo_html, logo_url, site_name, nav_menus, widget_areas.
<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * @var array $settings Merged slot values from manifest defaults + saved DB values.
 * @var array $common   Site-wide data: logo_html, logo_url, site_name, nav_menus, widget_areas.
 */

// Read responsive slot values using get_bp_var().
// Returns an array keyed by 'desktop', 'tablet', 'mobile'.
$header_bg      = get_bp_var( $settings, 'header_bg' );
$header_padding = get_bp_var( $settings, 'header_padding' );

// Read non-responsive slot values directly from $settings.
$primary_menu_id = (int) ( $settings['primary_menu'] ?? 0 );
$nav_link_color  = $settings['nav_link_color'] ?? 'inherit';

// Emit responsive CSS custom properties as a <style> block.
// This is the standard pattern used by all presets in this plugin.
$breakpoints = [
    'desktop' => '',
    'tablet'  => '@media (max-width: ' . SANILWB_BP_TABLET_MAX . 'px)',
    'mobile'  => '@media (max-width: ' . SANILWB_BP_MOBILE_MAX . 'px)',
];

?>

<style>
    <?php foreach ( $breakpoints as $bp => $media ) : ?>
        <?php if ( $media ) echo $media . ' {'; ?>
        .minimal-header {
            --minimal-header-bg: <?php echo esc_attr( $header_bg[ $bp ] ); ?>;
            --minimal-header-padding: <?php echo esc_attr( $header_padding[ $bp ] ); ?>;
        }
        <?php if ( $media ) echo '}'; ?>
    <?php endforeach; ?>
</style>

<header class="minimal-header">
    <div class="container">
        <div class="d-flex justify-content-between align-items-center">

            <a href="<?php echo esc_url( home_url( '/' ) ); ?>" class="minimal-header__logo">
                <?php echo $common['logo_html']; ?>
            </a>

            <nav class="minimal-header__nav" style="--minimal-nav-color: <?php echo esc_attr( $nav_link_color ); ?>">
                <?php
                wp_nav_menu( [
                    'menu'        => $primary_menu_id,
                    'container'   => null,
                    'menu_class'  => 'minimal-header__menu',
                    'items_wrap'  => '<ul class="%2$s">%3$s</ul>',
                ] );
                ?>
            </nav>

        </div>
    </div>
</header>

Important patterns:

  • Always check SANILWB_SL_Renderer::is_preview() before outputting widget area placeholders, so the admin preview iframe shows useful placeholder UI instead of a blank space.
  • Use get_bp_var() for any slot whose breakpoints array includes more than ["desktop"] — it gives back the [desktop, tablet, mobile] array ready for the CSS custom property loop.
  • Use $common['logo_html'] for the logo rather than calling get_custom_logo() directly.
  • Never echo unescaped user input. Use esc_html(), esc_attr(), esc_url() as appropriate.

Step 4 — Write style.scss

Create public/presets/header/minimal/style.scss. Write all layout and visual CSS for this preset here. Use the CSS custom properties you emitted from index.php.

.minimal-header {
    background: var(--minimal-header-bg);
    padding: var(--minimal-header-padding) 0;

    &__logo {
        text-decoration: none;

        img {
            max-height: 50px;
            width: auto;
        }
    }

    &__menu {
        list-style: none;
        margin: 0;
        padding: 0;
        display: flex;
        gap: 1.5rem;

        a {
            color: var(--minimal-nav-color);
            text-decoration: none;
            font-weight: 500;

            &:hover {
                opacity: 0.75;
            }
        }
    }
}

Compile the CSS:

npm run build:css

This compiles every style.scss inside every preset subfolder in place. After running it you should see public/presets/header/minimal/style.css appear.


Step 5 — Register Nav Menus and Widget Areas (optional)

If your preset needs WordPress nav menus or widget areas, create public/presets/header/minimal/hooks.php. The plugin auto-loads this file very early — before after_setup_theme and widgets_init — so all registrations must go inside add_action() calls.

<?php
if ( ! defined( 'ABSPATH' ) ) exit;

add_action( 'after_setup_theme', function () {
    // Register any nav menu locations this preset uses.
    register_nav_menus( [
        'minimal_primary' => 'Minimal Header — Primary Menu',
    ] );
} );

add_action( 'widgets_init', function () {
    // Register any widget areas (sidebars) this preset uses.
    register_sidebar( [
        'id'            => 'sanilwb_minimal_cta',
        'name'          => 'Sanil WB Minimal Header CTA',
        'description'   => 'Used by the Minimal header preset — right-side CTA area.',
        'before_widget' => '<div class="sanilwb-widget">',
        'after_widget'  => '</div>',
        'before_title'  => '',
        'after_title'   => '',
    ] );
} );

Nav menu locations registered here will appear in WordPress's Appearance → Menus (or the nav block editor). Widget areas will appear in Appearance → Widgets.


Step 6 — Add Demo Sidebar Content (optional)

If you registered widget areas in Step 5, you can ship default content for them so users can import it with one click. Add a demo_sidebars key to manifest.json:

{
    "label": "Minimal",
    "description": "...",
    "slot_groups": [ ... ],
    "demo_sidebars": {
        "sanilwb_minimal_cta": [
            {
                "type": "block",
                "content": "<!-- wp:buttons --><div class=\"wp-block-buttons\"><!-- wp:button --><div class=\"wp-block-button\"><a class=\"wp-block-button__link wp-element-button\">Subscribe</a></div><!-- /wp:button --></div><!-- /wp:buttons -->"
            }
        ]
    }
}

Each key in demo_sidebars must match a registered sidebar ID. Each entry in the array is one widget instance. The type field is either "block" (Gutenberg block markup) or "text" (classic text widget with title and text keys).

When the user clicks Import Demo Content in the Header & Footer admin panel, the handler replaces the sidebar's current content with these demo widgets.


Step 7 — Verify the Preset Appears in the Admin

Go to WordPress Admin → Header & Footer → Header tab. Your new "Minimal" preset should appear in the preset picker. Selecting it will show the slot groups you defined in manifest.json. Clicking Preview Changes will load the preview iframe using your index.php template.

If the preset does not appear, check:

  1. The folder is a direct child of public/presets/header/ (not nested deeper).
  2. manifest.json is valid JSON — use a JSON validator if unsure.
  3. Every slot has a non-empty breakpoints array.

Creating a content-type preset follows the same steps with two differences:

  1. The folder goes inside public/presets/{type}/ instead of public/presets/header/ or public/presets/footer/.
  2. Content presets currently have no configurable settings, so manifest.json should have "slot_groups": [].

The index.php for a content preset is rendered between get_header() and get_footer() in the theme template. It still receives $settings (empty for now) and $common, and it is free to use any WordPress template functions (the_title(), the_content(), have_posts(), etc.).

For archive and search presets that want to use the shared post loop and view toggle, include the shared partials:

$shared_dir = dirname( dirname( __DIR__ ) ) . '/_shared';

include $shared_dir . '/view-toggle.php';
include $shared_dir . '/post-loop.php';
include $shared_dir . '/view-toggle-script.php';

Set $no_posts_message and $no_posts_message_np before including post-loop.php to control the "no results" text.