Appearance
Promotions on the map
Mapsted CMS lets a property author promotion surfaces — banners, deal cards, sponsored POI tiles — that render alongside entities on the indoor map. The Mapsted Maps JavaScript API surfaces two events you can subscribe to from the host page:
| Event | When it fires | Typical use |
|---|---|---|
promotionClick | The user clicks or taps a promotion surface inside the iframe. | Analytics, deep-link routing, custom coupon UI overlay. |
promotionDetails | The iframe opens its built-in promotion-detail view. | Suppress the iframe-native panel and render your own host-page chrome. |
Both events deliver the same PromotionData payload shape so a single handler can route by event name.
What's in the payload
ts
interface PromotionData {
/** Stable Mapsted entity bound to this promotion (POI, building, floor). */
entity?: EntityData;
/** CMS-authored metadata forwarded verbatim. */
[key: string]: unknown;
}Common keys observed in production payloads (the CMS schema is authoritative — narrow at the call site):
imageUrl?: string— promotion artwork (CMS-uploaded)headline?: string/subheadline?: string— display textctaText?: string/ctaUrl?: string— call-to-actionvalidFrom?: string/validTo?: string— ISO 8601 windowtenantId?: string— multi-tenant property scoping
Banner-on-map example
This pattern listens for promotionClick, opens a host-page banner anchored to the click target, and lets you (the integrator) own the deal/coupon UI without fighting the iframe.
ts
import { init, on, off, navigateToFloorById } from '@mapsted/maps-js-api';
import type { PromotionData } from '@mapsted/maps-js-api';
const map = await init({
element: '#map',
propertyId: 603,
});
const promotionBanner = document.getElementById('promo-banner')!;
const handlePromotionClick = (promo: PromotionData) => {
// 1. Forward to your analytics layer.
// EntityData fields: buildingId, floorId, entityId — there is no .id or .name on EntityData
if (promo.entity) {
analytics.track('promo_click', {
entity_id: promo.entity.entityId,
tenant_id: promo.tenantId,
});
}
// 2. Render your own banner with the CMS-authored copy.
promotionBanner.innerHTML = `
<img src="${promo.imageUrl ?? ''}" alt="" />
<h3>${promo.headline ?? ''}</h3>
<p>${promo.subheadline ?? ''}</p>
<a href="${promo.ctaUrl ?? '#'}">${promo.ctaText ?? 'View deal'}</a>
`;
promotionBanner.style.display = 'block';
// 3. (Optional) Pan the map to the promotion's floor.
if (promo.entity?.floorId) {
navigateToFloorById(promo.entity.floorId).catch(() => {
/* graceful fallback — promo still rendered */
});
}
};
on('promotionClick', handlePromotionClick);
// Don't forget to clean up on unmount (React effect, Vue onUnmounted, etc.).
// off('promotionClick', handlePromotionClick);Sanitise CMS HTML
The CMS metadata is forwarded verbatim. If your tenants author HTML that you re-render, sanitise it before injecting into the DOM (e.g. DOMPurify.sanitize). The [key: string]: unknown index signature is intentional — narrow each field at the call site.
Suppressing the iframe-native detail view
If you'd rather render the entire promotion experience yourself (consistent with your app chrome, accessibility audit, etc.) listen for promotionDetails instead and prevent the user from opening the iframe-native UI by handling the click on the host page first.
ts
on('promotionDetails', (promo) => {
// The iframe is about to open its detail view — open yours and
// dismiss any iframe state via a no-op selectEntity call.
openHostDetailPanel(promo);
});Multi-tenant retail patterns
Property tenants commonly want their promotions to follow tenant-scoped rules. Use the tenantId metadata field plus the CMS audience targeting to gate which promotions surface where:
ts
on('promotionClick', (promo) => {
if (promo.tenantId !== currentTenantId) return; // out of scope
showLoyaltyOffer(promo);
});Common questions
Do promotion events fire on hover? No — only on click/tap. The iframe runtime debounces sub-50ms repeats per entity to prevent double-fires from accidental double-tap on touch devices.
Can I author promotions client-side? No. Promotions are authored in the Mapsted CMS and surfaced server-side with property-scoped audience targeting. The Mapsted Maps JavaScript API is the consumption layer.
Does the CMS schema vary? Yes — different property tenants may author different metadata fields. Treat the index signature as unknown and feature-detect at the call site. The entity field is the only stable contract.
See also
PromotionDatareference — full TypeDoc payload definition- Events reference —
promotionClick/promotionDetails— firing semantics + ordering - Customize popups — same pattern for overriding iframe-native popups
- Deep linking — wire promotion CTAs to shareable URLs