Earlier today, every basemap tile on Datespots suddenly had API KEY REQUIRED stamped across it.

The map still worked. Pins loaded, Leaflet was fine, and nothing in my deployment had broken. CARTO had changed how unauthenticated access to its free basemaps behaves.

The CARTO watermark

I’d been using CARTO’s standard Leaflet tile URLs without a key, the same pattern still found in plenty of OSM and CARTO examples. Instead of blocking those requests, CARTO began serving the tiles with a watermark.

The fix was straightforward: I created a free CARTO basemap key, kept it in server-side configuration rather than the repo, and wired it into the basemap requests used by both Map and Plan. That removed the watermark, and the free tier is more than enough for my current traffic.

Since I was already touching the basemap stack, I also replaced something I’d wanted to get rid of for a while.

Raster out, vectors in

My old basemap used raster tiles, pre-rendered images, and dark mode was essentially the light map transformed with CSS. It worked, but it was a hack.

I moved to CARTO’s native vector styles instead: Voyager in light mode and Dark Matter in dark mode. MapLibre renders the vector streets inside Leaflet, while Leaflet still handles my pins, clustering, filters, Near Me and routes.

That change was more involved than attaching an API key. MapLibre added client weight and worker assets, required CSP changes, and needed to be loaded without slowing down the rest of the site.

Which led into the second half of the work: making the map faster.

Don’t load a map until you need one

The Datespots home page is a city picker. It has no reason to download Leaflet, MapLibre or map-specific styles.

I split the client so that code stays out of the initial home-page load. The mapping stack loads when someone opens a city, and the heavier vector library can be prefetched when someone hovers Open map.

City pages got the same treatment on the data side. Previously, the initial HTML contained almost everything about every place, including descriptions and directions. With hundreds of places, I was shipping a lot of text before the map could do anything useful with it.

Now the first response contains what I need to draw the map and filters. Longer place copy is fetched separately in the background and versioned with the published catalog for caching. Opening a place card can wait for that copy; drawing 600 pins doesn’t.

I also preload the scripts, styles and catalog data a city page is about to need from the edge.

Why panning felt wrong

After that, load time was better but panning still had two noticeable problems.

The first was my pins. The clustering plugin removes markers outside the viewport as an optimization, which meant pins disappeared while dragging and popped back in when the pan ended. I now keep them mounted during a pan, render them on canvas instead of as hundreds of SVG elements, and avoid unnecessary work after the map stops moving.

The second was the vector basemap. MapLibre only renders so far beyond the viewport, so a quick pan away and back could expose blank areas while tiles caught up. I added some extra viewport padding, kept nearby tiles cached and removed the fade on pan.

I initially pushed the padding too far. Blank areas disappeared, but panning became heavier because I was asking the renderer to prepare too much offscreen geography. Backing it down gave me the better tradeoff.

There was one other small race: the initial “fit all pins” operation could finish after someone had already started moving the map, snapping the viewport back. Any user interaction now cancels that deferred fit.

What I didn’t change

I still do full page loads between cities. Each city has its own catalog, and starting from a fresh server-rendered page is simpler than carrying map state between them.

Leaflet also stays. MapLibre only replaced the basemap renderer; there was no reason to rewrite pins, filters, geolocation and routing along with it.

And there’s still no service worker. I tried that earlier and rolled it back. Normal caching, preloads and lazy loading are enough for what I’m doing here.

Takeaway

What started as an API KEY REQUIRED watermark ended up exposing several unrelated parts of the map stack.

CARTO authentication fixed the watermark. Vector tiles gave me real light and dark basemaps. Splitting the mapping code kept it off pages that don’t need it, while separating map data from place copy reduced the initial city payload. The remaining panning issues came down to treating pins and streets as two different renderers and tuning each accordingly.

The other lesson is simpler: “no API key required” is a third-party policy, not an architectural guarantee. If an external service changes that policy, credentials should be easy to add and rotate without forcing changes through the rest of the application.

The map looks the same at a glance. Underneath, considerably less work happens before you need it, and considerably less gets in the way once you start moving around.