America/Toronto
ProjectsMay 24, 2024

Google Maps Clustering — Webflow CMS Integration

image
Category
Details
Project TypeInteractive map integration with CMS-driven markers and clustering
PlatformWebflow CMS
TeamNeal Miran (Developer)
Tech StackGoogle Maps JavaScript API, @googlemaps/markerclusterer, JavaScript, Webflow CMS
StatusDelivered — 2024
DeliverySide project — built to help a friend
A designer friend needed to add an interactive Google Map to a client website he was building in Webflow. The site already had location data stored in the Webflow CMS, and the goal was to render each CMS entry as a map pin, with an info window showing the listing's image, title, and description when clicked. The initial requirement grew from a basic multi-pin map into a more specific ask: implement marker clustering so that pins at the same zoom level would visually group together rather than stacking on top of each other or cluttering the map at a wider zoom. The implementation was embedded directly into a Webflow page as a custom code block, pulling location data from the CMS using DOM elements that Webflow renders from its collection list. The Google Maps JavaScript API drove the map, and the @googlemaps/markerclusterer library handled clustering. Additional work covered customising the cluster icon to a branded SVG and exploring different clustering algorithms to tune how aggressively nearby pins were grouped.
  • CMS-Driven Marker Data: Location data was sourced directly from the Webflow CMS collection list rendered on the page. The map script read latitude, longitude, title, image, and description from hidden DOM elements that Webflow populated from its CMS, avoiding any direct API calls and keeping the data flow within Webflow's standard publishing model.
  • Info Window on Click: Each marker opened a custom-styled info window on click, displaying the listing image, title, and description inside a compact card. The info window was shared across all markers and reused rather than instantiated per marker, keeping memory usage low.
  • Marker Clustering: The @googlemaps/markerclusterer library grouped nearby markers into a single cluster element at lower zoom levels. Without clustering, a dense map with many pins is difficult to read and interact with — clustering resolved this by replacing overlapping markers with a count badge that expands as the user zooms in.
  • Custom SVG Cluster Renderer: The default cluster appearance was replaced with a custom renderer that generated an inline SVG directly in JavaScript. The SVG used two concentric circles — a semi-transparent outer ring and a solid inner disc in the client's brand red — with the cluster count rendered in matching red text over a white inner circle. The SVG was base64-encoded and passed as a data URI to a google.maps.Marker, giving full visual control without external image assets.
  • Custom Pin Icon: Individual markers used a custom location pin image hosted in Webflow's asset library rather than the default Google Maps teardrop, keeping the map visually consistent with the client's branding.
  • Clustering Algorithm Tuning: The @googlemaps/markerclusterer library supports pluggable clustering algorithms. After testing the default, GridAlgorithm was introduced with a configurable gridDistance parameter. Increasing the grid distance caused the algorithm to group pins that were further apart, resulting in fewer, larger clusters at the same zoom level. This was noticeable in practice — with the default algorithm the user had to zoom out four steps before pins clustered; with a tuned GridAlgorithm at gridDistance: 60, clustering kicked in a step earlier at equivalent zoom levels.
  • Side Panel Toggle: A button on the page toggled the visibility of a listing panel alongside the map, allowing users to switch between the map view and a list view of the same CMS data. The toggle was wired through a DOMContentLoaded listener.
