Skip to content

Messaging Tool Frontend Implementation

The messaging tool allows managing all on-site messaging from the backend using simple frontend integrations. Banners, modals, inline article prompts, and registration walls can all be configured and targeted from the CMS — with no frontend deployments required to update your messages.

If your frontend is built with a bundler (rollup, webpack, esbuild, vite, etc.), you can install the loader directly in your project.

Terminal window
npm install @whitebeardnl/messaging-loader
import { init } from '@whitebeardnl/messaging-loader';
init({
siteId: 1,
apiBase: 'https://messaging.domain.com/',
assetBase: 'https://api.domain.com/',
});

Add the following script tag to every page where messaging should appear, typically in the <head> or just before </body> or in your Tag Manager:

<script
data-api-base="https://messaging.domain.com/"
data-asset-base="https://api.domain.com/"
data-site-id="1"
type="text/javascript"
src="https://api.domain.com/js/messaging_loader.js"
async="true"
></script>

For developers testing the worker, you can use the following local environment URLs:

<script
data-api-base="http://localhost:8787/"
data-asset-base="http://localhost:8086/"
data-site-id="1"
type="text/javascript"
src="http://localhost:8086/js/messaging_loader.js"
async="true"
></script>

Make sure to also run Vite in messaging mode and to run the messaging worker.


Attribute Config field Required Default Description
data-site-id siteId Yes Publication ID for the site, as configured in the CMS. Must resolve to a positive number — missing/invalid values fire a wb:loader:error (reason: "missing-site-id") and abort the load.
data-api-base apiBase Yes - Base URL of the Messaging API.
data-asset-base assetBase Yes - Base URL for CMS API (used to resolve message CSS/JS assets).
data-endpoint endpoint No /messaging/embed Path (relative to apiBase) the loader posts the embed request to.
data-zone-selector zoneSelector No [data-wb-zone-key] CSS selector used to specify the attribute to look for when finding zones.
data-inline-selector inlineSelector No (auto-detected, see Inline Article Targeting) CSS selector used to inject inline article messages into the page content.
data-context context No {} Extra targeting context forwarded to the API. As a data-* attribute this must be a JSON string (e.g. data-context='{"tier":"premium"}').

Standard templates (banners, modals, stacked panels, etc.) require no further frontend work — the loader handles rendering and basic interactions automatically.


Messages can be targeted to specific locations on the page using zone keys, similar to how a DFP ad unit works. Place a zone container anywhere in your HTML:

<div data-wb-zone-key="zone_name"></div>

Then use that zone name when configuring the message placement in the CMS. The loader automatically discovers all zone containers on the page and includes their keys in the API request.

By default the loader looks for [data-wb-zone-key]. You can change this by setting data-zone-selector on the script tag:

<script
data-site-id="1"
data-zone-selector="[data-my-zone]"
src="https://api.domain.com/js/messaging_loader.js"
async="true"
></script>

Messages placed inside article content use a CSS selector to find the article body. You can specify one explicitly with data-inline-selector on the script tag. If omitted, the loader tries each of these selectors in order and uses the first match:

Priority Selector
1 [data-wb-article-body]
2 article .article-body
3 article .content-body
4 article
6 .article-texts

The recommended approach is to add data-wb-article-body to your article body element so the selector is explicit and immune to markup changes:

<div class="article-content" data-wb-article-body></div>

When a message’s placement has “Block remainder of article” enabled, the loader hides the rest of the article once the message mounts, and (if the message is dismissible) restores it when the visitor dismisses the message.

By default the loader determines what to hide by walking the DOM after the mounted message and removing everything that follows it inside the article body.

If your page already renders its own paywall teaser, you can take full control instead by wrapping that teaser (plus whatever CTA you already show) in an element with the article-paywall class, as a sibling of your article body element:

<div class="article-paywall">…your teaser content…</div>
<div class="article-content" data-wb-article-body>…full article…</div>

When .article-paywall is present, the loader appends the message inside it, hides the article body element entirely, and — on dismiss — removes the .article-paywall element and un-hides the article body, restoring it in full.


Some templates expose interactive behaviour that depends on site-specific logic such as captcha services, authentication flows, or subscription APIs. These require a frontend event integration.


The registration wall collects a visitor’s form input and delegates the full conversion flow — including captcha verification and the API call — to the frontend via a custom DOM event. The event contract is generic (wb:message:conversion*, not wb:message:register*), since the same wall markup can back other conversion actions besides registration (e.g. login, newsletter signup) — any template that opts into this flow fires the same events.

Event Fired on Detail
wb:message:conversion window { messageId, templateKey, zoneKey, surface, variantId, fields, resolve, reject }
wb:message:conversion:success window { messageId, templateKey, zoneKey, surface, variantId, fields }
wb:message:conversion:error window { messageId, templateKey, zoneKey, surface, variantId, fields, error }

fields is an object built from every named <input>/<select>/<textarea> inside the message, keyed by each element’s name attribute exactly as it appears in the HTML — for the registration wall that’s { full_name, email, password }. Because the messaging tool doesn’t know in advance which fields a given template’s form has, it forwards all of them verbatim rather than picking out specific ones, so a future template with different fields (a login form with just login_email/login_password, for example) works without any JS changes here.

Because the same event fires for every template that opts into this flow, use templateKey inside your handler to decide which real-world action to perform (register, log in, subscribe to a newsletter, etc.) — see the code sample below. Today registration_wall is the only template wired to this event, but the templateKey check future-proofs your integration against additional conversion-style templates without needing a new event name for each one.

When the visitor clicks the Register Now button, the messaging tool:

  1. Disables the button to prevent duplicate submissions.
  2. Fires wb:message:conversion on window with the collected fields and a resolve/reject pair.
  3. Waits for the frontend to call resolve() or reject(error).
    • On resolve() → fires wb:message:conversion:success and leaves the button disabled.
    • On reject(error) → displays the error message inside the wall, re-enables the button, and fires wb:message:conversion:error.

Listen for wb:message:conversion, branch on templateKey to identify which conversion action this is, and perform your captcha challenge and API call inside the handler:

window.addEventListener("wb:message:conversion", (event) => {
const { templateKey, fields, resolve, reject } = event.detail;
if (templateKey !== "registration_wall") {
// Handle other conversion-form templates here as they're introduced
// (e.g. a future "login_wall" or "newsletter_wall" templateKey).
return;
}
const { full_name: name, email: email, password: password } = fields;
yourCaptcha
.execute()
.then((token) => yourApi.register(name, email, password, token))
.then(resolve)
.catch(reject);
});

If registration fails, throw or reject with an Error whose message property contains a human-readable string — it will be shown directly to the user inside the wall:

.catch(err => reject(new Error('This email is already registered.')));

On a successful registration the visitor typically gains access to content, so you will usually want to reload the page:

window.addEventListener("wb:message:conversion:success", () => {
window.location.reload();
});

If you need to do something before reloading (e.g. set a session cookie, fire an analytics event), do it before calling resolve() inside the wb:message:conversion handler rather than in the success listener, so the reload only happens once everything is confirmed.