Creating Add-ons for Super Speedy Imports

June 5, 2026

This guide explains how to write add-ons for Super Speedy Imports. An add-on is a separate plugin (or theme code) that extends SSI’s behaviour without changing how data maps to a content type. If you want to define a new content-type mapping, see Creating Custom Templates instead — the two patterns complement each other.

Table of Contents

  1. Add-on vs Template — Which Do I Need?
  2. Add-on Architecture Overview
  3. File Locations and Loading
  4. Detecting SSI is Active
  5. The Additional Options Round-Trip
  6. Injecting UI Into the Media Section
  7. Mutating the Import Config Before Stages Run
  8. Enqueuing Admin Assets
  9. Adding or Removing Stages from an Add-on
  10. Filtering SSI’s Generated SQL
  11. SSI Version Requirements
  12. Hooks and Filters Reference
  13. Complete Example: Delayed Image Import
  14. Tips and Best Practices

Add-on vs Template — Which Do I Need?

You want to… Build a
Import a new content type (CPT, taxonomy with extra meta, etc.) Template — extend SSI_PostTemplate or SSI_ImportTemplate.
Add a checkbox / option that influences any import (Houzez, WooCommerce, plain posts) Add-on. Hook ssi_apply_additional_options and inject UI via ssi:media_section_rendered.
Defer expensive work to after the import finishes Add-on. Register a stage at a late priority via ssi_register_stages.
Replace SSI’s built-in image-download path with your own Add-on. Mutate $template->config['media'] to redirect into post_meta, then handle it on the consumer side. (This is what Delayed Image Import does — see Complete Example.)
Add fields to a specific template only Either: extend the template (Template guide), or target it from an add-on by checking instanceof in your hook callbacks.
Modify how delete / update SQL behaves Add-on. Hook ssi_filter_deletes or ssi_filter_updates.

The simple test: if your code only matters during one specific content type’s import, write a template. If it spans content types or modifies behaviour orthogonally, write an add-on.


Add-on Architecture Overview

An SSI add-on is a normal WordPress plugin that registers callbacks on SSI’s hooks. There’s no base class to extend, no auto-discovery, and no registry — SSI exposes a set of action and filter hooks, and the add-on hooks them.

A typical add-on consists of:

  • A main plugin file that bootstraps the add-on’s classes/callbacks.
  • One or more PHP classes holding the hook handlers (or just plain functions).
  • Optional JS loaded into the SSI admin screen for UI injection.
  • Optional CSS for any UI you inject.

There is no template-style auto-discovery — you choose where files live and enqueue them yourself via ssi_admin_enqueue_scripts.

my-ssi-addon/
├── my-ssi-addon.php          # Plugin main file: bootstraps + registers hooks
├── includes/
│   └── class-my-ssi-addon.php
└── assets/
    ├── js/
    │   └── my-ssi-addon-admin.js
    └── css/
        └── my-ssi-addon-admin.css

File Locations and Loading

Add-on files can live anywhere — your own plugin, a theme, or a must-use plugin. SSI doesn’t impose a directory structure.

Bootstrap from your plugin’s main file

