Next.js Quickstart
Next.js 14+ App Router. MapLibre is browser-only — must be a client component, ideally lazy-loaded.
Install
bash
npm install maplibre-gl react-map-glClient component
tsx
// app/components/MapView.tsx
'use client';
import { Map, NavigationControl } from 'react-map-gl/maplibre';
import 'maplibre-gl/dist/maplibre-gl.css';
export default function MapView() {
return (
<Map
initialViewState={{ longitude: 28.04, latitude: -26.20, zoom: 11 }}
style={{ width: '100%', height: '100vh' }}
mapStyle={`https://api.mapsfordevs.com/styles/standard.json?key=${process.env.NEXT_PUBLIC_MFD_PUB_KEY}`}
>
<NavigationControl position="top-right" />
</Map>
);
}Lazy load to skip SSR
tsx
// app/page.tsx
import dynamic from 'next/dynamic';
const MapView = dynamic(() => import('./components/MapView'), {
ssr: false,
loading: () => <div className="h-screen flex items-center justify-center">Loading map…</div>
});
export default function Home() {
return <MapView />;
}Server-side proxy (App Router route handler)
Hide your mfd_srv_* key, optionally cache tiles at the edge.
ts
// app/api/tiles/[z]/[x]/[y]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createHmac } from 'node:crypto';
export const runtime = 'nodejs';
export async function GET(
_req: NextRequest,
{ params }: { params: { z: string; x: string; y: string } }
) {
const path = `/tiles/${params.z}/${params.x}/${params.y}.pbf`;
const ts = String(Math.floor(Date.now() / 1000));
const sig = createHmac('sha256', process.env.MFD_SRV_SECRET!)
.update(`GET\n${path}\n${ts}`).digest('hex');
const upstream = await fetch(`https://tiles.mapsfordevs.com${path}`, {
headers: {
Authorization: `Bearer ${process.env.MFD_SRV_KEY!}`,
'X-MFD-Timestamp': ts,
'X-MFD-Signature': sig
},
cache: 'force-cache',
next: { revalidate: 86400 }
});
if (!upstream.ok) {
return NextResponse.json({ ok: false, error: 'upstream' }, { status: upstream.status });
}
return new NextResponse(upstream.body, {
status: 200,
headers: {
'Content-Type': 'application/x-protobuf',
'Cache-Control': 'public, max-age=86400, immutable'
}
});
}Then point MapLibre at your own origin:
tsx
mapStyle={{
version: 8,
sources: {
mfd: {
type: 'vector',
tiles: ['/api/tiles/{z}/{x}/{y}'],
minzoom: 0, maxzoom: 17,
attribution: '© OpenStreetMap · © MapsForDevs'
}
},
layers: [/* … */]
}}Environment
.env.local:
bash
NEXT_PUBLIC_MFD_PUB_KEY=mfd_pub_xxx # safe in browser
MFD_SRV_KEY=mfd_srv_xxx # server only — NO NEXT_PUBLIC_ prefix
MFD_SRV_SECRET=hex_secret_here # server onlyNEXT_PUBLIC_* prefix exposes to client. Anything without it is server-only.
Edge runtime caveat
runtime = 'edge' does not have node:crypto. Use globalThis.crypto.subtle.sign for HMAC at edge:
ts
async function signEdge(secret: string, msg: string) {
const key = await crypto.subtle.importKey(
'raw', new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
);
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(msg));
return Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
}