Interactive Maps — HTMQL Documentation

Interactive Maps

The bundled Leaflet library renders interactive maps with OpenStreetMap tiles, markers, popups, and shapes. The CSS and JS are served from /lib/leaflet.css and /lib/leaflet.js; the marker and layer images Leaflet references are served from /lib/images/, so keep that folder alongside the library files. Include the two files in your layout <head>:

<link rel="stylesheet" href="/lib/leaflet.css">
<script src="/lib/leaflet.js"></script>

A map needs a container element with an explicit height (Leaflet cannot measure an auto-sized element) and a few lines to create the map, add a tile layer, and place a marker. Driving it from Hyperscript keeps everything inline - no separate <script> block:

<div id="map" style="width:100%; height:360px;"
     _="on load
            set :map to L.map(me).setView([43.6532, -79.3832], 12)
            call L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 19, attribution: '&copy; <a href=https://www.openstreetmap.org/copyright target=_blank rel=noopener>OpenStreetMap</a> contributors'}).addTo(:map)
            call :map.attributionControl.setPrefix('<a href=https://leafletjs.com target=_blank rel=noopener>Leaflet</a>')
            call L.marker([43.6532, -79.3832]).addTo(:map)"
></div>

Attribution is required. The OSM tile usage policy mandates a visible "© OpenStreetMap contributors" credit on every map, so always pass the attribution option to L.tileLayer (as above) and never suppress it with {attributionControl: false}. Give the attribution <a> target=_blank rel=noopener, and override Leaflet's own link with :map.attributionControl.setPrefix('<a href=https://leafletjs.com target=_blank rel=noopener>Leaflet</a>'), so the credit links open in a new tab instead of navigating away from the app. That policy also prohibits bulk tile downloading; a commercial or high-traffic deployment should point the tile URL at a keyed provider (MapTiler, Carto, Stadia, Thunderforest) rather than tile.openstreetmap.org.

To plot rows from the database, select the coordinates and emit them into a JavaScript array with a {{range}} loop - the server renders the values before Hyperscript runs:

GET:places:SELECT Name, Latitude, Longitude FROM Place WHERE Latitude IS NOT NULL;
<div id="map" style="width:100%; height:360px;"
     _="on load
            set :map to L.map(me).setView([43.65, -79.38], 11)
            call L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {maxZoom: 19, attribution: '&copy; <a href=https://www.openstreetmap.org/copyright target=_blank rel=noopener>OpenStreetMap</a> contributors'}).addTo(:map)
            for place in [
                    {{range .places}}{ lat: {{.Latitude}}, lng: {{.Longitude}}, name: '{{.Name}}' },
                    {{end}}
                ]
                call L.marker([place.lat, place.lng]).addTo(:map)
                call (the result).bindPopup(place.name)
            end"
></div>

For an edit form that lets the user pick a location, bind the map to latitude/longitude inputs: trigger a custom event on the map element from each input's on change, and handle it on the map to pan the view and move the marker (see the complete example in the testing app's pages/map.htmql). The same approach with L.circle([lat, lng], {radius: metres}) draws an editable perimeter.

Leaflet draws maps but does not geocode. To convert a typed civic address into coordinates, call OpenStreetMap's free Nominatim service, which returns JSON over CORS so Hyperscript can fetch it directly from the browser:

<input id="addr" _="on keyup debounced at 500ms
        if my value's length < 3 then halt end
        fetch `https://nominatim.openstreetmap.org/search?format=jsonv2&limit=5&q=${encodeURIComponent(my value)}` as json
        -- 'it' is an array of { display_name, lat, lon }; show it as suggestions and, on select,
        -- call the map's setView([lat, lon], 16) and the marker's setLatLng([lat, lon])">

The public Nominatim server permits roughly one request per second and asks that applications identify themselves, so debounce keystrokes rather than geocoding on every key. Bias results with &countrycodes=ca or &viewbox=…, and for production traffic run your own Nominatim instance or use a commercial geocoder. The reverse direction (coordinates to a civic address) uses the /reverse?format=jsonv2&lat=…&lon=… endpoint. The testing app's pages/map.htmql includes a working address-lookup field with autocomplete.