DEV Community

Cover image for Building Custom Page Designer Components in B2C Commerce
SapotaCorp
SapotaCorp

Posted on • Originally published at sapotacorp.vn

Building Custom Page Designer Components in B2C Commerce

Page Designer ships with a small set of stock components, and the first real requirement from a merchant team almost always falls outside it. A custom b2c commerce page designer component is not much code: a JSON metadata file that describes the component to Business Manager, and an ISML template that renders it on the storefront. The interesting part is everything around those two files, because the platform has opinions about cartridge paths, attribute editors, region nesting, localization, and caching that the docs mention but do not really walk you through. This is the production loop our Salesforce team uses on B2C Commerce projects.

Page types vs component types

Before writing anything, get the two metadata families straight, because people conflate them and then wonder why their thing does not behave.

A page type describes a whole page: its top level layout and the set of regions a merchant can fill. Think "PDP landing" or "campaign page". A component type describes a single reusable block that drops into a region: a hero banner, a product carousel, a two column promo. You build component types far more often than page types.

Both are defined the same way, as JSON files under a known folder structure in a cartridge:

my_cartridge/cartridge/experience/
  pages/         (page type metadata)
  components/    (component type metadata)
  pages/*.isml   (page render templates)
  components/*.isml (component render templates)
Enter fullscreen mode Exit fullscreen mode

The folder name experience is not optional. Page Designer scans cartridges on the path for this exact structure, so a typo here means your type silently never appears.

The JSON metadata definition

The metadata file is the contract between your component and the merchant authoring UI. Here is a trimmed hero component definition:

{
  "name": "Hero Banner",
  "description": "Full width hero with headline and CTA",
  "group": "commerce_assets",
  "attribute_definition_groups": [
    {
      "id": "content",
      "name": "Content",
      "attribute_definitions": [
        { "id": "headline", "name": "Headline", "type": "string", "localizable": true, "required": true },
        { "id": "body", "name": "Body", "type": "text", "localizable": true },
        { "id": "image", "name": "Background", "type": "image", "required": true },
        { "id": "ctaUrl", "name": "CTA URL", "type": "url" },
        { "id": "fullBleed", "name": "Full bleed", "type": "boolean", "default_value": false }
      ]
    }
  ],
  "region_definitions": []
}
Enter fullscreen mode Exit fullscreen mode

The name and description are what the merchant sees in the component picker. The group controls which category it sorts into. Everything inside attribute_definitions is the heart of it: each entry becomes one field in the authoring form.

Attribute definitions drive the authoring form

This is the part worth internalizing. The merchant never sees your JSON. They see a form, and that form is generated entirely from your attribute definitions. The type you declare is not just a data type, it picks the editor control:

  • string gives a single line text input.
  • text gives a multi line text area.
  • markup gives a rich text editor.
  • boolean gives a checkbox.
  • enum-of-string gives a dropdown, with the choices you list under values.
  • image gives the image picker (returns a MediaFile).
  • file gives a content asset or file reference.
  • integer and double give numeric inputs with validation.
  • url gives a URL field with the page and category picker attached.

If a merchant complains the field is "the wrong kind of box", the fix is almost always the attribute type, not CSS. And the editor mapping is fixed: you cannot, with stock metadata, turn a string into a color swatch or a slider. When you need an editor that does not exist out of the box, that is a separate build, and we covered it in custom attribute editors for Page Designer.

A few attribute flags earn their keep. required: true blocks the merchant from publishing with the field empty. default_value prefills it. group (the attribute_definition_groups wrapper) collapses related fields into labeled sections so a component with fifteen attributes does not present as one intimidating scroll.

Localized attributes

Set localizable: true on any attribute holding text a merchant would translate, which in practice means almost all of them: headlines, body copy, alt text, CTA labels. The platform then stores a separate value per locale and shows the merchant a locale switcher on that field.

The trap is on the read side. In your render template you do not pick the locale yourself. The component model hands you the value already resolved for the request's active locale, so you just read content.headline and trust it. If you forgot localizable: true, the field stores a single value across every locale and the merchant has no way to translate it, which usually surfaces late, during the localization phase, as a "why can't I change the French copy" ticket.

Regions enable drag and drop nesting

Stock components are leaves. The moment you want a layout component that holds other components (a three column row, a tabbed section, a slider whose slides are themselves authored components) you need regions. A region is a named drop zone declared in the metadata:

"region_definitions": [
  { "id": "columns", "name": "Columns", "max_components": 3 }
]
Enter fullscreen mode Exit fullscreen mode

max_components caps how many children a merchant can drop, which is how you keep a "three column" component from becoming a seven column mess. You can also constrain which component types are allowed in a region from the metadata if you want a slider that only accepts slide components.

Regions are the mechanism behind the drag and drop nesting merchants expect. Declaring one in JSON is half the job; the other half is rendering it, below.

The render template and the component model

The ISML template lives next to the JSON, same base name, and it runs with a pdict carrying the component model. The model exposes the attribute values you defined plus helpers for regions:

<isset name="content" value="${pdict.content}" scope="page" />

<section class="hero ${content.fullBleed ? 'hero--full' : ''}">
    <img src="${content.image.absURL}" alt="${content.headline}" />
    <div class="hero__copy">
        <h1><isprint value="${content.headline}" /></h1>
        <isif condition="${content.body}">
            <div class="hero__body"><isprint value="${content.body}" encoding="off" /></div>
        </isif>
    </div>
</section>
Enter fullscreen mode Exit fullscreen mode

Two things to note. The image attribute comes through as a MediaFile, so you call .absURL rather than expecting a bare string. And isprint encoding still applies here exactly as it does in any ISML: markup and text content you intend to render as HTML needs encoding="off", but plain strings should stay encoded to avoid an XSS hole that sails through review. None of the Page Designer plumbing changes the ISML security rules.

For a component with regions, render the drop zone with the render API so children appear:

<isset name="region" value="${pdict.regions.columns}" scope="page" />
<div class="row">
    <isprint value="${PageRenderHelper.renderRegion(region)}" encoding="off" />
</div>
Enter fullscreen mode Exit fullscreen mode

That renderRegion call (from the *sfra/cartridge/experience/utilities helper, or your own wrapper) is what walks the child components a merchant dropped in and renders each one. Without it the region exists in the authoring UI but renders nothing on the storefront, which is a confusing state: the merchant drops components, sees them in the editor, and they vanish on the live page.

Gotcha: the cartridge must be on the path

This is the one that burns an hour the first time. Your component can be flawless and it will not appear in the component picker if the cartridge holding it is not on the site cartridge path. Page Designer only scans cartridges assigned to the site in Business Manager under Administration, Sites, Manage Sites, your site, Settings. Add the cartridge there, in the right order, then reload the Page Designer editor.

Order matters too, the same way it does for templates and controllers. If two cartridges define a component type with the same ID, the leftmost on the path wins, which is occasionally intentional (overriding a base component) and occasionally an accident you spend a while debugging.

Gotcha: caching of component output

Page Designer caches rendered component output, and during development that cache will convince you your ISML edits do nothing. Each component should set an explicit cache period in its render path; the SFRA pattern is to call setComponentCachePeriod (or the page level cache tag) so output is cached for a sane window in production. While iterating, lower it or disable it, and remember that uploading a new ISML does not force a cache flush on its own. The merchant publishing the page is what reliably busts it, so "I changed the template and nothing happened" usually means you are looking at cached output, not broken code.

If your component pulls in CMS managed content, the caching story gets one more layer, and we walk that in reusing Salesforce CMS content in Page Designer.

Takeaway

A custom Page Designer component is genuinely two files: JSON metadata that generates the merchant authoring form, and an ISML template that renders the component model. The leverage is in the details around them. Attribute types pick the editor a merchant gets, localizable decides whether content can be translated, regions plus renderRegion deliver the drag and drop nesting merchants assume they have, and the cartridge path plus caching decide whether your component shows up and updates at all. Get those right and the component drops cleanly into the authoring experience instead of generating support tickets.

If your team is standing up Page Designer or hitting walls on a custom component build, get in touch or see how our Salesforce practice runs B2C Commerce engagements.

Top comments (0)