Developer guide
Siteryn Checkout has a documented builder extension API. The Pro add-on is built entirely on it, so you can add your own elements, rules and styles the same way.
Architecture
- Free boots on
plugins_loadedand firessiteryn_checkout_loaded. Hook it from your plugin's main file. Pro starts there too and firessiteryn_checkout_pro_loaded. - Namespaces:
Siteryn\SiterynCheckout(builder classes in\Builder) andSiterynPro\SiterynCheckout. - There are no REST routes, shortcodes or blocks of its own. The editor and settings screen use nonce-checked admin-ajax actions that require
manage_woocommerce(below).
Admin AJAX actions
| Action | What it does |
|---|---|
siteryn_checkout_builder_load | Returns a page's editor layout, editor config, revision and status |
siteryn_checkout_builder_save · _publish | Validates and stores the posted layout (JSON) as draft or published; rejects an out-of-date revision |
siteryn_checkout_builder_unpublish · _reset | Unpublish (keep as draft), or reset the draft to a starter design (starter, default free-layout-1) |
siteryn_checkout_builder_preview | Renders a posted layout with sample data; nothing is saved |
siteryn_checkout_builder_cart_mode | Saves Cart page behavior (cart_mode) straight away |
siteryn_checkout_save_settings · siteryn_checkout_ajax_reset_settings | Save or reset a settings tab (One-Click Checkout) without a reload |
Builder actions check the siteryn_checkout_builder nonce (field nonce) and take a page_type. On the storefront the plugin only uses WooCommerce's own wc-ajax endpoints (for example apply_coupon and remove_from_cart).
How designs are stored
Each page has a published and a draft design, in non-autoloaded options siteryn_checkout_builder_{cart|checkout|thank_you} and …_draft. A design is JSON:
{
"schema_version": 1,
"page_type": "checkout",
"mode": "multi_step",
"steps": [ { "id": "information", "title": "Information" } ],
"settings": { "accent_color": "#2563eb", "card_style": "boxed" },
"sections": [ { "id": "s1", "type": "section", "step": "", "settings": {}, "rows": [
{ "id": "r1", "type": "row", "settings": {}, "columns": [
{ "id": "c1", "type": "column", "settings": { "width": 60 }, "blocks": [
{ "id": "b1", "type": "block", "name": "siteryn/checkout-billing", "step": "information", "settings": {} }
] } ] } ] } ]
}
Free stores only id and title per step; extensions add more per-step keys through siteryn_checkout_builder_sanitize_step (Pro adds description, icon, next_label and validate). Limits: up to 1000 nodes and 512 KB per design. IDs match [a-z0-9][a-z0-9_-]{0,63}, and block names match vendor/name. Storage can be swapped through the siteryn_checkout_builder_repository filter (a DesignRepository with get, save and delete).
Validation and fallback
Validator::validate( $raw, $page_type ) returns a ValidationResult:
is_valid(): no errors. This is required to save.is_renderable(): only limit errors. This is required to render, so a design that exceeds limits after a license lapse keeps working.
A published design that isn't renderable is replaced by the settings-derived default design, with an admin notice. Add your own rules with siteryn_checkout_builder_validate_layout.
Render pipeline
- On the Cart, Checkout and Thank You pages, WooCommerce's
cart/cart.php(andcart/cart-empty.php),checkout/form-checkout.phpandcheckout/thankyou.phpare swapped for the builder templates, only when no theme override exists. - The Checkout page, and the Cart page while Cart page behavior is "Siteryn Customized Cart", always render through
WC_Shortcodes::checkout()/cart(), whatever the page contains (blocks, shortcode or page-builder content), so the same path runs. Rendererwalks the tree. Each block'srender_whenandrender_callbackget( array $settings, RenderContext $context, array $block ).- CSS is generated from settings (
siteryn_checkout_builder_node_declarations,…_node_css_rules) and output with the HTML (siteryn_checkout_builder_render_output).
RenderContext provides get_page_type(), is_preview(), get_layout() and get( $key ). On Checkout, checkout is the WC_Checkout; on Thank You, order is the WC_Order and may_view says whether the visitor may see personal details.
Register a custom element
No editor JavaScript is needed: the editor builds the settings panel from your schema and renders the canvas on the server using your preview_callback.
use Siteryn\SiterynCheckout\Builder\BlockRegistry;
use Siteryn\SiterynCheckout\Builder\RenderContext;
add_action( 'siteryn_checkout_loaded', static function () {
add_action( 'siteryn_checkout_builder_register_blocks', static function ( BlockRegistry $registry ) {
$registry->register( 'acme/delivery-note', array( // Your own vendor prefix, never "siteryn/".
'title' => __( 'Delivery note', 'acme' ),
'category' => 'content', // woocommerce | layout | content | conversion
'icon' => 'dashicons-car',
'page_types' => array( 'checkout', 'thank_you' ),
'multiple' => false,
'settings' => array(
'text' => array( 'type' => 'text', 'tab' => 'content', 'label' => __( 'Text', 'acme' ), 'default' => 'Orders placed before 2 pm ship today.' ),
'style' => array( 'type' => 'select', 'tab' => 'style', 'label' => __( 'Style', 'acme' ), 'choices' => array( 'info' => 'Info', 'warning' => 'Warning' ), 'default' => 'info' ),
),
'render_callback' => 'acme_render_delivery_note',
'preview_callback' => 'acme_render_delivery_note', // Must not touch the real cart or order.
'render_when' => static function ( RenderContext $context ) {
return 'thank_you' !== $context->get_page_type() || false !== $context->get( 'order', false );
},
) );
} );
} );
function acme_render_delivery_note( array $settings, RenderContext $context, array $block ) {
return sprintf( '<p class="acme-note acme-note--%1$s">%2$s</p>', esc_attr( $settings['style'] ), esc_html( $settings['text'] ) );
}
Block arguments
| Argument | Default | Notes |
|---|---|---|
title, description, icon | name, '', dashicons-screenoptions | Shown in the element picker |
category | content | woocommerce, layout, content or conversion |
page_types | ['*'] | cart, checkout, thank_you |
settings | [] | Field map (see below) |
multiple | true | false makes it single-use |
required, required_group, conflicts | false, '', [] | Required elements must be visible on every device |
render_callback, preview_callback | Return escaped HTML | |
render_when | fn( RenderContext $ctx ): bool, storefront only | |
refresh | false | Checkout: re-render on WooCommerce's order-review update. Wrap output with CheckoutBlocks::fragment( $block, $class, $html ). Cart: add data-siteryn-checkout-builder-refresh="{block id}". |
Field types: text, textarea, select (with choices), checkbox, number (requires min and max), color, url, richtext, css_class and image. Each field has type, label, default and tab, and optionally help, responsive and show_if. Checkout elements render inside WooCommerce's single checkout form, so don't nest forms; use woocommerce_form_field() for inputs.
siteryn/… element makes a design invalid. Elements with other prefixes are kept silently (with their settings) while your plugin is inactive, and render nothing.Recipes
Change a limit
// Only max_sections, max_rows_per_section, max_columns_per_row,
// max_blocks_per_column and max_steps are read.
add_filter( 'siteryn_checkout_builder_limits', static function ( array $limits, $page_type ) {
if ( 'checkout' === $page_type ) {
$limits['max_steps'] = 4; // At least 2.
}
return $limits;
}, 20, 2 ); // After Pro (priority 10).
Add content to a rendered page or element
use Siteryn\SiterynCheckout\Builder\RenderContext;
add_filter( 'siteryn_checkout_builder_render_block', static function ( $html, array $block, $context ) {
if ( 'siteryn/checkout-place-order' !== $block['name'] ) {
return $html;
}
return $html . '<p class="acme-note">' . esc_html__( 'You will not be charged until it ships.', 'acme' ) . '</p>';
}, 10, 3 );
add_filter( 'siteryn_checkout_builder_render_output', static function ( $result, $layout, $context ) {
if ( $context instanceof RenderContext && ! $context->is_preview() && 'checkout' === $context->get_page_type() ) {
$result['css'] .= '.acme-note{font-size:13px;opacity:.8}';
}
return $result; // Keep both 'html' and 'css'.
}, 10, 3 );
Your own visibility rule
use Siteryn\SiterynCheckout\Builder\RenderContext;
add_filter( 'siteryn_checkout_builder_node_settings_schema', static function ( array $fields, $node_type, $page_type ) {
$fields['acme_audience'] = array(
'type' => 'select',
'tab' => 'visibility',
'label' => __( 'Audience', 'acme' ),
'choices' => array( '' => 'Everyone', 'guests' => 'Guests only', 'customers' => 'Logged-in customers only' ),
'default' => '',
);
return $fields;
}, 10, 3 );
add_filter( 'siteryn_checkout_builder_node_visible', static function ( $visible, array $node, RenderContext $context ) {
if ( ! $visible || $context->is_preview() ) {
return $visible;
}
switch ( $node['settings']['acme_audience'] ?? '' ) {
case 'guests': return ! is_user_logged_in();
case 'customers': return is_user_logged_in();
default: return $visible;
}
}, 10, 3 );
Pro's own condition rules are a fixed set; use this pattern for extra rules. Don't hide required elements. Add a check in siteryn_checkout_builder_validate_layout if needed.
Customize the Buy Now button
// Never for gift cards.
add_filter( 'siteryn_checkout_buy_now_visible', static function ( $show, $product, $context ) {
return $show && ! has_term( 'gift-cards', 'product_cat', $product->get_id() );
}, 20, 3 );
// Label and classes ($context is 'single' or 'loop').
add_filter( 'siteryn_checkout_buy_now_button', static function ( array $button, $product, $context ) {
$button['label'] = __( 'Buy now, pay later', 'acme' );
$button['classes'][] = 'acme-buy-now';
return $button;
}, 20, 3 );
// Where Buy Now goes (one argument).
add_filter( 'siteryn_checkout_buy_now_redirect', static function ( $url ) {
return add_query_arg( 'src', 'buy-now', $url );
}, 20 );
Show a custom field answer in emails Pro
add_filter( 'woocommerce_email_order_meta_fields', static function ( $fields, $sent_to_admin, $order ) {
if ( ! class_exists( '\SiterynPro\SiterynCheckout\FieldManager' ) || ! $order instanceof WC_Order ) {
return $fields;
}
$key = \SiterynPro\SiterynCheckout\FieldManager::order_meta_key( 'gift_message' ); // _siteryn_checkout_pro_field_gift_message
$value = (string) $order->get_meta( $key );
if ( '' !== $value ) {
// Plain text: WooCommerce's email templates escape these themselves.
$fields['acme_gift_message'] = array( 'label' => __( 'Gift message', 'acme' ), 'value' => $value );
}
return $fields;
}, 20, 3 );
Templates
| File | Version | Override |
|---|---|---|
templates/builder/cart.php · checkout.php · thankyou.php | 1.0.0 | A theme's woocommerce/cart/cart.php, checkout/form-checkout.php or checkout/thankyou.php replaces them and turns the builder off for that page. |
templates/cart/parts/cart-form.php | 1.0.0 | Copy to your-theme/siteryn-checkout/cart/parts/cart-form.php. Variables: $cart, $show_coupon, $coupon_label, $coupon_dropdown, $coupon_style. Outdated copies are flagged. |
Data
| Where | What |
|---|---|
siteryn_checkout_settings | Settings, including cart_mode (standard|siteryn|redirect) and Buy Now |
siteryn_checkout_builder_backups | The last 3 pre-upgrade copies per page and state |
siteryn_checkout_pro_designs_{page} · _revisions_{page} | Pro saved designs (20) and revisions (15) |
siteryn_checkout_pro_custom_fields · _core_fields | Pro field manager definitions |
Order meta _siteryn_checkout_pro_field_{id} | Custom field answers (HPOS-safe) |
Order meta _siteryn_checkout_pro_marketing_optin | yes / no |
Editor JavaScript API
window.siterynCheckoutBuilderEditor (version 2). Load your script with the dependency siteryn-checkout-builder, and only when that script is enqueued.
| Group | Members |
|---|---|
| State | pageType(), isLoaded(), isDirty(), isBusy(), root |
| Layout | getLayout(), replaceLayout( layout, message ), preview( layout ), refresh() |
| UI | notify(), confirm() (Promise), announce(), addAction(), addNodeAction(), addTopBarItem(), addShortcut(), addStepFields(), addSettingsContent(), addSectionPresets(), addNodeBadge() |
| Editing | selected(), getNode( id ), insertNode(), addStep(), limits(), changed(), redraw() |
| Events | on( 'load'|'save'|'publish'|'reset'|'unpublish', fn ), plus the DOM event siteryn-checkout-builder:{event} on root |
Pro's editor scripts (siteryn-checkout-pro-editor.js, -designs.js, -fields.js) are good working references.
Storefront JavaScript
There's no custom step-change event. Observe the DOM instead: progress items [data-siteryn-checkout-builder-step-item] get aria-current="step", and step content [data-siteryn-checkout-builder-step] toggles is-hidden.
( function () {
var form = document.querySelector( 'form.checkout[data-siteryn-checkout-builder-steps]' );
if ( ! form || ! window.MutationObserver ) { return; }
var last = null;
new MutationObserver( function () {
var item = document.querySelector( '[data-siteryn-checkout-builder-step-item][aria-current="step"]' );
var id = item ? item.getAttribute( 'data-siteryn-checkout-builder-step-item' ) : null;
if ( id && id !== last ) {
last = id;
document.dispatchEvent( new CustomEvent( 'acme:checkout-step', { detail: { stepId: id } } ) );
}
} ).observe( document.body, { subtree: true, attributes: true, attributeFilter: [ 'aria-current' ] } );
} )();
WooCommerce's own events (update_checkout, updated_checkout, checkout_error, updated_cart_totals…) keep working as usual.
Constants
SITERYN_CHECKOUT_PRO_LICENSE_PRODUCT_ID, SITERYN_CHECKOUT_PRO_LICENSE_API_BASE, SITERYN_CHECKOUT_PRO_LICENSE_ALLOW_HTTP (local development only), SITERYN_FEEDBACK_ENDPOINT and SCRIPT_DEBUG (unminified assets). See every hook in the Hooks reference.