How this site is built
Colophon
The map on the front page is the one thing here that could not have come out of a template. This is an account of how it works — the geometry underneath it, the network drawn on top, and the three or four versions that had to be thrown away first.
- Built with
- Next.js 16, React 19, TypeScript, Tailwind v4
- Served as
- Static files on a CDN. Nothing runs on request.
- Geometry
- Natural Earth, projected with d3-geo at build time
The network
The map is not a picture
The world on the front page is drawn from the real thing. An offscreen canvas is filled with Natural Earth coastline data, the pixels are read back, and a grid is walked across them: every sample that lands on land becomes a node in the network, and every sample that lands on water is thrown away. Nothing is placed by hand. The countries that glow are the same test run a second time against their own outlines, which is how a node knows whose land it is standing on.
Each node then finds its seven nearest neighbours, once, through a spatial hash — a grid of buckets, so a node compares itself against the handful of points near it rather than against every other node on the map. After that the structure never changes again. What changes is which connections are switched on.
Edges are not drawn, they are sampled. Every frame, nodes carrying enough charge roll for a connection to one of their neighbours, so what you are looking at is one draw from a distribution rather than a diagram. Each country emits a wavefront that recruits whatever it crosses, the cursor pushes the field apart as it passes, and a small fraction of the net fires at random so that something is always happening in the dark.
Safety
Three flashes a second
The first version re-rolled the whole graph every frame. A connection existed for exactly one frame and then vanished, which made the field look alive — and made it a screen-sized area of high-contrast lines appearing and disappearing sixty times a second. That is not a style. The accessibility guidelines put the limit at three flashes a second and this was twenty times over it, which makes it a photosensitivity hazard. It was caught in review, by somebody looking at the thing rather than reading the code.
The fix was to give a connection a life. An edge is born, fades up along a sine envelope, holds, and fades out again over most of a second; the graph is still resampled constantly, it just stopped flickering while doing it. The nodes had the same fault for the same reason. Their brightness decayed at 0.7 a frame, so a node went from lit to dark inside five frames — a twitch at roughly 12Hz, on every node at once. At 0.93 it glows and fades over about a third of a second instead.
Hard pixel churn — the share of the picture that changes abruptly from one frame to the next — now runs at about a quarter of one percent. That number is the constraint on anything done to this canvas later.
Mistakes
Two ways to make it meaningless
There are two ways to build a network like this so that it runs perfectly and tells you nothing, and both of them got built first.
The first was additive gain. A firing node passes charge to its neighbours, and in the first build it added that charge, summed over seven of them. That is positive feedback: within about a second every node on earth sat pinned at full brightness, which looks busy and means nothing at all. A node now passes on a fraction of its own activation rather than a sum, so a cascade falls off as the gain raised to the number of hops and dies after four or five. Activity reads as travelling because it is travelling, and because it stops.
The second was an indexing bug, and it survived two rounds of review because the output looked plausible. The sampling step is fractional, which makes the grid coordinates fractional, and a typed array read at a fractional index returns undefined — never less than the land threshold, so every other ocean sample was kept as land. The same undefined then coerced to zero when the ownership array was built, and zero is a valid country index. Half the world came out belonging to the first country in the list. The repair is one call to Math.floor in each direction. The lesson is that a plausible picture is not a test.
Performance
Sixty frames a second
Five and a half thousand nodes, each checked against every emitter and every neighbour, sixty times a second, on the CPU. Three things make that affordable, and the last two were not optional.
Nothing is allocated inside the frame loop. The distance from each node to each country, and the neighbour lists, are measured once into flat typed arrays, so the loop reads numbers out of contiguous memory and hands nothing to the garbage collector.
The nodes are split into two index lists — the ones standing on a visited country, and everything else — so each drawing pass walks only its own nodes rather than walking all of them and skipping half.
And brightness is quantised. Setting a fill colour per node means building and parsing a colour string several thousand times a frame, which turned out to cost more than the physics did; rounding alpha into twelve bands and drawing each band in one go replaces that with twelve state changes, and at this size the banding is invisible. The edges batch the same way, into eight strokes a frame instead of eleven hundred. Without the index lists and the bands, five thousand nodes ran at about 43fps. With them the field holds sixty.
Geometry
Drawing the world
The map is Natural Earth, at two resolutions at once. The shapes come from the coarse set, which is small enough to inline in the page and indistinguishable from finer data at this scale. That set also leaves out about sixty countries altogether — Singapore, Malta and Hong Kong among them — so the centroids are read from the detailed set instead, and anything too small to have a visible shape becomes a marker dot with a tap target large enough to hit. A dot is the honest way to show a country smaller than a pixel.
The background is one merged silhouette rather than 177 separate country outlines. Next inlines its data payload into the HTML next to the rendered markup, so every byte of path data is paid for twice; merging the countries into a single shape halved the page. The only borders left are the ones around the countries that have actually been visited, which is the right set to keep.
Antarctica is dropped. Equal Earth smears it into a band along the bottom edge and shoves every inhabited landmass upward to make room for it. The projection is then fitted to the whole globe and the viewBox cropped afterwards, because fitting to the land let a few remote Pacific islands set the bounding box and shrank every continent to three quarters of its size to leave room for specks.
One join had to be made carefully. The content is keyed by the two-letter country codes you would recognise from a passport, while the geometry identifies countries by ISO numeric code, and a generated file bridges the two by matching names. It has to exclude the codes ISO has withdrawn but that the platform still answers for: UK and GB both come back as “United Kingdom”, SU and RU both as “Russia”. Matching on name alone silently picked the dead code, and the map lost two countries without saying so.
Language
Mexico 68
Every visited country carries four concentric echoes of its own outline. Each one is the country’s own path scaled about its own centroid, which is the part that matters: the echo follows the real coastline instead of being a circle drawn near it.
The device is Lance Wyman’s, from the identity he designed for the 1968 Mexico City Olympics — radiating parallel outlines, taken from Huichol yarn art and crossed with the Op art of the period. He went on to draw the pictograms for the Mexico City Metro, which are still in the stations.
It is on the map for a reason beyond the reference. Cuba is 96 square units on a map a thousand units wide, and a flat fill at that size is a few pixels of pink that nobody notices. The echoes read as a signal coming off a place rather than a mark on a chart, and they do it without pretending the country is bigger than it is.
Flat fill
Four echoes of the same path
The aircraft
An F-117, thirteen pixels long
One aircraft crosses the map. It is an F-117 in planform, drawn once in a single file and shared by the vector map and the canvas — an earlier version was drawn twice and the two copies drifted apart immediately.
Two things make a shape this small read as a Nighthawk rather than as a generic dart. The wingtips have to be the rearmost points: the leading edges sweep back from the nose at about 72 degrees and do not stop until the back corners. And the trailing edge is a W — from each tip it cuts forward and inboard to a notch, then runs back out to a point on the centreline. That double notch is the most recognisable thing about the airframe. A first attempt put the tips two thirds of the way back with the tails projecting behind them, which turned the whole thing into a bird.
The V-tails are deliberately left out. Seen from directly above they are thin slivers sitting over the wing; filled, they merge into it and add nothing but a muddy rear edge, and at thirteen pixels they would be invisible regardless.
On the canvas the route is never drawn. A line ruled straight across the continents was tried and removed, so the aircraft, its exhaust and a short fading trail are the only evidence that a route exists at all — and the order of the stops is reshuffled on every load, so the flight is different each time you arrive. The vector map on the travels page does draw its route, because there the countries are strung in the order they were reached and the line is the sequence. The plume breathes on a slow sine rather than flickering, under the same rule as the edges: nothing on this canvas is allowed to strobe.
Photographs
Thirty-three megabytes, served as six hundred kilobytes
The eighteen photographs on the Cuba page weigh 33MB as they came off the phone. None of that is what a reader downloads. A build script reads every original, writes WebP copies at four widths, records the real pixel dimensions and samples the average colour; the pages use a plain img tag with a srcset, so the browser picks the width it needs and the recorded dimensions let it reserve the space before the file arrives. At phone width the whole set comes to about 600KB.
This happens at build time rather than at a host’s image endpoint, which is what keeps the output a folder of files that behaves the same way on Cloudflare Pages, on any other static host, or opened straight off a disk.
Alt text is generated from the filename, with a condition attached: the name has to contain at least one real word. Most of these files are called IMG_3488.JPG or a bare UUID, and passing that through would have a screen reader announce “IMG 3488”, or spell out a UUID one character at a time — both worse than a plain “Photo from Cuba”. So a filename earns its alt text or it gets the generic one.
Hosting
No server
Every page is rendered at build time and the output is a directory of HTML, CSS, JavaScript and images that a CDN serves as it finds them. Nothing runs on request. There is no runtime to patch, nothing to scale, and no process that can be down while the files are fine.
The motion follows the same principle. Every reveal on the site is a CSS scroll-driven animation, which the browser runs off the main thread with no scroll listener and no observer, and all of it is wrapped in a support query — so a browser that lacks it does not fall back to an approximation, it simply shows the content. That is the correct failure. Asking for reduced motion turns the whole lot off, and the canvas draws one still frame instead of animating.
Corrections
Something here wrong, or worth arguing about?
[email protected]