Kamero

Build a Location-Aware App with Next.js and a Free Geolocation API

Location-aware features can transform a generic web app into something that feels personal. In this tutorial, we'll build a Next.js application that detects the visitor's location, displays it on an interactive map, and uses the data to personalize the experience — all using a free geolocation API.

What We're Building

By the end of this tutorial, you'll have an app that:

Step 1: Create the Next.js Project

npx create-next-app@latest geo-app --typescript --app
cd geo-app

Step 2: Build the Geolocation API Route

If you're deploying to Vercel, you can read geolocation directly from request headers. Create an API route:

// app/api/geo/route.ts
import { geolocation, ipAddress } from "@vercel/functions";
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const geo = geolocation(request);
  const ip = ipAddress(request);

  const continent = request.headers.get("x-vercel-ip-continent");
  const timezone = request.headers.get("x-vercel-ip-timezone");
  const postalCode = request.headers.get("x-vercel-ip-postal-code");

  return NextResponse.json(
    {
      ip,
      city: geo.city,
      country: geo.country,
      countryRegion: geo.countryRegion,
      continent,
      latitude: geo.latitude,
      longitude: geo.longitude,
      timezone,
      postalCode,
      region: geo.region,
    },
    {
      headers: {
        "Access-Control-Allow-Origin": "*",
      },
    }
  );
}

Alternatively, you can call the hosted Kamero API at https://geo.kamero.ai/api/geo from any hosting provider.

Step 3: Create the Location Display Component

// app/components/LocationCard.tsx
"use client";

import { useEffect, useState } from "react";

interface GeoData {
  ip: string;
  city: string;
  country: string;
  latitude: string;
  longitude: string;
  timezone: string;
}

export default function LocationCard() {
  const [geo, setGeo] = useState<GeoData | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("/api/geo")
      .then((res) => res.json())
      .then(setGeo)
      .finally(() => setLoading(false));
  }, []);

  if (loading) return <div>Detecting your location...</div>;
  if (!geo) return <div>Could not detect location</div>;

  return (
    <div>
      <h2>Your Location</h2>
      <p><strong>IP:</strong> {geo.ip}</p>
      <p><strong>City:</strong> {geo.city}</p>
      <p><strong>Country:</strong> {geo.country}</p>
      <p><strong>Timezone:</strong> {geo.timezone}</p>
      <p><strong>Coordinates:</strong> {geo.latitude}, {geo.longitude}</p>
    </div>
  );
}

Step 4: Add an Interactive Map

Install Leaflet for the map:

npm install leaflet react-leaflet
npm install -D @types/leaflet

Create a map component (must be client-side only):

// app/components/Map.tsx
"use client";

import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import L from "leaflet";
import "leaflet/dist/leaflet.css";

const icon = L.icon({
  iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png",
  shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png",
  iconSize: [25, 41],
  iconAnchor: [12, 41],
});

export default function Map({ lat, lng, city }: {
  lat: number;
  lng: number;
  city: string;
}) {
  return (
    <MapContainer
      center={[lat, lng]}
      zoom={11}
      style={{ height: "400px", width: "100%", borderRadius: "1rem" }}
    >
      <TileLayer
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        attribution='&copy; OpenStreetMap contributors'
      />
      <Marker position={[lat, lng]} icon={icon}>
        <Popup>{city}</Popup>
      </Marker>
    </MapContainer>
  );
}

Since Leaflet requires the window object, use Next.js dynamic imports to disable SSR:

import dynamic from "next/dynamic";

const Map = dynamic(() => import("./components/Map"), {
  ssr: false,
  loading: () => <div>Loading map...</div>,
});

Step 5: Server-Side Personalization

For SEO-friendly personalization, you can read location headers in a Server Component (Vercel only):

// app/page.tsx (Server Component)
import { headers } from "next/headers";

export default async function Home() {
  const headerList = await headers();
  const city = headerList.get("x-vercel-ip-city") || "there";
  const country = headerList.get("x-vercel-ip-country") || "";

  return (
    <main>
      <h1>Hello from {decodeURIComponent(city)}!</h1>
      {country && <p>We see you\'re visiting from {country}</p>}
      {/* Client components for interactive features */}
    </main>
  );
}

Step 6: Deploy to Vercel

Push your code to GitHub and import it in Vercel, or use the CLI:

npm i -g vercel
vercel deploy --prod

Once deployed, the geolocation headers are automatically populated by Vercel's Edge Network. Your app will detect visitor locations globally with sub-50ms latency.

Alternative: Use the Hosted API

If you're not deploying to Vercel, you can use the hosted Kamero Geo API from any platform:

// Works from any hosting provider
const geo = await fetch("https://geo.kamero.ai/api/geo")
  .then(r => r.json());

Or deploy your own instance of the Kamero Geo API to Vercel and point your app at your custom domain.

What's Next?

From here, you could extend the app with:

Deploy Your Own Geo API

One-click deploy to Vercel. Open source, MIT licensed.

Deploy to Vercel →