LeafletWordPressElementorJavaScriptCSSmapsCartoDB

Ako vytvoriť vlastnú mapu s viacerými prevádzkami vo WordPress a Elementor pomocou Leaflet (zadarmo, bez mesačného poplatku)

Kompletná, funkčná mapa Leaflet.js s vlastnými špendlíkmi, zhlukovaním a brandovanými kartami pre WordPress a Elementor. Reálny kód zo siete šiestich reštaurácií, vrátane chyby s kartou na iOS, ktorú sa podarilo opraviť až na tretíkrát.

Milan PavlákMilan Pavlák
19 min čítania
Ako vytvoriť vlastnú mapu s viacerými prevádzkami vo WordPress a Elementor pomocou Leaflet (zadarmo, bez mesačného poplatku)

Ak ste čítali sprievodný článok o tom, prečo som odišiel od Google My Maps a platených nástrojov na mapy, toto je pokračovanie s reálnym kódom. Šesť reštauračných prevádzok, vlastné špendlíky ladiace so značkou, zhlukovanie a karty, postavené na Leaflet s nulovým priebežným nákladom.

Jedno treba povedať hneď na úvod: postavil som to tak, že som Claude opisoval, čo chcem, a upravoval to, čo mi vrátil. Nie je to výhovorka, je to skutočný postup a myslím, že sa oplatí byť k tomu úprimný. Hodnota nebola v tom, že by som ručne napísal každý riadok. Bola v tom vedieť, čo si vypýtať, zachytiť, čo je zle, a testovať dovtedy, kým to naozaj nefungovalo na reálnom telefóne. Tento postup je nižšie úprimne zdokumentovaný, aj s chybami, pretože práve v ladení sa skrýva najviac užitočných detailov.


Čo staviate

Jeden vložený prvok mapy, ktorý zobrazuje viacero prevádzok firmy, a to s:

  • vlastnými SVG špendlíkmi vo farbe vašej značky, nie predvolenou červenou kvapkou
  • zhlukovaním, takže blízke prevádzky sa zoskupia do číslovaného odznaku namiesto prekrývania
  • štýlovanou kartou po ťuknutí, s fotkou, názvom, adresou, popisom a telefónom
  • bez API kľúča, bez limitov používania, bez mesačného poplatku
  • plnou kontrolou nad každým vizuálnym detailom

Príkladom je po celý čas ten skutočný: Punjabi Dhaba, šesť reštauračných prevádzok, päť v Bratislave plus jedna v Piešťanoch, zlatá (#DCA26B) a tmavomodrá (#080D16) farba značky, karty s efektom matného skla.


Technológie

  • Leaflet — samotná mapová knižnica, open source, bez potreby kľúča
  • Leaflet.markercluster — jediný použitý doplnok, na zoskupovanie blízkych špendlíkov
  • CartoDB Voyager dlaždice — obrazový podklad mapy, bezplatná vrstva, pri bežnej návštevnosti bez potreby kľúča
  • Čisté HTML, CSS a vanilla JavaScript — žiadny build, žiadny framework

Všetko sa načítava z CDN (cdnjs pre Leaflet a doplnok na zhlukovanie), takže nie je čo inštalovať ani hostovať.


Krok 1: Kontajner a načítanie knižníc

Vložte toto do HTML widgetu v Elementore. Všetko medzi značkou style a uzatváracou značkou script je samostatné.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/MarkerCluster.css" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Jost:wght@400;600&family=Source+Sans+3:wght@400;600&display=swap" rel="stylesheet">

<div id="pd-map"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/leaflet.markercluster.js"></script>

⚠️ Ak vaša téma už načítava Jost a Source Sans Pro (skontrolujte nastavenia typografie témy), zmažte <link> na Google Fonts, aby sa tie isté písma nenačítavali dvakrát. Nechal som ho tam, aby sa mapa zobrazila správne aj vtedy, ak by sa použila na stránke, kde písma témy načítané nie sú.


Krok 2: Ako prežiť kaskádu CSS v Elementore a téme

Toto je časť, ktorá nie je nikde zdokumentovaná a stála reálny čas. Elementor a väčšina WordPress tém aplikujú na obrázky, nadpisy a odkazy dosť agresívne predvolené štýly — max-width, height: auto, resety písma a podobne. Ak píšete bežné CSS, téma takmer každý súboj vyhrá.

Riešením je uzavrieť všetko pod jedno ID kontajnera a zámerne používať !important, nie ako lenivý zvyk, ale preto, že cielene ruší pravidlá témy, ktoré sú samy !important alebo majú vysokú špecifickosť.

#pd-map {
  width: 100%; height: 560px; overflow: hidden;
  border-radius: 0;
  font-family: "Source Sans Pro","Source Sans 3", sans-serif;
}
@media (max-width: 640px) {
  #pd-map { height: 600px; }
  #pd-map .pd-card__img { height: 150px !important; }
}
@media (max-width: 400px) {
  #pd-map .pd-card__img { height: 150px !important; }
}

