listsof30 production

Clem's Travels

Two decades of travel writing on a spinnable 3D globe. Plans for where to go next, and 556 Google reviews for what to do once you arrive.

ViteReactTypeScriptTailwind CSS v4MapLibre GLanime.jsMotionLeafletCloudflare Pages
Landing on the globe: 52 countries, 121 places, 75 stories, 556 reviews.
Landing on the globe: 52 countries, 121 places, 75 stories, 556 reviews.
Scroll and the guides slide in beside the globe, continent by continent.
Scroll and the guides slide in beside the globe, continent by continent.
The index replays every arc, then filters by continent, status or trip type.
The index replays every arc, then filters by continent, status or trip type.
Sixteen plans so far. Filter by whether I went, the continent, how long you have, the season, the kind of trip.
Sixteen plans so far. Filter by whether I went, the continent, how long you have, the season, the kind of trip.
The atlas. Red is written up, blue is been but untold, teal is still to come, violet is a review I left.
The atlas. Red is written up, blue is been but untold, teal is still to come, violet is a review I left.
Zoom in and the reviews become individual places. Every violet dot is a restaurant, a hotel or a trailhead I wrote about.
Zoom in and the reviews become individual places. Every violet dot is a restaurant, a hotel or a trailhead I wrote about.

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.

Development timeline

38 logged updates over 108 days, 20 Jul 2026 to 4 Nov 2026.

  1. A Stars link into the wanessalabs.com planetariumfeature

    That URL redirects into the 3D walk's planetarium, where every place on this site is a pin on a globe and the 15 places with the most posts hang on the wall as cards.

  2. Glenwood Springs long weekend postfeature

    Added "Glenwood Springs long weekend" (published 2026-10-15) as a second post on the existing glenwood-springs pin rather than a new pin: same place, coordinate already present. Label is the post title minus its [Travel Advice] prefix.

  3. Laya Command Center integrationfeature

    registered in Laya as a watched repo; no code change.

  4. Lighthouse Audit Remediation: Performance, Accessibility, SEO & Themingdocs

    Following the /audit playbook, audited travel.listsof30.com (clem-travels/) with headless Lighthouse on emulated mobile device.

  5. A Google reviews layer: 556 reviews on the atlas and on place pagesfeature

    The sixth phase for /atlas and the first data source in this project that is not Substack and not Wanderlog. The export itself never enters the repo.

  6. the review marker goes violet, and yellow is measured outfeature

    Clemens asked for a brighter marker, "like yellow", then offered purple or gold when shown the numbers. Both were built; violet shipped.

  7. Copenhagen, San Sebastián, and a plan edited rather than addedfeature

    /plans goes from 14 to 16, and one existing plan was narrowed rather than added, which is the first time that has happened.

  8. A permanent page per place, and its own share cardfeature

    The ask. Sharing a place with friends meant pasting a listsof30.com Substack link, which promotes the post and not the site, and it could only ever carry one post.

