diff --git a/src/app/api/basemap-style/[id]/route.ts b/src/app/api/basemap-style/[id]/route.ts index 7bee38b..eaab311 100644 --- a/src/app/api/basemap-style/[id]/route.ts +++ b/src/app/api/basemap-style/[id]/route.ts @@ -24,7 +24,7 @@ export const dynamic = "force-dynamic"; const ALLOWED_STYLES = new Set(["liberty", "dark", "positron", "bright"]); const OFM_BASE = "https://tiles.openfreemap.org"; -const TILE_PROXY_PREFIX = "/api/basemap-tile"; +const TILE_PROXY_PATH = "/api/basemap-tile"; type StyleSource = { type: string; @@ -62,20 +62,31 @@ async function fetchJson(url: string): Promise { /** Replace `https://tiles.openfreemap.org/` with the architots * proxy prefix so the browser never talks to openfreemap directly. - * Leaves `{z}/{x}/{y}` template tokens intact. */ -function rewriteUpstreamUrl(value: string): string { + * Emits ABSOLUTE URLs — MapLibre loads tiles in a Web Worker context + * where relative URLs fail to parse ("Failed to construct 'Request': + * Failed to parse URL"). Leaves `{z}/{x}/{y}` template tokens intact. */ +function rewriteUpstreamUrl(value: string, origin: string): string { if (typeof value !== "string") return value; if (value.startsWith(OFM_BASE)) { - return `${TILE_PROXY_PREFIX}${value.slice(OFM_BASE.length)}`; + return `${origin}${TILE_PROXY_PATH}${value.slice(OFM_BASE.length)}`; } return value; } export async function GET( - _request: Request, + request: Request, { params }: { params: Promise<{ id: string }> }, ) { const { id } = await params; + // Build the absolute origin used in rewritten URLs. Honor proxy + // headers (Traefik forwards x-forwarded-{proto,host}) so URLs match + // the user's external view of the site even when the Next process + // sees the request on its internal hostname:port. + const proto = + request.headers.get("x-forwarded-proto") ?? new URL(request.url).protocol.replace(":", ""); + const host = + request.headers.get("x-forwarded-host") ?? request.headers.get("host") ?? new URL(request.url).host; + const origin = `${proto}://${host}`; if (!ALLOWED_STYLES.has(id)) { return NextResponse.json({ error: "unknown_style" }, { status: 404 }); } @@ -119,7 +130,7 @@ export async function GET( // that point. for (const source of Object.values(resolved)) { if (Array.isArray(source.tiles)) { - source.tiles = source.tiles.map(rewriteUpstreamUrl); + source.tiles = source.tiles.map((u) => rewriteUpstreamUrl(u, origin)); } } @@ -128,18 +139,21 @@ export async function GET( // multi-image-set styles). Handle both. let sprite: unknown = style.sprite; if (typeof sprite === "string") { - sprite = rewriteUpstreamUrl(sprite); + sprite = rewriteUpstreamUrl(sprite, origin); } else if (Array.isArray(sprite)) { sprite = sprite.map((s) => s && typeof s === "object" && "url" in s - ? { ...s, url: rewriteUpstreamUrl(String((s as { url: unknown }).url)) } + ? { + ...s, + url: rewriteUpstreamUrl(String((s as { url: unknown }).url), origin), + } : s, ); } const glyphs = typeof style.glyphs === "string" - ? rewriteUpstreamUrl(style.glyphs) + ? rewriteUpstreamUrl(style.glyphs, origin) : style.glyphs; return NextResponse.json(