Developer guide

How the loader works, how to run your scripts on newly loaded products, and how to adapt it from a theme or plugin.

FreePro

How loading works

There is no AJAX or REST endpoint for products. The script requests the archive's real next page (GET the next link, same origin, with X-Requested-With: XMLHttpRequest), parses it with DOMParser and copies the product elements into the current list. Scripts inside the fetched HTML never run, so anything your theme attaches to products must be re-run through the events below.

FreePro
NamespaceSiteryn\SiterynInfiniteScrollSiterynPro\SiterynInfiniteScroll
Bootplugins_loaded, then siteryn_is_loadedOn siteryn_is_loaded, then siteryn_is_pro_loaded
Storefront decisionOn wp: context → enabled → pagination state → siteryn_is_should_loadAdds contexts and rules through Free's filters
API versionSITERYN_IS_API_VERSION (1.0.0), for compatibility checks—
Script handlesiteryn-is (footer, deferred, no jQuery)siteryn-is-pro

JavaScript events

Events are dispatched on the product list and bubble, so listen on document.

EventdetailWhen
siteryn-is:initinstanceThe loader started
siteryn-is:before-loadinstance, url, page, triggerBefore appending a page; cancelable. trigger: scroll, button, retry, api, restore (Pro)
siteryn-is:loadedinstance, url, page, items, nextUrl, trigger, documentAfter products were appended
siteryn-is:before-replace · siteryn-is:replacedinstance, url · + page, items, nextUrl, documentPro numbered and Previous / Next page switches (before is cancelable)
siteryn-is:endinstanceNo more pages
siteryn-is:errorinstance, url, errorA load failed
siteryn-is:reset · siteryn-is:destroyinstance, url · instanceAfter reset() (url = new next URL) · destroy()

WordPress's jQuery post-load event also fires on document.body after every load. Pro's earlier pages (loaded above the list when a shopper who arrived on a later page scrolls up) fire only post-load, so listen to both.

( function ( $ ) {
	function init( root ) {
		// e.g. root.querySelectorAll( '.my-slider:not(.is-ready)' ).forEach( mySlider );
	}
	document.addEventListener( 'siteryn-is:loaded', function ( e ) { e.detail.items.forEach( init ); } );
	document.addEventListener( 'siteryn-is:replaced', function ( e ) { e.detail.items.forEach( init ); } );
	if ( $ ) {
		$( document.body ).on( 'post-load', function () { init( document ); } ); // also covers Pro's earlier pages
	}
} )( window.jQuery );

Catching the start reliably

The script is deferred, so siteryn-is:init usually fires before your own deferred scripts run. Load your script only where the loader runs, and handle existing instances too:

add_action( 'siteryn_is_enqueue_assets', function () {
	wp_enqueue_script( 'my-is-glue', get_stylesheet_directory_uri() . '/js/is-glue.js',
		array( 'siteryn-is' ), '1.0', array( 'in_footer' => true, 'strategy' => 'defer' ) );
} );
function attach( loader ) {
	if ( loader && ! loader.myGlue ) { loader.myGlue = true; /* … */ }
}
( window.SiterynInfiniteScroll ? window.SiterynInfiniteScroll.instances : [] ).forEach( attach );
document.addEventListener( 'siteryn-is:init', function ( e ) { attach( e.detail.instance ); } );

JavaScript API

MemberUse
SiterynInfiniteScroll.get( i ) · .instancesThe loader (first by default) · all loaders
SiterynInfiniteScroll.create( list, overrides )Start a loader on another list; returns null when the list is already enhanced, sits inside .siteryn-is-disable or has no next page. Overrides are merged one level deep, so a selectors override replaces the whole object
SiterynInfiniteScroll.formatNumber( n )A number in the page language's digits and grouping
SiterynInfiniteScroll.configThe page config (window.siterynIsConfig)
loader.load()Load the next page now; resolves true when products were appended
loader.setMode( 'infinite' | 'load_more' )Switch mode
loader.setButtonLabel( text )Change the Load More label shown when it isn't loading
loader.fetchDocument( url, signal )The single network call every load goes through. You may wrap it; it must resolve { html, url } or reject
loader.reset( nextUrl )Re-read the list after your own code replaced its products
loader.destroy()Stop and restore the theme pagination; then create() on a new list
document.addEventListener( 'siteryn-is:loaded', function ( e ) {
	if ( 'scroll' === e.detail.trigger && e.detail.page >= 4 ) {
		e.detail.instance.setMode( 'load_more' );
	}
} );