CMS list panel alongside the map — cluster visible at zoom level 12
CMS List View
Webflow Integration
Google Maps JavaScript APIGoogle Maps JavaScript API: Rendered the map and provided the Marker, InfoWindow, and Map classes used throughout. The API was loaded asynchronously with a callback to the initialisation function.
@googlemaps/markerclusterer@googlemaps/markerclusterer: The official Google Maps marker clustering library, loaded from the unpkg CDN. Provided the MarkerClusterer class and pluggable algorithm support, including GridAlgorithm.
JavaScript (Vanilla)JavaScript (Vanilla): All map logic was written in vanilla JavaScript embedded as a custom code block in Webflow. No additional frameworks or build tools were used.
Webflow CMSWebflow CMS: The client's chosen web builder and CMS. Location data was authored in Webflow's collection editor and rendered onto the page as HTML elements by the Webflow publishing pipeline. The map script consumed these elements directly from the DOM.
The following code is embedded directly into the Webflow page as a custom code block. Replace YOUR_API_KEY with your Google Maps API key and update the icon URL to point to your pin asset in Webflow's asset library. Step 1 — Add the CSS to the page's <head> custom code:
Css
<style>
.projname {
  font-family: Sanchezregular, sans-serif;
  font-size: 14px;
  line-height: 16px;
  color: black;
  font-weight: bold;
  margin-top: 2px;
  max-width: 170px;
}
.projdesc {
  font-family: Sanchezregular, sans-serif;
  font-size: 12px;
  line-height: 15px;
  color: #6d6e70;
  margin-top: 2px;
  max-width: 170px;
}
.projimg {
  margin-top: 10px;
  max-width: 180px !important;
  height: 120px;
}
</style>
Step 2 — Add the JavaScript and script tags to the page's </body> custom code:
Html
<script>
function initializeMap() {
  createMap({
    latitudeClass: 'latitude',
    longitudeClass: 'longitude',
    imageClass: 'listing-img',
    descriptionClass: 'listing-description',
    titleClass: 'listing-title'
  });
}

function createMap(config) {
  let titles       = document.getElementsByClassName(config.titleClass),
      latitudes    = document.getElementsByClassName(config.latitudeClass),
      longitudes   = document.getElementsByClassName(config.longitudeClass),
      images       = document.getElementsByClassName(config.imageClass),
      descriptions = document.getElementsByClassName(config.descriptionClass),
      markers      = [],
      locations    = [];

  for (let index = 0; index < titles.length; index++) {
    let imageUrl = images[index].src;
    if (latitudes[index].innerHTML !== "" && longitudes[index].innerHTML !== "") {
      locations.push({
        title: titles[index].innerHTML,
        lat:   Number(latitudes[index].innerHTML),
        lng:   Number(longitudes[index].innerHTML),
        content: '<div class="info-box">'
               +   '<img class="projimg" src="' + imageUrl + '"/>'
               +   '<p class="projname">' + titles[index].innerHTML + '</p>'
               +   '<p class="projdesc">' + descriptions[index].innerHTML + '</p>'
               + '</div>'
      });
    }
  }

  const map = new google.maps.Map(document.getElementById("map-wrapper"), {
    zoom: 5,
    center: { lat: 1.2789773525290893, lng: 103.83531939391086 },
    disableDefaultUI: false,
    mapId: '43d83e943216f158',
  });

  const infoWindow = new google.maps.InfoWindow();

  locations.forEach(function(location) {
    let marker = new google.maps.Marker({
      position: location,
      map: map,
      title: location.title,
      icon: 'https://YOUR_WEBFLOW_ASSET_URL/locationPin.png'
    });
    markers.push(marker);
    attachMarkerInfoWindow(marker, location.content);
  });

  function attachMarkerInfoWindow(marker, content) {
    marker.addListener("click", function() {
      infoWindow.setContent(content);
      infoWindow.open({ anchor: marker, map: map, shouldFocus: false });
    });
  }

  // GridAlgorithm groups pins that are further apart — increase gridDistance
  // to cluster more aggressively at the same zoom level.
  const algorithm = new markerClusterer.GridAlgorithm({
    gridDistance: 60,
  });

  new markerClusterer.MarkerClusterer({
    map: map,
    markers: markers,
    algorithm: algorithm,
    renderer: {
      render: function({ count, position }) {
        const svgMarkup = [
          '<svg width="50" height="50" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">',
            '<g style="opacity:.45;">',
              '<circle cx="12" cy="12" r="12" style="fill:#e61e28;stroke-width:0px;"/>',
            '</g>',
            '<circle cx="12" cy="12" r="10.5" style="fill:#e61e28;stroke-width:0px;"/>',
            '<circle cx="12" cy="12" r="8.5"  style="fill:#fff;stroke-width:0px;"/>',
            '<text x="12" y="16" fill="#E61E28" font-size="15" font-family="Arial" text-anchor="middle"></text>',
          '</svg>'
        ].join('');
        const svg = window.btoa(svgMarkup);
        return new google.maps.Marker({
          position: position,
          icon: {
            url: 'data:image/svg+xml;base64,' + svg,
            scaledSize: new google.maps.Size(50, 50)
          },
          label: {
            text:      String(count),
            color:     "#E61E28",
            fontSize:  "15px"
          }
        });
      }
    }
  });
}

