Jede Einzelhandelsmarke, jedes Franchise und jedes Dienstleistungsunternehmen braucht irgendwann einen Store Locator. Das ist die Seite, die Kunden besuchen, wenn sie bereits kaufen möchten und nur noch wissen müssen, welche Filiale sie aufsuchen sollen. Diese Seite falsch zu machen kostet echte Conversions. Sie richtig zu machen ist einfacher, als die meisten Entwickler erwarten.
Der Standardansatz war Google Maps, aber das bringt eine Kostenstruktur mit sich, die bei wachsendem Maßstab beißt. Eine stark frequentierte Einzelhandels-Website mit 50.000 monatlichen Besuchern, die eine Kartenseite aufrufen, kann über Nacht Hunderte von Euro an Karten-Lade- und Geocoding-Gebühren anhäufen. Es gibt keine kostenlose Stufe für den Produktionsbetrieb mehr, die Abrechnung ist undurchsichtig, und die DSGVO-Situation für EU-Unternehmen, die einen US-basierten Kartendienst verwenden, erhöht die Compliance-Last zusätzlich.
Dieses Tutorial erstellt einen vollständigen Store Locator, interaktive Karte, Adresssuche, synchronisiertes Listenpanel, Marker-Clustering und Klick-zu-Detail-Popups, mit der MapAtlas Maps API und der Geocoding API. Kein Framework erforderlich. Das vollständige funktionierende Beispiel passt in etwa 80 Zeilen HTML und JavaScript. Sie können es noch am selben Nachmittag, an dem Sie das lesen, in einen WordPress-Custom-HTML-Block, einen Shopify-Abschnitt oder ein beliebiges CMS einbauen.
Am Ende werden Sie Folgendes haben:
- Eine gerenderte Vektorkarte zentriert auf Ihr Filialnetz
- Marker, die aus einem JSON-Datenarray mit Clustering bei geringem Zoom geladen werden
- Eine Adresssuchleiste auf Basis der Geocoding API
- Ein synchronisiertes Listenpanel, das bei Karten-Klick hervorgehoben wird
- Ein mobil-responsives zweispaltiges Layout
Was ein Store Locator tatsächlich braucht
Bevor Sie eine Zeile Code schreiben, hilft es, die Anforderungen präzise zu formulieren. Ein funktionierender Store Locator hat vier bewegliche Teile:
- Eine Karte, die Tiles rendert, Schwenken und Zoomen akzeptiert und Marker zeigt.
- Eine Sucheingabe, die die eingegebene Adresse des Nutzers per Geocoding in Koordinaten umwandelt und dann die Karte neu zentriert.
- Eine Marker-Ebene, die jeden Filialstandort einzeichnet, nahe Pins bei geringem Zoom clustert und beim Klicken ein Detail-Popup öffnet.
- Ein Listenpanel, das Filialen nach Entfernung vom gesuchten Ort sortiert anzeigt, die aktive Filiale hervorhebt und mit der Karte synchron scrollt.
Das war's. Jede andere Funktion, Wegbeschreibungen, Öffnungszeiten, Warenbestand, ist eine Erweiterung, die auf diesen vier aufgebaut wird. Erstellen Sie zuerst den Kern.
Schritt 1: Das MapAtlas SDK laden
Die MapAtlas Maps API ist kompatibel mit der Mapbox GL JS-Schnittstelle, sodass jedes Mapbox-GL-JS-Tutorial oder Plugin direkt funktioniert. Fügen Sie die CDN-Links in Ihren <head>-Bereich ein:
<link
rel="stylesheet"
href="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css"
/>
<script src="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.js"></script>
Bei npm-Nutzung:
npm install @mapmetrics/mapmetrics-gl
Holen Sie sich Ihren kostenlosen API-Key auf portal.mapmetrics.org/signup. Ein Key deckt Karten-Tiles, Geocoding und Routing ab, keine separaten Zugangsdaten erforderlich.
Schritt 2: Ihre Filialdaten definieren
Filialdaten sind nur ein GeoJSON-FeatureCollection. Jedes Feature enthält die Koordinaten der Filiale und welche Eigenschaften Ihr Popup benötigt: Name, Adresse, Telefon, Öffnungszeiten.
const stores = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9041, 52.3676] },
properties: {
id: 1,
name: "Amsterdam Central",
address: "Stationsplein 12, 1012 AB Amsterdam",
phone: "+31 20 123 4567",
hours: "Mo–Sa 09:00–20:00"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.4777, 51.9244] },
properties: {
id: 2,
name: "Rotterdam Lijnbaan",
address: "Lijnbaan 10, 3012 EL Rotterdam",
phone: "+31 10 987 6543",
hours: "Mo–Sa 09:00–21:00"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [5.1214, 52.0907] },
properties: {
id: 3,
name: "Utrecht Centrum",
address: "Oudegracht 45, 3511 AB Utrecht",
phone: "+31 30 555 1234",
hours: "Mo–So 10:00–19:00"
}
}
]
};
In der Produktion würden Sie diese Daten von einem API-Endpunkt oder einem CMS abrufen. Die Struktur bleibt dieselbe, nur der Ursprung der Daten ändert sich.
Schritt 3: Die Karte rendern und Clustering hinzufügen
Initialisieren Sie die Karte, fügen Sie die Filialdaten als GeoJSON-Quelle mit aktiviertem Clustering hinzu, und zeichnen Sie die Cluster-Kreise und die einzelne Pin-Ebene. Das Mapbox-GL-JS-Clustering ist in der Quell-Definition integriert, kein Plugin erforderlich.
const map = new mapmetricsgl.Map({
container: 'map',
style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
center: [5.2913, 52.1326], // Zentrum der Niederlande
zoom: 7
});
map.on('load', () => {
// GeoJSON-Quelle mit Clustering hinzufügen
map.addSource('stores', {
type: 'geojson',
data: stores,
cluster: true,
clusterMaxZoom: 12,
clusterRadius: 50
});
// Cluster-Kreise
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'stores',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#3B82F6',
'circle-radius': ['step', ['get', 'point_count'], 20, 10, 28, 30, 36]
}
});
// Cluster-Zähler-Labels
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'stores',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-size': 13
},
paint: { 'text-color': '#ffffff' }
});
// Einzelne Filial-Pins
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'stores',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#EF4444',
'circle-radius': 8,
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
});
});
Ein Klick auf einen Cluster zoomt hinein, um einzelne Filialen zu zeigen. Ein Klick auf einen ungeclusterten Pin öffnet ein Popup.
Schritt 4: Popups und das Listenpanel verdrahten
Wenn ein Nutzer auf einen Filial-Pin klickt, zeigen Sie ein Popup auf der Karte und markieren die entsprechende Karte im Listenpanel. Beide Interaktionen sollten bidirektional sein: Ein Klick auf eine Listenkarte sollte die Karte auch zu dieser Filiale fliegen lassen.
// Klick auf ungeclusterten Store -> Popup öffnen + Listenkarte hervorheben
map.on('click', 'unclustered-point', (e) => {
const { coordinates } = e.features[0].geometry;
const { name, address, phone, hours, id } = e.features[0].properties;
new mapmetricsgl.Popup()
.setLngLat(coordinates)
.setHTML(`
<strong>${name}</strong>
<p>${address}</p>
<p>${phone}</p>
<p>${hours}</p>
`)
.addTo(map);
highlightCard(id);
});
// Klick auf Cluster -> hineinzoomen
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
const clusterId = features[0].properties.cluster_id;
map.getSource('stores').getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) return;
map.easeTo({ center: features[0].geometry.coordinates, zoom });
});
});
function highlightCard(id) {
document.querySelectorAll('.store-card').forEach(card => {
card.classList.toggle('active', card.dataset.id === String(id));
});
}
// Listenpanel aus Filialdaten aufbauen
function buildListPanel() {
const list = document.getElementById('store-list');
stores.features.forEach(({ properties, geometry }) => {
const card = document.createElement('div');
card.className = 'store-card';
card.dataset.id = properties.id;
card.innerHTML = `
<strong>${properties.name}</strong>
<p>${properties.address}</p>
<small>${properties.hours}</small>
`;
card.addEventListener('click', () => {
map.flyTo({ center: geometry.coordinates, zoom: 14 });
highlightCard(properties.id);
});
list.appendChild(card);
});
}
Schritt 5: Adresssuche mit der Geocoding API hinzufügen
Die Suchleiste nimmt den eingegebenen Standort des Nutzers, geocodiert ihn über die Geocoding API, fliegt die Karte zu diesem Punkt und sortiert das Listenpanel nach Entfernung neu.
async function searchLocation(query) {
const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
url.searchParams.set('text', query);
url.searchParams.set('key', 'YOUR_API_KEY');
const res = await fetch(url);
const data = await res.json();
if (!data.features.length) {
alert('Adresse nicht gefunden. Versuchen Sie eine Stadt oder Postleitzahl.');
return;
}
const [lng, lat] = data.features[0].geometry.coordinates;
// Karte zum gesuchten Ort fliegen
map.flyTo({ center: [lng, lat], zoom: 10 });
// Liste nach Entfernung vom gesuchten Punkt sortieren
const sorted = [...stores.features].sort((a, b) => {
const distA = haversine(lat, lng, a.geometry.coordinates[1], a.geometry.coordinates[0]);
const distB = haversine(lat, lng, b.geometry.coordinates[1], b.geometry.coordinates[0]);
return distA - distB;
});
document.getElementById('store-list').innerHTML = '';
sorted.forEach(feature => {
// Jede Karte neu rendern (buildListPanel-Logik wiederverwenden)
});
}
function haversine(lat1, lon1, lat2, lon2) {
const R = 6371;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
document.getElementById('search-btn').addEventListener('click', () => {
const query = document.getElementById('search-input').value.trim();
if (query) searchLocation(query);
});
Schritt 6: Mobil-responsives Layout
Ein Store Locator auf Mobilgeräten muss vertikal gestapelt werden, Karte oben, Liste unten, statt nebeneinander. Zwanzig Zeilen CSS bewältigen das mit einem einzigen Media-Query-Breakpoint.
#locator-wrapper {
display: flex;
height: 600px;
gap: 0;
}
#store-list {
width: 300px;
overflow-y: auto;
border-right: 1px solid #e5e7eb;
padding: 12px;
}
#map {
flex: 1;
}
.store-card {
padding: 12px;
border-radius: 8px;
cursor: pointer;
margin-bottom: 8px;
border: 2px solid transparent;
transition: border-color 0.2s;
}
.store-card.active {
border-color: #3B82F6;
background: #EFF6FF;
}
@media (max-width: 640px) {
#locator-wrapper {
flex-direction: column;
height: auto;
}
#store-list {
width: 100%;
border-right: none;
border-top: 1px solid #e5e7eb;
height: 280px;
}
#map {
height: 350px;
}
}
Abrechnungs- und DSGVO-Vergleich mit Google Maps
Wenn Sie Google Maps auf einer Einzelhandels-Website betreiben und sich fragen, warum die Rechnung diesen Monat höher als erwartet ausgefallen ist, sind Sie nicht allein. Die Maps JavaScript API berechnet pro Karten-Laden. Die Places API berechnet pro Autocomplete-Sitzung und pro Geocoding-Anfrage. Diese Kosten summieren sich schnell. Eine Website mit 50.000 monatlichen Besuchen, die jeweils einmal die Store-Locator-Seite laden, gibt allein für Karten-Ladevorgänge etwa 140 Euro pro Monat aus, bevor ein einziger Geocoding-Aufruf stattfindet.
MapAtlas verwendet monatliche Pauschalpreise. Es gibt keine Kosten pro Laden oder pro Anfrage, die ohne Vorwarnung ansteigen. Eine vollständige Aufschlüsselung finden Sie unter Google Maps API Pricing in 2026: The True Cost Breakdown und im MapAtlas vs. Google Maps-Vergleich.
Für EU-Entwickler ist der DSGVO-Aspekt ebenfalls wichtig. Google Maps leitet Daten über US-Infrastruktur weiter. MapAtlas ist EU-gehostet, ISO 27001 zertifiziert und verarbeitet alle Anfragen innerhalb der EU. Für Einzelhandelsunternehmen, die die Nutzereinwilligung bereits sorgfältig verwalten, entfernt die Verwendung eines EU-nativen Kartierungsanbieters einen weiteren Drittanbieter-Transfer aus Ihrer Datenschutzerklärung.
Alles zusammenfügen
Der vollständige Store Locator, HTML-Struktur, CSS-Layout, Karten-Init, Clustering, Popup-Handling, Listenpanel, Suche und Entfernungssortierung, passt bequem in eine Datei. Die Struktur sieht so aus:
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Store Locator</title>
<link rel="stylesheet"
href="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css" />
<script src="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.js"></script>
<style>
/* CSS aus Schritt 6 hier einfügen */
</style>
</head>
<body>
<div id="search-bar">
<input id="search-input" type="text" placeholder="Stadt oder Postleitzahl eingeben..." />
<button id="search-btn">Suchen</button>
</div>
<div id="locator-wrapper">
<div id="store-list"></div>
<div id="map"></div>
</div>
<script>
// Filialdaten, Karten-Init, Clustering, Popup, Listenpanel und Suche aus Schritten 2-5 einfügen
</script>
</body>
</html>
Das Ergebnis ist ein produktionsreifer Store Locator ohne externe Abhängigkeiten außer dem MapAtlas SDK. Kein Build-Schritt, kein Framework und keine laufenden Abrechnungsüberraschungen.
Wenn Sie Routing hinzufügen möchten, "Wegbeschreibung von meinem Standort zu dieser Filiale", nimmt die Routing API die Koordinaten des Nutzers und die der Filiale und gibt eine vollständige Wegbeschreibung zurück, die Sie als Linien-Ebene auf der Karte zeichnen können. Das Tutorial How to Add Interactive Maps to Your Website behandelt diesen nächsten Schritt im Detail.
Nächste Schritte
- Melden Sie sich für einen kostenlosen MapAtlas API-Key an, keine Kreditkarte erforderlich
- Durchsuchen Sie die Maps-API-Dokumentation für Clustering, benutzerdefiniertes Styling und Layer-Optionen
- Erkunden Sie die Geocoding API für Postleitzahl-Suche, Reverse Geocoding und Adress-Autocomplete
Häufig gestellte Fragen
Kann ich einen Store Locator ohne Google Maps bauen?
Ja. MapAtlas bietet eine Mapbox-GL-JS-kompatible Maps API und eine Geocoding API, die alle Funktionen eines Store Locators abdecken: interaktive Karte, Adresssuche, Marker-Clustering und Popups, ohne Kosten pro Laden und mit voller DSGVO-Konformität.
Wie viel kostet der Betrieb eines Store Locators auf MapAtlas im Vergleich zu Google Maps?
MapAtlas ist für gleichwertigen Einsatz etwa 75 % günstiger als Google Maps. Google Maps berechnet pro Karten-Laden und pro Geocoding-Anfrage, was sich auf einer stark frequentierten Einzelhandels-Website schnell summiert. MapAtlas verwendet monatliche Pauschalpreise ohne Anfrage-Überraschungen.
Funktioniert MapAtlas auf WordPress und Shopify?
Ja. Da MapAtlas reines JavaScript ohne Framework-Abhängigkeit ist, können Sie es in einen WordPress-Custom-HTML-Block, einen Shopify-Theme-Abschnitt oder jedes CMS einbetten, das das Hinzufügen eines Script-Tags und eines Div ermöglicht.

