Carto Voyager was returning 'API Key required' content for the
user (likely rate-limited or IP-blocked by Carto). Switched to
ESRI World_Street_Map which has CORS-friendly tiles with plenty
of place names and street labels.
URL format is different from Carto: {z}/{y}/{x}.jpg (y before x,
jpg not png). Updated the build to match. The local /esri/
proxy in default.conf handles the upstream CORS-incompatibility
by adding permissive CORS headers and a proper User-Agent.
Direct fetching from a.basemaps.cartocdn.com was returning
stripped/missing CORS headers in the user's browser, blocking
the img.src load. Even with cache-busters the failure persisted.
Solution: local nginx proxy at /carto/ that forwards to the
upstream and always adds Access-Control-Allow-Origin: *.
Key detail: the 'resolver' directive is required inside the
proxy_pass location block so nginx can resolve the upstream
hostname. Without it, the location block silently fails (the
'/' location falls through to try_files ... =404, and the
~*\.(png|jpg)$ regex also matches and silently fails for GETs).
Also added 'location ^~ /carto/' to give prefix matching strict
priority over the .png regex match that was stealing the request.
JS now uses /carto/voyager/ (relative URL through the same
origin) so CORS preflight is not even needed.