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.
To keep another plugin’s index or actions in step with what an import writes, see Adding compatibility for other plugins.
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
- Templates That Bring Their Own Stages
- Importing Records That Are Not Posts
- Adding Term Checks to the Data Tab
- 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 SuperSpeedyImports_PostTemplate or SuperSpeedyImports_ImportTemplate. |
| Add a checkbox / option that influences any import (Houzez, WooCommerce, plain posts) | Add-on. Hook superspeedyimports_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 superspeedyimports_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 superspeedyimports_filter_deletes or superspeedyimports_filter_updates. |
| Import records that are not posts (taxonomy terms) | A template shipped in an add-on plugin, declaring record_kind — see Importing Records That Are Not Posts. |
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 superspeedyimports_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 runs after every active plugin’s main file has loaded, which is early enough for the run-time hooks in this guide: they fire when an import runs or an SSI admin screen renders.
The exception is superspeedyimports_register_templates. SSI fires it while its own main file loads, so a listener added on plugins_loaded misses it. An add-on that registers a template adds that listener when its own plugin file loads, and that file must load before SSI’s. WordPress loads active plugins in alphabetical order of their path, so a plugin folder whose name sorts before super-speedy-imports works (the Term Descriptions add-on’s ssi-addon-term-descriptions does); must-use plugins always load before regular plugins.
// In the add-on's main file, at file load - not inside plugins_loaded.
add_action( 'superspeedyimports_register_templates', function () {
require_once plugin_dir_path( __FILE__ ) . 'includes/class-my-template.php';
new My_Template();
} );
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( 'SuperSpeedyImports_ImportTemplate' ) ) { ... }
// Standard WP test — slightly heavier, requires plugin.php loaded.
if ( is_plugin_active( 'super-speedy-imports/super-speedy-imports.php' ) ) { ... }
class_exists( 'SuperSpeedyImports_ImportTemplate' ) is the recommended check. SSI’s main file requires import-template-abstract.php early, so the class is available throughout the request lifecycle.
Upgrading an add-on written before SSI 2.79.0? Every hook, class and function was renamed from
ssi_*tosuperspeedyimports_*for the WordPress.org directory’s prefix rules, with no back-compatibility aliases. Nothing errors — your callbacks are simply never called. The Delayed Image Import add-on hit exactly this: it kept hookingssi_apply_additional_options, sodefer_image_downloadsstopped queueing images and they downloaded inline instead, with no warning anywhere.Rename every
ssi_hook, class and function tosuperspeedyimports_. Do NOT rename option keys, capabilities (ssi_manage_imports,ssi_run_imports), meta keys (_ssi_unique_item_id,ssi_updated), table names (wp_ssi_*) orwp-config.phpconstants — those deliberately kept the short prefix, and renaming them breaks stored data.
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 superspeedyimports_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( 'superspeedyimports_apply_additional_options', $template );
The receiver gets the SuperSpeedyImports_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( 'superspeedyimports_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( 'superspeedyimports_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, and no image URLs in 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. – has_featured_images and has_gallery_images re-evaluate to false, so attach-existing-images and attach-gallery-images are left out of the run. upload-remote-images still runs, finds no featured-image URLs in the batch table and downloads nothing. – 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( 'superspeedyimports_admin_enqueue_scripts', $hook );
Hook into it instead of registering a admin_enqueue_scripts handler that re-implements the same screen check:
add_action( 'superspeedyimports_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 (SuperSpeedyImports_PostTemplate, WooCommerce, custom CPT, etc.).
add_action( 'superspeedyimports_register_post_stages', function ( $template ) {
$template->addStage(
'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( 'superspeedyimports_register_stages', function ( $template ) { /* ... */ } );
// WooCommerce-only.
add_action( 'superspeedyimports_register_woocommerce_stages', function ( $template ) { /* ... */ } );
From outside the template class, use $template->addStage( $name, $priority, $callback, $description ), the public wrapper around the protected registerStage() a template calls on itself. The callback receives the template instance.
Remove a stage your add-on shouldn’t run
add_filter( 'superspeedyimports_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 superspeedyimports_apply_additional_options so SSI’s normal skip logic kicks in (the way DII does it).
The stage-registration actions fire when a runner prepares the template for a run (applyAuth(), called by the CLI, browser and scheduled runners), not when the template is constructed. The import config screen never registers stages, so code that needs its stages visible there registers them itself.
Templates That Bring Their Own Stages
A template that registers stages of its own (in registerTemplateStages(), or by overriding registerCoreStages()) needs two declarations so those stages behave like SSI’s built-in ones on all three runners.
Keep your stages in the import-mode presets
Available since 3.04.
The Import mode option (additional_options.import_mode) cuts the stage list to a preset for every mode except full. A stage missing from the preset is dropped, and the run still reports success. superspeedyimports_stage_presets filters the preset map before the cut. It is applied wherever the stage list is built (CLI, browser and scheduled runs), and the MCP describe-import-config ability reports the filtered presets.
add_filter( 'superspeedyimports_stage_presets', function ( $presets, $template ) {
if ( ! ( $template instanceof My_Term_Template ) ) {
return $presets;
}
$mine = array( 'source-fetch', 'xml-to-csv', 'load-csv', 'my-match', 'my-update' );
$presets['only_update'] = $mine;
return $presets;
}, 10, 2 );
$presets maps each mode to a list of stage names, or to null for “every registered stage”. Runners consult the presets only when the import mode is not full; full always runs every registered stage, so a full entry has no effect on a run. post-import-jobs is always kept. A template that supports fewer modes should also refuse the others in its preRunGuardError() override, which all three runners call before any stage runs.
Let a timed-out stage recover
Available since 3.04.
When a sequential stage’s browser request dies (it times out, the PHP process is killed, or PHP runs out of memory or time), the browser polls the server’s stage-status probe every 15 seconds. While the stage is still running it waits; when the stage finished it carries on. For a stage on the recoverable list that died, it re-runs the stage, as many times as it takes: there is no retry limit, and only Stop ends it. A stage not on the list that died fails the run, because re-running it has not been shown to be safe. On a small import (load-csv finished in under 10 seconds) the browser sends consecutive sequential stages in one request; if that request dies, the stages the server finished are kept and the first unfinished one is recovered on its own, as above. The scheduled runner’s stalled-run recovery uses the same recoverable list, also without a retry limit.
A stage that throws, or returns an error, is not a death: the run fails with its message and nothing is re-run.
Parallel stages are handled differently: when a segment request dies, the browser re-sends that segment, with a smaller chunk where the stage uses one, without consulting the probe, and the server re-runs it from its last committed position. WP-CLI re-spawns a worker that was killed or ran out of memory, for its whole segment. Work in a parallel stage must therefore be safe to replay: select the rows still to do by their state, rather than assuming each segment runs exactly once.
add_filter( 'superspeedyimports_recoverable_stages', function ( $stages ) {
$stages[] = 'my-match';
$stages[] = 'my-update';
return $stages;
} );
List a stage only when re-running the whole stage after a mid-stage death gives the same result: idempotent UPDATEs, or scratch state the stage rebuilds itself. Run its SQL through superspeedyimports_query(), superspeedyimports_get_results() or superspeedyimports_get_var(), which fingerprint each statement with the import and stage, so the recovery probe sees a statement that is still executing and waits for it. The list reaches the browser in the import/setup response as recoverable_stages.
Importing Records That Are Not Posts
SSI’s intake, config screen and completion screen assume posts unless a template says otherwise. A template that imports another kind of record, such as taxonomy terms, declares it in its registration info and a few overridable methods.
Registration info keys
Available since 3.04.
addTemplate() accepts two optional keys besides name, class and description:
protected function addTemplateInfo() {
self::addTemplate( array(
'name' => 'Term descriptions',
'class' => 'My_Term_Template',
'description' => 'Update descriptions of existing categories, tags and other terms',
'icon' => array( 'at' => 'at-list-bold', 'dash' => 'tag' ),
'record_kind' => 'term',
) );
}
| Key | Effect |
|---|---|
icon | The icon on the Imports tab: at is an Atlas icon class, dash a dashicon name (used when the Atlas pack is absent). Without it the icon follows the template’s post_type. |
record_kind | What one CSV row is: post when omitted, term for taxonomy terms. A taxonomy-term CSV is accepted only by a template whose record_kind is term. |
Taxonomy-term CSVs
Available since 3.1.0.
SSI recognises two shapes of taxonomy-term file from the header and the first 200 rows:
export: the Data tab’s term download. Its header startsterms.term_id, taxonomy, ssi_source_site, terms.name, terms.slug, term_taxonomy.parent, parent_path, term_taxonomy.count, term_taxonomy.description, optionally followed bytermmeta.<key>columns.list: any other term list. Ataxonomycolumn whose every non-empty sampled value is a taxonomy registered on the site, a term column (term ID, slug or name) and no post column (post type, post ID, post title, post name or SKU). Header names are compared after normalisation, soTerm ID,term_idandterm-idare the same column.
Creating an import from such a file with a template whose record_kind is not term is refused in the New Import dialog, by wp ssi create and by the MCP create-import ability, and the auto-mapper returns nothing for it. The dialog names the registered term template, or in paid builds with none registered, the add-on that imports term data.
| Function | Returns |
|---|---|
superspeedyimports_term_data_csv_kind( $path, $separator = ',' ) | 'export', 'list' or '' |
superspeedyimports_is_term_data_csv( $path, $separator = ',' ) | true for either kind |
superspeedyimports_is_same_site_term_data_csv( $path, $separator = ',' ) | true for an export file whose every row’s ssi_source_site is this site, so its Term IDs are this site’s own |
superspeedyimports_term_data_csv_refusal( $path, $template_class, $separator = ',' ) | WP_Error (code superspeedyimports_term_data_csv) when the file is term data and the template does not import terms, otherwise null |
superspeedyimports_term_template_class() | the class of the registered template with record_kind term, or '' |
superspeedyimports_template_record_kind( $class ) | the template’s record_kind, post by default |
superspeedyimports_addon_link( $slug ) | array( 'name' => …, 'url' => … ) for a paid add-on, or null. Defined only in paid builds (functions/addon-links.php); call it through function_exists() |
$separator is a character, or 'tab'.
Supplying the whole config for a recognised file
Available since 3.04.
superspeedyimports_pre_auto_config runs first in superspeedyimports_auto_config_from_export_csv(), which every intake path calls: the New Import dialog, wp ssi create, MCP create-import and the WP All Import migration. Return an array to use it as the whole auto-configuration; return the incoming null to let SSI’s mapper run.
add_filter( 'superspeedyimports_pre_auto_config', function ( $pre, $csv_path, $template_class, $field_separator, $decimal_separator, $ignored_columns, $row_cap ) {
if ( $template_class !== 'My_Term_Template' || ! superspeedyimports_is_term_data_csv( $csv_path, $field_separator ) ) {
return $pre;
}
$same_site = superspeedyimports_is_same_site_term_data_csv( $csv_path, $field_separator );
return array(
'template_mappings' => array(
'taxonomy' => 'taxonomy',
'term_slug' => 'terms.slug',
'term_description' => 'term_taxonomy.description',
),
'functions' => null,
'additional_options' => array(
// Term ID from this site's own export; otherwise the mapped Taxonomy and Slug fields.
'unique_id_components' => $same_site ? array( 'term:term_id' ) : array( 'term:taxonomy', 'term:term_slug' ),
'unique_id_source' => $same_site ? 'terms.term_id' : '',
),
'_auto_map_report' => array( 'mapped' => array(), 'conflicts' => array(), 'unmapped' => array() ),
);
}, 10, 7 );
The array has the same shape as the function’s own return. _auto_map_report is optional; when present it is saved as the import’s auto-map report, which the config screen shows. Answer only for your own template class. The match tokens follow the rule under Match tokens name mapped fields: term_slug is a field of the template in this example, and term:taxonomy_slug would not work.
Config screen: mapping blocks, matching and your own section
Available since 3.04.
getMappingSections() lists the core mapping blocks the config screen renders for the template: post (Post Fields), taxonomies, custom_fields (ACF/SCF), postmeta (the standard Post Meta block) and media (Images). The default is all five. A block not listed is not rendered, so nothing in it is shown, projected into the field list or saved. The list travels with every import load as mapping_sections, so one import’s choice never carries over to the next import opened on the same page.
public function getMappingSections() {
return array(); // this template renders its own section only
}
The template’s own fields render through the existing ssi:renderSection event: give each field a section in defineMappings() and render that section from the template’s auto-discovered JS (see the Templates guide). Rows created with generateStandardField() save at the top level of template_mappings like post fields. The section is the backing model of the field list, like core’s blocks: core hides any .ssi-template-section under the Ungrouped / Grouped field list, and the Grouped view heads its rows with the section’s own heading (the h2 or h3 in its .ssi-section-header), so your template needs no CSS of its own for either.
The matching section reads getUniqueIdentifierConfig(). Three optional preset keys and one config key serve records that are matched on an ID column:
| Key | Where | Effect |
|---|---|---|
source_column | preset | true shows the “which CSV column holds the value” selector, saved as additional_options.unique_id_source. The built-in Post ID preset sets it. |
source_label | preset | The selector’s label, e.g. Term ID CSV column. |
source_help | preset | One sentence under the selector. |
custom | config | false offers only the presets, with no Custom builder. |
The import’s record_kind also reaches the page in the load response as record_kind.
A template whose record_kind is term also gets term-import admin screens: no Preview first item (the preview handler refuses it with ssi_preview_not_supported), no delete-missing options or “Delete all … loaded by this import” link, no Check existing data (the handler refuses it), and, where a post import links to its post list, a link to each taxonomy’s WordPress term screen. Those taxonomies are the registered ones in the file’s taxonomy column (superspeedyimports_term_import_links( $import )), sent as term_taxonomies in the load response and the run summary.
Match tokens name mapped fields
The review dialog’s required-fields check enforces this rule.
A preset’s components are tokens of the form <type>:<key>. post: (a wp_posts column), meta: (a meta key) and tax: (a taxonomy) are core’s. Any other type is yours, and its key must be the key of a field in your defineMappings(), such as term:taxonomy for a field named taxonomy. Before a browser run, the review dialog looks each token’s key up as a mapped field and blocks Start Import with “<key> (match field) is not mapped” when it finds none. It cannot tell which fields an invented compound token such as term:taxonomy_slug needs, and matching on an unmapped field would give every row a blank identity, so name one token per field instead: term:taxonomy + term:term_slug.
The one exception is a preset with source_column: it matches on the CSV column chosen in the matching section, so its token names no field (term:term_id). The matching section summarises term:term_id as “Term ID (terms.term_id)” and any other token of your own by its field’s label (“Taxonomy + Slug”).
Completion screen and run counters
Available since 3.04.
getItemLabels() names the template’s records on the completion screen and in the matching section, and says which run counters hold its created and updated totals:
public function getItemLabels() {
return array( 'term', 'terms', 'updated_metric' => 'term_descriptions_updated' );
}
It accepts a list (singular, plural) or the keys singular, plural, created_metric and updated_metric. The metrics default to posts_inserted and posts_updated; the words default to the post type’s labels.
term_descriptions_updated is a run counter like posts_updated: log it from your stage with both $this->logger->logStageMetric( $stage, 'term_descriptions_updated', $n ) and $this->logger->updateSummaryMetrics( array( 'term_descriptions_updated' => $n ) ). It counts towards the run’s DB rows written and processed, and never towards the posts-affected count.
created_metric and updated_metric name keys in the run summary’s counters. Browser and scheduled runs execute stages in separate requests, so the summary is rebuilt from stage metrics at the end, and only for SSI’s own counter keys: rows_processed, posts_inserted, posts_updated, posts_type_converted, variations_created, variations_updated, postmeta_created, postmeta_updated, terms_created, term_relationships_created, term_descriptions_updated, posts_deleted, posts_trashed, postmeta_deleted, term_relationships_deleted, orphan_rows_deleted, remote_uploaded_images and thumbnails_set. A counter of your own reaches counters on every runner when your stage logs it with both logStageMetric() and updateSummaryMetrics() (from 3.49.0; before that, only on WP-CLI runs). Logged with logStageMetric() alone it appears in the stage metrics only. It does not count towards the run’s DB rows written.
Adding Term Checks to the Data Tab
Available since 3.50.0.
The Data tab’s taxonomy-term view runs two checks of its own in every edition: Missing description and Short description. An add-on adds further checks through the hooks below, and each one is offered, counted, filtered, paged, selected and downloaded exactly like the built-in two. The Term Descriptions add-on’s duplicate and repetition checks are built this way.
Register the check
add_filter( 'superspeedyimports_data_term_checks', function ( $checks ) {
$checks['myaddon_boilerplate'] = array(
'label' => 'Boilerplate description',
'group' => true, // findings carry a group_key
'default' => true, // checkbox starts ticked
);
return $checks;
} );
The key is what the findings table stores: 1 to 32 lower-case letters, digits or underscores. Prefix it with your add-on’s name. SSI prints a checkbox for every registered check, so the check needs no markup of its own. A malformed registration stops the term view and shows the error where the checkboxes would be.
Settings for your checks, such as a threshold, go in the term checks fieldset:
add_action( 'superspeedyimports_data_term_checks_fieldset', function () {
echo '<p><label for="myaddon-min">Groups of at least</label> <input type="number" id="myaddon-min" min="2" value="5"> terms</p>';
} );
Changing any input inside the fieldset clears the current results, as the built-in controls do.
Send your settings with the analysis
jQuery( document ).on( 'ssi:data-term-form', function ( event ) {
// event.operation is 'analyse' or 'columns'.
event.payload.options.myaddon_min = jQuery( '#myaddon-min' ).val();
} );
event.payload.options is posted as options[...] and reaches your analysis and your export columns unchanged apart from sanitize_text_field(). Validate your own keys.
Run the check
SSI runs its own checks into a temporary findings table private to the request, then applies superspeedyimports_data_term_analyse once. Insert a row for each finding, then add every check key you ran to the list you return:
add_filter( 'superspeedyimports_data_term_analyse', function ( $handled, $table, $taxonomy, $checks, $options, $in_use ) {
global $wpdb;
if ( ! in_array( 'myaddon_boilerplate', $checks, true ) ) {
return $handled;
}
$min = max( 2, (int) ( $options['myaddon_min'] ?? 5 ) );
// ... find the groups, then for each finding:
$ok = $wpdb->query( $wpdb->prepare(
'INSERT IGNORE INTO %i (id, finding, group_key) VALUES (%d, %s, %s)',
$table, $term_id, 'myaddon_boilerplate', $group_key
) );
if ( false === $ok ) {
throw new RuntimeException( $wpdb->last_error );
}
$handled[] = 'myaddon_boilerplate';
return $handled;
}, 10, 6 );
| Argument | Holds |
|---|---|
$handled | Keys of the checks already run. Add each key you run. |
$table | The findings table: id (the term ID), finding (your check key), group_key (VARCHAR(64), NULL for a check without groups); primary key (id, finding). |
$taxonomy | The validated taxonomy name. |
$checks | The requested checks SSI does not run itself. |
$options | The posted options[...] values. |
$in_use | true when the user asked for terms with a count above zero only. |
A requested check that no callback lists in $handled is an error: the user sees “No installed add-on ran the Boilerplate description check. Untick it or activate the add-on that provides it, then analyse again.” Findings stored under a key nobody requested are an error too. Throw an exception for any failure of your own; its message is shown in place of the results.
Insert one row per term and check. The table’s primary key ignores a second row for the same term and check, so a term belongs to at most one group per check.
Attach groups to the report
superspeedyimports_data_term_report is applied while the findings table still exists, so you can read your groups from it:
add_filter( 'superspeedyimports_data_term_report', function ( $report, $table, $taxonomy, $checks, $options, $in_use ) {
global $wpdb;
if ( ! in_array( 'myaddon_boilerplate', $checks, true ) ) {
return $report;
}
$report['myaddon_groups'] = $wpdb->get_results( $wpdb->prepare(
'SELECT group_key, COUNT(*) AS size FROM %i WHERE finding = %s GROUP BY group_key',
$table, 'myaddon_boilerplate'
), ARRAY_A );
return $report;
}, 10, 6 );
The report holds rows (the page of 50), total, page, pages, ids (every matching term ID), counts (check => findings), kind, taxonomy and checked_at. Each row carries group_keys (check => group key) when it belongs to a group. $checks here lists every requested check, the built-in ones included. Return the report with those keys intact.
Show the results
jQuery( document ).on( 'ssi:data-term-report', function ( event ) {
// event.report the report above
// event.table the results table (jQuery), rows are tr[data-term-id]
// event.container the results container (jQuery)
// event.analysed the request payload, options included
// event.fresh true for a new analysis, false for another page or filter
// event.select( ids ) adds term IDs to the selection the download uses
event.table.find( 'thead tr' ).append( jQuery( '<th>' ).text( 'Group' ) );
event.table.find( 'tbody tr[data-term-id]' ).each( function () {
var id = Number( jQuery( this ).attr( 'data-term-id' ) );
var row = event.report.rows.filter( function ( r ) { return r.id === id; } )[0];
jQuery( '<td>' ).text( row && row.group_keys ? row.group_keys.myaddon_boilerplate || '' : '' ).appendTo( this );
} );
} );
The event fires every time the term results are drawn.
Add columns to the downloaded CSV
The term download (Pro) posts the selected IDs with the analysis inputs, checks[] and options[...], and never the findings. Recompute what your columns hold from the database:
add_filter( 'superspeedyimports_data_term_export_columns', function ( $columns, $selection ) {
if ( in_array( 'myaddon_boilerplate', $selection['checks'], true ) ) {
$columns[] = 'myaddon_group';
}
return $columns;
}, 10, 2 );
add_filter( 'superspeedyimports_data_term_export_row', function ( $values, $record, $selection ) {
if ( in_array( 'myaddon_boilerplate', $selection['checks'], true ) ) {
$values['myaddon_group'] = myaddon_group_of( $record );
}
return $values;
}, 10, 3 );
$selection holds taxonomy, ids, columns (the chosen termmeta. columns), checks and options. $record holds term_id, name, slug, parent, count and description. Your columns follow the built-in ones. A blank column name or one that repeats another stops the download before it starts; a row without a value for one of your columns stops it at that row. To answer each row cheaply, compute everything once for $selection['ids'] in the columns filter and look it up in the row filter. + New Import lists your columns as not imported and imports the file unchanged.
add_action( 'superspeedyimports_data_term_download_buttons', function () {
echo '<button type="submit" class="button" data-ssi-action="myaddon_pack">Download for rewriting</button>';
} );
add_action( 'admin_post_myaddon_pack', function () {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Forbidden', 403 );
}
check_admin_referer( 'superspeedyimports_data_csv' );
$ids = json_decode( (string) wp_unslash( $_POST['ids'] ?? '' ), true );
$selection = superspeedyimports_data_term_selection(
sanitize_key( $_POST['taxonomy'] ?? '' ),
is_array( $ids ) ? $ids : array(),
array_map( 'sanitize_text_field', wp_unslash( (array) ( $_POST['columns'] ?? array() ) ) ),
array_map( 'sanitize_key', (array) ( $_POST['checks'] ?? array() ) ),
map_deep( wp_unslash( (array) ( $_POST['options'] ?? array() ) ), 'sanitize_text_field' )
);
// Stream your file; superspeedyimports_data_write_term_csv( $selection, $handle ) writes the term CSV.
} );
The button sits beside Download CSV for term selections only. data-ssi-action names the admin-post.php action the form goes to; the nonce, taxonomy, IDs, columns, checks and options are the same as for the CSV. superspeedyimports_data_term_selection() checks every ID belongs to the taxonomy and every column and check is supported, and throws otherwise. The download form, and so these buttons, exist in Pro only.
What the user sees without your add-on
With no add-on check registered, Pro and Super Speedy Imports Lite show one line under the term checks about the Term Descriptions add-on. Registering any check through superspeedyimports_data_term_checks removes it.
Filtering SSI’s Generated SQL
For lower-level tweaks, SSI exposes filters on the SQL it generates for delete and update operations:
Each filter receives an empty string and the import ID, and returns an SQL condition starting with AND, which SSI appends to the WHERE clause of that stage’s statements. Return the empty string to leave the stage unfiltered.
| Filter | Stage | Aliases available in the condition |
|---|---|---|
superspeedyimports_filter_deletes | process-deletes, when it collects the posts missing from the CSV (not applied with --delete-all) | pm (the post’s ssi_updated row in wp_postmeta; pm.post_id is the candidate post) |
superspeedyimports_filter_updates | update-posts | p (wp_posts) and import (the batch table) |
superspeedyimports_filter_updates_postmeta | update-postmeta, each per-key UPDATE | pm (wp_postmeta) and import (the batch table) |
// Never delete private posts.
add_filter( 'superspeedyimports_filter_deletes', function ( $condition, $import_id ) {
global $wpdb;
return $condition . " AND pm.post_id NOT IN (SELECT ID FROM {$wpdb->posts} WHERE post_status = 'private') ";
}, 10, 2 );
// Leave drafts out of update-posts.
add_filter( 'superspeedyimports_filter_updates', function ( $condition, $import_id ) {
return $condition . " AND p.post_status != 'draft' ";
}, 10, 2 );
Each filter is applied once per stage run; the condition it returns is evaluated by MySQL for every candidate row, so keep it cheap and indexable. These hooks are not applied in the WordPress.org edition of SSI.
On a product import with Keep sold items switched on, the delete query also excludes every product or variation in a completed, processing or on-hold order, through a derived table joined as ssi_sold. That exclusion is part of SSI’s own query, not a superspeedyimports_filter_deletes callback, and your condition is appended after it as the last part of the WHERE clause, so the two combine. Do not use ssi_sold as an alias of your own.
SSI Version Requirements
The hooks documented here landed in specific SSI versions:
| Hook | Available since |
|---|---|
superspeedyimports_register_templates | 1.x |
superspeedyimports_register_stages / superspeedyimports_register_post_stages / superspeedyimports_register_woocommerce_stages | 1.x |
superspeedyimports_filter_stages_to_run | 1.x |
superspeedyimports_filter_deletes / superspeedyimports_filter_updates | 1.x |
superspeedyimports_import_utils | 2.x |
ssi:renderSection (JS) | 2.x |
superspeedyimports_apply_additional_options | 2.34 |
ssi:media_section_rendered (JS) | 2.34 |
superspeedyimports_admin_enqueue_scripts | 2.34 |
post_meta array-of-list support in load-csv.php | 2.34 |
| Taxonomy-term CSV recognition and refusal functions | 3.1.0 |
superspeedyimports_stage_presets / superspeedyimports_recoverable_stages / superspeedyimports_pre_auto_config | 3.04 |
Template info keys icon and record_kind; getMappingSections(); getItemLabels(); uid preset source_column | 3.04 |
Term-import admin screens from record_kind; term_taxonomies; .ssi-template-section hidden under the field list; superspeedyimports_get_var() fingerprinted | 3.04 |
| Data tab term hooks: superspeedyimports_data_term_checks / _analyse / _report / _export_columns / _export_row, superspeedyimports_data_term_checks_fieldset / _download_buttons, ssi:data-term-form / ssi:data-term-report (JS) | 3.50.0 |
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, compare SUPERSPEEDYIMPORTS_VERSION, which SSI defines when its main file loads, using version_compare(). Checking whether a hook has callbacks (has_action()) does not tell you which SSI version is installed.
add_action( 'admin_notices', function () {
if ( defined( 'SUPERSPEEDYIMPORTS_VERSION' ) && version_compare( SUPERSPEEDYIMPORTS_VERSION, '2.34', '<' ) ) {
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( 'superspeedyimports_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( 'superspeedyimports_admin_enqueue_scripts', $hook );
// Fires when a runner prepares the template for a run (applyAuth()), not during
// construction and never on the config screen. Add custom stages.
do_action( 'superspeedyimports_register_stages', $template );
do_action( 'superspeedyimports_register_post_stages', $template );
do_action( 'superspeedyimports_register_woocommerce_stages', $template );
// Fire once per run on every runner (from 3.49.0): superspeedyimports_before_import_setup
// ( $import_id ) before the run's setup, then this pair around the stages. On browser and
// scheduled runs the two may fire in different PHP processes. after_import_stages fires once
// per run even when the run's final step is run again after its PHP process died (3.68.0).
do_action( 'superspeedyimports_before_import_stages', $template );
do_action( 'superspeedyimports_after_import_stages', $template );
// Data tab term view: print settings for your term checks inside the checks fieldset, and
// buttons beside Download CSV (a submit button with data-ssi-action="<admin-post action>").
do_action( 'superspeedyimports_data_term_checks_fieldset' );
do_action( 'superspeedyimports_data_term_download_buttons' );
// Fire around every stage on every runner (once per segment for a parallel stage):
// before the stage body, after it, and after the stage's finalize work.
do_action( 'superspeedyimports_stage_starting', $stage_name, $template );
do_action( 'superspeedyimports_stage_ending', $stage_name, $template );
do_action( 'superspeedyimports_stage_finalized', $stage_name, $template );
PHP filters
// Modify which stages run for an import.
add_filter( 'superspeedyimports_filter_stages_to_run', function ( $stages, $config, $template ) {
return $stages;
}, 10, 3 );
// Import-mode presets (mode => stage names, or null for all), before the stage list is cut.
add_filter( 'superspeedyimports_stage_presets', function ( $presets, $template ) { return $presets; }, 10, 2 );
// Stages a runner may re-run after a timeout or mid-stage death.
add_filter( 'superspeedyimports_recoverable_stages', function ( $stages ) { return $stages; } );
// Supply the whole auto-configuration for a CSV (return an array), or null to let SSI map it.
add_filter( 'superspeedyimports_pre_auto_config', function ( $pre, $csv_path, $template_class, $field_separator, $decimal_separator, $ignored_columns, $row_cap ) {
return $pre;
}, 10, 7 );
// Data tab term checks (see Adding Term Checks to the Data Tab).
add_filter( 'superspeedyimports_data_term_checks', function ( $checks ) { return $checks; } );
add_filter( 'superspeedyimports_data_term_analyse', function ( $handled, $table, $taxonomy, $checks, $options, $in_use ) { return $handled; }, 10, 6 );
add_filter( 'superspeedyimports_data_term_report', function ( $report, $table, $taxonomy, $checks, $options, $in_use ) { return $report; }, 10, 6 );
add_filter( 'superspeedyimports_data_term_export_columns', function ( $columns, $selection ) { return $columns; }, 10, 2 );
add_filter( 'superspeedyimports_data_term_export_row', function ( $values, $record, $selection ) { return $values; }, 10, 3 );
// Add an "AND ..." condition to SSI's delete / update statements (see Filtering SSI's Generated SQL).
add_filter( 'superspeedyimports_filter_deletes', function ( $condition, $import_id ) { return $condition; }, 10, 2 );
add_filter( 'superspeedyimports_filter_updates', function ( $condition, $import_id ) { return $condition; }, 10, 2 );
add_filter( 'superspeedyimports_filter_updates_postmeta', function ( $condition, $import_id ) { return $condition; }, 10, 2 );
// Register one-shot import utilities, run with: wp ssi <id> my-utility
add_filter( 'superspeedyimports_import_utils', function ( $utils ) {
$utils['my-utility'] = array(
'slug' => 'my-utility',
'message' => 'Running my utility...',
// Relative to the SSI plugin directory: SSI prepends its own directory to 'file'.
'file' => '../' . basename( __DIR__ ) . '/utilities/my-utility.php',
);
return $utils;
} );
A utility runs from WP-CLI only, as wp ssi <id> <slug>. SSI prepends its own plugin directory to file, so an absolute path does not work; the relative path above reaches an add-on installed in the same plugins directory as SSI. The file is included inside the template instance, so $this is the template.
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
});
// Data tab term view: add your settings to each request, then draw your results.
$(document).on('ssi:data-term-form', function (event) {
// event.payload (event.payload.options is posted as options[...]), event.operation
});
$(document).on('ssi:data-term-report', function (event) {
// event.report, event.table, event.container, event.analysed, event.fresh, event.select(ids)
});
// 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( 'superspeedyimports_apply_additional_options', array( $this, 'redirect_to_delayed_meta' ), 10, 1 );
add_action( 'superspeedyimports_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
superspeedyimports_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
superspeedyimports_apply_additional_options. DII’s PHP callback rewritesconfig['media'][...]→config['post_meta'][delayed_*]and returns. SSI re-evaluates flags (has_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;attach-existing-imagesandattach-gallery-imagesare left out, andupload-remote-imagesfinds no featured-image URLs to download. - 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 yoursuperspeedyimports_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 a reference implementation, the Complete Example above follows Delayed Image Import’s own integration (includes/class-ssi-integration.php and assets/js/dii-ssi-admin.js). When your work needs a new content-type mapping rather than a cross-cutting hook, see the Custom Templates guide.
Written for Super Speedy Imports 3.68.0.

Comments