<?php
/**
 * Plugin Name: My SSI Add-on
 * Description: Adds X feature to Super Speedy Imports.
 * Requires Plugins: super-speedy-imports
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

require_once plugin_dir_path( __FILE__ ) . 'includes/class-my-ssi-addon.php';

add_action( 'plugins_loaded', function () {
    ( new My_SSI_Addon() )->register_hooks();
}, 20 );

plugins_loaded priority 20 ensures SSI’s main file has loaded first. SSI itself initialises on init priority 5 (via the SuperSpeedySettings submodule), so by the time your hooks fire there’s no race.

When the user has SSI inactive

You don’t need a guard. Hooks that no one fires are free — your callbacks simply never run. If your add-on does only SSI things, consider showing an admin notice when SSI is inactive so users understand why nothing happens, but the registration itself is safe to do unconditionally.


Detecting SSI is Active

When you do need to gate a code path on SSI being present (e.g. before showing a settings panel, or before a class_exists()-style check), the cheapest tests are:

// Cheapest — works as soon as SSI's import-template-abstract.php has been required.
if ( class_exists( 'SSI_ImportTemplate' ) ) { ... }

// Standard WP test — slightly heavier, requires plugin.php loaded.
if ( is_plugin_active( 'super-speedy-imports/super-speedy-imports.php' ) ) { ... }

class_exists( 'SSI_ImportTemplate' ) is the recommended check. SSI’s main file requires import-template-abstract.php early, so the class is available throughout the request lifecycle.


The Additional Options Round-Trip

Most add-ons need a per-import setting (a checkbox, a select). SSI exposes two ways to add one:

Option A: register a definition (the formal way)

If your add-on is template-aware (extends or works closely with one template), define the option from PHP via the template’s getAdditionalOptionsFields(). SSI will render the control, persist its value, and re-render it on reload — all automatically. See Additional Options in the Templates guide.

This path is appropriate when the option belongs to a template you control.

Option B: inject the control into the DOM (the cross-cutting way)

If your add-on works across templates (a checkbox in the Media section regardless of which template the user picked), define no PHP definition. Instead, append a <input> / <select> to the Media section in JS, wrapped in an element with class ssi-sectioned-additional-option.

SSI’s ssi_collectAdditionalOptions() (in assets/super-speedy-imports.js) scans for that class at save time and persists every input it finds into the import’s additional_options JSON automatically. You don’t have to write any save code.

<div class="ssi-sectioned-additional-option my-addon-wrapper">
  <label>
    <input type="checkbox" name="my_addon_toggle" value="1" />
    Enable my add-on for this import
  </label>
</div>

After save, the import’s stored additional_options blob will contain:

{
  "my_addon_toggle": true
}

The save vs load asymmetry — read this carefully

When SSI re-renders the import config form, it sends two different things to the JS:

Path What’s in it Use it for
data.config.additional_options[key] Option definitions (objects with label, section, value, etc.). Only populated for keys registered via PHP getAdditionalOptionsFields(). Definition-based options (Option A).
data.saved_additional_options[key] The raw saved values (booleans / strings) for every key that was persisted, regardless of whether it has a definition. DOM-injected options (Option B) — this is where your value actually is.

If your add-on uses Option B (DOM injection), the value you wrote at save time lives in data.saved_additional_options, not in data.config.additional_options. Reading from the wrong path is a silent bug — the option saves correctly but appears unchecked on every reload.

// CORRECT for DOM-injected options
var checked = !!(data && data.saved_additional_options && data.saved_additional_options.my_addon_toggle);

// INCORRECT — data.config.additional_options.my_addon_toggle is undefined
// (no definition was registered), so checked is always false on reload.
var checked = !!(data && data.config && data.config.additional_options && data.config.additional_options.my_addon_toggle);

Defensive read that handles both:

var v;
if (data && data.saved_additional_options && typeof data.saved_additional_options.my_addon_toggle !== 'undefined') {
    v = data.saved_additional_options.my_addon_toggle;
} else if (data && data.config && data.config.additional_options && data.config.additional_options.my_addon_toggle) {
    var def = data.config.additional_options.my_addon_toggle;
    v = (typeof def === 'object' && def !== null && 'value' in def) ? def.value : def;
}
var checked = (v === true || v === '1' || v === 1 || v === 'true');

PHP / JSON round-trips can produce true, '1', 1, or 'true' depending on platform and serialiser, so the truthy check covers all four.


Injecting UI Into the Media Section

SSI fires a JS event on document after rendering the Media section in the import config UI:

// SSI fires this — your add-on listens.
jQuery(document).trigger('ssi:media_section_rendered', [mediaSectionContent, data]);

Your admin JS:

jQuery(function ($) {
    $(document).on('ssi:media_section_rendered', function (e, $container, data) {
        if (!$container || !$container.length) return;
        if ($container.find('.my-addon-wrapper').length) return; // idempotent

        var $wrapper = $('<div>', { 'class': 'ssi-sectioned-additional-option my-addon-wrapper' });
        var $cb = $('<input>', { type: 'checkbox', name: 'my_addon_toggle', value: '1' });

        // Read saved value — see "The save vs load asymmetry" above.
        var v = (data && data.saved_additional_options && data.saved_additional_options.my_addon_toggle);
        if (v === true || v === '1' || v === 1) $cb.prop('checked', true);

        $wrapper.append($('<label>').append($cb).append(' Enable my feature'));
        $container.append($wrapper);
    });
});

The event arguments are:

Argument Type Description
$container jQuery The Media section’s content <div> — append your UI to this.
data object The full import data object, including data.csv_headers, data.config, data.saved_additional_options, etc.

Other UI hook points

Hook Where it fires Use for
ssi:renderSection Per template-defined section in the import config Template-specific section UI (covered in the Templates guide).
ssi:media_section_rendered After Media section rendered Cross-cutting controls relating to images.
SSITemplates.addAdditionalOptions(data, templateMappings) callback After Additional Options section rendered Cross-cutting controls that don’t relate to images. (Templates guide section “JavaScript Extension Points”.)

If none of those land your control where you want it, fall back to a MutationObserver watching #ssi-form-fields-container. Avoid this if you can — events are cheaper and survive layout changes better.


Mutating the Import Config Before Stages Run

SSI fires a PHP action after parsing the import config (after image flags are set, before stages execute, before the batch table is created):

do_action( 'ssi_apply_additional_options', $template );

The receiver gets the SSI_ImportTemplate instance. Public properties you can read or mutate:

Property Type Purpose
$template->additional_options array Saved values from the user’s import config (the JSON we discussed in the previous section, decoded). Read-only by convention.
$template->config array The mapping config: config['media'], config['post_meta'], config['taxonomies'], post fields, etc. Mutate this to change what gets imported.
$template->has_featured_images bool Auto-recomputed by SSI after the action returns; you don’t need to set it.
$template->has_gallery_images bool Auto-recomputed.
$template->has_postmeta bool Auto-recomputed.

After your callback returns, SSI re-evaluates the three has_* flags from the (possibly mutated) $template->config. So you only need to mutate $config; the flags follow.

Pattern: gate behaviour on a saved option

add_action( 'ssi_apply_additional_options', function ( $template ) {
    if ( empty( $template->additional_options['my_addon_toggle'] ) ) {
        return;
    }
    // ... mutate $template->config or otherwise act ...
} );

Pattern: redirect a media field into post_meta

This is the core of Delayed Image Import. The user mapped featured_imagemerchant_image_url. With the toggle on, we want SSI to write the URL string into a post-meta key instead of downloading the image.

add_action( 'ssi_apply_additional_options', function ( $template ) {
    if ( empty( $template->additional_options['defer_image_downloads'] ) ) {
        return;
    }

    if ( ! isset( $template->config['post_meta'] ) || ! is_array( $template->config['post_meta'] ) ) {
        $template->config['post_meta'] = array();
    }

    if ( ! empty( $template->config['media']['featured_image'] ) ) {
        $template->config['post_meta']['delayed_image_url'] = $template->config['media']['featured_image'];
        unset( $template->config['media']['featured_image'] );
    }
    if ( ! empty( $template->config['media']['gallery_images'] ) ) {
        $template->config['post_meta']['delayed_gallery_image_urls'] = $template->config['media']['gallery_images'];
        unset( $template->config['media']['gallery_images'] );
    }
} );

After this: – The batch table will have delayed_image_url / delayed_gallery_image_urls columns instead of featured_image / gallery_images. – SSI’s insert-postmeta and update-postmeta stages will write the URL strings to wp_postmeta via bulk SQL, exactly as for any other post-meta mapping. – The image-download stages (upload-remote-images, attach-gallery-images) will skip themselves because has_featured_images and has_gallery_images re-evaluate to false. – No new SSI stage was registered, no code path duplicated.

Pattern: redirect a multi-source field

The Featured Image is a single source (one CSV column). Gallery Images is multi-source (an array of CSV column entries). SSI’s load-csv stage handles array-of-list values in both the media and post_meta branches — it joins entries with | before writing the column. As long as your add-on simply moves the value across, you don’t need to pre-process anything.

If you’re targeting SSI < 2.34 you may find the post_meta branch doesn’t yet support array-of-list — the fix is in stages/core/load-csv.php and shipped in 2.34. Declare Requires Plugins: super-speedy-imports and a minimum SSI version in your plugin header to make this explicit.


Enqueuing Admin Assets

SSI fires this action only after its own admin screen check passes:

do_action( 'ssi_admin_enqueue_scripts', $hook );

Hook into it instead of registering a admin_enqueue_scripts handler that re-implements the same screen check:

add_action( 'ssi_admin_enqueue_scripts', function ( $hook ) {
    wp_enqueue_script(
        'my-ssi-addon-admin',
        plugins_url( 'assets/js/my-ssi-addon-admin.js', __FILE__ ),
        array( 'super-speedy-imports-js', 'jquery' ),
        '1.0.0',
        true
    );
    wp_enqueue_style(
        'my-ssi-addon-admin',
        plugins_url( 'assets/css/my-ssi-addon-admin.css', __FILE__ ),
        array(),
        '1.0.0'
    );
} );

Listing super-speedy-imports-js as a dependency guarantees your script loads after SSI’s main JS — important if your script binds to events SSI fires.


Adding or Removing Stages from an Add-on

Add a stage from outside the template

Your add-on can register a stage on any template using the same hooks the templates themselves use. Stages registered this way run alongside the template’s built-in stages and respect the priority ordering.

// Adds a stage to every Post-derived template (SSI_PostTemplate, WooCommerce, custom CPT, etc.).
add_action( 'ssi_register_post_stages', function ( $template ) {
    $template->registerStage(
        'my-addon-cleanup',     // unique name
        2000,                   // priority — higher = later
        function ( $tpl ) {
            global $wpdb;
            // ... your stage body ...
        },
        'My add-on cleanup stage'
    );
} );

// Adds a stage to every template (post-based or otherwise).
add_action( 'ssi_register_stages', function ( $template ) { /* ... */ } );

