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 sources 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.
Sixteen plans are in it now, all built in Wanderlog, and fifteen of them carry a pin on the map. 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 was more picker than two plans needed. Sixteen have grown into it, which is the argument for building the filter before the catalog deserves it rather than after.
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 been-there and want-to-go on one map, any layer switchable off, and the layer state lives in the URL, so a shortlist is a link I can send someone. A third layer joined later, which the next section is about.
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.
What I Did When I Got There
The posts say where I went. They never said what I ate, and I have been answering that by text message for years.
I had already written it down, just not here. 556 reviews on Google Maps, the oldest from March 2016, most of them restaurants. They are now a third layer on the atlas and a section on every place page: 99 in Denver, 102 in New York. Zoom into a city and the violet dots are the actual doors I walked through.
The number I started with was wrong, and it was my own number. I had planned this around “four thousand reviews”, a figure I had repeated for years without checking. My profile says 683 reviews and 3,301 photos. Those add to 3,984, which is almost certainly the four thousand I had in my head: I had been counting the photos as reviews and quoting the total as if I had written all of it. The export holds 600 entries, 556 of them usable, which is where the real number lands. Everything got smaller and simpler once the file was in front of me, because 556 markers do not need the machinery four thousand would have.
The obvious way to connect a review to a post was to look at which posts already link that place on Google. I counted the links before building it. There are 568 of them across 40 posts, and 513 are shortened, either maps.app.goo.gl or goo.gl/maps, so the match only works if I ask Google to expand about ninety percent of them. Worse, finding those links means reading my own post bodies, and this site has one rule it does not bend, which is that it links to posts and never reads them. So a review attaches to a place by distance instead, within 50 km, and the place page’s existing list of posts does the rest. 465 of them land on one of my places. The 91 that do not still sit on the map, because a meal in a town I never wrote about is still a meal I had.
Three kinds of entry never made it out of the export. 16 had no location at all and would have drawn at coordinates zero, zero, which is open water off Ghana. 27 carry a zero star rating, which is Google’s way of recording a contribution that is not a review. One, a food tour in Tokyo, exports at a coordinate in the middle of the Pacific Ocean, because it is a business with no fixed address and Google had to put it somewhere. The count of what I threw away is stored in the file, so it cannot quietly drift.
Then I asked for the dots to be brighter. Yellow, I said. Yellow is not available on a map like this. Against the pale tiles it scores 1.25, where the grey I had already thrown out for being invisible scored 2.66. Gold reaches 1.89, which is still worse than the colour I had rejected. The violet that shipped scores 6.43, ahead of the red I use for written up places, and it is the only cold colour on the map, so a dense city separates into two things instead of one warm smear.
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 all there at once, and the index is always the plain, complete fallback.
Status
Live at travel.listsof30.com. Refreshing the reviews is one command against a new Takeout export, run by hand, a few times a year. The export never goes in the repo, the build never reaches the network, and a malformed data file fails the build rather than reaching the site.
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.