⚠️ Každý selektor nižšie je uzavretý ako #pd-map .niečo. Ak si to kopírujete do vlastného projektu, ten vzor zachovajte — bez predpony #pd-map vás vlastné CSS témy pravdepodobne nepredvídateľne prepíše, podľa poradia načítania.

Veľkosť špendlíkov

#pd-map .pd-pin,
#pd-map svg.pd-pin {
  width: 43px !important; height: 58px !important; max-width: none !important;
  display: block !important; filter: drop-shadow(0 4px 6px rgba(0,0,0,.4));
}

max-width: none !important je tu dôležité — väčšina tém nastavuje globálne img, svg { max-width: 100% }, čo bez tohto prepísania stlačí váš špendlík na šírku jeho kontajnera.

Odznak zhluku

#pd-map .pd-cluster {
  width: 48px !important; height: 48px !important; border-radius: 50% !important;
  background: #DCA26B !important; border: 3px solid #fff !important;
  box-shadow: 0 3px 7px rgba(0,0,0,.4) !important;
  display: flex !important; align-items: center !important; justify-content: center !important;
}
#pd-map .pd-cluster span {
  font-family: "Jost", sans-serif !important; font-weight: 600 !important;
  font-size: 17px !important; color: #080D16 !important;
}

⚠️ Nahraďte #DCA26B a #080D16 farbami vlastnej značky.

Rám karty

#pd-map .leaflet-popup.pd-wrap .leaflet-popup-content-wrapper {
  padding: 0 !important;
  border-radius: 16px !important;
  overflow: hidden !important;
  background-color: rgba(255, 255, 255, 0.66) !important;
  -webkit-backdrop-filter: blur(10px) !important;
  backdrop-filter: blur(10px) !important;
  box-shadow: rgba(0, 0, 0, 0.25) 0px 0px 16px !important;
}
#pd-map .leaflet-popup.pd-wrap .leaflet-popup-content {
  margin: 0 !important; width: 280px !important;
}
#pd-map .leaflet-popup.pd-wrap .leaflet-popup-tip {
  background: rgba(255, 255, 255, 0.66) !important;
  box-shadow: rgba(0, 0, 0, 0.25) 0px 0px 16px !important;
}

Efekt matného skla je len backdrop-filter: blur() na polopriehľadnom bielom pozadí. Je to dvojriadkový efekt, ktorý vyzerá podstatne drahšie, než v skutočnosti je.

Typografia obsahu karty

#pd-map .pd-card__img {
  width: 100% !important; height: 170px !important; max-width: none !important;
  object-fit: cover !important; display: block !important;
  margin: 0 !important; border-radius: 0 !important;
}
#pd-map .pd-card__body { padding: 24px !important; }
#pd-map .pd-card__title {
  margin: 0 0 8px !important; padding: 0 !important;
  font-family: "Jost", sans-serif !important; font-weight: 400 !important;
  text-transform: none !important; letter-spacing: normal !important;
  font-size: 20px !important; line-height: 1.25 !important; color: #DCA26B !important;
}
#pd-map .pd-card__addr {
  display: inline-block !important; margin: 0 0 12px !important;
  font-family: "Jost", sans-serif !important; font-weight: 400 !important;
  font-size: 12px !important; color: #000 !important; text-decoration: none !important;
}
#pd-map .pd-card__desc {
  margin: 0 0 14px !important; padding: 0 !important;
  font-family: "Source Sans Pro","Source Sans 3", sans-serif !important;
  font-weight: 400 !important; font-size: 14px !important; line-height: 1.5 !important; color: #000 !important;
}
#pd-map .pd-card__phone {
  font-family: "Jost", sans-serif !important; font-weight: 400 !important;
  font-size: 14px !important; color: #000 !important; text-decoration: none !important;
}
#pd-map .pd-card__addr:hover, #pd-map .pd-card__phone:hover { color: #DCA26B !important; }

⚠️ text-transform: none !important je tu preto, že veľa tém núti nadpisy do veľkých písmen. Skôr než usúdite, že tento riadok nepotrebujete, overte, či to vaša téma robí.

Poistka pre zmestenie na mobile

#pd-map .leaflet-popup.pd-wrap .leaflet-popup-content {
  max-height: 78vh !important; overflow-y: auto !important;
}

