Implementing Interactive Maps (Google Maps/Yandex) on Website
The choice between Google Maps and Yandex.Maps is determined by audience: for Russian market, Yandex covers details unavailable in Google. Both platforms require API keys, billing, and attention to performance.
Google Maps: Basic Integration
<!-- Load API asynchronously -->
<script>
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})
({key: "YOUR_API_KEY", v: "weekly"});
</script>
async function initMap(containerId: string) {
const { Map, InfoWindow } = await google.maps.importLibrary('maps') as google.maps.MapsLibrary
const { AdvancedMarkerElement } = await google.maps.importLibrary('marker') as google.maps.MarkerLibrary
const map = new Map(document.getElementById(containerId)!, {
center: { lat: 55.7558, lng: 37.6173 }, // Moscow
zoom: 14,
mapId: 'YOUR_MAP_ID', // for AdvancedMarker and Cloud Styling
gestureHandling: 'cooperative', // Ctrl+scroll for zoom — doesn't interfere with page scroll
disableDefaultUI: false,
zoomControl: true,
mapTypeControl: false,
streetViewControl: false,
fullscreenControl: true,
restriction: {
latLngBounds: { north: 85, south: -85, west: -180, east: 180 },
strictBounds: false,
},
})
return { map, InfoWindow, AdvancedMarkerElement }
}
Custom Markers and Clustering
import { MarkerClusterer } from '@googlemaps/markerclusterer'
interface Location {
lat: number
lng: number
title: string
description: string
category: string
}
async function addMarkersWithClustering(map: google.maps.Map, locations: Location[]) {
const { AdvancedMarkerElement } = await google.maps.importLibrary('marker') as google.maps.MarkerLibrary
const infoWindow = new google.maps.InfoWindow()
const markers = locations.map(loc => {
// Custom DOM element instead of default marker
const pin = document.createElement('div')
pin.className = `map-pin map-pin--${loc.category}`
pin.innerHTML = `<span class="map-pin__label">${loc.title}</span>`
const marker = new AdvancedMarkerElement({
map,
position: { lat: loc.lat, lng: loc.lng },
content: pin,
title: loc.title,
})
marker.addListener('click', () => {
infoWindow.setContent(`
<div class="map-popup">
<h3>${loc.title}</h3>
<p>${loc.description}</p>
<a href="/locations/${loc.title}" target="_blank">More</a>
</div>
`)
infoWindow.open(map, marker)
})
return marker
})
// Cluster nearby markers
new MarkerClusterer({
map,
markers,
algorithm: new SuperClusterAlgorithm({ radius: 60 }),
renderer: {
render: ({ count, position }) => new google.maps.Marker({
label: { text: String(count), color: '#fff', fontSize: '12px' },
position,
icon: { url: '/icons/cluster.svg', scaledSize: new google.maps.Size(40, 40) },
zIndex: 1000 + count,
}),
},
})
}
Yandex.Maps 3.0 (Modern API)
npm install @yandex/ymaps3-types
// Initialization via module import (Yandex.Maps 3.0)
async function initYandexMap(containerId: string) {
await ymaps3.ready
const { YMap, YMapDefaultSchemeLayer, YMapDefaultFeaturesLayer } = ymaps3
const map = new YMap(
document.getElementById(containerId)!,
{
location: {
center: [37.6173, 55.7558], // In Yandex order: [lng, lat]
zoom: 14,
},
behaviors: ['drag', 'scrollZoom', 'pinchZoom'],
}
)
map.addChild(new YMapDefaultSchemeLayer({ theme: 'light' }))
map.addChild(new YMapDefaultFeaturesLayer())
return map
}
// Markers in Yandex.Maps 3.0
async function addYandexMarkers(map: any, locations: Location[]) {
const { YMapMarker, YMapClusterer, clusterByGrid } = await ymaps3.import('@yandex/[email protected]')
const getMarkerElement = (loc: Location) => {
const el = document.createElement('div')
el.className = `ymap-pin ymap-pin--${loc.category}`
el.textContent = loc.title
return el
}
const markers = locations.map(loc => ({
id: loc.title,
geometry: { type: 'Point', coordinates: [loc.lng, loc.lat] },
properties: loc,
}))
const clusterer = new YMapClusterer({
method: clusterByGrid({ gridSize: 64 }),
features: markers,
marker: (feature) => new YMapMarker(
{ coordinates: feature.geometry.coordinates },
getMarkerElement(feature.properties)
),
cluster: (coordinates, features) => {
const el = document.createElement('div')
el.className = 'ymap-cluster'
el.textContent = String(features.length)
return new YMapMarker({ coordinates }, el)
},
})
map.addChild(clusterer)
}
Performance: Lazy Initialization
Loading Google Maps SDK adds ~240KB JS. Load only on interaction:
import { useState, useRef } from 'react'
export function LazyMap({ center, zoom = 14 }: { center: { lat: number; lng: number }; zoom?: number }) {
const [loaded, setLoaded] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
function loadMap() {
if (loaded) return
setLoaded(true)
}
return (
<div className="map-wrapper" style={{ position: 'relative', height: 400 }}>
{!loaded && (
<div
className="map-placeholder"
onClick={loadMap}
style={{
position: 'absolute', inset: 0,
background: `url(https://maps.googleapis.com/maps/api/staticmap?center=${center.lat},${center.lng}&zoom=${zoom}&size=800x400&key=${API_KEY}) center/cover`,
cursor: 'pointer',
}}
>
<button className="map-load-btn" aria-label="Load interactive map">
Click to load map
</button>
</div>
)}
{loaded && (
<div ref={containerRef} style={{ height: '100%' }} id="map" />
)}
</div>
)
}
Static Maps API — cheaper and faster for static preview.
Geocoding: Address → Coordinates
// Google Geocoding API
async function geocode(address: string): Promise<{ lat: number; lng: number }> {
const { Geocoder } = await google.maps.importLibrary('geocoding') as google.maps.GeocodingLibrary
const geocoder = new Geocoder()
const result = await geocoder.geocode({ address, region: 'us', language: 'en' })
if (!result.results.length) throw new Error(`Address not found: ${address}`)
const { lat, lng } = result.results[0].geometry.location
return { lat: lat(), lng: lng() }
}
// Server-side geocoding via Laravel (don't waste client requests)
// composer require spatie/geocoder
class GeoController extends Controller
{
public function geocode(Request $request): JsonResponse
{
$address = $request->validate(['address' => 'required|string'])['address'];
$result = Geocoder::getCoordinatesForAddress($address);
return response()->json([
'lat' => $result['lat'],
'lng' => $result['lng'],
'accuracy' => $result['accuracy'],
]);
}
}
Routing and Routes
// Display route between two points
async function showRoute(map: google.maps.Map, origin: string, destination: string) {
const { DirectionsService, DirectionsRenderer, TravelMode } = await google.maps.importLibrary('routes') as any
const service = new DirectionsService()
const renderer = new DirectionsRenderer({
map,
suppressMarkers: false,
polylineOptions: { strokeColor: '#6366f1', strokeWeight: 4 },
})
const result = await service.route({
origin,
destination,
travelMode: TravelMode.DRIVING,
region: 'us',
language: 'en',
provideRouteAlternatives: true,
})
renderer.setDirections(result)
// Route information
const route = result.routes[0].legs[0]
return {
distance: route.distance?.text,
duration: route.duration?.text,
}
}
CSP for Google Maps
// Laravel: allow Google Maps in Content Security Policy
$csp = [
"script-src 'self' https://maps.googleapis.com https://maps.gstatic.com",
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
"img-src 'self' data: https://*.googleapis.com https://*.gstatic.com",
"connect-src 'self' https://maps.googleapis.com",
"frame-src https://www.google.com",
];
Timeframe
Static map with marker and popup — 1 day. With clustering, lazy loading, geocoding, and custom styles — 2–3 days. With routing, address search, multiple marker types, and mobile optimization — 1 week.







