Build Content Blocks for Site-Wide Regions in PWA Kit

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 useComponent fetch of the site-wide component in your app shell, rendered inline through <Region>.
  • An EmbeddedSubtreeProvider wrapper (new in storefront-next-runtime@1.2.0) that tells the design runtime this subtree is a site-wide region, not a page.
  • Two new components: a Header layout that hosts the region and an AnnouncementBanner content block, in addition to their registry and type-map entries.
  • Two new cartridge descriptors, one of which uses the site-wide-region "embedded": true flag and an explicit component_id.

Prerequisites 

  • PWA Kit v3.21 or later.
  • Page Designer integration with PWA Kit. See Integrate Page Designer with PWA Kit.
  • @salesforce/commerce-sdk-react with the useComponent hook. Import useComponent from the package root. Import Region/Page/registry/PageDesignerProvider from the /page-designer subpath.
  • @salesforce/storefront-next-runtime@1.1.0 or later—this is the version that exports EmbeddedSubtreeProvider from @salesforce/storefront-next-runtime/design/react/core. Site-Wide Regions don’t work on version 0.4.2.
  • Node 24 or later for tooling and local development.
  • A SLAS client with the sfcc.shopper-experience scope—useComponent calls the Shopper Experience getComponent endpoint.

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 the getComponent B2C 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.

1// file: app/page-designer/layouts/header/index.jsx
2
3import React from 'react'
4import PropTypes from 'prop-types'
5import {Region} from '@salesforce/commerce-sdk-react/page-designer'
6
7/**
8 * Header component (site-wide region host).
9 *
10 * A single Page Designer component (instance id `header`) whose sole purpose is to
11 * expose an `announcement` region above the storefront header. It holds no chrome of its
12 * own—the storefront's own `Header`/`AboveHeader` remain the visual header. Authors place
13 * content blocks (e.g. Announcement Banner) into the `announcement` region.
14 *
15 * @param {object} props
16 * @param {object} props.component - The Page Designer component data (injected by the V2 pipeline).
17 * @returns {React.ReactElement|null} - The rendered announcement region, or null when absent.
18 */
19export const Header = ({component}) => {
20  return <Region component={component} regionId="announcement" />
21}
22
23Header.propTypes = {
24  component: PropTypes.object.isRequired
25}
26
27Header.displayName = 'PageDesignerHeader'
28
29export 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 the announcement region of this component instance.
  • component is required. When the V2 pipeline renders this component (via <Region>/<Component>), it injects the component’s own data as the component prop. 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.