// WooCommerce-only.
add_action( 'ssi_register_woocommerce_stages', function ( $template ) { /* ... */ } );

Use the same $template->registerStage( $name, $priority, $callback, $description ) API documented in the Templates guide. The callback receives the template instance.

Remove a stage your add-on shouldn’t run

add_filter( 'ssi_filter_stages_to_run', function ( $stages, $config, $template ) {
    if ( ! empty( $template->additional_options['my_addon_toggle'] ) ) {
        unset( $stages['upload-remote-images'] );
        unset( $stages['attach-gallery-images'] );
    }
    return $stages;
}, 10, 3 );

This is a sledgehammer — preferred when you’ve fully replaced what those stages did. For more nuanced changes, prefer mutating $template->config in ssi_apply_additional_options so SSI’s normal skip logic kicks in (the way DII does it).


Filtering SSI’s Generated SQL

For lower-level tweaks, SSI exposes filters on the SQL it generates for delete and update operations:

// Add a WHERE clause to the delete query.
// $where_clause is appended; return it modified.
add_filter( 'ssi_filter_deletes', function ( $where_clause, $import_id ) {
    return $where_clause . " AND p.post_status != 'private'";
}, 10, 2 );

// Modify the SET clause of the bulk-update query.
add_filter( 'ssi_filter_updates', function ( $set_clause ) {
    return $set_clause . ', post_modified = NOW()';
} );

