Skip to content

Powered by Grav CMS + Helios Theme

Extension zones

Extension zones are named places in the admin and on the storefront where an add-on can draw something without editing a template or a view. This page is for add-on developers, and for merchants who want to reorder what add-ons put on their screens.

Before you begin

  • Mounting a widget needs a plugin that subscribes to onKahunaCartRegisterZones.
  • Reordering zones in the admin needs kahunacart.settings. Reading the catalogue and switching dev mode on needs kahunacart.orders.view.

Zones and widgets

A zone is a named slot on a screen. order.details.side is the narrow column beside an open order in the admin; cart.express is the wallet-button region on the cart page. Zone ids are {entity}.{scope} or {entity}.{scope}.{slot}, lowercase.

A widget is what an add-on mounts into a zone. Widget ids are {plugin}.{widget}, such as kahunacart-licenses.keys. The merchant's saved widget order refers to widget ids. The plugin can therefore move the class that renders a widget without affecting the saved layout.

Zone ids are stable for the life of the plugin. Renaming one is a breaking change.

Note

There are no .before and .after zone modifiers. Position is the merchant's decision. A widget's position is only the default order on a store where nobody has rearranged anything.

The zone catalogue

Zone Kind Where it draws
order.list admin Above the orders table, under the search bar and filter chips
order.details admin The main column of an open order, after the totals and before the history
order.details.side admin The narrow column beside an open order, under the customer card
order.details.actions admin The action row of an open order, after Mark paid, Ship and Refund and before Cancel
product.list admin Above the products table
product.details admin The main column of the product editor, after the built-in panels
product.details.side admin The aside of the product editor, under Status and Categories
customer.details admin An open customer record, after the account panel and before the orders table
customer.details.side admin Beside the customer record, after the address cards
cart.express storefront The wallet-button region on the cart page, above the checkout button
checkout.express storefront The wallet-button region at the top of checkout, above the address form
account.panels alias The panels on an account page. Catalogued here, registered through onKahunaCartAccountPanels
settings.sections alias The add-on screens in the admin sidebar. Catalogued here, registered through onKahunaCartAdminSections

An alias zone gets its contents from an older event that still works. Mounting a widget into one is refused, with a log line naming the event to use instead. See Admin sections and Customer accounts.

The three widgets the base plugin mounts

Widget Zones Draws
kahunacart.express cart.express, checkout.express The wallet buttons every eligible provider registered, at the merchant's one height, radius and theme. See Payment providers
kahunacart.custom-fields order.details.side, product.details.side, customer.details.side The custom fields declared on that record
kahunacart.webhook-deliveries order.details.side What the outbound subscriptions were told about that order. It draws nothing when there is nothing to say

All three are in the catalogue like any add-on's widgets, so a merchant can move or reorder them.

Find a zone id with dev mode

  1. In the admin sidebar, click KahunaCart, then Extensions.
  2. In the Zones panel, tick Show zone ids on every screen.

Every zone on every admin screen now draws its own id, and each widget in it draws its widget id. An empty zone shows as an outlined region with its id above it.

Dev mode is per admin user, not per store; your colleague's screens are unchanged. It is the one user-scoped key the base plugin ships in the settings store, layout.dev_mode. See Custom fields and the settings store.

Reorder the widgets in a zone

  1. In the admin sidebar, click KahunaCart, then Extensions.
  2. Find the zone in the Zones panel.
  3. Use the ↑ and ↓ arrows beside each widget.

The order is saved immediately in the layout namespace of the settings store, under a key named after the zone. Back to the declared order removes the saved list so the widgets fall back to their declared positions.

Widgets absent from the saved list appear after those in it, so widgets from a plugin installed later are appended. The list retains ids for widgets no longer mounted, so disabling and re-enabling a plugin restores its widgets to their saved positions.

Mount a widget from an add-on

Subscribe to onKahunaCartRegisterZones. The event carries registry, a ZoneRegistry. See Events.

An admin widget