Práve toto spôsobilo reálnu chybu, ktorá je podrobne vysvetlená nižšie. Nechajte to tam — je to poistka, ktorá zaručí, že karta nikdy nemôže byť vyššia ako obrazovka, ani na krátkom telefóne na šírku.


Krok 3: Údaje o vašich prevádzkach

var IMG = {
  prievozska: "https://yourdomain.com/wp-content/uploads/2026/07/1.webp",
  cubicon: "https://yourdomain.com/wp-content/uploads/2026/07/3.webp",
  skypark: "https://yourdomain.com/wp-content/uploads/2026/07/5.webp",
  nivy: "https://yourdomain.com/wp-content/uploads/2026/07/7.webp",
  sancova: "https://yourdomain.com/wp-content/uploads/2026/07/9.webp",
  piestany: "https://yourdomain.com/wp-content/uploads/2026/07/11.webp",
};

var venues = [
  {
    name: "Názov vašej prevádzky",
    address: "Ulica 18, 821 09 Mesto",
    mapUrl: "https://maps.app.goo.gl/your-google-maps-share-link",
    phone: "0900 000 000",
    description: "Krátky popis toho, čím je práve táto prevádzka výnimočná.",
    image: IMG.prievozska,
    coords: [48.148296, 17.1480764]
  },
  // zopakujte pre každú prevádzku
];

⚠️ Toto je časť, ktorú celú nahradíte vlastnými údajmi. Všetko ostatné v kóde sa dá použiť tak, ako je.

Ako získať správne súradnice

Stojí za to to spomenúť, pretože to spôsobilo reálnu chybu na živom webe. Pri prvom pokuse som pre súradnice použil približné odhady namiesto presných hodnôt. Jedna z nich, Prievozská, bola vedľa asi o 550 metrov. Na mape pri zbežnom pohľade a oddialení vyzerala v poriadku, no ukazovala na nesprávnu budovu.

Spoľahlivý spôsob, ako získať presné súradnice: otvorte prevádzku v Google Mapách, cez funkciu Zdieľať získajte skrátený odkaz maps.app.goo.gl, potom ten odkaz otvorte a po presmerovaní si prečítajte súradnice z adresného riadka alebo URL stránky. Neodhadujte ich okom priamo na mape. Získajte súradnice z toho istého odkazu, ktorý už používate pre preklik na navigáciu, aby zaručene sedeli.


Krok 4: Inicializácia mapy, špendlíky a zhlukovanie

var map = L.map("pd-map", { scrollWheelZoom: false });

L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", {
  attribution: "&copy; OpenStreetMap &copy; CARTO", maxZoom: 19
}).addTo(map);

var pinSvg =
  '<svg class="pd-pin" width="43" height="58" viewBox="0 0 34 46" xmlns="http://www.w3.org/2000/svg">' +
    '<path d="M17 0C7.6 0 0 7.6 0 17c0 12 17 29 17 29s17-17 17-29C34 7.6 26.4 0 17 0z" fill="#DCA26B"/>' +
    '<circle cx="17" cy="17" r="6" fill="#080D16"/>' +
  '</svg>';
var pdIcon = L.divIcon({
  html: pinSvg, className: "", iconSize: [43, 58], iconAnchor: [22, 58], popupAnchor: [0, -54]
});

⚠️ Nahraďte #DCA26B a #080D16 v SVG farbami vlastnej značky — nech sedia s tým, čo ste nastavili v CSS odznaku zhluku vyššie.

scrollWheelZoom: false je zámerné. Bez toho by návštevníkovi, ktorý posúva stránku a má kurzor nad mapou, zachytilo posúvanie priblíženie mapy namiesto toho, aby stránka scrollovala ďalej. Nižšie sa po kliknutí opäť zapne.

Zostavenie HTML karty

function esc(s){ return String(s).replace(/[&<>"]/g, function(c){
  return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];
}); }

function popupHtml(v){
  var img  = v.image ? '<img class="pd-card__img" src="'+v.image+'" alt="'+esc(v.name)+'">' : '';
  var addr = v.mapUrl
    ? '<a class="pd-card__addr" href="'+esc(v.mapUrl)+'" target="_blank" rel="noopener noreferrer">'+esc(v.address)+'</a>'
    : '<span class="pd-card__addr">'+esc(v.address)+'</span>';
  var desc = v.description ? '<p class="pd-card__desc">'+esc(v.description)+'</p>' : '';
  var tel  = v.phone.replace(/\s+/g, "");
  return img +
    '<div class="pd-card__body">' +
      '<h3 class="pd-card__title">'+esc(v.name)+'</h3>' +
      addr + desc +
      '<a class="pd-card__phone" href="tel:'+esc(tel)+'">'+esc(v.phone)+'</a>' +
    '</div>';
}

