Page Designer Integration with Storefront Next

Page Designer is a visual, drag-and-drop content management tool for B2C Commerce that business users use to create, schedule, and publish personalized storefront pages without extensive developer intervention. It provides a low-code environment where merchants can build engaging consumer experiences, such as homepages, campaign landing pages, and product highlights, by using a library of reusable functional components. Developers build the reusable components that merchants use.

In Storefront Next, developers build Page Designer components as React components instead of ISML templates. ISML pages and templates aren't used in Storefront Next.

How It Works for Developers

Developers build the underlying building blocks that business users later manipulate. To learn more about how to build reusable pages and components in Page Designer, see Page Designer Readme in GitHub.

Core Architecture

The Page Designer system is built on these fundamental concepts that define how content is structured and rendered.

  • Pages: Top-level containers fetched via the ShopperExperience API that serve as the foundation for the storefront experience.
  • Regions: Named areas within a page or component that act as placeholders where components can be nested.
  • Components: Reusable UI elements, such as heroes, carousels, and grids, that contain specific functional logic and styling.
  • Component Data: Asynchronous data retrieved by component-specific loaders to populate the UI elements.

Implement Page Designer Components

Page Designer components are React components that you can decorate with metadata. You can configure the metadata in Business Manager.

Component Structure

Every Page Designer component follows this structure.

src/components/my-component/
├── index.tsx          # Main component with decorators

Metadata Decorators

Components use three decorators to define their metadata.

Decorator Purpose
@Component Registers the component with Page Designer.
@AttributeDefinition Defines configurable attributes.
@RegionDefinition Defines nested regions for container components.

Basic Component Example

This code example defines a Hero Banner Page Designer component. @Component registers the banner, and @AttributeDefinition on HeroMetadata declares optional configurable fields, such as Title, Image, and CTA link, a call-to-action link. A fallback export renders a loading skeleton. The default export is the main Hero that shows the image, title, and optional CTA. You can configure the Hero Banner in Page Designer.

// src/components/hero/index.tsx
import { Component } from "@/lib/decorators/component";
import { AttributeDefinition } from "@/lib/decorators/attribute-definition";

@Component("hero", {
  name: "Hero Banner",
  group: "odyssey_base",
  description: "Prominent banner section with title, image, and CTA",
})
export class HeroMetadata {
  @AttributeDefinition({
    name: "Title",
    type: "string",
    required: false,
  })
  title?: string;

  @AttributeDefinition({
    name: "Image",
    type: "image",
    required: false,
  })
  imageUrl?: string;

  @AttributeDefinition({
    name: "CTA Link",
    type: "url",
    required: false,
  })
  ctaLink?: string;
}

// Fallback component (shown while loading)
export function fallback({ title }: Partial<HeroProps>) {
  return (
    <div className="h-64 bg-gray-200 animate-pulse">
      {title && <span className="sr-only">{title}</span>}
    </div>
  );
}

// Main component (default export required)
export default function Hero({ title, imageUrl, ctaLink, ctaText }: HeroProps) {
  return (
    <section className="hero">
      <img src={imageUrl} alt={title} />
      <h1>{title}</h1>
      {ctaLink && <a href={ctaLink}>{ctaText}</a>}
    </section>
  );
}

Attribute Types

These attribute types are available for @AttributeDefinition.

Type Description
string Single-line text
text Multi-line text
markup Rich text/HTML
integer Whole numbers
boolean True or false toggle
image Image picker
url URL input
product Product picker
category Category picker
page Page reference
enum Dropdown selection
cms_record CMS content reference

Component Patterns: Fallbacks and Loaders

Fallback Components

We recommend that every Page Designer component exports a fallback function. The fallback skeleton is shown while the component loads.

// Lightweight skeleton - no heavy logic
export function fallback({ title }: Partial<MyComponentProps>) {
  return (
    <div className="h-48 bg-gray-200 animate-pulse rounded">
      {title && <span className="sr-only">Loading {title}</span>}
    </div>
  );
}

Best Practices:

  • Keep fallbacks lightweight (no state, effects, or data fetching).
  • Match the approximate dimensions of the real component.
  • Use CSS animations for loading indication.

Data Loaders

Export a loader object for components that need dynamic data.

This code example defines a ProductCarousel that gets its list from a dual loader. A server loader calls fetchProducts using component.data?.categoryId and returns products. The component loads data on the server only, therefore the client loader returns null. The component receives already-resolved data and title as props—no Promise handling in the component. It renders the title and a list of products as ProductCards.