Use sparingly. These run inside the inner loop of large imports — anything heavy here multiplies by row count.


SSI Version Requirements

The hooks documented here landed in specific SSI versions:

Hook Available since
ssi_register_templates 1.x
ssi_register_stages / ssi_register_post_stages / ssi_register_woocommerce_stages 1.x
ssi_filter_stages_to_run 1.x
ssi_filter_deletes / ssi_filter_updates 1.x
ssi_import_utils 2.x
ssi:renderSection (JS) 2.x
ssi_apply_additional_options 2.34
ssi:media_section_rendered (JS) 2.34
ssi_admin_enqueue_scripts 2.34
post_meta array-of-list support in load-csv.php 2.34

Declare your add-on’s SSI requirement in the plugin header:

/**
 * Plugin Name: My SSI Add-on
 * Description: …
 * Requires Plugins: super-speedy-imports
 */

For runtime checks, you can detect a specific hook’s availability with did_action after init:

add_action( 'admin_notices', function () {
    if ( class_exists( 'SSI_ImportTemplate' ) && ! has_action( 'ssi_admin_enqueue_scripts' ) ) {
        echo '<div class="notice notice-warning"><p>My SSI Add-on requires Super Speedy Imports 2.34 or later.</p></div>';
    }
} );

Hooks and Filters Reference

A condensed list, focused on the hooks add-ons actually use. The Templates guide has a fuller list of template-internal hooks.

PHP actions

// Fires after SSI parses the import config and sets image flags, before stages run
// and before the batch table is created. Mutate $template->config here.
do_action( 'ssi_apply_additional_options', $template );

