Storefront Next Extensions
An extension is a self-contained folder under src/extensions/<name>/ that adds UI, routes, providers, translations, config and server hooks to the storefront without rewriting base files. The base declares named slots (<UITarget targetId="..." />); an extension's target-config.json says which of its components fill which slot.
Your project ships with several extensions already (store locator, BOPIS, multiship, and demo extensions such as BNPL and ratings and reviews). Look at them before building: they are the reference implementations. See Extension Examples.
Decision gate: should this be an extension?
Run this before writing code; the full walkthrough is in Base Audit.
- Trace the route in
src/routes/down to the place you want to change. Does the base already render equivalent content there? - Already rendered: restyle it (tokens or a component variant, see
storefront-next:sfnext-theming) or fill an existing slot; do not add a parallel copy. - Not rendered and a slot exists: write an extension.
- Not rendered and no slot fits: edit the base route or component directly (you own the whole project) and, if it is a reusable seam, add a
<UITarget>there so later extensions can plug in.
Discover what already exists:
pnpm extensions:list # all UITarget ids and server action hook ids (add --json for tooling)
sfnext extensions list # installed extensions
rg "UITarget" src/ # where a slot is rendered
Structure
src/extensions/my-extension/
├── target-config.json # which components/providers/hooks plug in where
├── components/ # UI, including the components named in target-config.json
├── routes/ # auto-registered routes (flat-route file naming)
├── providers/ # context providers (listed in target-config.json)
├── hooks/ context/ middlewares/ stores/ # as needed
├── locales/<lang>/translations.json
├── config.ts # optional client-side defaults
├── server-config.ts # optional server-only defaults
└── README.md
Create the scaffold with sfnext extensions create -n "My Extension" -d "What it does" (adds the folder, a README and an entry in src/extensions/config.json). Import across the project with the normal alias: @/extensions/my-extension/.... There is no @extensions alias.
target-config.json
{
"components": [
{ "targetId": "sfcc.header.before.cart", "path": "extensions/my-extension/components/header/badge.tsx", "order": 0 }
],
"contextProviders": [
{ "path": "extensions/my-extension/providers/my-provider.tsx", "order": 0 }
],
"actionHooks": [
{ "hookId": "sfcc.checkout.payments.afterSubmitPayment", "handler": "extensions/my-extension/hooks/after-payment.ts", "order": 0 }
]
}
pathis relative tosrc/and points at a module whose default export is the component (no required props; read context or hooks inside). Multiple components on one slot render in ascendingorder; duplicate orders warn because their order is not deterministic.- A slot without children is a replacement slot. A slot that wraps base content (
<UITarget targetId="..."><Base /></UITarget>) is a wrapper slot: the extension component receives the base content as children. - Optional per component:
"enabled": falseto switch it off, and"hint"as a label for the dev-mode overlay. A top-level"devOnly": trueskips the whole file in production builds. - Extension components only inject when a
target-config.jsonexists insrc/extensions/<name>/. - Every
targetIdmust exist as a<UITarget>in the source; otherwise the build fails and names the orphan. Ids are namespaced: use the exactsfcc.-prefixed ids reported bypnpm extensions:list(for examplesfcc.header.before.cart,sfcc.pdp.bnpl.message,sfcc.pdp.reviews.section,sfcc.cart.orderSummary.before). - In
pnpm dev, UITarget dev mode shows markers so you can see which slots exist and what fills them.
Server-side hooks (actionHooks, checkout steps) have their own semantics: Action Hooks.
Routes, translations, config
- Routes: any file in
routes/is merged into the route tree with React Router flat-route naming:_app.store-locator.tsx(inside the app layout),action.set-selected-store.ts(action),resource.stores.ts(resource route). Seesrc/extensions/store-locator/routes/. - Translations:
locales/<lang>/translations.jsonis namespacedext+ PascalCase folder name (my-extension->extMyExtension):useTranslation('extMyExtension').pnpm devandpnpm buildrunpnpm locales:aggregate-extensionsfor you and writesrc/extensions/locales/; that folder is generated, do not edit it. - Client config:
config.tsdefault-exports a plain, JSON-serializable object. It is merged intoconfig.app.extension.<camelName>(read withuseConfig()in components orgetConfig()on the server) and can be overridden per environment withPUBLIC__app__extension__<camelName>__<key>. It is exposed to the browser: never put secrets there. - Server-only config:
server-config.tslands atconfig.app.serverExtension.<camelName>, has noPUBLIC__override, is unreachable from client code (the build fails if a client chunk imports it), and is read withgetConfig(context)in loaders, actions and middleware. pnpm config:aggregate-extensions(also run by dev and build) writessrc/extensions/config/; generated, do not edit. Folder names must be letters, digits and hyphens starting with a letter. Seedocs/README-CONFIG.md.
Integration markers
Extensions that modify base files mark their edits with comments so they can be installed and removed cleanly:
selectedStoreMiddleware /** @sfdc-extension-line SFDC_EXT_STORE_LOCATOR */,
// @sfdc-extension-block-start SFDC_EXT_STORE_LOCATOR
import { StoreLocatorProvider } from '@/extensions/store-locator/providers/store-locator';
// @sfdc-extension-block-end SFDC_EXT_STORE_LOCATOR
/** @sfdc-extension-file SFDC_EXT_STORE_LOCATOR */ // marks an entire file as belonging to the extension
Use them when the extension needs edits outside its folder (middleware registration, a nav link, a loader) and you want sfnext extensions remove to be able to undo them. Each marker name (SFDC_EXT_*) is a key in src/extensions/config.json. Install, remove and marker details: CLI and Install.
Removing or replacing a demo extension
BNPL, customer preferences, ratings and reviews, and product content ship with mock fixtures. To integrate a real provider, replace the bodies of their API functions (for example src/extensions/bnpl/lib/api/bnpl.server.ts). To drop one, use sfnext extensions remove -e SFDC_EXT_BNPL, then run pnpm locales:aggregate-extensions. docs/README-FEATURE-STUBS.md describes finding stubs with grep -r "@feature-stub" src/.
Best practices
- Keep the extension self-contained; reach into the base only through slots, hooks and markers.
- Prefer a slot over editing a base file; add a slot when you keep editing the same spot.
- Use
orderdeliberately and give each slot entry a distinct value. - Import via
@/extensions/...and keep translations in the extension's own namespace. - After adding a slot, extension or locale, run
pnpm devonce so the aggregation and registries regenerate, and commit nothing fromsrc/extensions/locales/orsrc/extensions/config/by hand.
Related Skills
storefront-next:sfnext-overview- project map and where things livestorefront-next:sfnext-components- component conventions used inside extensionsstorefront-next:sfnext-routing- how extension routes join the route treestorefront-next:sfnext-i18n- translation namespaces and locale filesstorefront-next:sfnext-configuration-config.tslayering andPUBLIC__overridesstorefront-next:sfnext-theming- token and brand changes that do not need an extensionstorefront-next:sfnext-commerce-features- checkout, multiship, BOPIS flowsstorefront-next:sfnext-page-designer- merchant-editable blocksb2c:b2c-hooks- B2C platform-side hooks (different from Storefront Next action hooks)