Technology · · 6 min read

Deep Links Into a Photo Gallery

How a link can open my photo gallery on a single photograph.

Photograph © Ken Reid

Deep Links Into a Photo Gallery

29 October 2026 · technology photography

Paste www.kenreid.co.uk/gallery.html?photo=318 into a browser and my photography page opens full screen on Brutus, my dog. He's frame 318, in a gallery that loads 24 at a time, so before it can show him the page builds a few hundred tiled thumbnails on a phone (more on a wider screen) and scrolls to his.

This post covers the two things the gallery now reads from its address, ?photo= and ?tag=, how it writes them back while you browse, and why it does that with the History API's replaceState. Part of the series on how this site is built.

Quick jargon guide

  • Query string: the part of a URL after the question mark, like ?photo=318.
  • Deep link: a link to something inside a page, not just to the page.
  • Lightbox: the full-screen viewer a photo opens in, with arrows to page through the rest.
  • History API: the browser functions a page uses to change its own address without reloading.
  • Lazy image: an image the browser only fetches once it gets near the screen.

Every frame went to Instagram

The strip of ten random photographs near the bottom of the homepage, and of other top-level pages on the site, used to send every single frame clicked to my Instagram profile. Not to that photo on Instagram, but to my user profile, which was pretty "meh".

And the gallery itself had no means - yet - to point to a specific shot. It read ?tag= once, when the page opened, to start on a filter, but otherwise the URL wasn't considered. So if you filtered to wildlife and copied the address to send to someone, they got the whole gallery. Also "meh".

I changed the strip so each frame links to gallery.html?photo= and its number, and then had the gallery read that.

Brindle Staffordshire bull terrier with a blue collar gazing at the camera
© Ken Reid. All rights reserved. Brutus, who lives at gallery.html?photo=318.

Opening on the right frame

When the page starts, it reads both parameters from the address. ?tag= goes first and is applied before drawing, so the first batch of tiles is already the filtered one, and a tag the gallery doesn't have is dropped from the address. A filter you click is animated, with the tiles that survive it sliding to their new rows, but a filter from the address isn't, because there's nothing on screen yet to slide. Then, if there's a ?photo=, the gallery:

  1. Looks for the frame in the current set. If a filter hides it but the frame exists, the filter goes (from the page and from the address) and it looks again. If the frame doesn't exist at all, ?photo= comes off the address and that's the end of it, so a link to a frame I've deleted opens the gallery as normal.
  2. Loads batches until that frame's tile is in the grid, without updating the counter under the grid for every batch on the way.
  3. Scrolls the tile to the middle of the screen, puts keyboard focus on it, and opens the lightbox at that tile's position in the grid, which Magnific Popup (the lightbox library) takes as a second argument[1].

The lightbox on this site doesn't fade in, it grows out of the tile you clicked (unless your system is set to reduce motion). A copy of the thumbnail flies from the tile to where the full-size frame will sit, and the full frame takes over when it arrives (it comes from a GitHub release, and can take a moment). The copy that flies is the thumbnail itself, so it has to have loaded. A deep-linked tile was off screen a moment ago and its thumbnail is lazy, so the page switches that one image to eager and waits for it, for 1.5 seconds at most, before opening.

Building hundreds of tiles to show one photograph sounds wasteful. It would be if every tile fetched its thumbnail, but they're all lazy, and the browser only fetches the ones near the screen[2]. I timed it locally in testing - Brutus opens in 0.3 seconds, or 1.3 with Chrome's CPU throttled four times. Frame 42, a railway line I shot running into snowy hills near Inverness, is around 660th in the list (it runs newest first). On a phone the page builds 682 tiles to reach it and fetches about 70 thumbnails, and the lightbox is open about 0.4 seconds after the page starts loading, or about 1.7 seconds throttled.

Writing it back

The address follows you around. Picking a filter writes ?tag=. Each frame you look at in the lightbox writes ?photo=, and closing the lightbox takes it off again. Whatever is in the address bar is a link to what's on screen, and there's a Copy link button next to the frame number under every photo in the lightbox, which copies that frame's link (with the filter kept on if there is one).

I put all of that through one function:

function writeUrl(changes) {
    if (!window.history || !window.history.replaceState) return;
    try {
        window.history.replaceState(window.history.state, '', galleryHref(changes));
    } catch (e) {}
}

pushState adds a new entry to the browser's history[3]. replaceState swaps the current entry for a new one[4], and neither of them reloads the page. If the gallery used pushState, every frame you paged through would be an entry. After thirty photos, Back would walk you through all thirty before it left the gallery. I'd already been through this on the blog listing, where a pushState per filter click buried the page you came from under a pile of near-identical addresses, and Back stopped meaning "leave the blog". So the gallery replaces.

Previews don't work (yet). The gallery is one static HTML page, so a chat app that unfurls gallery.html?photo=318 gets the gallery's share card, whatever the query string says.

Closing

Paging changes which tile the lightbox belongs to. If you open Brutus and press the right arrow once, you're on frame 316, a lone protester in a city square with a "Ban This Plastic Poison" placard. Close the lightbox there and the photo shrinks back into tile 316, not Brutus's, and the page scrolls that tile into view first if it's gone off screen. Keyboard focus lands on 316 as well. Once the lightbox has closed, the address drops ?photo=, back to plain gallery.html.

Common questions

Can one link open a filter and a photo together?

Yes, gallery.html?tag=wildlife&photo= and a number opens that frame inside the wildlife set, as long as the frame is tagged wildlife. If it isn't, the filter is dropped and the frame opens in the whole gallery.

References

  1. Magnific Popup. Documentation. https://dimsemenov.com/plugins/magnific-popup/documentation.html
  2. web.dev. Browser-level image lazy loading for the web. https://web.dev/articles/browser-level-image-lazy-loading
  3. MDN Web Docs. History: pushState() method. https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
  4. MDN Web Docs. History: replaceState() method. https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState