Building a Custom Map for Your Website with GeoJSON
Build a custom map website with GeoJSON and Leaflet.js. Step-by-step tutorial using Notion data. Start mapping in under 30 minutes.
Building a custom map website with GeoJSON requires three things: your location data in GeoJSON format, the Leaflet.js library, and about 20 lines of JavaScript. Export your Notion places, add the code below, and you'll have an interactive map running in minutes.
Adding an interactive map to your website transforms static location lists into engaging visual experiences. Whether you're showcasing travel destinations on a blog, displaying business locations for customers, or mapping research sites for a project, a custom map website with GeoJSON tells your story better than text alone. This tutorial walks through building one using GeoJSON data from Notion to Maps and the Leaflet mapping library.
Why Build a Custom Map Website with GeoJSON
Embedded maps from Google My Maps or similar services work for basic needs, but they come with limitations. You can't fully control the styling, you're stuck with their branding, and customization options are restricted. Building your own map gives you complete control over appearance, behavior, and the data you display.
A custom map also means your data stays yours. You're not locked into a platform that might change its terms or pricing. The GeoJSON format is an open standard, and Leaflet is open-source software. Your map will work as long as web browsers exist.
Getting Your GeoJSON from Notion to Maps
Start by exporting your Notion database through Notion to Maps. Connect your Notion account, select the database containing your Place properties, and download the GeoJSON export. You'll get a file structured like this:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [2.3522, 48.8566]
},
"properties": {
"name": "Paris Office",
"category": "Headquarters",
"employees": 150
}
}
]
}
Each location from your Notion database becomes a Feature with its coordinates and all the properties you've defined. Save this file somewhere accessible to your website, either as a static file or served through an API endpoint.
Setting Up Leaflet
Leaflet is a lightweight, mobile-friendly mapping library that's perfect for most custom map needs. Add it to your HTML page by including the CSS and JavaScript from a CDN:
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
Create a container element for your map with a defined height. Leaflet needs explicit dimensions to render properly:
<div id="map" style="height: 500px; width: 100%;"></div>
Initialize the map with a center point and zoom level. These values determine what users see when the page loads:
const map = L.map('map').setView([48.8566, 2.3522], 5);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
The tile layer provides the base map imagery. OpenStreetMap tiles are free and don't require an API key, making them ideal for getting started. You can switch to Mapbox, Stadia, or other providers later for different visual styles.
Adding Markers from GeoJSON
With the map initialized, loading your GeoJSON data takes just a few lines. Fetch the file and pass it to Leaflet's GeoJSON layer:
fetch('/data/locations.geojson')
.then(response => response.json())
.then(data => {
L.geoJSON(data).addTo(map);
});
Leaflet automatically creates markers for each point in your FeatureCollection. The map now displays all your Notion locations, but they're using default blue markers with no interactivity. Let's improve that.
Customizing Popups and Styling
Popups display information when users click a marker. Use the onEachFeature option to bind popups containing your Notion properties:
L.geoJSON(data, {
onEachFeature: function(feature, layer) {
const props = feature.properties;
layer.bindPopup(`
<strong>${props.name}</strong><br>
Category: ${props.category}<br>
Employees: ${props.employees}
`);
}
}).addTo(map);
For custom marker icons, use the pointToLayer option. This function runs for each point feature and returns the marker to display:
L.geoJSON(data, {
pointToLayer: function(feature, latlng) {
return L.circleMarker(latlng, {
radius: 8,
fillColor: '#3b82f6',
color: '#1e40af',
weight: 2,
fillOpacity: 0.8
});
},
onEachFeature: function(feature, layer) {
layer.bindPopup(`<strong>${feature.properties.name}</strong>`);
}
}).addTo(map);
You can make styling dynamic based on properties. For example, color-code markers by category or size them by a numeric value. The feature object is available in both functions, giving you access to all your Notion data.
Fitting the Map to Your Data
Rather than hardcoding a center point, you can automatically fit the map to show all your markers. After adding the GeoJSON layer, use its bounds:
const geojsonLayer = L.geoJSON(data).addTo(map);
map.fitBounds(geojsonLayer.getBounds(), { padding: [50, 50] });
This ensures users see all locations regardless of how spread out they are, with some padding around the edges for visual comfort.
Hosting Considerations
For static sites, simply include your GeoJSON file in your build and reference it with a relative path. For dynamic data that updates frequently, consider fetching directly from the Notion to Maps API endpoint instead of a static file. This keeps your map synchronized with your Notion database without manual exports.
If your GeoJSON file is large, implement lazy loading or clustering. Leaflet's MarkerCluster plugin groups nearby markers at low zoom levels, improving performance and readability for datasets with hundreds or thousands of points.
The combination of Notion as your data source, Notion to Maps for export, and Leaflet for rendering creates a powerful workflow. Update locations in Notion, export fresh GeoJSON, and your website map reflects the changes. No coding required for data updates, just the initial setup described here.
Need your data in other formats? Notion to Maps also exports to KML for Google Earth, GPX for GPS devices, and CSV for spreadsheet analysis.
Frequently Asked Questions
Can I use Google Maps instead of Leaflet for my custom map website?
Yes, Google Maps JavaScript API supports GeoJSON through map.data.loadGeoJSON(). However, Google Maps requires an API key and has usage-based pricing after the free tier. Leaflet with OpenStreetMap tiles is completely free with no API key required, making it better for most projects. If you need Google's specific features like Street View integration or their styling options, the Google Maps API works well with GeoJSON exported from Notion to Maps.
How do I add search functionality to my GeoJSON map?
For client-side search, filter your GeoJSON features array based on property values and update the map layer. Libraries like Leaflet-search add a search control that queries feature properties. For larger datasets, consider a server-side search that returns filtered GeoJSON. The simplest approach is a text input that filters features by name: features.filter(f => f.properties.name.toLowerCase().includes(query)).
What's the best way to handle hundreds of markers without slowing down the map?
Use the Leaflet.markercluster plugin, which groups nearby markers into clusters that expand on click or zoom. Install it via CDN or npm, then wrap your GeoJSON layer: L.markerClusterGroup().addLayer(L.geoJSON(data)). For thousands of points, consider switching to Mapbox GL JS which uses WebGL rendering for better performance, or implement viewport-based loading that only fetches visible markers.
Can I convert my GeoJSON map to work offline?
Yes, for offline maps you need to cache both the GeoJSON data and map tiles. Store GeoJSON in localStorage or IndexedDB. For tiles, use a service worker to cache OpenStreetMap tiles as users browse, or pre-download a tile package for a specific region using tools like Mobile Atlas Creator. The Leaflet.offline plugin simplifies tile caching. Note that offline tile storage can require significant space depending on zoom levels and area covered.
How do I add different marker icons based on category in my GeoJSON?
Use the pointToLayer option with conditional logic based on feature properties. Create an icon mapping object, then select the appropriate icon: const icons = { restaurant: restaurantIcon, hotel: hotelIcon }; return L.marker(latlng, { icon: icons[feature.properties.category] || defaultIcon }). You can use Leaflet's L.icon() for custom images or L.divIcon() for CSS-styled markers. This approach works seamlessly with Notion data where you've categorized places using a Select property.