// Server-side data loading
export const loader = {
  server: async ({ component, request, context }) => {
    // Fetch data on the server
    const products = await fetchProducts(component.data?.categoryId);
    return { products };
  },
  client: async ({ component, request }) => {
    // Optional: Client-side data loading
    return null;
  },
};

export default function ProductCarousel({ data, title }: ProductCarouselProps) {
  // `data` is already resolved - not a Promise!
  return (
    <div>
      <h2>{title}</h2>
      {data?.products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Key Points:

  • The data prop is always resolved when your component receives it.
  • The internal Component wrapper handles Promise resolution.
  • Your fallback is shown while data loads.
  • Multiple components load data in parallel.

Components with Regions

Container components can define the regions where other components can be placed.

This code example defines a Grid Layout Page Designer component that acts as a container with two regions. @Component registers it and @RegionDefinition declares Main Content with up to 6 components, and Sidebar with up to 3 components. GridMetadata adds an optional Columns attribute. A fallback export renders a simple loading grid. The default Grid component renders a two-column layout and uses the Region component to render the nested content for main and sidebar. That way, you can place other components in each region in Page Designer.

import { Component } from "@/lib/decorators/component";
import { RegionDefinition } from "@/lib/decorators/region-definition";
import { Region } from "@/components/region";

@Component("grid", {
  name: "Grid Layout",
  group: "odyssey_base",
  description: "A flexible grid container for arranging components",
})
@RegionDefinition([
  {
    id: "main",
    name: "Main Content",
    maxComponents: 6,
  },
  {
    id: "sidebar",
    name: "Sidebar",
    maxComponents: 3,
  },
])
export class GridMetadata {
  @AttributeDefinition({
    name: "Columns",
    type: "integer",
    defaultValue: 2,
  })
  columns?: number;
}

export function fallback() {
  return <div className="grid gap-4 animate-pulse" />;
}

export default function Grid({ component, columns = 2 }: GridProps) {
  return (
    <div className={`grid grid-cols-${columns} gap-4`}>
      <div className="col-span-1">
        {/* Nested region - uses component mode */}
        <Region component={component} regionId="main" />
      </div>
      <div className="col-span-1">
        <Region component={component} regionId="sidebar" />
      </div>
    </div>
  );
}

Region Definition Options

These options are available for the region definition metadata.

@RegionDefinition([
    {
        id: 'content',                    // Required: Unique region ID
        name: 'Content Area',             // Required: Display name in Business Manager
        maxComponents: 5,                 // Optional: Maximum components allowed
        minComponents: 1,                 // Optional: Minimum components required
        componentTypeInclusions: [        // Optional: Only allow these component types
            'sfnext.hero',
            'sfnext.productCarousel',
        ],
        componentTypeExclusions: [        // Optional: Disallow these component types
            'sfnext.grid',                // Prevent nested grids
        ],
    },
])

Implement Page Designer Pages

A Page is the route-level definition that defines where Page Designer content can be placed.

Page Structure

This code example defines a Category Page with @PageType and two page regions: banner and promoContent. The route loader loads page and category data via fetchPageWithComponentData. The page component renders, in order: the banner region (with a skeleton fallback), a fixed ProductGrid for the category, and the promoContent region. Banner and promo slots are configurable in Page Designer. The route’s category defines the product grid.

// src/routes/category.$categoryId.tsx
import { PageType } from "@/lib/decorators/page-type";
import { RegionDefinition } from "@/lib/decorators/region-definition";
import { Region } from "@/components/region";

@PageType({
  name: "Category Page",
  description: "Category listing page with product grid",
  supportedAspectTypes: ["category"],
})
@RegionDefinition([
  {
    id: "banner",
    name: "Category Banner",
    maxComponents: 1,
  },
  {
    id: "promoContent",
    name: "Promotional Content",
    maxComponents: 3,
  },
])
export class CategoryPageMetadata {}

export async function loader({ params, context }: LoaderFunctionArgs) {
  const page = await fetchPageWithComponentData({
    pageId: "category",
    categoryId: params.categoryId,
  });

  return { page };
}

export default function CategoryPage({ loaderData }: Route.ComponentProps) {
  return (
    <div>
      {/* Page-level region */}
      <Region page={loaderData.page} regionId="banner" fallbackElement={<BannerSkeleton />} />

      {/* Main category content */}
      <ProductGrid categoryId={loaderData.categoryId} />

      {/* Additional promotional content */}
      <Region page={loaderData.page} regionId="promoContent" />
    </div>
  );
}

Page Metadata Options

Option Description
name Display name in Business Manager
description Page description
supportedAspectTypes The B2C Commerce aspect types that this page supports

Supported Aspect Types

Aspect Type Use Case Valid Assignments
pdp Product Detail Pages Categories and Products
plp Category Pages Categories

Region Usage in Pages

Use page mode for top-level regions.

<Region
  page={loaderData.page} // Page data from loader
  regionId="main" // Region ID matching @RegionDefinition
  fallbackElement={<Skeleton />} // Optional: Loading state
  errorElement={<ErrorMessage />} // Optional: Error state
/>

Use component mode for nested regions inside container components.

<Region
  component={component} // Component object (synchronous)
  regionId="content" // Region ID
/>

important: Don't mix page and component props. Use one or the other.

Generate Cartridges

The build process automatically generates B2C Commerce cartridge metadata from your decorated components and pages.

Automatic Generation on Build

pnpm build

This runs pnpm cartridge:generate as part of the build process and does these steps.

  1. Scans src/ for @Component and @PageType decorators.
  2. Extracts metadata from decorators.
  3. Generates JSON files in cartridges/app_storefrontnext_base/cartridge/experience/.

Manual Generation

pnpm cartridge:generate

Generated Output Structure

cartridges/app_storefrontnext_base/cartridge/experience/
├── components/
│   └── odyssey_base/
│       ├── hero.json
│       ├── productCarousel.json
│       └── grid.json
├── pages/
│   ├── homePage.json
│   ├── productDetailPage.json
│   └── categoryPage.json
└── aspects/
    └── (aspect definitions)

Generated Metadata Format

Component metadata (hero.json):

{
  "name": "Hero Banner",
  "description": "Prominent banner section with title, image, and CTA",
  "group": "odyssey_base",
  "arch_type": "headless",
  "region_definitions": [],
  "attribute_definition_groups": [
    {
      "id": "hero",
      "name": "Hero Banner",
      "attribute_definitions": [
        {
          "id": "title",
          "name": "Title",
          "type": "string",
          "required": false
        }
      ]
    }
  ]
}

Page metadata (homePage.json):

{
  "name": "Home Page",
  "description": "Main landing page",
  "arch_type": "headless",
  "region_definitions": [
    {
      "id": "main",
      "name": "Main Content Region",
      "max_components": 5
    }
  ],
  "supported_aspect_types": [],
  "route": "/"
}

Key Notes

  • arch_type: "headless": All generated metadata includes arch_type: "headless". This flag tells Page Designer that the page or component is rendered by the headless storefront (Storefront Next) rather than by a B2C Commerce controller. When Page Designer opens a page marked as headless, it loads the Managed Runtime (MRT) storefront URL in the editor iframe instead of a classic B2C Commerce controller-based page. This behavior enables Page Designer to render and edit React components directly.
  • route: Automatically extracted from your file-based routing structure (for example, src/routes/category.$categoryId.tsx → /category/:categoryId). When Page Designer loads a page, the {page_route} variable in the iframe URL is resolved using the route field from the page type metadata.
  • Manual additions: If you must add metadata manually, place JSON files in the cartridges/ directory structure.

Deploy Cartridges

Deploy the generated cartridge to your B2C Commerce instance.

Prerequisites

Create a dw.json file in the storefront-next-dev package directory.

{
  "hostname": "your-instance.demandware.net",
  "username": "your-username",
  "password": "your-password",
  "code-version": "version1"
}

note: For more detailed steps for setting up the dw.json file and deploying the cartridge, see Deploy the Storefront Next Cartridge for Page Designer.

Deploy the Cartridge with the push Command

To deploy the cartridge, run this command.

pnpm push

The command:

  1. Generates cartridge metadata if GENERATE_AND_DEPLOY_CARTRIDGE_ON_MRT_PUSH is enabled.
  2. Deploys the cartridge to B2C Commerce via WebDAV.
  3. Pushes the storefront bundle to Managed Runtime.

Manual Cartridge Deployment

To deploy only the cartridge without pushing to Managed Runtime (MRT), use these commands.

# Generate metadata
pnpm cartridge:generate

# Deploy to B2C Commerce
pnpm cartridge:deploy

Deployment Process

  1. ZIP Creation: Cartridge directory is zipped.
  2. Upload: ZIP uploaded to B2C Commerce via WebDAV.
  3. Unzip: Archive extracted on the server.
  4. Cleanup: Temporary ZIP file deleted.

Business Manager Setup

Prerequisites

  1. Cartridge Path: Add app_storefrontnext_base to your site's cartridge path in Business Manager.

    • Go to Administration > Sites > Manage Sites > [Your Site] > Settings.
    • Add app_storefrontnext_base to the cartridge path.
  2. MRT Linking: Make sure that your MRT environment is linked to your B2C Commerce instance. The MRT environment is linked if the storefront was set up in Business Manager, or you can link it in MRT's Environment Settings page. The MRT storefront URL in Page Designer is automatically configured when exactly one MRT environment is linked to the specific B2C Commerce instance and site ID. No manual URL configuration is required. Page Designer uses this URL to populate the headless_url object, (containing edit_url, preview_url, and info_url) that controls where the editor iframe points.

    If the Page Designer iframe appears blank, verify that your MRT environment is correctly linked to your B2C Commerce instance and site.

Using Page Designer

  1. Go to Merchant Tools > Content > Page Designer.
  2. Create a page or edit an existing one.
  3. Select a page type that matches your Storefront Next page types.
  4. Add components to regions using the visual editor.
  5. Configure component attributes in the properties panel.
  6. Publish the page.

Page Assignment

You can assign pages to:

  • Specific entities - A particular product, category, or content asset
  • Default pages - Fallback for all entities of a type

Live Preview

When editing in Page Designer:

  1. Business Manager reads the route from page metadata.
  2. Loads your storefront in an iframe with the appropriate URL.
  3. Shows live preview as you configure components.

For the storefront to load inside the Page Designer iframe, the frame-ancestors Content Security Policy directive must allow both 'self' and your Business Manager hostname (for example, https://your-instance.commercecloud.salesforce.com). The default value of 'self' isn't sufficient. Extend the directive in config.server.ts. See Security Response Headers.

note: Previewing a local development environment in Page Designer isn't supported. To use Page Designer's live preview, deploy your storefront to a Managed Runtime environment. For a local development workaround, see Local Development.

Shopper Context Preview

To preview a page in the context of a specific Customer Group or on a specific date:

  1. Enable the feature toggle in Business Manager: Turn on the "Shopper Context" feature toggle in Business Manager.
  2. Configure MRT environment variable: Set PUBLIC__app__features__shopperContext__enabled to true in your Managed Runtime environment.

Known Limitations

  • No live editing without saving: Changes made in the Page Designer properties panel aren't reflected in the preview iframe in real time. You must save changes to see them in the preview.
  • Hybrid storefronts — link builder restriction: In hybrid deployments (SFRA and Storefront Next running together), the Page Designer link builder can only link to pages within the same technology stack. Headless pages can link to other headless pages, and SFRA pages can link to other SFRA pages, but cross-stack linking isn't supported.

Local Development

Local development with Page Designer using localhost URLs isn't supported. To test Page Designer integration, deploy your storefront to an MRT environment.

As a temporary workaround, use a local development cartridge to redirect Page Designer to your local server. Download the cartridge from Local Dev Cartridge (ZIP).

Setup Steps

  1. Deploy the cartridge to your B2C Commerce instance.
  2. Add the cartridge to your Business Manager cartridge path.
  3. In Business Manager, go to Administration > SF Next Local Dev.
  4. Enter your local development URL (for example, http://localhost:3000).
  5. Run the service worker.
  6. Click Enable Headless URL Override.

important: Click the checkbox only once. Due to a known UI limitation, the checkbox doesn't reflect the correct state after toggling.

Troubleshooting

Issue Solution
Components don't appear Verify that the cartridge is in the cartridge path and deployed.
Page doesn't load Check that route in page metadata matches your routing.
Attributes aren't configurable Make sure that @AttributeDefinition decorators are correct.
Region is empty Verify that regionId matches between code and metadata.
PD iframe is blank Verify that your MRT environment is linked to the correct B2C Commerce instance and site ID. See Managed Runtime Administration.
Preview flickering/refreshing Ensure that the "cookies enabled" setting is configured in your MRT environment. See Managed Runtime Administration.

Merchant Storefront Next Customization

Page Designer contains Storefront Next template pages that merchants can add and customize. Page Designer can connect to the storefront associated with a site. See Page Designer Content Management for Merchants.

Content Slot Migration from SFRA

If you're migrating from Salesforce Storefront Reference Architecture (SFRA) to Storefront Next and use content slots, follow this workaround until the planned Embedded Content feature becomes available. See Migrate Content Slots from SFRA to Storefront Next.