// Fires inside SSI's enqueue_scripts() AFTER the screen check passes.
// Use this to enqueue admin JS/CSS without re-implementing the screen detection.
do_action( 'ssi_admin_enqueue_scripts', $hook );

// Fires during template construction. Add custom stages.
do_action( 'ssi_register_stages',            $template );
do_action( 'ssi_register_post_stages',       $template );
do_action( 'ssi_register_woocommerce_stages', $template );

// Fires before / after stages execute for an import.
do_action( 'ssi_before_import_stages', $template );
do_action( 'ssi_after_import_stages',  $template );

PHP filters

// Modify which stages run for an import.
add_filter( 'ssi_filter_stages_to_run', function ( $stages, $config, $template ) {
    return $stages;
}, 10, 3 );

// Modify SQL generated by SSI's delete / update operations.
add_filter( 'ssi_filter_deletes', function ( $where, $import_id ) { return $where; }, 10, 2 );
add_filter( 'ssi_filter_updates', function ( $set_clause ) { return $set_clause; } );

// Register one-shot import utilities (run from the Tools tab).
add_filter( 'ssi_import_utils', function ( $utils ) {
    $utils['my-utility'] = array(
        'slug'    => 'my-utility',
        'message' => 'Running my utility...',
        'file'    => plugin_dir_path( __FILE__ ) . 'utilities/my-utility.php',
    );
    return $utils;
} );

JavaScript events (jQuery)

// Per-section rendering during import config form generation.
$(document).on('ssi:renderSection', function (event) {
    // event.sectionName, event.fields, event.postFields, event.container,
    // event.importData, event.templateMappings, event.csvHeaders
});

// After Media section rendered.
$(document).on('ssi:media_section_rendered', function (e, $container, data) {
    // $container is the Media section content div.
    // data is the full import data object.
});

Complete Example: Delayed Image Import

Delayed Image Import (DII) is the canonical SSI add-on. It defers image downloads from import time to first page-view. The integration with SSI is two callbacks — about 70 lines of code total — using all of the patterns above.

Folder structure (DII)

delayed-image-import/
├── delayed-image-import.php
├── includes/
│   └── class-ssi-integration.php
└── assets/
    └── js/
        └── dii-ssi-admin.js

Bootstrap

// delayed-image-import.php — relevant excerpt
require_once __DIR__ . '/includes/class-ssi-integration.php';

add_action( 'plugins_loaded', function () {
    ( new DII_SSI_Integration() )->register_hooks();
}, 20 );

PHP integration (includes/class-ssi-integration.php)

<?php
class DII_SSI_Integration {

    public function register_hooks() {
        add_action( 'ssi_apply_additional_options', array( $this, 'redirect_to_delayed_meta' ), 10, 1 );
        add_action( 'ssi_admin_enqueue_scripts',    array( $this, 'enqueue_admin_script' ),     10, 1 );
    }

    /**
     * When the user has the Defer toggle on, move image-mapping config
     * across to post_meta so SSI's normal post_meta stages do the writes.
     */
    public function redirect_to_delayed_meta( $template ) {
        if ( ! is_object( $template ) ) return;
        if ( empty( $template->additional_options['defer_image_downloads'] ) ) return;

        if ( ! isset( $template->config['post_meta'] ) || ! is_array( $template->config['post_meta'] ) ) {
            $template->config['post_meta'] = array();
        }

        if ( ! empty( $template->config['media']['featured_image'] ) ) {
            $template->config['post_meta']['delayed_image_url'] = $template->config['media']['featured_image'];
            unset( $template->config['media']['featured_image'] );
        }
        if ( ! empty( $template->config['media']['gallery_images'] ) ) {
            $template->config['post_meta']['delayed_gallery_image_urls'] = $template->config['media']['gallery_images'];
            unset( $template->config['media']['gallery_images'] );
        }
        // SSI auto-recomputes has_featured_images / has_gallery_images / has_postmeta
        // after this action returns. We don't touch them.
    }

    public function enqueue_admin_script( $hook ) {
        wp_enqueue_script(
            'dii-ssi-admin',
            plugins_url( 'assets/js/dii-ssi-admin.js', dirname( __DIR__ ) . '/delayed-image-import.php' ),
            array( 'super-speedy-imports-js', 'jquery' ),
            '1.0.0',
            true
        );
    }
}