1// file: app/page-designer/content/announcement-banner/index.jsx
2
3import React from 'react'
4import PropTypes from 'prop-types'
5import {
6  Box,
7  Skeleton,
8  Text,
9  Link as ChakraLink
10} from '@salesforce/retail-react-app/app/components/shared/ui'
11import Link from '@salesforce/retail-react-app/app/components/link'
12import {isAbsoluteURL} from '@salesforce/retail-react-app/app/page-designer/utils'
13
14const HEIGHT_STYLE = {
15  sm: {py: 1.5, fontSize: 'xs'},
16  md: {py: 3, fontSize: 'sm'},
17  lg: {py: 5, fontSize: 'md'}
18}
19
20const ALIGNMENT_JUSTIFY = {
21  left: 'flex-start',
22  center: 'center',
23  right: 'flex-end'
24}
25
26const COLOR_SCHEME_STYLE = {
27  primary: {bg: 'blue.600', color: 'white'},
28  secondary: {bg: 'gray.100', color: 'gray.800'},
29  destructive: {bg: 'red.600', color: 'white'}
30}
31
32const normalize = (value, allowed, fallback) => (allowed.includes(value) ? value : fallback)
33
34/**
35 * Announcement Banner component.
36 *
37 * A banner for announcements, promotions, and alerts. Rendered as an authorable
38 * content block; typically placed in the header's `announcement` region.
39 *
40 * @param {object} props
41 * @param {string} props.message - The announcement text (required; renders nothing when empty).
42 * @param {string} [props.linkUrl] - Optional link target. Rendered only with linkText.
43 * @param {string} [props.linkText] - Optional link label. Rendered only with linkUrl.
44 * @param {string} [props.colorScheme] - primary | secondary | destructive (default primary).
45 * @param {string} [props.height] - sm | md | lg (default md).
46 * @param {string} [props.alignment] - left | center | right (default center).
47 * @returns {React.ReactElement|null} - AnnouncementBanner component.
48 */
49export const AnnouncementBanner = ({
50  message,
51  linkUrl,
52  linkText,
53  colorScheme,
54  height,
55  alignment
56}) => {
57  if (!message) return null
58
59  const heightStyle = HEIGHT_STYLE[normalize(height, ['sm', 'md', 'lg'], 'md')]
60  const resolvedAlignment = normalize(alignment, ['left', 'center', 'right'], 'center')
61  const colorStyle =
62    COLOR_SCHEME_STYLE[normalize(colorScheme, ['primary', 'secondary', 'destructive'], 'primary')]
63
64  const isAbsolute = isAbsoluteURL(linkUrl)
65  const LinkWrapper = isAbsolute ? ChakraLink : Link
66  const linkProps = isAbsolute ? {href: linkUrl} : {to: linkUrl}
67
68  return (
69    <Box
70      role="status"
71      className={'announcement-banner'}
72      data-testid={'announcement-banner'}
73      display="flex"
74      alignItems="center"
75      gap={2}
76      px={{base: 4, md: 10}}
77      letterSpacing="wide"
78      justifyContent={ALIGNMENT_JUSTIFY[resolvedAlignment]}
79      {...heightStyle}
80      {...colorStyle}
81    >
82      <Text textAlign={resolvedAlignment} margin={0}>
83        {message}
84        {linkUrl && linkText && (
85          <>
86            {' '}
87            <LinkWrapper
88              {...linkProps}
89              textDecoration="underline"
90              fontWeight="medium"
91              whiteSpace="nowrap"
92              color="inherit"
93            >
94              {linkText}
95            </LinkWrapper>
96          </>
97        )}
98      </Text>
99    </Box>
100  )
101}
102
103AnnouncementBanner.propTypes = {
104  message: PropTypes.string,
105  linkUrl: PropTypes.string,
106  linkText: PropTypes.string,
107  colorScheme: PropTypes.string,
108  height: PropTypes.string,
109  alignment: PropTypes.string
110}
111
112AnnouncementBanner.displayName = 'AnnouncementBanner'
113
114/**
115 * Suspense fallback for the Announcement Banner. Mirrors the default md/center/primary
116 * height so switching from fallback to real content does not shift layout.
117 *
118 * @returns {React.ReactElement} - Skeleton placeholder.
119 */
120export function AnnouncementBannerFallback() {
121  return (
122    <Box
123      aria-hidden="true"
124      display="flex"
125      alignItems="center"
126      justifyContent="center"
127      px={{base: 4, md: 10}}
128      py={3}
129      bg="blue.600"
130    >
131      <Skeleton height="16px" width="192px" />
132    </Box>
133  )
134}
135
136AnnouncementBannerFallback.displayName = 'AnnouncementBannerFallback'
137
138export default AnnouncementBanner
139
140// The V2 registry reads a module's named `fallback` export to render during the
141// client Suspense boundary while the component chunk loads.
142export {AnnouncementBannerFallback as fallback}

In this example:

  • The prop names are the descriptor’s attribute ids. message, linkUrl, linkText, colorScheme, height, alignment map 1:1 to the attribute_definitions you 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 (from page-designer/utils) picks between the Chakra Link (external href) and the app router Link (internal to), so internal links stay client-side.
  • The fallback named export is the Suspense placeholder. The V2 registry renders a module’s fallback export while the component’s code-split chunk loads. Exporting AnnouncementBannerFallback as fallback wires 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:

1// file: app/page-designer/content/index.js
2
3export * 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.

1// file: app/components/_app/index.jsx  (excerpts)
2// Add useComponent to the existing commerce-sdk-react import
3import {
4  useAccessToken,
5  useCategory,
6  useComponent,
7  useShopperBasketsMutation,
8  useUsid
9} from '@salesforce/commerce-sdk-react'
10
11// Add Region to the existing page-designer import
12import {PageDesignerProvider, Region} from '@salesforce/commerce-sdk-react/page-designer'
13import PageDesignerInit from '@salesforce/retail-react-app/app/components/page-designer-init'
14
15// New for site-wide regions: the embedded-subtree provider from storefront-next-runtime 1.2.0
16import {EmbeddedSubtreeProvider} from '@salesforce/storefront-next-runtime/design/react/core'

