/* iVillas shared site script. Host at a real JS URL, e.g. https://ivillas.org/app.js/ (same trick as style.css: a CMS page whose slug ends in .js, served as JS). Every landing loads this one file via the loader in each MainText (or once in the template
). Edit here once -> whole site updates; bump ?v= (or ?clear_cache=1) to bust cache. Keep it dependency-free, defensive (pages may or may not have a given block), and idempotent. */ (function () { 'use strict'; if (window.__ivInit) return; // guard against double-injection window.__ivInit = true; function ready(fn) { if (document.readyState !== 'loading') fn(); else document.addEventListener('DOMContentLoaded', fn); } function txt(el) { return el ? (el.textContent || '').replace(/\s+/g, ' ').trim() : ''; } function abs(u) { try { return new URL(u, location.href).href; } catch (e) { return u || ''; } } ready(function () { // 1) Harden outbound/affiliate links: any target=_blank gets rel noopener (perf + security). document.querySelectorAll('a[target="_blank"]').forEach(function (a) { var rel = (a.getAttribute('rel') || '').split(/\s+/).filter(Boolean); ['noopener', 'noreferrer'].forEach(function (t) { if (rel.indexOf(t) < 0) rel.push(t); }); a.setAttribute('rel', rel.join(' ')); }); // 1b) Cloaked booking CTAs: the affiliate URL lives in data-book, not href, so it never // appears as a crawlable in the static HTML (Googlebot's standard crawl reads // href values; it doesn't fire click handlers, so this keeps the link out of its graph). // Real visitors still get a normal new-tab navigation on click or Enter/Space (the anchor // has no href so it needs role=button + tabindex=0 in the HTML to stay keyboard-reachable). document.querySelectorAll('[data-book]').forEach(function (a) { function go() { window.open(a.getAttribute('data-book'), '_blank', 'noopener,noreferrer'); } a.addEventListener('click', function (e) { e.preventDefault(); go(); }); a.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } }); }); // 1c) Booking click-under. The FIRST qualifying internal-link click per device-hour opens the // reader's target in a new tab and sends THIS tab to Booking.com, so the affiliate cookie // is set even if they later reach Booking on their own. The Booking target is the page's // own cloaked data-book URL — our affiliate `label` is already baked into it, so there's no // second place to keep in sync and nothing to encode. It lives OVER the normal s // (never replaces them: the crawler still follows real links). Order is critical — open the // new tab FIRST, then mark + redirect; on iOS Safari a popup opened AFTER a same-tab nav is // queued gets killed, stranding the reader. Mark only after a real window.open so a blocked // popup doesn't burn the hour. Rationale + field lessons: repo CLICKUNDER note. (function () { var book = document.querySelector('[data-book]'); var B = book && book.getAttribute('data-book'); if (!B) return; // no affiliate target here -> never fire var KEY = 'iv_cu', TTL = 3600000; // one hour, per device function seen() { try { return Date.now() - (+localStorage.getItem(KEY) || 0) < TTL; } catch (_) { try { return !!sessionStorage.getItem(KEY); } catch (__) { return false; } } } function mark() { try { localStorage.setItem(KEY, String(Date.now())); } catch (_) { try { sessionStorage.setItem(KEY, '1'); } catch (__) { /* private mode: skip */ } } } document.addEventListener('click', function (e) { if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; // keep open-in-new-tab etc. var a = e.target.closest && e.target.closest('a'); if (!a) return; if (a.hasAttribute('data-book') || a.classList.contains('btn-stay') || a.getAttribute('target') === '_blank') return; // already Booking / new tab var h = a.getAttribute('href') || ''; if (!h || h.charAt(0) === '#') return; // in-page anchor if (a.protocol && a.protocol !== 'http:' && a.protocol !== 'https:') return; // mailto:, tel: if (a.host && a.host !== location.host) return; // external domain if (seen()) return; var w = window.open(a.href, '_blank'); // 1. reader's target first if (!w) return; // popup blocked -> normal click mark(); // 2. mark after a real open e.preventDefault(); location.href = B; // 3. this tab -> Booking }, true); // capture: before page handlers })(); // 2) Lazy-load images that didn't opt out (below-the-fold gallery/cards). Hero can set // loading="eager" in the HTML to stay excluded. document.querySelectorAll('img:not([loading])').forEach(function (img) { img.setAttribute('loading', 'lazy'); img.setAttribute('decoding', 'async'); }); // 3) SEO layer. The CMS serves only the MainText, so the arrives empty (no