Embedding the Pega Customer Service Web Messaging widget in a Constellation portal

Embedding the Pega Customer Service Web Messaging widget in a Constellation portal (using a custom DX component)

A question that comes up fairly often: how do I drop the hosted Web Messaging chat widget into a Constellation portal? Constellation doesn’t give you a supported, out-of-the-box place to paste the widget’s <script> tag, and hacking it into a low-level rule tends to be brittle across upgrades.

The clean, upgrade-safe answer is a custom Constellation DX component that injects the Web Messaging script once per session and exposes the widget configuration (script URL, id, load behavior, data attributes) as component properties you can set right inside App Studio.

This walkthrough covers the full build: scaffolding the component, defining its properties, writing the script-injection logic, testing in Storybook, publishing, and wiring it into a landing page. The full source is on GitHub if you’d rather just clone it: GitHub - frankmark94/DXwidgetforWebMessaging: A custom Pega Constellation DX component (PAGE/Widget) that loads the hosted Web Messaging widget once per session, with configurable script URL, id, async/defer, and optional data-* attributes. · GitHub


What you’ll build

A PAGE Widget DX component called WebMessaging that:

  • Injects the hosted Web Messaging widget script once per session (idempotent — no duplicate script tags if the component re-mounts).
  • Exposes the script URL and loading options as configurable properties, so the same component works across dev/UAT/prod without code changes.
  • Supports optional data-* attributes via a JSON map for any widget-specific configuration.
  • Shows up in the App Studio component picker like any other Constellation component.

Prerequisites

  • Pega Platform™ 24.2 or later
  • Node 20 / npm 10 (or Node 18 / npm 8)
  • Git 2.30+
  • A Web Messaging channel already configured in Pega Customer Service, so you have a widget script URL
  • Access to publish DX components to your Pega environment (component builder credentials)

Step 1 — Grab your Web Messaging widget script URL

In your Pega Customer Service app, open the Web Messaging channel and copy the hosted widget script URL. It looks like this:

https://widget.use1.chat.pega.digital/<your-widget-guid>/widget.js

Hold onto this — it becomes the default value for the component’s scriptSrc property, and it’s what you’ll confirm/override in App Studio later.


Step 2 — Initialize a DX Components project

If you don’t already have a Constellation DX Components project, scaffold one following Pega’s Initialize a project guide. Once the starter repo is in place, install dependencies and authenticate against your environment:

npm install
npm run authenticate


Step 3 — Generate a PAGE Widget component

Use the component builder to create a new component. Choose type Widget with subtype PAGE — a PAGE widget renders as a region on a landing page, which is exactly where we want the chat script to live.

npm run createComponent

The generator creates a component folder (in this project, PegaFranklin_Extensions_WebMessagingWidget) with the standard files: config.json, index.tsx, styles.ts, mock.ts, and a demo.stories.tsx.


Step 4 — Define the configurable properties

The properties you declare in config.json are what appear in the App Studio property panel. Here’s the full config for the WebMessaging component:

{
  "name": "PegaFranklin_Extensions_WebMessagingWidget",
  "label": "WebMessaging",
  "description": "Web Messaging widget loader",
  "organization": "PegaFranklin",
  "version": "1.0.0",
  "library": "Extensions",
  "allowedApplications": [],
  "componentKey": "PegaFranklin_Extensions_WebMessagingWidget",
  "type": "Widget",
  "subtype": "PAGE",
  "icon": "OneColumnPage.svg",
  "properties": [
    {
      "name": "scriptSrc",
      "label": "Widget script URL",
      "format": "TEXT",
      "required": true,
      "defaultValue": "https://widget.use1.chat.pega.digital/<your-widget-guid>/widget.js"
    },
    { "name": "scriptId", "label": "Script tag id", "format": "TEXT", "defaultValue": "pega-wm-chat" },
    { "name": "async",    "label": "Load script with async", "format": "CHECKBOX", "defaultValue": true },
    { "name": "defer",    "label": "Load script with defer", "format": "CHECKBOX", "defaultValue": true },
    { "name": "autoLoad", "label": "Auto-load on mount",      "format": "CHECKBOX", "defaultValue": true },
    { "name": "dataAttrs","label": "Data attributes (JSON map)", "format": "TEXT", "defaultValue": "{}" },
    {
      "label": "Conditions",
      "format": "GROUP",
      "properties": [
        { "name": "visibility", "label": "Visibility", "format": "VISIBILITY" }
      ]
    }
  ],
  "defaultConfig": {}
}

What each property does:

Property Type Default Purpose
scriptSrc TEXT your widget URL The hosted Web Messaging script to load
scriptId TEXT pega-wm-chat ID on the injected <script> tag; used for the duplicate-load guard
async CHECKBOX true Sets async on the script tag
defer CHECKBOX true Sets defer on the script tag
autoLoad CHECKBOX true Whether to inject the script automatically when the component mounts
dataAttrs TEXT {} JSON map of data-* attributes to apply to the script tag

Step 5 — Implement the script-injection logic

The component itself is small. On mount it checks whether a script with the configured scriptId already exists; if not, it builds the <script> tag, applies any data-* attributes, and appends it to <head>. It deliberately does not tear the script down on unmount, which avoids reload thrash if the component re-renders.

