tapped = false, 1500); navigator.clipboard?.writeText(location.href.split('#')[0] + '#ten-years-of-travel-on-a-globe')" >Ten years of travel, on a globe
Trips come from Nomads, where I log them anyway, over its MCP server. Trips in the same country are grouped into stops, and posts and photos are attached to the stop they belong to. Every edit I make on Nomads flows through the next morning, and things that no longer match drop off rather than being deleted, so they come back if the trip does.
There are pages for every year, continent, country, region and place, a globe with the routes between them, and the map data to download as KML or GeoJSON.
tapped = false, 1500); navigator.clipboard?.writeText(location.href.split('#')[0] + '#flights-from-the-confirmation-emails')" >Flights, from the confirmation emails
Flights are read out of booking emails, and the e-tickets and invoices attached to them, from Kiwi, Gotogate and the airlines themselves. Layovers are joined into one journey, and airports in the same city count as one. The plane comes from the ticket where it says, or from the airline's fleet where it doesn't, and hovering a plane, airport or airline shows a card with its photo and scores.
tapped = false, 1500); navigator.clipboard?.writeText(location.href.split('#')[0] + '#places-and-where-i-stayed')" >Places, and where I stayed
Stays come from Airbnb, Booking.com and Agoda confirmations, with each listing's photo and rating, copied before the links to them expire. Photos come from Instagram exports and from Google Photos, read straight out of Takeout archives in Google Drive: a small reader fetches just the file it needs from a multi-gigabyte zip with range requests, without downloading the archive. Posts, check-ins and reviews fill in the rest, with repeat visits to a place counted by matching nearby check-ins.
tapped = false, 1500); navigator.clipboard?.writeText(location.href.split('#')[0] + '#share-cards-that-move')" >Share cards that move
Every page has its own share card, rendered by a Cloudflare Worker I wrote and open-sourced, which runs a headless browser and stores the result in R2. It can emulate dark mode and reduced motion, and record video too, so sharing the travels page posts a ten-second clip scrolling through the last three years, in dark mode.
tapped = false, 1500); navigator.clipboard?.writeText(location.href.split('#')[0] + '#projects-skills-and-the-people-behind-them')" >Projects, skills and the people behind them
Projects, skills, employers and references are all connected, so every skill and employer gets a page at its own short URL, like steve.ly/laravel. Project write-ups like this one are built from blocks in the admin, and the résumé is drawn from the same data, with print styles that summarize older roles and drop the links on paper.
tapped = false, 1500); navigator.clipboard?.writeText(location.href.split('#')[0] + '#small-things-done-properly')" >Small things, done properly
⌘K search across projects, skills, employers, countries and places, which also works from the browser's address bar and jumps straight to an exact match
Dark mode and reduced motion that follow your system, from the first paint
Structured data on every page, and a sitemap of every place
Public pages that set no cookies, so Cloudflare can cache them for a month
A Worker that serves photos and video from R2, range requests and all
Asteroids, if you know the Konami code
tapped = false, 1500); navigator.clipboard?.writeText(location.href.split('#')[0] + '#under-the-hood')" >Under the hood
Laravel 13 and PHP 8.4, Blade, Tailwind 4 and Alpine, with globe.gl and MapLibre
A Filament admin, which replaced Nova in 2026
Pest tests for the imports, the zip reader, the structured data and every public page, with fixtures in place of real data
CI and deploys on self-hosted GitHub Actions runners, and weekly pull requests for dependency updates
tapped = false, 1500); navigator.clipboard?.writeText(location.href.split('#')[0] + '#five-versions')" >Five versions
2008: XHTML Strict and jQuery, with a portfolio written in XML and styled with XSL, from my university days.
2010: HTML5, a blog, a portfolio and a lab, with a Markdown admin, from my time at Wired Media.
2013: responsive, with a CV page, while I was at Simpleweb.
2016: a résumé and a sortable admin.
2020: this one, on Laravel, with share cards from day one.
