Build content blocks that merchants add to site-wide regions, such as the header and the mega menu. These site-wide content blocks appear on every page containing the region. With content blocks in site-wide regions, merchants maintain brand consistency and save time when updating promotional messages or navigation elements that appear site-wide. For example, merchants can add an announcement banner to a header that appears on every page in the storefront.
Let's walk though the process of building an announcement banner content block that can be added to the storefront header as an example. The announcement banner will appear on all page headers in the storefront. A Page Designer component (instance id header) exposes an announcement region above the storefront's header. This task involves two React components, their registry entries, and two B2C descriptors.
- A
useComponentfetch of the site-wide component in your app shell, rendered inline through<Region>. - An
EmbeddedSubtreeProviderwrapper (new instorefront-next-runtime@1.2.0) that tells the design runtime this subtree is a site-wide region, not a page. - Two new components: a
Headerlayout that hosts the region and anAnnouncementBannercontent block, in addition to their registry and type-map entries. - Two new cartridge descriptors, one of which uses the site-wide-region
"embedded": trueflag and an explicitcomponent_id.
Prerequisites
- PWA Kit v3.21 or later.
- Page Designer integration with PWA Kit. See Integrate Page Designer with PWA Kit.
@salesforce/commerce-sdk-reactwith theuseComponenthook. ImportuseComponentfrom the package root. ImportRegion/Page/registry/PageDesignerProviderfrom the/page-designersubpath.@salesforce/storefront-next-runtime@1.1.0or later—this is the version that exportsEmbeddedSubtreeProviderfrom@salesforce/storefront-next-runtime/design/react/core. Site-Wide Regions don't work on version0.4.2.- Node 24 or later for tooling and local development.
- A SLAS client with the
sfcc.shopper-experiencescope—useComponentcalls the Shopper ExperiencegetComponentendpoint.
Use New Fields from the PWA Kit Template
The Site-Wide Regions for Content Blocks feature requires PWA Kit v3.21 or later, which includes new fields that this feature uses. If you built your storefront with v3.21 or later of the template, your storefront contains the new fields. Otherwise, add the new fields manually.
The new fields are:
embedded: This field indicates whether the header component can contain a content block.component_id: This field identifies the header and is required for calling thegetComponentB2C Commerce API (SCAPI) call to get the embedded content block in a page that's not in Page Designer—a page that's not a product detail or listing, about us, or home page. The get Component API endpoint is:GET /experience/shopper-experience/v1/organizations/{organizationId}/components/{componentId}. See Shopper Experience endpoints.
Step 1: Add the Header Layout Component
The header component holds no chrome of its own—your storefront's existing Header or AboveHeader remain the visual header. This component's only job is to expose the announcement region so content managers have a place to drop blocks. It is a thin <Region> passthrough.
// file: app/page-designer/layouts/header/index.jsx
import React from 'react'
import PropTypes from 'prop-types'
import {Region} from '@salesforce/commerce-sdk-react/page-designer'
/**
* Header component (site-wide region host).
*
* A single Page Designer component (instance id `header`) whose sole purpose is to
* expose an `announcement` region above the storefront header. It holds no chrome of its
* own—the storefront's own `Header`/`AboveHeader` remain the visual header. Authors place
* content blocks (e.g. Announcement Banner) into the `announcement` region.
*
* @param {object} props
* @param {object} props.component - The Page Designer component data (injected by the V2 pipeline).
* @returns {React.ReactElement|null} - The rendered announcement region, or null when absent.
*/
export const Header = ({component}) => {
return <Region component={component} regionId="announcement" />
}
Header.propTypes = {
component: PropTypes.object.isRequired
}
Header.displayName = 'PageDesignerHeader'
export default Header
In this example:
- The header component renders a single named region.
<Region component={component} regionId="announcement" />renders whatever content blocks a merchandiser placed in theannouncementregion of this component instance. componentis required. When the V2 pipeline renders this component (via<Region>/<Component>), it injects the component's own data as thecomponentprop. You pass it to the nested<Region>.- Export the header component from the layouts barrel so that the component map can import it (Step 4).
Step 2: Add the AnnouncementBanner Content Block
This is the actual content block a merchandiser drops into the header's announcement region. It is a leaf component: no regions, just editable attributes.
// file: app/page-designer/content/announcement-banner/index.jsx
import React from 'react'
import PropTypes from 'prop-types'
import {
Box,
Skeleton,
Text,
Link as ChakraLink
} from '@salesforce/retail-react-app/app/components/shared/ui'
import Link from '@salesforce/retail-react-app/app/components/link'
import {isAbsoluteURL} from '@salesforce/retail-react-app/app/page-designer/utils'
const HEIGHT_STYLE = {
sm: {py: 1.5, fontSize: 'xs'},
md: {py: 3, fontSize: 'sm'},
lg: {py: 5, fontSize: 'md'}
}
const ALIGNMENT_JUSTIFY = {
left: 'flex-start',
center: 'center',
right: 'flex-end'
}
const COLOR_SCHEME_STYLE = {
primary: {bg: 'blue.600', color: 'white'},
secondary: {bg: 'gray.100', color: 'gray.800'},
destructive: {bg: 'red.600', color: 'white'}
}
const normalize = (value, allowed, fallback) => (allowed.includes(value) ? value : fallback)
/**
* Announcement Banner component.
*
* A banner for announcements, promotions, and alerts. Rendered as an authorable
* content block; typically placed in the header's `announcement` region.
*
* @param {object} props
* @param {string} props.message - The announcement text (required; renders nothing when empty).
* @param {string} [props.linkUrl] - Optional link target. Rendered only with linkText.
* @param {string} [props.linkText] - Optional link label. Rendered only with linkUrl.
* @param {string} [props.colorScheme] - primary | secondary | destructive (default primary).
* @param {string} [props.height] - sm | md | lg (default md).
* @param {string} [props.alignment] - left | center | right (default center).
* @returns {React.ReactElement|null} - AnnouncementBanner component.
*/
export const AnnouncementBanner = ({
message,
linkUrl,
linkText,
colorScheme,
height,
alignment
}) => {
if (!message) return null
const heightStyle = HEIGHT_STYLE[normalize(height, ['sm', 'md', 'lg'], 'md')]
const resolvedAlignment = normalize(alignment, ['left', 'center', 'right'], 'center')
const colorStyle =
COLOR_SCHEME_STYLE[normalize(colorScheme, ['primary', 'secondary', 'destructive'], 'primary')]
const isAbsolute = isAbsoluteURL(linkUrl)
const LinkWrapper = isAbsolute ? ChakraLink : Link
const linkProps = isAbsolute ? {href: linkUrl} : {to: linkUrl}
return (
<Box
role="status"
className={'announcement-banner'}
data-testid={'announcement-banner'}
display="flex"
alignItems="center"
gap={2}
px={{base: 4, md: 10}}
letterSpacing="wide"
justifyContent={ALIGNMENT_JUSTIFY[resolvedAlignment]}
{...heightStyle}
{...colorStyle}
>
<Text textAlign={resolvedAlignment} margin={0}>
{message}
{linkUrl && linkText && (
<>
{' '}
<LinkWrapper
{...linkProps}
textDecoration="underline"
fontWeight="medium"
whiteSpace="nowrap"
color="inherit"
>
{linkText}
</LinkWrapper>
</>
)}
</Text>
</Box>
)
}
AnnouncementBanner.propTypes = {
message: PropTypes.string,
linkUrl: PropTypes.string,
linkText: PropTypes.string,
colorScheme: PropTypes.string,
height: PropTypes.string,
alignment: PropTypes.string
}
AnnouncementBanner.displayName = 'AnnouncementBanner'
/**
* Suspense fallback for the Announcement Banner. Mirrors the default md/center/primary
* height so switching from fallback to real content does not shift layout.
*
* @returns {React.ReactElement} - Skeleton placeholder.
*/
export function AnnouncementBannerFallback() {
return (
<Box
aria-hidden="true"
display="flex"
alignItems="center"
justifyContent="center"
px={{base: 4, md: 10}}
py={3}
bg="blue.600"
>
<Skeleton height="16px" width="192px" />
</Box>
)
}
AnnouncementBannerFallback.displayName = 'AnnouncementBannerFallback'
export default AnnouncementBanner
// The V2 registry reads a module's named `fallback` export to render during the
// client Suspense boundary while the component chunk loads.
export {AnnouncementBannerFallback as fallback}
In this example:
- The prop names are the descriptor's attribute ids.
message,linkUrl,linkText,colorScheme,height,alignmentmap 1:1 to theattribute_definitionsyou author in Step 5. Keep them in sync. if (!message) return null. A banner with no message renders nothing—a required attribute, defensively enforced in the component too.- Token-based color/height/alignment, normalized.
normalize()clamps any unexpected value back to a safe default (md/center/primary) so a bad authored value can't break layout or contrast. - Absolute vs. relative links.
isAbsoluteURL(frompage-designer/utils) picks between the ChakraLink(externalhref) and the app routerLink(internalto), so internal links stay client-side. - The
fallbacknamed export is the Suspense placeholder. The V2 registry renders a module'sfallbackexport while the component's code-split chunk loads. ExportingAnnouncementBannerFallback as fallbackwires that up; the skeleton mirrors the default banner height to avoid layout shift.
Add a content barrel so the component map can import from a single path:
// file: app/page-designer/content/index.js
export * from './announcement-banner'
Step 3: Wire the Site-Wide Region into the App Shell
In your full storefront shell (StorefrontApp), fetch the header component with useComponent and render its announcement region above the storefront header, wrapped in EmbeddedSubtreeProvider.
// file: app/components/_app/index.jsx (excerpts)
// Add useComponent to the existing commerce-sdk-react import
import {
useAccessToken,
useCategory,
useComponent,
useShopperBasketsMutation,
useUsid
} from '@salesforce/commerce-sdk-react'
// Add Region to the existing page-designer import
import {PageDesignerProvider, Region} from '@salesforce/commerce-sdk-react/page-designer'
import PageDesignerInit from '@salesforce/retail-react-app/app/components/page-designer-init'
// New for site-wide regions: the embedded-subtree provider from storefront-next-runtime 1.2.0
import {EmbeddedSubtreeProvider} from '@salesforce/storefront-next-runtime/design/react/core'
Inside StorefrontApp, fetch the site-wide component alongside the other top-level data hooks:
// file: app/components/_app/index.jsx (inside StorefrontApp)
// Site-wide header region: a Page Designer component (instance id `header`) that
// exposes an `announcement` region rendered above the storefront header.
// `useComponent` is Page-Designer-mode aware (handles mode/pdToken internally).
const {data: embeddedHeader} = useComponent({parameters: {componentId: 'header'}})
Then render the region above <AboveHeader />, gated on the component existing:
// file: app/components/_app/index.jsx (inside the header wrapper, non-checkout branch)
<Box {...styles.headerWrapper}>
{!isCheckout ? (
<>
{embeddedHeader && (
<EmbeddedSubtreeProvider embedded>
<Region component={embeddedHeader} regionId="announcement" />
</EmbeddedSubtreeProvider>
)}
<AboveHeader />
<Header
onMenuClick={onOpen}
{/* …existing props… */}
/>
</>
) : (
{/* …checkout header… */}
)}
</Box>
In this example:
useComponent({parameters: {componentId: 'header'}})fetches the site-wide component. ThecomponentIdis a fixed, well-known ID ('header'). It matches the descriptor'scomponent_idin Step 5. Unlike Content Block Editor, nothing supplies this ID per-request. The storefront always asks for the same component.useComponentis already Page-Designer-mode aware. It readsmodeorpdTokenfrom the provider config (the Content Block Editor plumbing you already have) and switches torawResponse: truein design or preview mode. You don't add any mode handling here.- Gate on
embeddedHeader. Until the component resolves—or if no header content is authored—render nothing. There is no banner or layout shift beyond the component's own fallback. EmbeddedSubtreeProvider embeddedmarks this subtree as a site-wide region, which is the new runtime piece. It tells the design runtime that the wrapped<Region>is a site-wide region living in your layout (not a Page Designer page), so Content Block Editor can target and edit it in place. Theembeddedboolean prop turns that behavior on.- The
embeddedboolean prop renders the sameannouncementregion as theHeaderlayout component (Step 1). The app-shell path (<Region component={embeddedHeader} regionId="announcement" />) is what shows on the live storefront. TheHeaderlayout component is the registry-resolved rendering used when the component is reached through the V2 pipeline (e.g. in Content Block Editor). Both render the identical region. - Not on checkout. The header region renders only in the non-checkout branch, matching where the storefront normally shows its header.
Step 4: Register the New Components
Add the two new components to the eager type map and the lazy importer registry. Site-wide regions add Header and AnnouncementBanner.
// file: app/page-designer/component-map.js
import {ImageWithText, ImageTile} from '@salesforce/retail-react-app/app/page-designer/assets'
import {
Carousel,
Header,
MobileGrid1r1c,
MobileGrid2r1c,
MobileGrid2r2c,
MobileGrid2r3c,
MobileGrid3r1c,
MobileGrid3r2c
} from '@salesforce/retail-react-app/app/page-designer/layouts'
import {AnnouncementBanner} from '@salesforce/retail-react-app/app/page-designer/content'
// Map Page Designer component type IDs to React components
export const PAGEDESIGNER_TO_COMPONENT = {
'commerce_assets.imageAndText': ImageWithText,
'commerce_assets.imageTile': ImageTile,
'commerce_layouts.carousel': Carousel,
'commerce_layouts.header': Header,
'commerce_layouts.mobileGrid1r1c': MobileGrid1r1c,
'commerce_layouts.mobileGrid2r1c': MobileGrid2r1c,
'commerce_layouts.mobileGrid2r2c': MobileGrid2r2c,
'commerce_layouts.mobileGrid2r3c': MobileGrid2r3c,
'commerce_layouts.mobileGrid3r1c': MobileGrid3r1c,
'commerce_layouts.mobileGrid3r2c': MobileGrid3r2c,
'commerce_assets.announcementBanner': AnnouncementBanner
}
Export Header from the layouts barrel so the map import above resolves:
// file: app/page-designer/layouts/index.js
export * from './carousel'
export * from './header'
export * from './mobileGrid1r1c'
export * from './mobileGrid2r1c'
export * from './mobileGrid2r2c'
export * from './mobileGrid2r3c'
export * from './mobileGrid3r1c'
export * from './mobileGrid3r2c'
Add the two lazy importers to the registry:
// file: app/page-designer/registry.js (added lines)
export function initializeRegistry() {
// …existing importers (imageAndText, imageTile, carousel, mobileGrid*)…
registry.registerImporter('commerce_layouts.carousel', () => import('./layouts/carousel'))
registry.registerImporter('commerce_layouts.header', () => import('./layouts/header'))
registry.registerImporter('commerce_assets.announcementBanner', () =>
import('./content/announcement-banner')
)
}
Each typeId is {group}.{componentId}, which must equal {descriptor-folder}.{descriptor-filename}. For site-wide regions that means:
| typeId | Map Key | Registry Importer Key | Descriptor Path |
|---|---|---|---|
commerce_layouts.header |
Header |
commerce_layouts.header |
commerce_layouts/header.json |
commerce_assets.announcementBanner |
AnnouncementBanner |
commerce_assets.announcementBanner |
commerce_assets/announcementBanner.json |
important: All three columns must match byte-for-byte, or the component silently fails to resolve.
Step 5: Author and Upload the Descriptors (Cartridge)
As with Content Block Editor, the React side only teaches the storefront how to render. The B2C Commerce side needs descriptors for both new components. The header descriptor is the one that uses the site-wide-region fields.
The header descriptor
// file: cartridges/app_pwa_base/cartridge/experience/components/commerce_layouts/header.json
{
"name": "Header",
"description": "Header component that exposes a site-wide announcement region above the storefront header.",
"group": "commerce_layouts",
"arch_type": "headless",
"embedded": true,
"component_id": "header",
"region_definitions": [
{
"id": "announcement",
"name": "Announcement",
"description": "Displayed above the header."
}
],
"attribute_definition_groups": [
{
"id": "header",
"name": "Header",
"description": "Global site header with an announcement region.",
"attribute_definitions": []
}
]
}
In this example:
"embedded": true. This field marks the component as hosting a site-wide region—a fixed component that lives in your storefront chrome, not one dropped into a Page Designer page. It is the flag Content Block Editor and the Content Blocks editor use to treat it as a site-wide region."component_id": "header". This field fixes the instance ID so the storefront can fetch it by a known ID—exactly thecomponentIdyour app shell passes touseComponent({parameters: {componentId: 'header'}})in Step 3.region_definitionsdeclares theannouncementregion. The regionid(announcement) is what both the app-shell<Region regionId="announcement">and theHeaderlayout component render. It's where the merchandiser drops the banner.- No editable attributes on the header.
attribute_definitionsis empty—the header is a structural container. The editable content lives in the banner it hosts.
note: embedded and component_id are site-wide-region fields. A standard Content Block Editor component (leaf or layout, like the carousel) doesn't use these. If you're adding a normal editable component, follow the Content Block Editor guide's descriptor shape instead.
The Announcement Banner Descriptor
The banner is a normal leaf content block—no embedded flag. Its attribute_definitions are the props the React component reads (Step 2).
// file: cartridges/app_pwa_base/cartridge/experience/components/commerce_assets/announcementBanner.json
{
"name": "Announcement Banner",
"description": "A banner for announcements, promotions, and alerts.",
"group": "commerce_assets",
"arch_type": "headless",
"region_definitions": [],
"attribute_definition_groups": [
{
"id": "announcementBanner",
"name": "Announcement Banner",
"description": "A banner for announcements, promotions, and alerts.",
"attribute_definitions": [
{
"id": "message",
"name": "Message",
"type": "string",
"required": true,
"description": "The announcement text."
},
{
"id": "linkUrl",
"name": "Link URL",
"type": "url",
"required": false,
"description": "Optional link target. Shown only when Link Text is also set."
},
{
"id": "linkText",
"name": "Link Text",
"type": "string",
"required": false,
"description": "Optional link label. Shown only when Link URL is also set."
},
{
"id": "colorScheme",
"name": "Color Scheme",
"type": "enum",
"required": false,
"values": ["primary", "secondary", "destructive"],
"default_value": "primary",
"description": "Token-based color treatment for guaranteed contrast."
},
{
"id": "height",
"name": "Height",
"type": "enum",
"required": false,
"values": ["sm", "md", "lg"],
"default_value": "md",
"description": "Vertical density of the banner."
},
{
"id": "alignment",
"name": "Alignment",
"type": "enum",
"required": false,
"values": ["left", "center", "right"],
"default_value": "center",
"description": "Horizontal alignment of the message."
}
]
}
]
}
note: The six attribute ids (message, linkUrl, linkText, colorScheme, height, alignment) are exactly the props the AnnouncementBanner component destructures. The enum values and default_values match the component's normalize() allow-lists and fallbacks—keep the two definitions in lockstep.
Deploy the Cartridge and Register
Deploy the cartridge to B2C Commerce using the CLI. See Cartridges. Then add it to your site's cartridge path in Administration > Sites > Manage Sites > {site} > Settings. After the descriptors are live, author the content in the Content Block editor in Merchant Tools > Content > Content Blocks. Create a block of the announcement-banner type, set its properties, and add it as a site-wide region on the header. To learn more, see the steps in Add a Content Block in a Site-Wide Region for Storefront Next. The Page Designer steps in Business Manager are identical for any supported storefront type.
Next, reload any storefront page—the banner renders above the header. Open it in the focused visual canvas to edit it in Content Block Editor.
Step 6: (Optional) Mock Data for Tests and Storybook
If you test the components in isolation, this mock mirrors the shape useComponent returns for the header, including a nested announcement banner.
// file: app/mocks/page-designer.js (excerpt—site-wide-region additions)
export const mockAnnouncementBanner = {
message: 'Free standard shipping on orders over $50',
linkUrl: '/sale',
linkText: 'Shop the sale',
colorScheme: 'primary',
height: 'md',
alignment: 'center'
}
export const mockEmbeddedHeader = {
id: 'header',
typeId: 'commerce_layouts.header',
regions: [
{
id: 'announcement',
components: [
{
id: 'announcement-banner-1',
typeId: 'commerce_assets.announcementBanner',
data: mockAnnouncementBanner
}
]
}
]
}
mockEmbeddedHeader has id: 'header' and typeId: 'commerce_layouts.header', and its single announcement region holds one commerce_assets.announcementBanner component whose data is the banner attributes. This is the exact shape <Region component={embeddedHeader} regionId="announcement"> expects—use it to render the subtree without a live getComponent call.