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:
- Detects the visitor's city, country, and coordinates from their IP
- Shows their location on an interactive Leaflet map
- Displays a personalized welcome message
- Works on both server and client side
- Deploys to Vercel with one click
Step 1: Create the Next.js Project
npx create-next-app@latest geo-app --typescript --app
cd geo-appStep 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/leafletCreate 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='© 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 --prodOnce 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:
- Currency conversion based on the visitor's country
- Language detection and i18n routing
- Nearby location search using the coordinates
- Weather data based on the detected city
- Server-side analytics logging visitor geography