Funkcia esc() je dôležitá, ak by názvy prevádzok, adresy alebo popisy mohli obsahovať znaky ako & alebo úvodzovky — bez ošetrenia by to narušilo HTML štruktúru karty. Lacná poistka, ktorú sa oplatí nechať, aj keď sú vaše aktuálne údaje čisté.

Skupina zhlukov

var cluster = L.markerClusterGroup({
  showCoverageOnHover: false, maxClusterRadius: 60,
  removeOutsideVisibleBounds: false,
  iconCreateFunction: function (c) {
    return L.divIcon({
      html: '<div class="pd-cluster"><span>' + c.getChildCount() + '</span></div>',
      className: "", iconSize: [48, 48]
    });
  }
});

venues.forEach(function (v) {
  cluster.addLayer(
    L.marker(v.coords, { icon: pdIcon })
      .bindPopup(popupHtml(v), { className: "pd-wrap", maxWidth: 300, closeButton: false, closeOnClick: false, autoClose: true, autoPanPadding: [16, 16] })
  );
});
map.addLayer(cluster);
map.fitBounds(cluster.getBounds(), { padding: [45, 45] });

maxClusterRadius: 60 určuje, ako blízko musia byť špendlíky, v pixeloch, kým sa spoja do odznaku zhluku. Znížte ho, ak chcete, aby špendlíky zostávali samostatné častejšie; zvýšte ho, ak chcete agresívnejšie zoskupovanie.

map.fitBounds(cluster.getBounds(), ...) je to, čo pri načítaní automaticky priblíži a vycentruje mapu tak, aby sa zmestili všetky prevádzky, namiesto toho, aby ste natvrdo nastavovali stred a úroveň priblíženia, ktoré po pridaní či odobratí prevádzky nemusia sedieť.

removeOutsideVisibleBounds: false vyzerá ako drobné nastavenie výkonu. Nie je. Je to oprava najvážnejšej chyby v celej tejto stavbe, podrobne vysvetlená nižšie.


Krok 5: Správanie kliknutia a posúvania

var lastPopupOpen = 0;
map.on("popupopen", function () { lastPopupOpen = Date.now(); });

map.on("click", function () {
  map.scrollWheelZoom.enable();
  if (Date.now() - lastPopupOpen > 500) { map.closePopup(); }
});
map.on("mouseout", function () { map.scrollWheelZoom.disable(); });

Tento blok existuje výlučne pre chybu na iOS nižšie. Prečítajte si tú sekciu, prečo je napísaný takto.


Chyba, ktorú sa podarilo opraviť až na tretíkrát

Chcem si tým prejsť celým, pretože výsledná oprava nie je tá zrejmá, a ak budete stavať niečo podobné, môžete naraziť na to isté.

Príznak

Konkrétne na iPhone sa po ťuknutí na špendlík karta otvorila a hneď zasa zavrela. Na počítači to fungovalo. Na Androide to fungovalo. Prejavovalo sa to len v iOS Safari.

Pokus jeden: predpoklad, že ide o bežný ghost click na iOS

Existuje známa zvláštnosť, keď jedno ťuknutie v mobilnom prehliadači niekedy vyvolá aj zamýšľané kliknutie, aj druhé „ghost" kliknutie krátko po ňom, a ak nemáte logiku zatvárania po kliknutí ošetrenú, to druhé kliknutie zatvorí to, čo prvé práve otvorilo.

Oprava tohto konkrétneho problému je priamočiara: na karte nastavte closeOnClick: false a namiesto toho si zaznamenajte, kedy sa karta otvorila, a potom ignorujte akékoľvek kliknutie na mapu v krátkom okne po ňom (500 ms), pričom skutočné ťuknutie mimo ju po uplynutí toho okna stále zatvorí.

Presne to robí kód v Kroku 5. Je to reálna oprava reálneho, bežného problému. Túto chybu neopravila.

Pokus dva: predpoklad, že ide o veľkosť zobrazenia

Keď to oprava ghost clicku nevyriešila, ďalšia rozumná hypotéza bola, že zmenšenie kontajnera mapy pre mobil a zníženie výšky fotky dá karte viac priestoru na vykreslenie bez toho, aby sa spustilo to, čo ju odrezávalo.

Ani to nepomohlo. V tej chvíli už hádanie neviedlo nikam — bolo treba to naozaj zreprodukovať a odmerať.

Pokus tri: zreprodukovať a odmerať

V prehliadači zmenšenom na reálnu šírku zobrazenia iPhonu bola samotná chyba viditeľná a merateľná. Pri mobilnej šírke mal kontajner mapy výšku 460 px. Najvyššia karta, pre prevádzku Sky Park, mala sama osebe 473 px, teda už viac než mapa. Pripočítajte 58 px výšky špendlíka a potrebný priestor bol 531 px. Do kontajnera s výškou 460 px sa fyzicky nezmestil.

