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.

FreeProBuilder API 1

Architecture

Admin AJAX actions

ActionWhat it does
siteryn_checkout_builder_loadReturns a page's editor layout, editor config, revision and status
siteryn_checkout_builder_save · _publishValidates and stores the posted layout (JSON) as draft or published; rejects an out-of-date revision
siteryn_checkout_builder_unpublish · _resetUnpublish (keep as draft), or reset the draft to a starter design (starter, default free-layout-1)
siteryn_checkout_builder_previewRenders a posted layout with sample data; nothing is saved
siteryn_checkout_builder_cart_modeSaves Cart page behavior (cart_mode) straight away
siteryn_checkout_save_settings · siteryn_checkout_ajax_reset_settingsSave 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:

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

  1. On the Cart, Checkout and Thank You pages, WooCommerce's cart/cart.php (and cart/cart-empty.php), checkout/form-checkout.php and checkout/thankyou.php are swapped for the builder templates, only when no theme override exists.
  2. 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.
  3. Renderer walks the tree. Each block's render_when and render_callback get ( array $settings, RenderContext $context, array $block ).
  4. 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

ArgumentDefaultNotes
title, description, iconname, '', dashicons-screenoptionsShown in the element picker
categorycontentwoocommerce, layout, content or conversion
page_types['*']cart, checkout, thank_you
settings[]Field map (see below)
multipletruefalse makes it single-use
required, required_group, conflictsfalse, '', []Required elements must be visible on every device
render_callback, preview_callbackReturn escaped HTML
render_whenfn( RenderContext $ctx ): bool, storefront only
refreshfalseCheckout: 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.

An unknown 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

FileVersionOverride
templates/builder/cart.php · checkout.php · thankyou.php1.0.0A 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.php1.0.0Copy 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

WhereWhat
siteryn_checkout_settingsSettings, including cart_mode (standard|siteryn|redirect) and Buy Now
siteryn_checkout_builder_backupsThe 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_fieldsPro field manager definitions
Order meta _siteryn_checkout_pro_field_{id}Custom field answers (HPOS-safe)
Order meta _siteryn_checkout_pro_marketing_optinyes / no

Editor JavaScript API

window.siterynCheckoutBuilderEditor (version 2). Load your script with the dependency siteryn-checkout-builder, and only when that script is enqueued.

GroupMembers
StatepageType(), isLoaded(), isDirty(), isBusy(), root
LayoutgetLayout(), replaceLayout( layout, message ), preview( layout ), refresh()
UInotify(), confirm() (Promise), announce(), addAction(), addNodeAction(), addTopBarItem(), addShortcut(), addStepFields(), addSettingsContent(), addSectionPresets(), addNodeBadge()
Editingselected(), getNode( id ), insertNode(), addStep(), limits(), changed(), redraw()
Eventson( '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.