Admin JS (assets/js/dii-ssi-admin.js)

jQuery(function ($) {
    $(document).on('ssi:media_section_rendered', function (e, $container, data) {
        if (!$container || !$container.length) return;
        if ($container.find('.dii-defer-wrapper').length) return; // idempotent

        // Read saved value — see "The save vs load asymmetry" in the guide.
        var v;
        if (data && data.saved_additional_options && typeof data.saved_additional_options.defer_image_downloads !== 'undefined') {
            v = data.saved_additional_options.defer_image_downloads;
        }
        var checked = (v === true || v === '1' || v === 1 || v === 'true');

        var $wrapper = $('<div>', {
            'class': 'ssi-sectioned-additional-option dii-defer-wrapper'
        });
        var $cb = $('<input>', {
            type: 'checkbox',
            name: 'defer_image_downloads',
            id: 'dii-defer-image-downloads',
            value: '1'
        });
        if (checked) $cb.prop('checked', true);

        var $label = $('<label>', { 'for': 'dii-defer-image-downloads' })
            .append($cb)
            .append(' Defer image downloads');

        $wrapper.append($label);
        $container.append($wrapper);
    });
});

Round-trip walkthrough

  1. Save — User checks the box and saves the import config. SSI’s ssi_collectAdditionalOptions() finds the <input name="defer_image_downloads"> inside .ssi-sectioned-additional-option, captures its boolean state, and persists additional_options.defer_image_downloads = true into the import row.
  2. Reload — User reopens the import config. SSI fetches the saved additional_options blob and exposes it as data.saved_additional_options. The DII JS reads the value and pre-checks the box.
  3. Run — User clicks Import. SSI parses config, fires ssi_apply_additional_options. DII’s PHP callback rewrites config['media'][...]config['post_meta'][delayed_*] and returns. SSI re-evaluates flags (now has_featured_images = false). Stages run: load-csv populates delayed_image_url / delayed_gallery_image_urls columns from CSV; insert-postmeta / update-postmeta write them to wp_postmeta via bulk SQL; upload-remote-images and attach-gallery-images skip themselves.
  4. First view — A visitor hits the imported product. DII’s separate page-view code (no SSI dependency) reads the delayed-meta post-meta and downloads the image at that point.

Total integration code added by DII: two PHP callbacks, one JS event listener. SSI’s existing pipeline does everything else.


Tips and Best Practices

  1. Always prefer mutating $template->config over registering a stage. SSI’s existing stages handle the result correctly. Adding a stage means a new code path you have to maintain.
  2. Keep callbacks idempotent. Hook callbacks may fire more than once if the user toggles a setting and saves multiple times — make sure your code handles being called twice with the same input. The DII JS check for .dii-defer-wrapper already existing is the pattern.
  3. Don’t touch has_featured_images / has_gallery_images / has_postmeta directly in your ssi_apply_additional_options callback. SSI re-evaluates them from $template->config after your callback returns. Mutating both creates ambiguity if they disagree.
  4. Read saved values from data.saved_additional_options. data.config.additional_options only holds definitions for keys you registered via PHP getAdditionalOptionsFields(). Reading from the wrong place is the #1 add-on bug.
  5. Use ssi-sectioned-additional-option as your wrapper class — it’s the magic that makes SSI’s collector pick up your input automatically.
  6. Declare Requires Plugins: super-speedy-imports in your plugin header. WordPress 6.5+ shows users a clear dependency chain; for older WP it’s still useful documentation.
  7. List super-speedy-imports-js as a dependency when enqueuing your admin JS so you load after SSI’s main JS and the events you bind to are guaranteed to exist.
  8. Don’t hot-reload assets in production. Use time() as the version arg only for local dev — it busts every CDN and proxy cache on every page view.
  9. Fail quietly if SSI is missing. Hooks that don’t fire cost nothing. Notices are useful for users who installed your add-on without SSI, but don’t crash the site or fatal.
  10. Test the round-trip. The most common add-on bug is “saves correctly but doesn’t reload correctly.” Always verify save → reload → run on a real import.

For more information, look at the source of Delayed Image Import (includes/class-ssi-integration.php and assets/js/dii-ssi-admin.js) for a reference implementation, or the Custom Templates guide when your work needs a new content-type mapping rather than a cross-cutting hook.

×
1/1