Host-handled link clicks (bonus CTA in-page routing)
Host-handled link clicks (bonus CTA in-page routing)
By default, a link inside a widget message — including the Claim CTA on a
CRM bonus card — opens in a new browser tab (target="_blank"). A host page can
take over instead: the widget hands the URL back and the page routes in its own
SPA, with no new tab and no full page reload.
Nothing needs to be built on the Hoory side. The SDK already ships the switch
(forceManualLinkManagement) and the event (hoory:on-link-click).
1. Enable it
forceManualLinkManagement is read once, when the SDK boots, so it must be set
on window.hoorySettings before hoorySDK.run(...):
<script>
window.hoorySettings = {
locale: 'en',
forceManualLinkManagement: true, // the host page owns link clicks
};
(function (d, t) {
var BASE_URL = 'https://app.hoory.com';
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src = BASE_URL + '/packs/js/sdk.js';
g.defer = true;
g.async = true;
s.parentNode.insertBefore(g, s);
g.onload = function () {
window.hoorySDK.run({
websiteToken: 'YOUR_WEBSITE_TOKEN',
baseUrl: BASE_URL,
});
};
})(document, 'script');
</script>
2. Handle the click
window.addEventListener('hoory:on-link-click', function (e) {
var href = e.detail.href; // the clicked URL, e.g. https://crm.example.com/bonuses/claim/42
if (!href) return;
var target = new URL(href, window.location.origin);
if (target.origin === window.location.origin) {
// Own site: route in place. The widget stays open and mounted.
router.push(target.pathname + target.search); // Vue Router
// history.pushState({}, '', target.pathname + target.search); // vanilla
} else {
// Third-party URL: still needs a real navigation.
window.open(href, '_blank', 'noopener');
}
});
The event is a plain DOM CustomEvent on window:
| contract | |
|---|---|
| event name | hoory:on-link-click |
event.detail |
{ href: string } |
| fired for | anchors inside a message body (.message-content) or a card message (.card-message) |
Register the listener as early as possible — before the customer can open the
widget. It does not depend on hoory:ready.
3. Try it locally
The widget test page is wired up as a working example:
http://localhost:8080/widget_tests?manualLinks
?manualLinkssetsforceManualLinkManagement: true.- Push a bonus card to that contact (see
crm-bonus-e2e-testing-guide.pdf) and click Claim. - With the flag on: no new tab, the widget stays open, and the page renders a “Host page handled this link” panel with the intercepted URL. The same URL is logged to the console.
- Without the flag (drop
?manualLinks): the CTA opens a new tab, unchanged.
Relevant source, if you need to trace it:
| file | role |
|---|---|
app/javascript/widget/helpers/linkInterception.js |
which links are handed to the host |
app/javascript/widget/App.vue |
intercepts the click, posts onLinkClick to the SDK |
app/javascript/sdk/IFrameHelper.js |
re-dispatches it as hoory:on-link-click on the host window |
Caveats
- The switch is global, not bonus-specific. With it on, every link in a
message body or card message is handed to the host — bot cards and agent
replies included. The host must be able to deal with arbitrary URLs; the
fallback branch above (
window.open) is not optional. - Widget chrome is not handed over (header menu, help-centre articles, “powered by”). Those are the widget’s own navigation.
- Only the anchor itself is intercepted, not nested markup. A card’s image
link (click target is the
<img>) or a body link wrapping other elements (<a><strong>…) still opens in a new tab. Pre-existing behaviour; bonus CTAs are plain text inside the anchor, so they are unaffected. - Bonus click tracking is unaffected. By DOM event ordering, the CTA’s
tracking_uribeacon fires on the element’s own click handler before the window-level interception runs, so CRM click stats stay accurate whether or not the host handles the navigation. - Popup blockers. If you route to a third party with
window.openfrom inside this listener, the call is one tick removed from the original user gesture and some browsers may block it. Preferwindow.location.assign(href)when leaving the site for good.