Creating Add-ons for Super Speedy Imports
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
- Table of Contents
- Add-on vs Template — Which Do I Need?
- Add-on Architecture Overview
- File Locations and Loading
- Detecting SSI is Active
- The Additional Options Round-Trip
- Injecting UI Into the Media Section
- Mutating the Import Config Before Stages Run
- Enqueuing Admin Assets
- Adding or Removing Stages from an Add-on
- Filtering SSI’s Generated SQL
- SSI Version Requirements
- Hooks and Filters Reference
- Complete Example: Delayed Image Import
- Tips and Best Practices
Table of Contents
- Add-on vs Template — Which Do I Need?
- Add-on Architecture Overview
- File Locations and Loading
- Detecting SSI is Active
- The Additional Options Round-Trip
- Injecting UI Into the Media Section
- Mutating the Import Config Before Stages Run
- Enqueuing Admin Assets
- Adding or Removing Stages from an Add-on
- Filtering SSI’s Generated SQL
- SSI Version Requirements
- Hooks and Filters Reference
- Complete Example: Delayed Image Import
- 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_image → merchant_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
- 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 persistsadditional_options.defer_image_downloads = trueinto the import row. - Reload — User reopens the import config. SSI fetches the saved
additional_optionsblob and exposes it asdata.saved_additional_options. The DII JS reads the value and pre-checks the box. - Run — User clicks Import. SSI parses config, fires
ssi_apply_additional_options. DII’s PHP callback rewritesconfig['media'][...]→config['post_meta'][delayed_*]and returns. SSI re-evaluates flags (nowhas_featured_images = false). Stages run:load-csvpopulatesdelayed_image_url/delayed_gallery_image_urlscolumns from CSV;insert-postmeta/update-postmetawrite them to wp_postmeta via bulk SQL;upload-remote-imagesandattach-gallery-imagesskip themselves. - 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
- Always prefer mutating
$template->configover registering a stage. SSI’s existing stages handle the result correctly. Adding a stage means a new code path you have to maintain. - 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-wrapperalready existing is the pattern. - Don’t touch
has_featured_images/has_gallery_images/has_postmetadirectly in yourssi_apply_additional_optionscallback. SSI re-evaluates them from$template->configafter your callback returns. Mutating both creates ambiguity if they disagree. - Read saved values from
data.saved_additional_options.data.config.additional_optionsonly holds definitions for keys you registered via PHPgetAdditionalOptionsFields(). Reading from the wrong place is the #1 add-on bug. - Use
ssi-sectioned-additional-optionas your wrapper class — it’s the magic that makes SSI’s collector pick up your input automatically. - Declare
Requires Plugins: super-speedy-importsin your plugin header. WordPress 6.5+ shows users a clear dependency chain; for older WP it’s still useful documentation. - List
super-speedy-imports-jsas 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. - 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. - 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.
- 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.