Skip to content

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-gl

Client 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 only

NEXT_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('');
}