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.
Storefront routes
Section titled “Storefront routes”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.
Register callback objects
Section titled “Register callback objects”For example, the provider can return a plugin-owned CFC:
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.
Header actions
Section titled “Header actions”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.
Product badges
Section titled “Product badges”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.
{ '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.
Product-detail UI
Section titled “Product-detail UI”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.
Store line configuration
Section titled “Store 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:
{ '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.
Pricing adjustments
Section titled “Pricing adjustments”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.
Subtotal display event
Section titled “Subtotal display event”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.