---
schema_version: "1"
id: 0c200009f6e5cc17bada0eff3aa5015fa0c07c51e36abcb1a931638a8a4f2a20
title: Build Content Blocks for Site-Wide Regions in PWA Kit
collection: developer
version: current
locale: en-us
portal_url: https://docs.salesforce.com/developer/current/en-us/commerce-cloud/pwa-kit-managed-runtime/guides/pwa-pd-site-wide-regions.md
canonical_url: https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/pwa-pd-site-wide-regions.html
source_site: developer.salesforce.com
content_type: documentation
content_hash: 7e940b5a306ffa06f6a894921315c5779f62d89c3e8278f1ef6bfc456c51f3ff
products:
  - Composable Storefront
  - Salesforce APIs
---
# 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](https://github.com/SalesforceCommerceCloud/pwa-kit/) v3.21 or later.
- Page Designer integration with PWA Kit. See [Integrate Page Designer with PWA Kit](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/page-designer.md).
- `@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](https://github.com/SalesforceCommerceCloud/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](https://developer.salesforce.com/docs/commerce/commerce-api/references/shopper-experience?meta=Summary) 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.

```jsx
// 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 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.

```jsx
// 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`, `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:

```js
// 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`.

```jsx
// 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:

```jsx
// 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:

```jsx
// 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. 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`.

```js
// 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:

```js
// 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:

```js
// 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

```json
// 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 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.

**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).

```json
// 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 `id`s (`message`, `linkUrl`, `linkText`, `colorScheme`, `height`, `alignment`) are exactly the props the `AnnouncementBanner` component destructures. The `enum` `values` and `default_value`s 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](https://developer.salesforce.com/docs/commerce/b2c-commerce/guide/b2c-cartridges.html). 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](https://developer.salesforce.com/docs/commerce/sfnext/guides/sfnext-page-designer-content-blocks.md) 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.

```js
// 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.

## See Also

- [Integrate Page Designer with PWA Kit](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/page-designer.md)
- [Salesforce Help: Page Designer Content Blocks](https://help.salesforce.com/s/articleView?id=cc.b2c_content_blocks.htm&type=5)
