One script, three display modes and a fallback for sites where a script cannot be added. The widget is under 4 kB gzipped and has no dependencies.
1. Basic embed
Put the script anywhere on the page, ideally with the defer attribute.
<script src="https://cdn.callania.eu/w.js" defer></script>
<div data-callania="your-slug" data-mode="inline"></div>
The script finds every element with a data-callania attribute and renders the widget into it. Elements added later (SPA, site editor) are picked up too.
2. Three display modes
| Mode | What it does |
|---|---|
data-mode="inline" | The widget is part of the page. Best for a dedicated “Booking” page. |
data-mode="popup" | Opens in a dialog on click. Attach your own button with data-trigger=".my-button"; without it we render ours. |
data-mode="badge" | A floating button in the corner of the screen, on every page. |
<!-- your own button opens the dialog -->
<button class="my-button">Book now</button>
<div data-callania="your-slug" data-mode="popup" data-trigger=".my-button"></div>
<!-- floating button -->
<div data-callania="your-slug" data-mode="badge" data-label="Book"></div>
3. Deep links
The widget and the hosted page understand the same parameters. They work without JavaScript too — they are resolved on the server at render time.
| Parameter | Meaning |
|---|---|
service | slug of the service to preselect |
staff | slug of the staff member (“no preference” is the default) |
date | the day the calendar should open on (2026-08-14) |
checkin / checkout | stay range for accommodation |
guests | number of guests |
promo | discount code |
lang | interface language (sk, cs, en) |
currency | currency |
ref | your own source label — stored with the booking |
Parameters can also be given as attributes on the element — those take precedence over the page address:
<div data-callania="your-slug"
data-mode="inline"
data-service="beard-trim"
data-staff="george"
data-lang="en"></div>
4. Events for your site
The widget sends the host page named events via postMessage. That is more reliable than injecting foreign code into the confirmation page.
window.addEventListener('message', (e) => {
if (e.data?.source !== 'callania') return;
switch (e.data.event) {
case 'callania.service_selected': // picked a service
case 'callania.time_selected': // picked a time
case 'callania.booking_started': // filling in details
case 'callania.booking_completed': // done → pixel
dataLayer.push({ event: e.data.event, ...e.data.payload });
}
});
If window.dataLayer exists on the page, the widget pushes the events into it itself. It also fires them as a CustomEvent on window.
| Event | Payload | |
|---|---|---|
callania.service_selected | { service, price } | |
callania.time_selected | { startAt, service } | |
callania.booking_started | `{ service \ | room, nights }` |
callania.booking_completed | { code, value, currency } |
5. Fallback without a script
Wix, older WordPress or some CMSes will not let you add a script. A plain iframe works then — with loading="lazy" and a fixed height so there is no layout shift.
<iframe src="https://your-slug.callania.eu/?embed=1"
width="100%" height="760" loading="lazy"
title="Book an appointment" style="border:0"></iframe>
And for a QR code or a link in bio, a plain link is enough:
https://your-slug.callania.eu/?service=beard-trim&ref=instagram-bio
6. Widget height
The widget reports its height to the parent page as { source: "callania", event: "resize", payload: { height } }. The loader handles it for you, so you do not have to resize anything. With a plain iframe, set the height yourself or catch the message.
7. Visit source
The source is not carried in a cookie — inside an iframe it would be third-party and Safari and blockers would drop it. The loader therefore takes utm_*, gclid, fbclid, msclkid and ref from your page address and appends them to the widget address. The server stores them as first-party data for 30 days.
Partner links are attributed on a first click model, ads and campaigns on a last non-direct click model within a 30-day window. Your own ads therefore will not take a booking away from a partner.
8. All widget attributes
Only one is required — data-callania. Everything else has a sensible default.
| Attribute | Meaning |
|---|---|
data-callania | the business slug, e.g. salon-zatoka. Required. |
data-mode | inline (default), popup or badge. |
data-service | slug of the service to preselect. Without it the customer picks. |
data-lang | sk, cs or en. Without it, the language of the business. |
data-trigger | CSS selector of your button for popup. Without it the widget renders its own. |
data-label | text on the floating button for badge. |
data-domain | the custom domain of the business, if you have one set up. Without it the {slug}.callania.eu subdomain is used. |
9. iCal feed for portals
Every accommodation unit has its own iCal availability feed. You will find the address in the admin under Portals and iCal; it contains a signed token, so it can be pasted straight into Booking.com or Airbnb.
https://your-slug.callania.eu/ical/<token>.ics
The feed is recalculated as soon as a booking changes, not when the portal asks — that lets it answer 304 Not Modified for unchanged content so the portal does not download needlessly. In the other direction we pull portal feeds every minute, conditionally via If-None-Match.
The token in the address is the only thing protecting the feed, so treat it like a password. If it leaks, it can be rotated in the admin — the portal then adds the feed again.
10. Consent for source tracking — data-consent
On your site you are the controller, not us. Consent for tracking the visit source is therefore yours to collect — with your cookie banner, in your words. We only ask for it.
<div data-callania="your-slug" data-consent="granted"></div>
| Value | What it means |
|---|---|
granted | the visitor consented — we may store where they came from (30 days) |
denied | they did not consent — we measure nothing |
| attribute missing | we measure nothing. The default is no |
Without consent the source is not transmitted at all — not only is it not stored, it is not even put into the address. Sending it and trusting the other side to drop it means having it in the access log.
The attribute can be changed at runtime. When a visitor changes their choice in your banner, rewrite data-consent and embed the widget again — reloading the page is simpler.
Booking works the same either way. Without consent you simply will not see where the customer came from; the appointment, the confirmation and the reminder do not change.
Our cookie banner appears neither on your site nor on your hosted page. It would speak for you and ask in the wrong name.
11. Content in shadow DOM
The widget runs in its own <callania-widget> with a shadow DOM. Your CSS cannot reach it and our CSS will not break your page — not even if you have global rules for iframe or button.