Toto je skutočný sled udalostí, ktorý sa dial:

  1. Karta sa otvorí a Leaflet sa pokúsi automaticky posunúť mapu tak, aby bola nadrozmerná karta celá viditeľná
  2. Aby to dosiahol, musí posunúť mapu tak ďaleko, že sám špendlík sa dostane mimo viditeľnú oblasť
  3. Doplnok markercluster štandardne aktívne odstraňuje každý marker, ktorý sa dostane mimo viditeľnú oblasť — optimalizácia výkonu pre mapy so stovkami markerov, ktorá markery mimo obrazovky maže z DOM
  4. Odstránenie markera zničí aj jeho kartu
  5. Karta, ktorá sa práve pred chvíľou otvorila, zmizne, pretože marker, ku ktorému patrila, už neexistuje

To je ten skutočný mechanizmus. S ghost clickom nemal nič spoločné. Oprava ghost clicku z prvého pokusu bola stále správna a oplatí sa ju nechať, no riešila iný, menší problém, ktorý len vyzeral podobne.

Skutočná oprava, tri časti

Zabráňte doplnku mazať markery mimo obrazovky. Pri celkovo šiestich prevádzkach nemá udržanie všetkých načítaných bez ohľadu na pozíciu posunu žiadny podstatný vplyv na výkon:

var cluster = L.markerClusterGroup({
  removeOutsideVisibleBounds: false,
  // ...
});

Toto je oprava príčiny. Bez nej nemá nič ostatné zmysel.

Zabezpečte, aby sa karta naozaj zmestila, aby automatický posun vôbec nemusel tlačiť špendlík tak ďaleko mimo obrazovky:

@media (max-width: 640px) {
  #pd-map { height: 600px; }
  #pd-map .pd-card__img { height: 150px !important; }
}

Overené meraním po zmene: najvyššia karta v novej mobilnej veľkosti mala 453 px, plus 58 px špendlík, spolu 511 px, s rezervou vnútri nového kontajnera s výškou 600 px.

Poistka pre hraničné prípady, ako krátky telefón na šírku, kde ani pevná výška nemusí stačiť:

#pd-map .leaflet-popup.pd-wrap .leaflet-popup-content {
  max-height: 78vh !important; overflow-y: auto !important;
}

Ak by karta na niektorej obrazovke bola aj tak príliš vysoká, roluje sa vo vnútri seba, namiesto toho, aby nútila mapu prispôsobiť sa jej a znovu spustila tú istú chybu.

Úprimné obmedzenie

Všetky tri opravy som overil tak, že som problém naozaj zreprodukoval v zmenšenom prehliadači a odmeral skutočné výšky v pixeloch pred a po. Čo sa v tomto prostredí overiť nedalo, bol samotný reálny sled dotykových udalostí na iOS. Prehliadač na počítači zmenšený na šírku telefónu sa vykreslí v správnych rozmeroch, no nespúšťa tie isté dotykové udalosti ako fyzický iPhone. Rozmerová oprava je dokázaná. Konečné potvrdenie, že je vyriešené aj samotné dotykové správanie, si vyžaduje test na reálnom zariadení.

Ak si to prispôsobujete pre vlastný projekt s viac než šiestimi či siedmimi prevádzkami, otestujte to na reálnom telefóne skôr, než to budete považovať za úplne vyriešené pre váš prípad, najmä ak je niektorý obsah vašej karty dlhší než tu.


Pred spustením: čo skontrolovať

Doména obrázkov. Ak to staviate a testujete na staging URL, nezabudnite pred spustením prepnúť objekt IMG na produkčné URL médií. Ľahko sa na to zabudne, pretože staging obrázky fungujú aj po spustení webu, až kým staging nespadne.

Načítanie písiem. Ak vaša téma už načítava písma, ktoré používate, zmažte <link> na Google Fonts z Kroku 1, aby sa nenačítavali dvakrát.

Externé závislosti. Toto riešenie načítava Leaflet a doplnok na zhlukovanie z cdnjs a dlaždice mapy z CartoDB za behu. Je to bežné a spoľahlivé, no ak nechcete závisieť od cudzieho CDN vôbec, obidva súbory Leaflet sa dajú hostovať priamo vo vašej téme.


Ako to prispôsobiť vlastnému projektu

Jediná časť, ktorá si vyžaduje reálnu úpravu, je Krok 3: vaše prevádzky, obrázky a súradnice. Kroky 1, 2, 4 a 5 sú štruktúrne a mali by fungovať so zovšeobecnenými farbami značky a vlastným ID kontajnera, ak nechcete priamo použiť pd-map.

