Twenty years of travel writing was sitting inside one flat index post. Roughly 135 named places, about 60 with real published guides, the rest listed but never written. It read like a packing list. The travel was a story, and a bulleted page could not tell it.
What It Is
The landing page is a 3D globe you can spin. On load, flight paths draw outward from home in the order the trips actually happened, markers popping in as each arc lands. Scroll, and the globe flies continent to continent, the guides for each place sliding in beside it. Click a marker and it opens the post written about that spot, the real one, on Substack.
It ends where a directory would have started: a browsable index, an atlas, and a plain street map, for when you already know where you want to go.
The Data Problem
Everything on the globe came from three things I already owned, and nothing was scraped. The index post gave the taglines in my own words. A Substack export gave the exact links, titles, and dates. An old Google My Maps gave the coordinates. Reconciling the three surfaced the good stuff a single source hides: the publish date is almost never the travel date. A post that went out in 2024 covers a 2013 trip. So the globe animates in the order I traveled, not the order I published, which is the difference between a story and a changelog.
One honest find fell out of the build. Five of the links point at posts that do not exist yet, and the old blog’s certificate is broken, so its links warn in the browser. Both are written down rather than papered over.
The Golden Source
That old Google My Maps was a one-time coordinate dump at first. Now it is the living record of where I have been, and it is the one thing I keep updated. After a trip I drop a pin there and nowhere else, and a sync reconciles the map into the dataset for me. New pins arrive as plain markers. Everything else it notices, a moved pin, an unmatched place, goes into a report. Nothing I wrote by hand is touched.
It stays honest about what it cannot know. The country on a new pin is filled in from its coordinates and flagged for me to confirm. A pin it cannot place, it reports instead of guessing. A place already in the dataset that it cannot find on the map is a line in that report, never a deletion. Geography is pulled in. The words stay mine to write later.
Where Should I Go?
The archive answers where I have been. It never answered the question people actually ask me, so there is now a plans section that does.
Two plans are in it so far, both built in Wanderlog. Ten days in New York, which I took, laid out as an open trip anyone can copy. Three days out to the Great Sand Dunes, which I have not done yet. Whether I went is a field on every plan rather than something you infer, because the map has to know: a trip I took is already drawn on the been-there layer, and drawing it again as “want to go” would say the opposite of the truth.
The chips filter on that, plus continent, length, season and vibe. Five axes over two plans is more picker than the content needs today. It stays because the content is the part that grows.
The full agenda opens in Wanderlog. On a plan’s own page their map loads only when you press the button, so nothing of theirs runs on my page unless you ask for it.
The atlas puts both halves on one map, either layer switchable off, and the layer state lives in the URL, so a shortlist is a link I can send someone.
Building it caught a label I had got wrong. I had badged thirty-nine places “someday”, which reads like a wish. They are places I went and have not written up, and their own taglines say so in the past tense. They read “untold” now. Without that fix, “someday” and “want to go” would have sat on the same map meaning two different things.
The grey I drew them in scored 1.90 against the water on that map, which is why I could never find them. They are ink blue now, and the number is 4.81.
The Discipline
The site links out, it never mirrors. Post bodies are never copied. The preview cards use each post’s own share image and its one-line subtitle, self-hosted so the page depends on nothing at runtime except free OpenStreetMap tiles. No Google, no API keys, no backend, no accounts.
Reduced motion is a real path, not an afterthought. Turn it on and the globe still renders, the arcs are simply all there at once, and the index is always the plain, complete fallback.
Status
Live at travel.listsof30.com. New travel guides publish on the fifteenth of the month, and adding one is a single paste: the entry is derived from the URL, validated against the schema, its thumbnail fetched, and shown as a diff before it ever ships.
Adding a plan takes one Wanderlog share link and the fields I have to write myself: the sentence, the length, the season, whether I went. The map embed is worked out from that same link, because the id in the share URL and the id in the embed URL turn out to be the same id.
The refresh that fetches a cover has to check more than the response code. A wrong Wanderlog trip id returns a perfectly healthy 200 with their generic marketing page, so a link with a typo in it would have cached as a success. It looks for the description tag instead, and skips anything that does not have one.
Stack
Vite, React, and TypeScript with Tailwind. The globe is MapLibre GL on free vector tiles; the flat map is Leaflet. Animation is anime.js for the arc timing and Motion for the scroll. The dataset is one YAML file, checked by Zod at build time, so a malformed entry fails the build instead of reaching the map.




