Website: ads, SEO and analytics¶
Research from 2026-10-23. A technical plan for the real website, not a final decision (see the open question on the website stack).
This page explains what the website needs technically so that ads work well, the salon is found on Google, and we can measure where bookings come from. How to run the ads themselves: Ads.
Summary¶
- A plain HTML/CSS/JS site is enough. Everything below runs in the visitor's browser: no server, no database, no framework.
- One URL per language (English at
/, then/de/,/fr/,/it/), with the text in the HTML. - Shared CSS and JS; business facts from
business.yml; a check script against drift between languages. - Cookie banner with consent first, then the Google tag and the Meta Pixel.
- The booking is reported from the Cal.com embed to Google and Meta: this one event drives conversion tracking, ad optimisation and remarketing.
Structure (plain HTML, no generator)¶
We keep a plain static site, edited with an AI coding agent. The only real risk is drift: the same content exists in several languages, and one copy changes while the others do not. These rules keep it manageable:
| Rule | Why |
|---|---|
One page per language, like the prototype: /index.html (English), /de/index.html, … plus Impressum and privacy policy |
Only about 4–8 HTML files |
Shared files: one styles.css, one main.js (map, booking, tracking, consent) |
Design and logic exist only once; only the text differs per language |
| Text in the HTML, not switched by JavaScript | Google indexes each language; ads land on the right language |
Business facts (prices, hours, address, phone) taken from business.yml |
One place to change; the agent updates all language files from it |
Check script (e.g. make site-check): same prices and phone in every language as in business.yml, hreflang on every page, same sections in the same order |
Catches drift automatically |
| Every change in all languages at once | The same rule as for these docs (EN + RU) |
Reconsider the approach (e.g. a static site generator) only if the site grows to many pages (blog, one page per service) or someone without AI tools must edit it regularly.
Small additions without our own server¶
| Need | Solution |
|---|---|
| Contact | WhatsApp, phone and email links; or a form service [compare] |
| Selling vouchers online | A payment link (e.g. Stripe Payment Links) or the booking tool's voucher feature |
| Server-side tracking (optional, later) | Cal.com webhook → a small serverless function (e.g. Cloudflare Workers, Netlify Functions) |
Hosting¶
| Option | Cost | Note |
|---|---|---|
| Infomaniak (together with domain and email) | A small hosting plan, a few CHF per month [verify] |
Data in Switzerland: simplest for the privacy policy |
| Cloudflare Pages / Netlify | Free | Fast, serverless functions included; US/EU companies, name them in the privacy policy |
| GitHub Pages | Free | Fine for the prototype; for the real site the options above are better |
Languages and URLs¶
- Separate URL per language:
alpformflow.ch/(English, the default),/de/,/fr/,/it/.- Ads: a German ad must land on a German page. On Google Search, the landing page language is now a matching signal (see Ads).
- SEO: Google indexes each language separately only with its own URL.
- Sharing: a link sent on WhatsApp opens in the right language.
- Analytics: visits and bookings per language are visible directly.
<html lang="de">(oren…) on every page.- hreflang tags on every page, pointing to all language versions, plus
x-default. /is the English page and also thex-default(for all other languages).- If the browser language is DE, FR or IT, a small dialog asks in that language whether to switch ("Möchten Sie diese Seite auf Deutsch sehen?"). Other browser languages see no dialog.
- The answer is remembered, so the dialog does not come back. No automatic redirect: Google always sees the English page.
- The cookie banner appears after the dialog is closed.
- Language selector (a small drop-down in the header) links to the same page in the other language, not to the home page.
- The current section is kept: while scrolling, the address follows the section (
#prices,#faq…), so switching language opens the same section. - The prototype already uses this structure (since 2026-10-24).
Booking calendar language (Cal.com)¶
- By default Cal.com shows the booking page in the visitor's browser language. We cannot choose it from the website.
- Possible workaround (not decided yet, to discuss with the partners): one event type per language, each with its "Interface Language" set (event type settings; the API field is
interfaceLanguage)[verify in our Cal.com account]:
| Page | Cal.com event type (example) | Interface Language |
|---|---|---|
/ |
anti-cellulite |
English |
/de/ |
anti-cellulite-de |
Deutsch |
/fr/ |
anti-cellulite-fr |
Français |
/it/ |
anti-cellulite-it |
Italiano |
- Create the English one first, then Duplicate it for the other languages and change the slug, title, description and Interface Language.
- All belong to the same person, so they share one calendar: a booking in one blocks the time in all of them
[verify]with a test booking. - Our own texts (service name, description, booking questions) are written in the event type's language.
- Keep them in sync: duration, price, availability and booking questions must be the same in all four. Change one → change all.
- The website would then give each page its own event type in
data-booking-cal(frombusiness.yml). Today all pages use one event type. - Cal.com is loaded only on the first click on "Book", so visitors who don't book make no request to cal.com.
SEO basics¶
On every page
- Unique
<title>and meta description per language, with place and service: "Anti-Cellulite-Massage in Saanen | Alpform & Flow". - One
<h1>, clear headings, the address and phone as text (not only in an image or the map). - Canonical URL (each language version points to itself).
- Open Graph tags (title, description, image): the preview when the link is shared on WhatsApp, Instagram, Facebook.
- Alt texts on images, in the page's language.
Structured data (schema.org)
- JSON-LD block
HealthAndBeautyBusinessorDaySpa, generated frombusiness.yml: name, address, geo coordinates, phone, opening hours, prices, URL, booking link, images,sameAs(Instagram, Facebook, Google Business Profile). - Check with Google's Rich Results Test.
Files and tools
sitemap.xmlwith all language versions (and hreflang),robots.txt.- Google Search Console and Bing Webmaster Tools: submit the sitemap, watch indexing and search terms. Bing also feeds ChatGPT/Copilot answers. Both are set up for the "opening soon" page (step by step).
- IndexNow (with the full site): tells Bing and the other engines that use it (Yandex, Seznam, Naver, Yep) right away which pages are new or changed, so updated prices or opening hours reach Bing, ChatGPT and Copilot faster. Google does not use it; for Google the sitemap stays the way.
- A key file at the site root (e.g.
https://alpformflow.ch/<key>.txt), generated by the site build. - After each upload, one request to
https://api.indexnow.org/indexnowwith the changed addresses and the key, as a step in the deploy command. - Not for the "opening soon" page: 4 pages that do not change, and Bing already reads the sitemap.
- A key file at the site root (e.g.
- The same name, address and phone as on Google Business Profile and all listings (see Local search presence).
Speed (also an ad factor: slow landing pages cost more per click)
- Images in WebP/AVIF, the right size,
loading="lazy"below the first screen. - Fonts hosted on our own server, not loaded from Google Fonts (faster, and no data goes to Google without consent).
- Little JavaScript; load the booking widget, map and tags only when needed.
- Check with PageSpeed Insights (Core Web Vitals) on a phone.
Analytics and tracking¶
What we use¶
| Tool | Purpose | Cookies / consent |
|---|---|---|
| Google tag (Google Ads conversion tracking) | Cost per booking from Google ads, remarketing | Yes, after consent |
| Meta Pixel | Cost per booking from Meta ads, remarketing | Yes, after consent |
Website statistics: GA4, or a cookie-free tool (e.g. Plausible, Matomo, Cloudflare Web Analytics) [compare] |
Visits, pages, languages, channels | GA4: after consent. Cookie-free tools: often without a banner [verify] |
| Google Search Console, Bing Webmaster Tools | Search terms, indexing | No (nothing on the website) |
GA4 is free but needs consent, so it misses visitors who decline. A cookie-free tool counts all visits but does not feed Google Ads. Possible combination: a cookie-free tool for statistics plus the Google tag and Meta Pixel only for ads.
Events to track¶
| Event | When | Why |
|---|---|---|
page_view |
Each page | Visits per language and channel |
book_click |
Click on a "Book" button | Interest; compare with bookings |
booking_completed |
Cal.com reports a successful booking | The main event: cost per booking, ad optimisation, remarketing |
whatsapp_click, phone_click, email_click |
Click on a contact link | Bookings outside Cal.com |
directions_click (with app: google / apple / waze / android-chooser), copy_location |
Directions and copy location buttons | Local interest |
voucher_click |
Click on the voucher link | Voucher interest |
Reporting the booking from Cal.com¶
The Cal.com window is an iframe from another domain, so our tag cannot see inside it. The embed sends JavaScript events to our page; we pass them on:
Cal.ns["anti-cellulite"]("on", {
action: "bookingSuccessfulV2", // [verify] event name in the Cal.com embed docs
callback: (e) => {
gtag("event", "booking_completed", { method: "cal.com" }); // Google
fbq("track", "Schedule"); // Meta standard event
}
});
- People who book via the direct link (Instagram bio, Google Business Profile) never visit our website. Cal.com has its own integrations (Google Analytics, Google Tag Manager, Meta Pixel) for its booking pages
[verify]which plans include them.
UTM tags¶
Every link we place (ads, Instagram bio, Google Business Profile, QR codes, partners) gets UTM tags. The allowed values and the register of all links are on their own page: UTM links.
Example: https://alpformflow.ch/?utm_source=flyer&utm_medium=print&utm_campaign=opening-2026&utm_content=a6-station
Reading them needs no code: GA4, Plausible, Matomo and the ad platforms read UTM tags from the address of the first page automatically and remember the source for the whole visit. The website's job is to not lose them and to pass them on to the booking.
1. Do not lose the parameters
- Redirects keep the query string:
http→https,www→ withoutwww. - No UTM tags on internal links (menu, language switcher, buttons): they would start a new visit with a wrong source.
- Canonical without parameters:
<link rel="canonical" href="https://alpformflow.ch/de/">, so Google does not index?utm_…copies of the page.
2. Pass the source on to the booking
The booking happens in the Cal.com iframe, so we connect booking_completed to the source ourselves:
// main.js: remember the campaign info of this visit (first page)
const p = new URLSearchParams(location.search);
const keys = ["utm_source","utm_medium","utm_campaign","utm_content","utm_term","gclid","fbclid"];
const utm = Object.fromEntries(keys.filter(k => p.get(k)).map(k => [k, p.get(k)]));
try { if (Object.keys(utm).length) sessionStorage.setItem("utm", JSON.stringify(utm)); } catch (_) {}
const saved = (() => { try { return JSON.parse(sessionStorage.getItem("utm") || "{}"); } catch (_) { return {}; } })();
// a) send the source with the booking event
Cal.ns["anti-cellulite"]("on", {
action: "bookingSuccessfulV2", // [verify] event name
callback: () => gtag("event", "booking_completed", { ...saved })
});
// b) store the source with the booking in Cal.com (booking data, webhooks)
// Cal.com accepts metadata as query parameters: metadata[key]=value [verify]
const meta = Object.entries(saved).map(([k, v]) => `metadata[${k}]=${encodeURIComponent(v)}`).join("&");
document.querySelectorAll(".js-book").forEach(b =>
b.dataset.calLink = `salon/anti-cellulite${meta ? "?" + meta : ""}`);
- a) gives bookings per source in GA4/Plausible and Google Ads.
- b) keeps the source on the booking itself in Cal.com, even when the visitor declined cookies
[verify]. Useful later, e.g. "which channel brings clients who buy packages". sessionStorageholds only campaign labels, no personal data, and is cleared when the tab closes. Mention it in the privacy policy[verify].
3. Analyse
| Where | What you see |
|---|---|
| GA4 → Reports → Acquisition → Traffic acquisition | Visits by source/medium/campaign; with the key event booking_completed: bookings per source |
| GA4 → Explore | Own tables, e.g. campaign × language page × bookings |
| GA4 → Admin → Channel groups | A custom channel "Print" for utm_medium=print (GA4 does not know it) |
| Plausible / Matomo | Sources with UTM filters; goals for bookings |
| Google Ads / Meta Ads Manager | Cost and bookings per campaign and ad |
| Cal.com bookings / webhook data | The source stored with each booking (point 2b), exportable to a spreadsheet |
Every month: per channel, cost ÷ bookings = cost per booking, compared with the limit in the private notes.
Remarketing: how it works¶
Visitor opens alpformflow.ch/de/
→ Google tag / Meta Pixel load (only after cookie consent)
→ Google/Meta recognise the person (cookie + logged-in Google/Instagram account)
→ they add the person to OUR list "website visitors"
Visitor completes a booking in the Cal.com window
→ our page catches the Cal.com booking event
→ we send "booking_completed" to Google/Meta
→ they add the person to the list "booked"
In Google Ads / Meta Ads Manager:
audience = "website visitors (last 30 days)" MINUS "booked"
- We never see who is on the lists, only their size.
| Platform | Minimum list size to run ads | How long people stay on the list |
|---|---|---|
| Google (Display / Demand Gen / YouTube) | about 100 active users in the last 30 days [verify] |
up to 540 days |
| Google Search | about 1,000 [verify] |
— |
| Meta | about 100 people [verify] |
up to 180 days |
- Losses: declined cookies, Safari/iPhone privacy features, ad blockers. Perhaps 40–60% of visitors end up on the lists
[verify]. - Install the tags from day one: lists fill only from the moment the tag is live.
- Later (optional): Meta Conversions API and Google enhanced conversions: the booking is also reported from a server (Cal.com webhook → serverless function), so fewer bookings are lost. Needs a privacy policy update.
Cookie consent¶
- Advertising tags (Google tag, Meta Pixel) set advertising cookies. Google requires consent for personalised ads from users in the EEA, the UK and Switzerland
[verify], and many of our clients are EU tourists. - Cookie banner with a real choice: "Accept" and "Decline" equally visible; categories (necessary / statistics / marketing). Open-source libraries, e.g. CookieConsent (orestbida) or Klaro
[compare]. - Google Consent Mode v2: everything starts as "denied"; tags work fully only after "Accept". The Meta Pixel loads only after consent.
- Without consent the visitor is not tracked and not added to any list. That is normal; we lose part of the data.
- The banner itself in all site languages.
- The privacy policy names every tool (Google Ads, Meta Pixel, statistics tool, Cal.com, map tiles, hosting). See Website & marketing law.
- Map: the prototype loads OpenStreetMap tiles directly. This sends the visitor's IP address to the tile server: it is mentioned in the privacy policy template; alternatively load the map only after a click
[verify].
Launch checklist¶
/(English),/de/,/fr/,/it/withlang, hreflang,x-default, canonical, Open Graph; language dialog on/- Title and meta description per page and language
- schema.org JSON-LD from
business.yml sitemap.xml,robots.txt; Search Console and Bing Webmaster Tools set up; sitemap resubmitted after the launch- IndexNow key file and the IndexNow request in the deploy command; launch announced to IndexNow
- Fonts self-hosted, images optimised, PageSpeed checked on a phone
- Cookie banner + Consent Mode v2 in all languages
- Google tag + conversion
booking_completed; Meta Pixel +Scheduleevent; tested with real test bookings - Contact click events, UTM tags on all our own links and QR codes
- Cal.com event types and language checked (one event type, or one per language if decided), test booking; booking calendar starts on the opening day
- Privacy policy and Impressum checked (fiduciary or lawyer)
make site-checkpasses
Sources¶
- Google Ads Developer Blog – language targeting changes 2026
- Cal.com – embed events, Guideflow – event interface language in Cal.com
- Google Ads Help – Demand Gen audiences, Exclusions
- Google Search Central – localized versions (hreflang)
- schema.org – HealthAndBeautyBusiness, schema.org – DaySpa
- IndexNow – documentation