Ak máte výrazne viac než šesť či sedem prevádzok, sú tu dva rozdiely. removeOutsideVisibleBounds: false prestáva byť zadarmo — pri vyššom počte markerov sa stáva reálnym kompromisom vo výkone a oplatilo by sa otestovať, či je predvolené správanie zhlukovania pri vašich veľkostiach kariet vôbec problém, skôr než ho úplne vypnete. A výpočet výšky karty zo sekcie o iOS sa oplatí znovu overiť s dĺžkou vášho obsahu, keďže dlhší popis roztiahne kartu viac než pri kartách Punjabi Dhaba.

Pri šiestich prevádzkach a podobne tento presný kód funguje.


Kompletný súbor

Všetko vyššie, spojené do jedného bloku, pripravené vložiť do HTML widgetu v Elementore po výmene vlastných farieb, obrázkov a údajov o prevádzkach:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/MarkerCluster.css" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Jost:wght@400;600&family=Source+Sans+3:wght@400;600&display=swap" rel="stylesheet">

<style>
  #pd-map {
    width: 100%; height: 560px; overflow: hidden;
    border-radius: 0;
    font-family: "Source Sans Pro","Source Sans 3", sans-serif;
  }
  @media (max-width: 640px) {
    #pd-map { height: 600px; }
    #pd-map .pd-card__img { height: 150px !important; }
  }

  #pd-map .pd-pin,
  #pd-map svg.pd-pin {
    width: 43px !important; height: 58px !important; max-width: none !important;
    display: block !important; filter: drop-shadow(0 4px 6px rgba(0,0,0,.4));
  }

  #pd-map .pd-cluster {
    width: 48px !important; height: 48px !important; border-radius: 50% !important;
    background: #DCA26B !important; border: 3px solid #fff !important;
    box-shadow: 0 3px 7px rgba(0,0,0,.4) !important;
    display: flex !important; align-items: center !important; justify-content: center !important;
  }
  #pd-map .pd-cluster span {
    font-family: "Jost", sans-serif !important; font-weight: 600 !important;
    font-size: 17px !important; color: #080D16 !important;
  }

  #pd-map .leaflet-popup.pd-wrap .leaflet-popup-content-wrapper {
    padding: 0 !important;
    border-radius: 16px !important;
    overflow: hidden !important;
    background-color: rgba(255, 255, 255, 0.66) !important;
    -webkit-backdrop-filter: blur(10px) !important;
    backdrop-filter: blur(10px) !important;
    box-shadow: rgba(0, 0, 0, 0.25) 0px 0px 16px !important;
  }
  #pd-map .leaflet-popup.pd-wrap .leaflet-popup-content {
    margin: 0 !important; width: 280px !important;
  }
  #pd-map .leaflet-popup.pd-wrap .leaflet-popup-tip {
    background: rgba(255, 255, 255, 0.66) !important;
    box-shadow: rgba(0, 0, 0, 0.25) 0px 0px 16px !important;
  }

  #pd-map .pd-card__img {
    width: 100% !important; height: 170px !important; max-width: none !important;
    object-fit: cover !important; display: block !important;
    margin: 0 !important; border-radius: 0 !important;
  }

  #pd-map .pd-card__body { padding: 24px !important; }

  #pd-map .pd-card__title {
    margin: 0 0 8px !important; padding: 0 !important;
    font-family: "Jost", sans-serif !important; font-weight: 400 !important;
    text-transform: none !important; letter-spacing: normal !important;
    font-size: 20px !important; line-height: 1.25 !important; color: #DCA26B !important;
  }
  #pd-map .pd-card__addr {
    display: inline-block !important; margin: 0 0 12px !important;
    font-family: "Jost", sans-serif !important; font-weight: 400 !important;
    font-size: 12px !important; color: #000 !important; text-decoration: none !important;
  }
  #pd-map .pd-card__desc {
    margin: 0 0 14px !important; padding: 0 !important;
    font-family: "Source Sans Pro","Source Sans 3", sans-serif !important;
    font-weight: 400 !important; font-size: 14px !important; line-height: 1.5 !important; color: #000 !important;
  }
  #pd-map .pd-card__phone {
    font-family: "Jost", sans-serif !important; font-weight: 400 !important;
    font-size: 14px !important; color: #000 !important; text-decoration: none !important;
  }
  #pd-map .pd-card__addr:hover, #pd-map .pd-card__phone:hover { color: #DCA26B !important; }

  #pd-map .leaflet-popup.pd-wrap .leaflet-popup-content {
    max-height: 78vh !important; overflow-y: auto !important;
  }
</style>

