# 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](https://github.com/SalesforceCommerceCloud/storefront-next-template/blob/main/README-PAGE-DESIGNER.md).

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

```tsx
// 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.

```tsx
// 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 `ProductCard`s.

```tsx
// 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.

```tsx
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.

```tsx
@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.

```tsx
// 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.

```tsx
<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.

```tsx
<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

```bash
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

```bash
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`):

```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`):

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

```json
{
  "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](/docs/commerce/b2c-commerce/guide/sfnext-pd-deploy-cartridge.md).
:::

### Deploy the Cartridge with the `push` Command

To deploy the cartridge, run this command.

```bash
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.

```bash
# 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](/docs/commerce/b2c-commerce/guide/sfnext-security-headers.md).

:::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](#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)](https://resources.docs.salesforce.com/rel1/doc/en-us/static/misc/pd_sfnext_local_dev.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](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/managed-runtime-administration.md). |
| Preview flickering/refreshing  | Ensure that the "cookies enabled" setting is configured in your MRT environment. See [Managed Runtime Administration](https://developer.salesforce.com/docs/commerce/pwa-kit-managed-runtime/guide/managed-runtime-administration.md).             |

## 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](/docs/commerce/b2c-commerce/guide/sfnext-page-designer-customize-sf.md).

## 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](/docs/commerce/b2c-commerce/guide/sfnext-page-designer-content-slots.md).