import { useEffect } from 'react';
import { withConfiguration } from '@pega/cosmos-react-core';
import type { PConnFieldProps } from './PConnProps';
import './create-nonce';

import StyledPegaFranklinExtensionsWebMessagingWidgetWrapper from './styles';

interface PegaFranklinExtensionsWebMessagingWidgetProps extends PConnFieldProps {
  scriptSrc: string;
  scriptId?: string;
  async?: boolean;
  defer?: boolean;
  autoLoad?: boolean;
  dataAttrs?: string;
}

function PegaFranklinExtensionsWebMessagingWidget(props: PegaFranklinExtensionsWebMessagingWidgetProps) {
  const {
    scriptSrc,
    scriptId = 'pega-wm-chat',
    async = true,
    defer = true,
    autoLoad = true,
    dataAttrs
  } = props;

  useEffect(() => {
    if (!autoLoad) {
      return undefined;
    }

    // Idempotent guard: don't inject twice
    const existing = document.getElementById(scriptId) as HTMLScriptElement | null;
    if (existing) {
      return undefined;
    }

    const script = document.createElement('script');
    script.id = scriptId;
    script.src = scriptSrc;
    if (async) script.async = true;
    if (defer) script.defer = true;

    // Apply optional data-* attributes
    if (dataAttrs) {
      try {
        const parsed: Record<string, string> = JSON.parse(dataAttrs);
        Object.entries(parsed).forEach(([key, value]) => {
          if (key && typeof value !== 'undefined') {
            script.setAttribute(`data-${key}`, String(value));
          }
        });
      } catch (e) {
        // ignore JSON parse errors to avoid breaking the portal
      }
    }

    document.head.appendChild(script);

    return () => {
      // Don't remove the script on unmount to prevent reload thrash.
      // Optionally clean up if the widget exposes a destroy API.
      try {
        if ((window as any).PegaWM && typeof (window as any).PegaWM.destroy === 'function') {
          (window as any).PegaWM.destroy();
        }
      } catch {
        // ignore
      }
    };
  }, [autoLoad, defer, async, scriptId, scriptSrc, dataAttrs]);

  return (
    <StyledPegaFranklinExtensionsWebMessagingWidgetWrapper>
      <div id="wm-root" aria-label="Web Messaging widget" />
    </StyledPegaFranklinExtensionsWebMessagingWidgetWrapper>
  );
}

export default withConfiguration(PegaFranklinExtensionsWebMessagingWidget);

A couple of things worth calling out:

  • The scriptId duplicate guard is the whole trick. Because the same id is checked before every injection, the widget script loads exactly once per session even if Constellation mounts/unmounts the region.

  • create-nonce.ts wires the component into Constellation’s CSP nonce handling so the injected script survives a strict Content Security Policy:

    /* eslint-disable camelcase */
    // @ts-ignore
    if (window?.__webpack_nonce__) {
      // @ts-ignore
      __webpack_nonce__ = window.__webpack_nonce__;
    }
    
    

Step 6 — Preview locally in Storybook

Before publishing, validate the component in Storybook. The demo story mocks the properties so you can confirm the script injects and the widget renders.

npm run startStorybook


Step 7 — Build and publish to your Pega environment

Once it looks good locally, build and publish:

npm run buildComponent
npm run publish

After publishing, the WebMessaging component is available in your Pega application’s component picker.


Step 8 — Add and configure the component in App Studio

  1. In App Studio, go to Channels → Landing Pages and open (or create) a landing page.
  2. Edit a region and open the component picker. Search for WebMessaging — it appears alongside the built-in Constellation components:

  1. Add it to the region. In the property panel, confirm the Widget script URL (and adjust scriptId, async/defer, autoLoad, or dataAttrs as needed):

  1. Save and preview.

Step 9 — Whitelist the widget host in your CSP

If your portal enforces a Content Security Policy (it should), add the widget host to your script-src allowlist, or the browser will block the injected script:

script-src 'self' https://widget.use1.chat.pega.digital;


Result

With the component published and added to the landing page, the Web Messaging widget loads and renders directly inside the Constellation portal — the chat launcher and window behave exactly as they would on a standalone site, but now they live inside your portal:


Wrap-up

Building this as a DX component keeps the integration Constellation-native: it’s configurable per environment, upgrade-safe, and reusable across portals and landing pages without touching low-level rules. The idempotent injection guard keeps it from double-loading, and the CSP nonce hook keeps it working under a strict security policy.

Full source, including config.json, the component, the demo story, and a Storybook setup, is here: GitHub - frankmark94/DXwidgetforWebMessaging: A custom Pega Constellation DX component (PAGE/Widget) that loads the hosted Web Messaging widget once per session, with configurable script URL, id, async/defer, and optional data-* attributes. · GitHub

Helpful docs:

Happy to answer questions in the thread if anyone runs into CSP or loading issues.

2 Likes

@FranklinMarkley thanks for sharing! I’ve added to our Constellation 101 :slight_smile:

Enjoyed this article?

See suggested articles from our Constellation 101 series and view all our Knowledge Shares from our User Experience Expert Circle.