<div id="pd-map"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.5.3/leaflet.markercluster.js"></script>
<script>
(function () {

  var IMG = {
    prievozska: "https://yourdomain.com/wp-content/uploads/2026/07/1.webp",
    cubicon: "https://yourdomain.com/wp-content/uploads/2026/07/3.webp",
    skypark: "https://yourdomain.com/wp-content/uploads/2026/07/5.webp",
    nivy: "https://yourdomain.com/wp-content/uploads/2026/07/7.webp",
    sancova: "https://yourdomain.com/wp-content/uploads/2026/07/9.webp",
    piestany: "https://yourdomain.com/wp-content/uploads/2026/07/11.webp",
  };

  var venues = [
    {
      name: "Punjabi Dhaba Prievozská",
      address: "Prievozská 18, 821 09 Bratislava",
      mapUrl: "https://maps.app.goo.gl/zQrWBxRPnA4nnRDo6",
      phone: "0948 076 528",
      description: "Naša najväčšia reštaurácia s pravou indickou atmosférou a kapacitou až do 200 miest. Ideálna na večerné posedenia s rodinou, priateľmi, alebo pri organizovaní väčších akcií, ako osláv, či teambuildingov.",
      image: IMG.prievozska,
      coords: [48.148296, 17.1480764]
    },
    {
      name: "Punjabi Dhaba Cubicon",
      address: "Staré Grunty 24, 841 04 Bratislava",
      mapUrl: "https://maps.app.goo.gl/kq2JsfLVdiuKP9JD9",
      phone: "0911 768 951",
      description: "Útulná prevádzka s menšou kapacitou, ktorá sa teší vysokej obľúbenosti mladými rodinkami, alebo partiami priateľov, ktoré tu nájdu súkromie a pokojnú atmosféru.",
      image: IMG.cubicon,
      coords: [48.1578282, 17.0701555]
    },
    {
      name: "Punjabi Dhaba Šancová",
      address: "Šancová 92, 831 04 Bratislava",
      mapUrl: "https://maps.app.goo.gl/p8ZM4SYSqE7JcYK76",
      phone: "0910 115 591",
      description: "Úplne prvá a zároveň naša najmenšia prevádzka Punjabi Dhaba, ktorá bola otvorená na Slovensku už v roku 2015. Má autentický charakter a atmosféru, je ideálna pre ľudí, ktorí chcú spoznať prostredie pravej indickej pocestnej reštaurácie.",
      image: IMG.sancova,
      coords: [48.1577262, 17.121859]
    },
    {
      name: "Punjabi Dhaba Sky Park",
      address: "SKY PARK Offices, Bottova 2a, 811 09 Bratislava",
      mapUrl: "https://maps.app.goo.gl/wT4YktSufbDoNQoM9",
      phone: "0914 411 978",
      description: "Moderná prevádzka prispôsobená požiadavkám náročnejších klientov v lokalite Sky Park, vhodná pre obedy s kolegami, biznis stretnutia, ale aj menšie eventy, ako napríklad teambuildingy vo forme nie len večere, ale aj zážitkového varenia s výkladom.",
      image: IMG.skypark,
      coords: [48.1433829, 17.1256078]
    },
    {
      name: "Punjabi Dhaba Nivy",
      address: "OC Nivy, Mlynské nivy 5/A, 821 08 Bratislava",
      mapUrl: "https://maps.app.goo.gl/12nzhmw8AWQ8ZqD98",
      phone: "0914 411 978",
      description: "Prevádzka vo forme street-foodového kiosku v stravovacej zóne nákupného centra Nivy, ktorá ponúka nie len indické snacky, ale aj vybrané obľúbené jedlá našich zákazníkov.",
      image: IMG.nivy,
      coords: [48.1464901, 17.1308627]
    },
    {
      name: "Punjabi Dhaba Piešťany",
      address: "Nitrianska 1857/24, 921 01 Piešťany",
      mapUrl: "https://maps.app.goo.gl/Vys8CB72viuCANPM6",
      phone: "0904 372 438",
      description: "Najnovšia prevádzka, jediná, ktorá sa nenachádza v Bratislave. Prináša kvalitu a jedinečnosť chutí Punjabi Dhaba aj mimo hlavného mesta, aby bola opäť raz bližšie ku svojim zákazníkom v rámci Slovenska.",
      image: IMG.piestany,
      coords: [48.5869733, 17.8326497]
    }
  ];

  var map = L.map("pd-map", { scrollWheelZoom: false });

  L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", {
    attribution: "&copy; OpenStreetMap &copy; CARTO", maxZoom: 19
  }).addTo(map);

  var pinSvg =
    '<svg class="pd-pin" width="43" height="58" viewBox="0 0 34 46" xmlns="http://www.w3.org/2000/svg">' +
      '<path d="M17 0C7.6 0 0 7.6 0 17c0 12 17 29 17 29s17-17 17-29C34 7.6 26.4 0 17 0z" fill="#DCA26B"/>' +
      '<circle cx="17" cy="17" r="6" fill="#080D16"/>' +
    '</svg>';
  var pdIcon = L.divIcon({
    html: pinSvg, className: "", iconSize: [43, 58], iconAnchor: [22, 58], popupAnchor: [0, -54]
  });

  function esc(s){ return String(s).replace(/[&<>"]/g, function(c){
    return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];
  }); }

  function popupHtml(v){
    var img  = v.image ? '<img class="pd-card__img" src="'+v.image+'" alt="'+esc(v.name)+'">' : '';
    var addr = v.mapUrl
      ? '<a class="pd-card__addr" href="'+esc(v.mapUrl)+'" target="_blank" rel="noopener noreferrer">'+esc(v.address)+'</a>'
      : '<span class="pd-card__addr">'+esc(v.address)+'</span>';
    var desc = v.description ? '<p class="pd-card__desc">'+esc(v.description)+'</p>' : '';
    var tel  = v.phone.replace(/\s+/g, "");
    return img +
      '<div class="pd-card__body">' +
        '<h3 class="pd-card__title">'+esc(v.name)+'</h3>' +
        addr + desc +
        '<a class="pd-card__phone" href="tel:'+esc(tel)+'">'+esc(v.phone)+'</a>' +
      '</div>';
  }

  var cluster = L.markerClusterGroup({
    showCoverageOnHover: false, maxClusterRadius: 60,
    removeOutsideVisibleBounds: false,
    iconCreateFunction: function (c) {
      return L.divIcon({
        html: '<div class="pd-cluster"><span>' + c.getChildCount() + '</span></div>',
        className: "", iconSize: [48, 48]
      });
    }
  });

  venues.forEach(function (v) {
    cluster.addLayer(
      L.marker(v.coords, { icon: pdIcon })
        .bindPopup(popupHtml(v), { className: "pd-wrap", maxWidth: 300, closeButton: false, closeOnClick: false, autoClose: true, autoPanPadding: [16, 16] })
    );
  });
  map.addLayer(cluster);
  map.fitBounds(cluster.getBounds(), { padding: [45, 45] });

  var lastPopupOpen = 0;
  map.on("popupopen", function () { lastPopupOpen = Date.now(); });

  map.on("click", function () {
    map.scrollWheelZoom.enable();
    if (Date.now() - lastPopupOpen > 500) { map.closePopup(); }
  });
  map.on("mouseout", function () { map.scrollWheelZoom.disable(); });
})();
</script>