Inside StorefrontApp, fetch the site-wide component alongside the other top-level data hooks:

1// file: app/components/_app/index.jsx  (inside StorefrontApp)
2// Site-wide header region: a Page Designer component (instance id `header`) that
3// exposes an `announcement` region rendered above the storefront header.
4// `useComponent` is Page-Designer-mode aware (handles mode/pdToken internally).
5const {data: embeddedHeader} = useComponent({parameters: {componentId: 'header'}})

Then render the region above <AboveHeader />, gated on the component existing:

1// file: app/components/_app/index.jsx  (inside the header wrapper, non-checkout branch)
2<Box {...styles.headerWrapper}>
3    {!isCheckout ? (
4        <>
5            {embeddedHeader && (
6                <EmbeddedSubtreeProvider embedded>
7                    <Region component={embeddedHeader} regionId="announcement" />
8                </EmbeddedSubtreeProvider>
9            )}
10            <AboveHeader />
11            <Header
12                onMenuClick={onOpen}
13                {/* …existing props… */}
14            />
15        </>
16    ) : (
17        {/* …checkout header… */}
18    )}
19</Box>

In this example:

  • useComponent({parameters: {componentId: 'header'}}) fetches the site-wide component. The componentId is a fixed, well-known ID ('header'). It matches the descriptor’s component_id in Step 5. Unlike Content Block Editor, nothing supplies this ID per-request. The storefront always asks for the same component.
  • useComponent is already Page-Designer-mode aware. It reads mode or pdToken from the provider config (the Content Block Editor plumbing you already have) and switches to rawResponse: true in 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 embedded marks 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. The embedded boolean prop turns that behavior on.
  • The embedded boolean prop renders the same announcement region as the Header layout component (Step 1). The app-shell path (<Region component={embeddedHeader} regionId="announcement" />) is what shows on the live storefront. The Header layout 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.

1// file: app/page-designer/component-map.js
2
3import {ImageWithText, ImageTile} from '@salesforce/retail-react-app/app/page-designer/assets'
4import {
5  Carousel,
6  Header,
7  MobileGrid1r1c,
8  MobileGrid2r1c,
9  MobileGrid2r2c,
10  MobileGrid2r3c,
11  MobileGrid3r1c,
12  MobileGrid3r2c
13} from '@salesforce/retail-react-app/app/page-designer/layouts'
14import {AnnouncementBanner} from '@salesforce/retail-react-app/app/page-designer/content'
15
16// Map Page Designer component type IDs to React components
17export const PAGEDESIGNER_TO_COMPONENT = {
18  'commerce_assets.imageAndText': ImageWithText,
19  'commerce_assets.imageTile': ImageTile,
20  'commerce_layouts.carousel': Carousel,
21  'commerce_layouts.header': Header,
22  'commerce_layouts.mobileGrid1r1c': MobileGrid1r1c,
23  'commerce_layouts.mobileGrid2r1c': MobileGrid2r1c,
24  'commerce_layouts.mobileGrid2r2c': MobileGrid2r2c,
25  'commerce_layouts.mobileGrid2r3c': MobileGrid2r3c,
26  'commerce_layouts.mobileGrid3r1c': MobileGrid3r1c,
27  'commerce_layouts.mobileGrid3r2c': MobileGrid3r2c,
28  'commerce_assets.announcementBanner': AnnouncementBanner
29}

Export Header from the layouts barrel so the map import above resolves:

1// file: app/page-designer/layouts/index.js
2export * from './carousel'
3export * from './header'
4export * from './mobileGrid1r1c'
5export * from './mobileGrid2r1c'
6export * from './mobileGrid2r2c'
7export * from './mobileGrid2r3c'
8export * from './mobileGrid3r1c'
9export * from './mobileGrid3r2c'

Add the two lazy importers to the registry:

1// file: app/page-designer/registry.js  (added lines)
2export function initializeRegistry() {
3  // …existing importers (imageAndText, imageTile, carousel, mobileGrid*)…
4  registry.registerImporter('commerce_layouts.carousel', () => import('./layouts/carousel'))
5  registry.registerImporter('commerce_layouts.header', () => import('./layouts/header'))
6  registry.registerImporter('commerce_assets.announcementBanner', () =>
7    import('./content/announcement-banner')
8  )
9}

