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 needskahunacart.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
- In the admin sidebar, click KahunaCart, then Extensions.
- 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
- In the admin sidebar, click KahunaCart, then Extensions.
- Find the zone in the Zones panel.
- 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
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:
// 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.
$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():
{{ 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.
$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.