Vymeňte objekt IMG a pole venues za vlastné prevádzky, nahraďte #DCA26B a #080D16 všade farbami svojej značky a toto je pripravené vložiť do HTML widgetu v Elementore.

Často kladené otázky

Potrebujem na to API kľúč do Google Máp?

Nie. Leaflet je open source a dlaždice CartoDB Voyager, ktoré používa, sú pri bežnej návštevnosti bezplatné, takže v celom riešení nie je žiadny API kľúč ani spoplatnené používanie.

Bude to fungovať s bezplatnou verziou Elementora?

Áno. Celá mapa ide do bežného HTML widgetu, ktorý je aj v bezplatnom Elementore. Nič tu nezávisí od Elementor Pro.

Koľko prevádzok to zvládne?

Šesť zvládne bez problémov a do niekoľkých desiatok to škáluje bez väčšieho rozmýšľania. Nad tým sa nastavenie removeOutsideVisibleBounds: false, ktoré opravuje chybu s kartou na mobile, stáva reálnym kompromisom vo výkone, takže pri stovkách markerov by ste ho znovu otestovali.

Prečo nepoužiť rovno Google My Maps alebo platený nástroj?

Môžete a pri dvoch prevádzkach vložená Google mapa postačí. Tento postup je pre prípad, keď chcete, aby mapa presne sadla k vašej značke a zároveň nemala mesačný poplatok, čo platené nástroje spolu ponúknuť nevedia. Ten kompromis rozoberá sprievodný článok.

Musím Leaflet hostovať sám?

Nie. Načítava sa z CDN, takže nie je čo inštalovať. Ak nechcete závisieť od cudzieho CDN, obidva súbory Leaflet si viete hostovať priamo vo svojej téme.

Spojme sa

Chcete sa porozprávať o vašom projekte? Ozvite sa mi cez ktorýkoľvek z týchto kanálov.

Sídlo v Bratislave, Slovensko. K dispozícii pre projekty po celom svete.

Try me — I'm alive