PHP
public function onKahunaCartRegisterZones(Event $event): void
{
    $event['registry']->mount('order.details.side', 'kahunacart-warranty.summary', [
        'label' => 'Warranty',
        'position' => 20,
        'script_path' => 'plugin://kahunacart-warranty/admin-next/widgets/summary.js',
    ], 'kahunacart-warranty');
}

The admin fetches that file from GET /kahunacart/zones/widgets/{id}.js and imports it once. The script registers the widget under the same id:

JS
// admin-next/widgets/summary.js
window.KahunaCart.registerZoneWidget('order.details.side', 'kahunacart-warranty.summary', {
    label: 'Warranty',
    position: 20,
    render(el, { data, api, esc }) {
        el.innerHTML = `<section class="panel"><h3>Warranty</h3><p>${esc(data.order.number)}</p></section>`;
    },
});
render argument What it is
el The element to write into. Returning a string writes it for you
data What the screen left for the zone: { order }, { product }, { customer }, { orders } or { products }, depending on the zone
ctx The same context an admin section is handed: apiFetch, currency, exponent, base
api A shortcut for ctx.apiFetch
esc HTML-escape a string. Use it on everything that came from the store
zone, widget The ids, so one function can serve several mounts
host The view element the zone sits in

render() runs again every time the screen around it redraws, so keep it a pure function of data and hold your own state on the element.

script_path must resolve inside the plugins directory, the same rule as an admin section's script. It never appears in a response.

Tip

A section script that is already loaded can also register a widget, with no script_path and no PHP mount. Call window.KahunaCart.registerZoneWidget() with the zone id. The base plugin draws its own custom-field editor this way.

A storefront widget

A storefront zone takes html: a string, or a callable handed the zone's context that returns markup.

PHP
$event['registry']->mount('cart.express', 'kahunacart-wallet.button', [
    'label' => 'Express pay',
    'position' => 10,
    'html' => static fn (array $context): string => '<button type="button" class="kahunacart-wallet">Pay now</button>',
], 'kahunacart-wallet');

A theme prints the zone with kahunacart_zone():

TWIG
{{ kahunacart_zone('cart.express', {order: order, items: items}) }}

The stock cart and checkout templates already print cart.express and checkout.express. The zone renders as one <div class="kahunacart-zone" data-zone="…">, with each widget in its own <div class="kahunacart-widget" data-widget="…">. An empty zone renders nothing, so a theme can print it unconditionally. See Twig functions and filters.

A widget whose callable throws is dropped and logged. The page still renders.

Declare a zone on your own screen

An add-on that ships an admin section can declare zones on it, so other add-ons can extend its page.

PHP
$registry = $event['registry'];
$registry->declare(
    'warranty.details.side',
    'Warranty record, side',
    'kahunacart-warranty',
    ZoneRegistry::KIND_ADMIN,
    'Beside a warranty record, under the product card.'
);
Argument Notes
$id {entity}.{scope} or {entity}.{scope}.{slot}, lowercase. Anything else is refused and logged
$label What the Extensions screen calls it. Falls back to the id
$owner Your plugin slug
$kind ZoneRegistry::KIND_ADMIN or KIND_STOREFRONT
$description One sentence saying where it draws, shown on the Extensions screen

Declare before you mount. A widget for a zone nobody has declared is refused and logged.

First registration wins for zone ids and widget ids alike. A second plugin claiming either is refused, with a log line naming the first.

The endpoints

Method Path Permission
GET /kahunacart/zones kahunacart.orders.view
GET /kahunacart/zones/widgets/{id}.js kahunacart.orders.view
GET PUT /kahunacart/settings-store/layout kahunacart.orders.view, then each key's own

GET /kahunacart/zones returns every zone with its widgets in the merchant's order, the saved layouts, and whether the calling user has dev mode on. The MCP tool is list_zones.

Reordering is a write to the settings store: PUT /kahunacart/settings-store/layout with the zone id as the key and the widget ids as the value. Dev mode is dev_mode in the same namespace. The admin shell fetches the widget-script route itself; it is not a caller-facing endpoint. See REST API.