document.addEventListener('DOMContentLoaded', function() {
  var toggleButton = document.getElementById('toggleButton');
  var listing      = document.getElementById('listing');
  toggleButton.addEventListener('click', function() {
    listing.style.display = (listing.style.display === 'none') ? 'block' : 'none';
  });
});
</script>

<!-- Marker clusterer must load before the Maps API callback fires -->
<script src="https://unpkg.com/@googlemaps/markerclusterer/dist/index.min.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initializeMap"></script>
Webflow does not expose its CMS data as a JSON endpoint accessible to client-side scripts — data is rendered as HTML by Webflow's server when the page is published. The approach of reading from hidden DOM elements (using getElementsByClassName to collect latitude, longitude, title, image, and description fields rendered by a Webflow collection list) is a well-known pattern in the Webflow community but requires careful coordination between the Webflow designer's collection list structure and the class names the JavaScript expects. Any mismatch — a renamed class, a missing field, a collection list not on the page — silently produces an empty map. The @googlemaps/markerclusterer library's default renderer can be replaced by providing a renderer object with a render method that receives the cluster's count and position and returns a google.maps.Marker. Generating a branded SVG inside JavaScript — building the SVG markup as a template string, base64-encoding it with window.btoa, and wrapping it in a data URI — was the most self-contained approach, avoiding external asset hosting while allowing full control over the visual. Getting the label text to overlay the SVG correctly required coordinating the SVG's text element (which controlled placement and fill) with the google.maps.Marker's label property (which controlled what the Google Maps layer drew on top), since the SVG text element in this implementation was intentionally left blank and the count was supplied through the marker label instead. Marker clustering behaviour is a function of two things: the algorithm and its parameters. The default algorithm did not produce the grouping density the client wanted — pins were only clustering at a zoom level where the map was already too far out to be useful for navigation. Switching to GridAlgorithm and exposing gridDistance as a tunable parameter made the clustering tighter at practical zoom levels. The tradeoff is that a higher gridDistance can group markers that are geographically further apart than the user might expect — worth testing against the actual data distribution of the client's locations. The existing map implementation on the site used an older version of the Google Maps JavaScript API — one that relied on the legacy google.maps.Marker constructor and the now-deprecated script loading pattern. Google has been progressively deprecating parts of the older Maps API in favour of the newer google.maps.marker.AdvancedMarkerElement class, and some of the library behaviours the original code depended on had shifted or been removed in the version the project was targeting. Adding marker clustering was not as straightforward as dropping in the @googlemaps/markerclusterer library on top of the existing code — the clustering library expected a consistent marker interface, and the API version mismatch surfaced as silent failures and missing features rather than clear error messages. Aligning the API version, the clustering library version, and the existing custom code required working through the changes incrementally to identify which parts of the original script broke under the newer API and which could carry over unchanged. Working on a Webflow project as an outside contributor without familiarity with the Webflow editor added friction. Webflow's interface is tailored to designers, and the relevant custom code blocks and collection list structure are not immediately obvious without experience in the tool. Testing the JavaScript changes required republishing the page each time, since Webflow does not offer a local preview for custom code in the standard workflow. A reference project that could be cloned and inspected — in this case a community-built CMS map template — was the fastest path to understanding the existing structure before making changes. The map was successfully updated to include marker clustering with a custom branded cluster renderer. Pins that were previously overlapping or visually dense at common zoom levels were grouped into labelled clusters, improving readability without any loss of data — zooming in expanded each cluster back to its individual markers. The custom SVG renderer gave the client visual control over the cluster appearance, tying it to the site's brand colour without requiring external image assets. The GridAlgorithm configuration gave a lever for adjusting clustering sensitivity to match the density of the client's actual location data. The project was a practical illustration of how the @googlemaps/markerclusterer library's pluggable renderer and algorithm interfaces work together — the renderer controls the visual, the algorithm controls which markers get grouped — and how both can be tuned independently.

Related projects