هر برند خردهفروشی، فرانشایز و کسبوکار خدماتی در نهایت به یابنده فروشگاه نیاز دارد. این صفحهای است که مشتریان وقتی قصد خریدی دارند از آن بازدید میکنند، آنها فقط میخواهند بدانند کدام شاخه را وارد شوند. اشتباه کردن این صفحه هزینههای تبدیل واقعی دارد. انجام صحیح آن سادهتر از آنچه اکثر توسعهدهندگان انتظار دارند.
رویکرد استاندارد Google Maps بوده است، اما این دارای ساختار هزینهای است که در مقیاس بزرگ دردناک است. یک سایت خردهفروشی مشغول با ۵۰۰۰۰ بازدید ماهانه که صفحه نقشه را بارگیری میکند میتواند صدها یورو در اتهامات بارگیری نقشه و جیوکوڈینگ در یک شب جمع کند. دیگر هیچ سطح رایگان برای استفاده تولید وجود ندارد، صورتحساب مبهم است، و وضعیت GDPR برای کسبوکارهای EU که از سرویس نقشهبرداری مستقر در ایالات متحده استفاده میکنند سربار انطباق اضافی اضافه میکند.
این آموزش یک یابنده فروشگاه کامل، نقشه تعاملی، جستجوی آدرس، پنل فهرست همگام، خوشهبندی مارکر و پاپاپهای کلیک برای جزئیات را میسازد، با استفاده از MapAtlas Maps API و Geocoding API. هیچ فریمورک لازم نیست. مثال کاملی که کار میکند تقریباً در ۸۰ خط HTML و جاوا اسکریپت جا میگیرد. شما میتوانید آن را در یک بلوک HTML سفارشی وردپرس، بخش Shopify یا هر CMS در همان بعد از ظهری که این را میخوانید رها کنید.
در پایان خواهید داشت:
- یک نقشه برداری رندر شده در مرکز شبکه فروشگاههای شما
- مارکرهایی که از آرایه دادههای JSON با خوشهبندی در جوم پایین بارگیری میشوند
- نوار جستجوی آدرس که توسط Geocoding API قدرتیافته است
- یک پنل فهرست همگام که در کلیک نقشه برجسته میشود
- یک طرح دو ستونی واکنشپذیر برای موبایل
What a Store Locator Actually Needs
Before writing a line of code, it helps to be precise about the requirements. A working store locator has four moving parts:
- A map that renders tiles, accepts pan and zoom, and shows markers.
- A search input that geocodes the user's typed address to coordinates, then recentres the map.
- A marker layer that plots each store location, clusters nearby pins at low zoom, and opens a detail popup on click.
- A list panel that shows stores sorted by distance from the searched location, highlights the active store, and scrolls in sync with the map.
That is it. Every other feature, directions, opening hours, inventory stock, is an enhancement layered on top of these four. Build the core first.
Step 1: Load the MapAtlas SDK
The MapAtlas Maps API is compatible with the Mapbox GL JS interface, so any Mapbox GL JS tutorial or plugin works directly. Add the CDN links to your page <head>:
<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>
If you are using npm:
npm install @mapmetrics/mapmetrics-gl
Get your free API key at portal.mapmetrics.org/signup. One key covers map tiles, geocoding, and routing, no separate credentials to juggle.
Step 2: Define Your Store Data
Store data is just a GeoJSON FeatureCollection. Each feature carries the store's coordinates and whatever properties your popup needs: name, address, phone, opening hours.
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: "Mon–Sat 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: "Mon–Sat 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: "Mon–Sun 10:00–19:00"
}
}
]
};
In production you would fetch this from an API endpoint or a CMS. The structure stays the same, the only difference is where the data originates.
Step 3: Render the Map and Add Clustering
Initialize the map, add the store data as a GeoJSON source with clustering enabled, and paint the cluster circles and individual pin layer. Mapbox GL JS clustering is built into the source definition, no plugin required.
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], // Centre of the Netherlands
zoom: 7
});
map.on('load', () => {
// Add GeoJSON source with clustering
map.addSource('stores', {
type: 'geojson',
data: stores,
cluster: true,
clusterMaxZoom: 12,
clusterRadius: 50
});
// Cluster circles
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 count 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' }
});
// Individual store 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'
}
});
});
Clicking a cluster zooms in to reveal individual stores. Clicking an unclustered pin opens a popup.
Step 4: Wire Up Popups and the List Panel
When a user clicks a store pin, show a popup on the map and highlight the matching card in the list panel. Both interactions should be bidirectional, clicking a list card should also fly the map to that store.
// Click unclustered store → open popup + highlight list card
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);
});
// Click cluster → zoom in
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));
});
}
// Build list panel from store data
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);
});
}
Step 5: Add Address Search with the Geocoding API
The search bar takes a user's typed location, geocodes it via the Geocoding API, flies the map to that point, and re-sorts the list panel by distance.
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('Address not found. Try a city or postcode.');
return;
}
const [lng, lat] = data.features[0].geometry.coordinates;
// Fly map to searched location
map.flyTo({ center: [lng, lat], zoom: 10 });
// Sort list by distance from searched point
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 => {
// Re-render each card (reuse buildListPanel logic)
});
}
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);
});
Step 6: Mobile-Responsive Layout
A store locator on mobile must stack vertically, map on top, list below, rather than side by side. Twenty lines of CSS handles this with a single 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;
}
}
Billing and GDPR Comparison with Google Maps
اگر Google Maps را بر روی سایت خردهفروشی اجرا کردهاید و تعجب میکنید چرا صورتحساب این ماه بیشتر از حد انتظار آمده است، تنها نیستید. Maps JavaScript API برای هر بارگیری نقشه هزینه دارد. Places API برای هر جلسه خودکمپلت و برای هر درخواست جیوکوڈینگ هزینه دارد. این هزینهها به سرعت ترکیب میشوند. یک سایت که ۵۰۰۰۰ بازدید در ماه دارد، هر کدام یک بار صفحه یابنده فروشگاه را بارگیری میکند، حدود ۱۴۰ یورو در ماه فقط برای بارگیری نقشهها قبل از یک درخواست جیوکوڈینگ صرف میکند.
MapAtlas از طرحهای ماهانه پایدار استفاده میکند. هیچ هزینه هر بارگیری یا هر درخواستی وجود ندارد که بدون هشدار افزایش یابد. شما میتوانید تفکیک کامل را در Google Maps API Pricing in 2026: The True Cost Breakdown و MapAtlas vs. Google Maps comparison بخوانید.
برای توسعهدهندگان EU زاویه GDPR نیز مهم است. Google Maps دادهها را از طریق زیرساخت ایالات متحده مسیریابی میکند. MapAtlas میزبان EU است، دارای گواهی ISO 27001 و تمام درخواستها را در EU پردازش میکند. برای کسبوکارهای خردهفروشی که قبلاً رضایت مشتری را به دقت مدیریت میکنند، استفاده از یک ارائهدهنده نقشهبرداری بومی EU یک انتقال شخص ثالث دیگر را از سیاست حریمخصوصی شما حذف میکند.
Putting It All Together
یابنده فروشگاه کامل، ساختار HTML، طرح CSS، مقدمه نقشه، خوشهبندی، مدیریت پاپآپ، پنل فهرست، جستجو و مرتبسازی فاصله، به راحتی در یک فایل جا میگیرد. ساختار چنین به نظر میرسد:
برنامه نویسی (کد HTML فایل) حفظ میشود
نتیجه یابنده فروشگاهی است که برای تولید آماده است و بدون هیچ وابستگی خارجی فراتر از MapAtlas SDK. هیچ مرحله ساخت، هیچ فریمورک و هیچ تعجب صورتحسابی جاری وجود ندارد.
اگر نیاز به افزودن مسیریابی دارید، "دستورالعمل از مکان من به این فروشگاه بدست آورید"، Routing API مختصات کاربر و مختصات فروشگاه را میگیرد و یک مسیر کامل مرحله به مرحله را برمیگرداند که میتوانید بهعنوان یک لایه خط بر روی نقشه رسم کنید. How to Add Interactive Maps to Your Website آموزش این مرحله بعدی را به تفصیل پوشش میدهد.
Next Steps
- برای یک کلید MapAtlas API رایگان ثبت نام کنید، ندیم کارت اعتباری لازم نیست
- برای خوشهبندی، استایل سفارشی و گزینههای لایه Maps API documentation را مرور کنید
- برای جستجوی کد پستی، جیوکوڈینگ معکوس و خودکمپلت آدرس Geocoding API را کاوش کنید
Frequently Asked Questions
Can I build a store locator without Google Maps?
Yes. MapAtlas provides a Mapbox GL JS-compatible Maps API and a Geocoding API that cover every feature a store locator needs, interactive map, address search, marker clustering, and popups, with no per-load billing and full GDPR compliance.
How much does a store locator cost to run on MapAtlas vs Google Maps?
MapAtlas is roughly 75% cheaper than Google Maps for equivalent usage. Google Maps charges per map load and per geocoding request, which adds up fast on a busy retail site. MapAtlas uses flat monthly plans with no per-request surprises.
Does MapAtlas work on WordPress and Shopify?
Yes. Because MapAtlas is pure JavaScript with no framework dependency, you can embed it in a WordPress custom HTML block, a Shopify theme section, or any CMS that lets you add a script tag and a div.

