Developer guide
Read and change the wishlist from JavaScript, move or hide the button, add data to wishlist rows, override templates and hook into Pro. Every example is checked against the code.
Architecture
- Namespaces:
Siteryn\SiterynWishlist(Free) andSiterynPro\SiterynWishlist(Pro). - Boot: Free runs on
plugins_loadedand firessiteryn_wishlist_loaded; Pro boots on that hook and firessiteryn_wishlist_pro_loaded. - Free stores nothing on the server. The wishlist lives in the browser's
localStorage. Pro adds a per-user list (user meta) when sync or multiple wishlists is on. - Pro extends Free only through its hooks, the
siteryn_wishlist_page_toolbaraction, and the JavaScript API below.
JavaScript API
window.SiterynWishlist (script handle siteryn-wishlist, loaded on every page while the wishlist is enabled):
| Method | Returns |
|---|---|
getItems() | Product IDs (numbers) |
has( id ) · count() | Boolean · number |
add( id ) · remove( id ) · toggle( id ) | Updated IDs. Pass numbers, not strings. |
getAddedAt( id ) | ISO date string or null |
STORAGE_KEY · UPDATED_EVENT | siteryn_wishlist_items · siteryn_wishlist:updated |
Storage format: localStorage['siteryn_wishlist_items'] = [{"id":123,"addedAt":"2026-10-18T09:00:00.000Z"}]. Use default-language product IDs; the buttons' data-product-id already holds them.
document.addEventListener( 'DOMContentLoaded', function () {
if ( ! window.SiterynWishlist ) { return; }
var prev = SiterynWishlist.getItems();
document.addEventListener( SiterynWishlist.UPDATED_EVENT, function ( e ) {
var now = e.detail.items; // detail: { items, count, source: 'local' | 'storage' }
now.filter( function ( id ) { return prev.indexOf( id ) === -1; } )
.forEach( function ( id ) { console.log( 'added', id ); } );
prev.filter( function ( id ) { return now.indexOf( id ) === -1; } )
.forEach( function ( id ) { console.log( 'removed', id ); } );
prev = now.slice();
} );
} );
// New [data-siteryn-wishlist-toggle] buttons pick up their state on this event:
document.dispatchEvent( new CustomEvent( 'siteryn_wishlist:updated', {
detail: { items: SiterynWishlist.getItems(), count: SiterynWishlist.count() }
} ) );
Settings passed to the script: window.siterynWishlist (restUrl, storageKey, wishlistPageUrl, lang, tooltip, i18n), and on the wishlist page window.siterynWishlistPage (showPrice, showAddedDate, showStock, removeControlStyle, buttonStyle).
Other event: siteryn_wishlist:item-rendered (detail { element, product, context: 'own'|'shared' }) for each wishlist page row.
Moving or hiding the button
add_action( 'siteryn_wishlist_loaded', function () {
remove_action( 'woocommerce_single_product_summary',
array( \Siteryn\SiterynWishlist\Frontend::class, 'render_single_product_button' ), 35 );
} );
add_action( 'woocommerce_product_meta_end', function () {
if ( function_exists( 'siteryn_wishlist_button' ) ) {
siteryn_wishlist_button( get_the_ID() );
}
} );
Loop buttons: render_loop_button on woocommerce_after_shop_loop_item (15), append_loop_button_to_product_image_block on render_block_woocommerce/product-image (10), render_card_hook_button on each action in siteryn_wishlist_card_hooks (default siteryn_product_card_after_body, 10), and the theme-card fallback mark_product_card on woocommerce_post_class (10) with render_card_buttons on wp_footer (5). The script then moves each button to the chosen Position. There's no filter to hide one product's button; wrap the template:
add_filter( 'siteryn_wishlist_locate_template', function ( $path, $name ) {
if ( 'button.php' !== $name || '' === $path ) { return $path; }
$GLOBALS['my_wl_button_tpl'] = $path;
return __DIR__ . '/button-guard.php';
}, 10, 2 );
// button-guard.php
// $product_id = isset( $product_id ) ? absint( $product_id ) : 0;
// if ( $product_id && ! has_term( 'gift-cards', 'product_cat', $product_id ) ) {
// include $GLOBALS['my_wl_button_tpl'];
// }
Adding data to wishlist rows
add_filter( 'siteryn_wishlist_product_data', function ( $data, $product ) {
$data['sku'] = $product->get_sku();
return $data;
}, 10, 2 );
document.addEventListener( 'siteryn_wishlist:item-rendered', function ( e ) {
var body = e.detail.element.querySelector( '.siteryn-wishlist-page__item-body' );
if ( ! body || ! e.detail.product.sku ) { return; }
var span = document.createElement( 'span' );
span.className = 'my-wl-sku';
span.textContent = 'SKU: ' + e.detail.product.sku;
body.appendChild( span );
} );
Load that script with siteryn-wishlist-page as a dependency.
Templates
Copy a template to your-theme/siteryn-wishlist/:
| Template | Version · variables |
|---|---|
button.php | 1.0.2 · $product_id, $label_add, $label_remove, $icon, $icon_custom_url, $active_icon, $active_icon_custom_url, $icon_position, $display_type, $style, $size, $context, $alignment, $position, $loop_position, $loop_corner, $loop_reveal_on_hover, $loop_match_add_to_cart… |
wishlist-page.php | 1.0.1 · $layout, $width, $empty_message, $page_title, $button_style |
sign-in-required.php | 1.0.0 |
An admin notice lists theme copies with an older @version. Keep the markup contract:
button.php:button[data-siteryn-wishlist-toggle][data-product-id]withdata-label-add,data-label-remove,aria-pressed;.siteryn-wishlist-toggle__label; the.siteryn-wishlist-toggle-alignwrapper and its modifiers, including--pos-{over-image|after-title|after-price|after-add-to-cart},--corner-{top-left|…|center-center},--on-hoverand--match-cart, which the script reads. Older copies ofbutton.phptrigger the outdated-template notice.wishlist-page.php:[data-siteryn-wishlist-page], aul[data-siteryn-wishlist-list], and[data-siteryn-wishlist-empty],-item-count,-list-header,-loading,-shared-notice; anddo_action( 'siteryn_wishlist_page_toolbar' )for Pro's controls.
REST routes
| Route | Access |
|---|---|
GET /siteryn-wishlist/v1/products?include=1,2,3 | Public. Up to 100 visible products: id, display_id, name, permalink, image, price_html, in_stock, stock_status, purchasable. Optional lang (WPML/Polylang) returns each product in that language. |
GET /siteryn-wishlist-pro/v1/lists · POST /lists/{id}/items · DELETE /lists/{id}/items/{product} Pro | Logged in, licensed, with sync or multiple wishlists on. Send X-WP-Nonce. The default list's id is default. There is no route to read a single list; use GET /lists. |
POST /lists · PATCH|DELETE /lists/{id} | Logged in, licensed, Multiple wishlists on. Up to 20 lists of 100 items; the default list can't be deleted. |
POST /lists/{id}/add-to-cart | Logged in, licensed, Add all to cart on. Optional product_ids (otherwise the stored list). Returns { added, skipped }. |
GET /shared/{token} | Public; public lists only. |
POST /estimate-requests · POST /events | Estimate form (nonce) · activity counters. |
Pro
$lists = get_user_meta( $user_id, '_siteryn_wishlist_pro_lists', true );
$ids = array();
foreach ( (array) $lists as $list ) {
if ( 'default' === ( $list['id'] ?? '' ) ) {
$ids = wp_list_pluck( $list['items'], 'product_id' );
}
}
$products = \Siteryn\SiterynWishlist\ProductRepository::get_products_data( $ids );
Wishlist page buttons are styled with CSS variables --siteryn-wishlist-page-btn-bg, -border, -text, -hover-bg, -hover-border, -hover-text and -radius (they replace the old --siteryn-wishlist-cart-* variables). window.SiterynWishlistProSyncing is true while sync copies a customer's saved items. The admin bar node id is siteryn-wishlist.
add_filter( 'siteryn_wishlist_pro_back_in_stock_email', function ( $email, $product, $user ) {
$email['subject'] = sprintf( 'Good news, %s: %s is back!', $user->display_name, $product->get_name() );
return $email; // array( 'subject' => …, 'message' => … ), plain text
}, 10, 3 );
<?php
if ( function_exists( '\SiterynPro\SiterynWishlist\siteryn_wishlist_pro_header_icon' ) ) {
\SiterynPro\SiterynWishlist\siteryn_wishlist_pro_header_icon(); // namespaced
}
?>
add_action( 'siteryn_wishlist_pro_loaded', function () {
class My_Acme_Provider extends \SiterynPro\SiterynWishlist\AI\Providers\OpenAICompatibleProvider {
public function get_id() { return 'acme'; }
public function get_label() { return 'Acme AI'; }
public function get_default_model() { return 'acme-chat-1'; }
public function get_suggested_models() { return array( 'acme-chat-1' ); }
protected function base_url( \SiterynPro\SiterynWishlist\AI\ProviderConfig $c ) {
return 'https://api.acme.ai/v1';
}
}
add_filter( 'siteryn_wishlist_pro_ai_providers', function ( $p ) {
$p['acme'] = new My_Acme_Provider();
return $p;
} );
} );
// wp-config.php: define( 'SITERYN_WISHLIST_PRO_AI_ACME_API_KEY', '…' );
Behind a proxy or CDN, map the real visitor IP for rate limits with the siteryn_wishlist_pro_client_ip filter.
See the Hooks reference for every action and filter.