Skip to content

Extend the storefront with plugins

Storefront plugins contribute through callbacks. Themes render the prepared contributions. Do not patch theme files or assume the active theme has the Default theme’s DOM structure.

Return route structs from getStorefrontRouteDefinitions(). Use the same controller/action/method fields as admin routes, with these differences:

  • Route names start with plugin.<plugin-id>.storefront..
  • Paths are under /plugins/<plugin-id>, without the admin base path.
  • NetSeller normalizes the area to shop.
  • No customer authentication is added automatically. Your action must enforce the relevant customer, company and shop access.
  • State-changing requests still require CSRF protection.

A plugin controller returns a response struct. It can supply a plugin-owned view or redirect using a named URL. Do not make a private customer route public merely because its URL is hard to guess.

For example, the provider can return a plugin-owned CFC:

Provider method
public array function getStorefrontProductBadgeContributors() {
return [new netseller_plugin_example_hello.services.badge_contributor()];
}

That CFC implements buildStorefrontProductBadgeContributions(context). Every callback in this page returns an array; return [] when it does not apply.

Provider method: getStorefrontHeaderActionContributors().

Callback: buildStorefrontHeaderActionContributions(context).

The context supplies requestContext, datasource, shop, languageId, theme, urls and labels. Each returned action has:

Field Type Meaning
id string Required stable contribution ID.
html string Required prepared HTML. Encode all dynamic text and attributes.
placement string commerce_actions, also the default.
sortOrder number Optional, default 100.

This callback explicitly permits prepared HTML; registry definitions do not. Keep actions accessible, build URLs from named routes and enforce permissions in their destination actions.

Provider method: getStorefrontProductBadgeContributors().

Callback: buildStorefrontProductBadgeContributions(context).

Context fields are datasource, productId, shopId, languageId, fallbackLanguageId, surface and existingBadges.

Return {label, key, color} structs. label is required plain text; translate it before returning. key is an optional stable identifier, otherwise derived from the label. color is optional three- or six-digit hexadecimal colour.

One returned badge
{ 'key' = "example-label", 'label' = "Made to order", 'color' = "245C45" }

Apply your plugin’s eligibility rules before returning a badge; the example shows the result shape, not a rule for every product.

Provider method: getLineItemConfigurationContributors().

The returned objects can implement buildStorefrontProductDetailContributions(context) to add inputs around the add-to-cart controls. Context includes:

Context fields Purpose
datasource, shopId Data and shop scope.
languageId, fallbackLanguageId, localeCode Language and formatting.
currencyId, priceListId Pricing context.
product, productData, variantSelection Prepared product and selection data.
configurationLabels, labels Prepared labels.

Each returned struct has required id and html, optional sortOrder=100, and placement: before_quantity (default), after_quantity, before_add_to_cart or after_add_to_cart.

Use this UI only to collect input. Validate it again on the server when constructing the line configuration.

The same objects can implement buildLineItemConfigurationSegments(context). The add-to-cart context contains datasource, shopId, languageId, fallbackLanguageId, productId, productVariantId, quantity, cartKey, customerContext, configurationLabels and submitted formData.

Validate ownership and submitted values before returning a segment:

One configuration segment
{
'key' = "example.hello",
'lineKey' = "validated-choice-a",
'snapshot' = { 'choice' = "a" },
'display' = {
'cart' = [{ 'label' = "Option", 'value' = "Choice A" }],
'order' = [{ 'label' = "Option", 'value' = "Choice A" }]
}
}

key is required. lineKey distinguishes configurations when constructing the cart line identity. snapshot stores a struct of validated values; do not store secrets. display maps default, cart, order or admin to arrays of plain-text {label, value} rows.

For invalid input, throw LineItemConfiguration.Validation with a translated user-facing message. Never accept a client-supplied price as authoritative.

Provider method: getLineItemPricingContributors().

Callback: buildLineItemPricingAdjustments(context).

The cart supplies datasource, shopId, currencyId, priceListId, productId, productVariantId, quantity, pricingQuantity and normalized lineItemConfiguration. Return an array of structs with numeric unitPriceDelta.

NetSeller applies these deltas only to resolved pricing, sums them into the unit price and derives the line total using the quantity. Calculate adjustments on the server from validated configuration and your plugin’s pricing rules. Return [] if no adjustment applies.

A product configurator replacing the quantity UI can publish its subtotal to the theme:

productRoot.dispatchEvent(new CustomEvent("ns:product-subtotal:update", {
bubbles: true,
detail: {
amount: 149.5,
moneyFormat: preparedPrice.moneyFormat,
visible: true
}
}));

Here productRoot is the product-detail root element and preparedPrice is the prepared pricing data used by your UI. The plugin calculates the amount; the theme formats and displays it. Use visible: false when there is no complete subtotal. This browser event affects display, not server-side pricing.