Show the earlier 30 entries
  1. Four more trip plans, and the first been-and-still-want-to-gofeature

    /plans went from 10 real plans to 14: Oaxaca and Mexico City, New Zealand, Hong Kong, Costa Rica. All four live, added under the standing Wanderlog authorisation from 2026-09-11.

  2. Eight trip plans, and the geocoder's failure modesfeature

    /plans went from 2 real plans to 10, all live, each shipped and verified individually rather than in a batch. PRs #69, #73, #74, #75, #76, #77, #79.

  3. pnpm plans:add, and what it deliberately does not dofeature

    Clemens asked whether a skill was needed to turn a Wanderlog link into a site update. The deploy half is already covered by /deploy.

  4. "Been there, not written up" stops being greyfeature

    Clemens could not pick those places out on /atlas. Measured rather than eyeballed, marker against the backgrounds actually painted:

  5. "Where should I go?", Wanderlog trip plans at /plansfeature

    The archive answers "where has he been". It never answered "where should I go", which is the question a reader actually arrives with.

  6. A real share link closes the last unknowns, and finds two trapsfeature

    Clemens supplied a real public share link. Every remaining [VERIFY] on the plans work is now closed by observation, and verifying them turned up two defects that guessing would have shipped.

  7. First real plan, an intent axis, and a distinct want-to-go colourfeature

    Clemens supplied the New York trip as real content: a trip he took, an "Open Trip" in Wanderlog, 10 days.

  8. Hardening pass, and a spec whose reason had expireddocs

    Same session, after the atlas. Three real defects and one non-change.

  9. Second real plan, and the first true want-to-go pinfeature

    Great Sand Dunes National Park and Preserve, a 3 day roadtrip, intent: wishlist. Supplied by Clemens with the length, the framing and the vibe.

  10. The atlas, and a mislabel it exposedfeature

    Follow-on to the plans surface, same session. Clemens accepted the map-only embed and asked for a want-to-go layer sitting on top of the been-there map, in a new tab.

  11. Site-anatomy exportfeature

    Read-only export from prd/lineage/roadmap plus the project page source. Redacted for public use: no commit hashes, no deployment IDs, no hosting-project names.

  12. Append "Epic 56 Hours in NY" to the nyc cardfeature

    First run of the Option B content-ingestion path decided 2026-07-21: Clemens pasted the post URL, the entry was derived, appended, validated, and shown as a diff. Post is listsof30.com/p/epic-56-hours-in-ny, published 2021-12-07, added as…

  13. Remove dead seelemons archive links, retire legacy-onlyfeature

    seelemons.com is no longer active, so every source: seelemons post rendered a dead … ↗ (archive) link. 15 destinations kept their live listsof30 post.

  14. Six new travel posts: previews + globe OG share cardfeature

    Ran pnpm fetch:previews --resume — resolved 5 new og:image thumbnails (sag-harbor, glenwood-springs, moab, mexico-city, monterey-bay); Phuket rides under the existing bangkok pin. 63/63 published destinations now have previews.

  15. Decision: pnpm sync:map, reconcile-only geography syncfeature

    pnpm sync:map closes that gap without touching curation.

  16. Phase 3 shipped; decision: self-hosted link-preview cardsfeature

    Phase 3 (hero SVG map engine) built and approved. Reduced motion renders the whole map complete and still.

  17. Phase 4 built: chapters, explore mode, preview cardsfeature

    Scenes 2-4 shipped. Continent chapters are a sticky world map with scrolling steps; entering a chapter animates the SVG viewBox to frame that continent (Motion), and off-continent arcs/markers dim.

  18. Pivot: the 3D globe is the main pagefeature

    After comparing three globe styles (MapLibre, Globe.gl photo, Globe.gl passport), Clemens chose MapLibre (V1) and asked to make it the landing experience, weaving the posts off it, with the arc animation on the globe.

  19. Decision: journey.order is keyed on travel date, not publish datefeature

    This is the most consequential decision in the project.

  20. Decision: anime.js v4 + Motion, not GSAPfeature

    Considered GSAP-only (single library, ScrollTrigger included).

  21. Decision: build-time arc precomputation, no geo library shipsfeature

    The world SVG is generated from Natural Earth 110m via the world-atlas package. Underlying data is public domain, package is ISC.

  22. Decision: dual home anchorsfeature

    The index post says of Colorado: "I live here now!" Home changed.

  23. Decision: Miami / South Beach ships as a stubfeature

    The index post links South Beach to listsof30. The only unclaimed slug in the export was travel-advice-clemens-guide-to-haggling, which reads as a general how-to rather than a Miami post.

  24. Decision: three-source reconciliation, nothing scrapedfeature

    The dataset is built from three sources, all owned by Clemens, none scraped.

  25. Decision: two deliberate divergences from wanessalabs-gamesfeature

    Also considered and declined: TypeScript 7.0.2, the latest release. It is the Go-based rewrite and toolchain support is still settling.

  26. Decision: validation failures block the buildfeature

    Zod covers types; three assertions cover invariants Zod cannot express:

  27. Finding: legacy-only collapsed from 11 to 3feature

    Only Ohio, Valencia, and Sonoma are seelemons-only.

  28. Finding: entry count is 114, not the estimated 74feature

    The Phase 1 roster table estimated 74 entries with per-continent counts. That estimate was wrong and was presented with false precision.

  29. Finding: the posts.csv export is stale and its is_published column liesfix

    Two data-integrity issues, both worked around, both recorded.

  30. Phase 0: auditdocs

    Reference project for Cloudflare Pages conventions: wanessalabs-games/ (pnpm, Vite, wrangler.toml with pages_build_output_dir).

Written from this project's decision log as work happened, not afterwards. 1 entry was withheld from publication. See every project →

What's next

1 open item

Plans, not promises. Taken from this project's own roadmap.