Each typeId is {group}.{componentId}, which must equal {descriptor-folder}.{descriptor-filename}. For site-wide regions that means:

typeIdMap KeyRegistry Importer KeyDescriptor Path
commerce_layouts.headerHeadercommerce_layouts.headercommerce_layouts/header.json
commerce_assets.announcementBannerAnnouncementBannercommerce_assets.announcementBannercommerce_assets/announcementBanner.json

All three columns must match byte-for-byte, or the component silently fails to resolve.

Important

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 

1// file: cartridges/app_pwa_base/cartridge/experience/components/commerce_layouts/header.json
2{
3  "name": "Header",
4  "description": "Header component that exposes a site-wide announcement region above the storefront header.",
5  "group": "commerce_layouts",
6  "arch_type": "headless",
7  "embedded": true,
8  "component_id": "header",
9  "region_definitions": [
10    {
11      "id": "announcement",
12      "name": "Announcement",
13      "description": "Displayed above the header."
14    }
15  ],
16  "attribute_definition_groups": [
17    {
18      "id": "header",
19      "name": "Header",
20      "description": "Global site header with an announcement region.",
21      "attribute_definitions": []
22    }
23  ]
24}

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 the componentId your app shell passes to useComponent({parameters: {componentId: 'header'}}) in Step 3.
  • region_definitions declares the announcement region. The region id (announcement) is what both the app-shell <Region regionId="announcement"> and the Header layout component render. It’s where the merchandiser drops the banner.
  • No editable attributes on the header. attribute_definitions is empty—the header is a structural container. The editable content lives in the banner it hosts.

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.

Note

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).

1// file: cartridges/app_pwa_base/cartridge/experience/components/commerce_assets/announcementBanner.json
2{
3  "name": "Announcement Banner",
4  "description": "A banner for announcements, promotions, and alerts.",
5  "group": "commerce_assets",
6  "arch_type": "headless",
7  "region_definitions": [],
8  "attribute_definition_groups": [
9    {
10      "id": "announcementBanner",
11      "name": "Announcement Banner",
12      "description": "A banner for announcements, promotions, and alerts.",
13      "attribute_definitions": [
14        {
15          "id": "message",
16          "name": "Message",
17          "type": "string",
18          "required": true,
19          "description": "The announcement text."
20        },
21        {
22          "id": "linkUrl",
23          "name": "Link URL",
24          "type": "url",
25          "required": false,
26          "description": "Optional link target. Shown only when Link Text is also set."
27        },
28        {
29          "id": "linkText",
30          "name": "Link Text",
31          "type": "string",
32          "required": false,
33          "description": "Optional link label. Shown only when Link URL is also set."
34        },
35        {
36          "id": "colorScheme",
37          "name": "Color Scheme",
38          "type": "enum",
39          "required": false,
40          "values": ["primary", "secondary", "destructive"],
41          "default_value": "primary",
42          "description": "Token-based color treatment for guaranteed contrast."
43        },
44        {
45          "id": "height",
46          "name": "Height",
47          "type": "enum",
48          "required": false,
49          "values": ["sm", "md", "lg"],
50          "default_value": "md",
51          "description": "Vertical density of the banner."
52        },
53        {
54          "id": "alignment",
55          "name": "Alignment",
56          "type": "enum",
57          "required": false,
58          "values": ["left", "center", "right"],
59          "default_value": "center",
60          "description": "Horizontal alignment of the message."
61        }
62      ]
63    }
64  ]
65}

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.

Note

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.

1// file: app/mocks/page-designer.js  (excerpt—site-wide-region additions)
2export const mockAnnouncementBanner = {
3  message: 'Free standard shipping on orders over $50',
4  linkUrl: '/sale',
5  linkText: 'Shop the sale',
6  colorScheme: 'primary',
7  height: 'md',
8  alignment: 'center'
9}
10
11export const mockEmbeddedHeader = {
12  id: 'header',
13  typeId: 'commerce_layouts.header',
14  regions: [
15    {
16      id: 'announcement',
17      components: [
18        {
19          id: 'announcement-banner-1',
20          typeId: 'commerce_assets.announcementBanner',
21          data: mockAnnouncementBanner
22        }
23      ]
24    }
25  ]
26}

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.

See Also