Page config

window.siterynIsConfig (filter it with siteryn_is_frontend_config):

KeyContent
mode · buttonLoadinginfinite or load_more · button or text (While Loading)
mobile{ mode, breakpoint } (On Small Screens, Small Screen Width)
threshold · timeoutTrigger Distance in px · request timeout in ms (20000)
animate · focusNew · showEndFade In New Products · Move Focus to New Products · Show End of Results Message
selectors{ container, item, pagination, next, count }
state{ currentPage, totalPages, totalProducts, nextUrl } from the main query
i18n{ loading, end, error, retry, button, loaded }
version · locale · context · templateIdPlugin version · BCP 47 locale · shop or product_cat · the controls <template> id

Markup and classes

Custom selectors in code

add_filter( 'siteryn_is_settings_get_all', function ( $s ) {
	if ( is_admin() ) {
		return $s; // otherwise the settings form shows (and saves) these values
	}
	$s['container_selector']    = '.my-grid';
	$s['item_selector']         = '.product-card'; // direct children of the list
	$s['pagination_selector']   = '.my-pagination';
	$s['next_selector']         = '.my-pagination a.next';
	$s['result_count_selector'] = '.my-result-count';
	return $s;
} );

Templates

TemplateTheme overrideContains
controls.php (Free)your-theme/siteryn-infinite-scroll/controls.phpLive region, loader, Load More, end and error messages, sentinel
pro-controls.php (Pro)your-theme/siteryn-infinite-scroll/pro-controls.phpProduct counter
pro-extras.php (Pro)your-theme/siteryn-infinite-scroll/pro-extras.phpPrevious / Next pager, Show All button, top progress bar

Keep every data-siteryn-is-* attribute. controls.php receives $settings, $context and $pagination; keep its @version header, because an outdated copy triggers an admin notice.

Data

NameTypeContent
siteryn_is_settings · siteryn_is_versionOptionFree settings · version
siteryn_is_pro_settings · siteryn_is_pro_versionOptionPro settings (including term_rules) · version
siteryn_license_infinite-scroll_key · _status · _updateOptionPro license key and update check (not autoloaded) · cached status (autoloaded, read on storefront requests)
siteryn_license_infinite-scroll_retry · _lockTransientLicense server back-off after a failed check (1 hour) · request lock (30 seconds)
siteryn_is_outdated_templatesTransientOutdated template check, 1 day
siteryn_is_pro_license_checkCronDaily license check

No post or term meta is written. Uninstalling removes each plugin's own options (on every site of a network).

Endpoints

No REST routes, shortcodes, blocks or public AJAX actions. Admin endpoints require manage_woocommerce and a nonce:

ActionUsed for
wp_ajax_siteryn_is_save_settingsSaving a settings tab
admin_post_siteryn_is_reset_settings · admin_post_siteryn_is_pro_reset_settingsReset to Defaults
wp_ajax_siteryn_deactivation_feedbackThe optional deactivation survey on the Plugins screen (needs activate_plugins)
wp_ajax_siteryn_is_pro_license_actionActivate, deactivate or check the license (do)

wp-config constants

ConstantEffect
SCRIPT_DEBUGLoad unminified CSS and JS
SITERYN_IS_PRO_LICENSE_API_BASELicense server URL (default https://siteryn.com/wp-json/siteryn/v1)
SITERYN_IS_PRO_LICENSE_ALLOW_HTTPAllow a plain-HTTP license server (local development only)

Every hook, with more examples, is on Hooks reference.