A personal world travel map. Mark the countries you've been to, share your map with a link, and compare it side-by-side with a friend's. No login, no backend — your data stays in your browser.
- Click countries to mark them visited / planning / want-to-visit
- Per-country notes and visit dates in a side panel
- Shareable links with server-rendered OG image previews for iMessage, Twitter, Discord, etc.
- Side-by-side comparison with a friend's map (overlap, gaps, "add their places to my list")
- Country search with
Cmd/Ctrl + K, focuses + zooms the map to the result - Light and dark mode, fully responsive
- Local-first: everything is stored in
localStorage; no account, no tracking beyond anonymous usage analytics
npm install
npm run dev
# open http://localhost:3000| Command | Description |
|---|---|
npm run dev |
Dev server with Turbopack |
npm run build |
Production build |
npm run start |
Serve the production build |
npm run lint |
ESLint (flat config) |
npm run format |
Format every file with Prettier |
npm run format:check |
Check formatting without writing (CI mode) |
npm run build:cities |
Rebuild city catalog from Natural Earth sources |
- Next.js 15 (App Router) deployed on Vercel
- React 19 + TypeScript
- D3 (
d3-geo,d3-zoom) for the interactive map - TopoJSON world data, bundled locally for SSR-safe rendering
- Tailwind CSS 4 + shadcn/ui primitives
- Lucide icons, Sonner toasts, react-day-picker for the visit-date picker
When you share, Stamped stores a short snapshot of your map in Upstash Redis (via the Vercel Marketplace) and gives you a short link:
/m/k7x9m2
What's stored: your map name, country statuses, and city statuses (notes and visit dates are never included).
Retention: links expire after 90 days of inactivity; updating your map from the same browser refreshes the expiry and updates the same link.
No account: your browser keeps an anonymous edit token in localStorage so edits don't create a new link. No email, login, or personal profile is required.
The shared page:
- Renders a read-only map from the stored snapshot
- Generates a dynamic OG image via
opengraph-image.tsxfor iMessage, Twitter, Discord, etc. - Offers Compare with my map —
/compare/[id]overlays the visitor'slocalStoragedata with the shared map
Local setup: install the Upstash integration in Vercel, then vercel env pull .env.development.local (see .env.example).
app/
├── components/ UI primitives + composed components (MapView, NoteSidebar, CountrySearch, ...)
├── compare/[them]/ Compare-with-a-friend route
├── m/[data]/ Read-only shared map viewer + dynamic OG image
├── hooks/ Custom hooks (useMapData)
├── utils/ Pure utilities (geo, share payload, stats, storage)
├── lib/ Server helpers (Redis share store)
├── api/share/ Create and update share links
├── constants/ Status palette, continents, dimensions
└── contexts/ Theme provider
components/ui/ shadcn-generated primitives
public/
├── world-atlas/ Bundled TopoJSON country boundaries (countries-110m.json)
└── cities/ Generated city catalog (populated-places.json)
scripts/
├── build-city-catalog.mjs Builds public/cities/populated-places.json
└── sources/ Natural Earth GeoJSON inputs (not required at runtime)
- Country boundaries and names from Natural Earth (Admin 0 – Countries), packaged as TopoJSON via world-atlas (ISC License © 2013-2019 Michael Bostock)
- City names and locations from Natural Earth Populated Places (10m cultural vectors), filtered to capitals and major cities and bundled locally
- Icons from Lucide, ISC License
Map boundaries, country labels, and city locations reflect Natural Earth’s cartographic choices, not a political position by Stamped.
Map geometry and city search both come from bundled static files, not a live API. When Natural Earth or world-atlas releases updates (or you want to change which cities are included), refresh the data locally and commit the regenerated assets.
Source: world-atlas countries-110m (TopoJSON, ~177 countries at 110m resolution).
File: public/world-atlas/countries-110m.json
Steps:
- Download or build a new
countries-110m.jsonfrom world-atlas (or convert from a newer Natural Earth Admin 0 shapefile using topojson). - Replace
public/world-atlas/countries-110m.json. - If new ISO numeric codes appear, add them to the continent buckets in
app/constants/continents.tsso stats (“continents visited”) stay correct. - Run
npm run buildand smoke-test: click countries, search (Cmd/Ctrl+K), share links, compare view, OG image.
Note: The 110m map omits many small states and territories (e.g. Singapore, Monaco, Hong Kong as separate polygons). City pins for those places still work; only country-level clicking is limited.
Sources (place in scripts/sources/):
ne_10m_populated_places.geojson— Natural Earth Populated Places (10m)ne_10m_admin_0_countries.geojson— Natural Earth Admin 0 – Countries (10m)
Output: public/cities/populated-places.json (generated; do not edit by hand)
Steps:
-
Download fresh GeoJSON from Natural Earth (or the natural-earth-vector repo) into
scripts/sources/with the filenames above. -
Run:
npm run build:cities
This rebuilds the catalog with:
- Zero-padded ISO numeric
countryCodevalues (aligned with the map) countryNameon each city and a top-levelcountryNameslookup (for places missing from the 110m map)
- Zero-padded ISO numeric
-
Adjust filters in
scripts/build-city-catalog.mjsif needed (default: Admin-0 capitals + primary citiesSCALERANK <= 5+ secondarySCALERANK = 6). -
Commit both any source updates under
scripts/sources/(if you version them) and the regeneratedpublic/cities/populated-places.json. -
Run
npm run buildand test: city search, stamp/unstamp, country sidebar city picker, share/compare with cities, zoom-to-pin.
| Change | What to do |
|---|---|
| New cities / country name fixes only | Regenerate catalog (build:cities); existing share links and localStorage maps keep working. |
| Country codes change (rare) | Users’ saved maps may point at old codes; consider a one-time migration in app/utils/storage.ts or bump share format (below). |
| Breaking share payload | Increment SHARE_FORMAT_VERSION in app/utils/share.ts and add a decode path for older versions if you still want old links to work. Old links without a decoder will show “unsupported version”. |
User maps and notes live in the browser (localStorage); refreshing Natural Earth data does not migrate or delete user data automatically.
-
npm run lint -
npm run build - Spot-check a large country, a microstate city (e.g. Singapore), and a shared link
- Confirm search shows country names, not numeric codes