The Last SPA Router You'll Need
I went through a number of iterations on how to manage my images over on snugug.photography. I started with doing everything in Lightroom (my desktop library manager of choice) and extracting the EXIF data at build time to grab everything I needed from there. This had a few problems, the biggest being that while I could write titles, descriptions, and alt text in Lightroom, that it was incredibly cumbersome to do so. So I pivoted, and built a tiny Firebase app to help me.
This Firebase app does a couple of neat things: I drop a folder of images into a storage bucket, it reads those images, extracts and normalizes the EXIF data, and puts them into a database so I have a cache of that data. With database entries for everything, I can now manipulate the data a little easier and add site-specific metadata as I see fit. It also opens the ability to integrate external tools into my workflow, like the only thing approaching a good use for generative LLMs I’ve personally seen so far: writing alt text for images. To manage all of this, I found I needed a little content management system, and because it’s a highly interactive system with deep user sessions, it’s one of the few circumstances where a single page app (SPA) makes sense.
My first run at it was pretty simple; a single client:only
component I threw into my Astro site that had all the logic stuffed in. It worked, it was a little messy, but it did the job. I had actually tried dividing it into sub components and building it “correctly”, but I’ve both truly never liked any of the compromises most SPA routers make (namely being required to use their components to make navigation work, or hash navigation, or or surprise links are div
s) and integrating an SPA route with subroutes into Astro in dev is a surprisingly unsolved pattern that was causing me issues. But I found the need to expand what my CMS was capable of, so I embarked on a rewrite.
Because this was only for me, I decided to try playing around with the Navigation API, an standards-based API specifically for managing all the tricky edge cases of trying to do client-side routing. It’s available in Chromium based browsers, but has positive signals, and implementation work, from Safari and Firefox, and after using it for a morning, honestly, it can’t come soon enough to them. For a basic URL based routing system like I’ve got, the flow is basically as follows:
- Add a navigation event listener
- Check to see if the destination URL should be owned by your router
- If not, return, if so, set your view.
It’s really only those three steps. Throw in “set initial view” and “render your view” for a total of 5 steps, and, with only a smidge of hyperbole, I can say that this is probably the simplest, easiest, client side router you’ll ever use. And the exact same pattern, with almost the exact same code, can be used with any framework (or without a framework!). It’s really a game changer. Here’s what it looks like:
That’s it! That’s all the logic you need to set up your router! ~5 lines of vanilla, use anywhere with anything JavaScript. Write proper links with a
tags, and this works, no special components necessary. You need to navigate programmatically? that’s covered, too, with navigation.navigate()
, passing in the URL or path you want to navigate to. It’s so straight forward, so simple, so easy (and I really don’t like using those words when describing tech) that writing a wrapping or helper library won’t help ergonomics or understanding. It’s an excellent API, and hats off to the group that came up with it.
Now, you’ll be asking, how does this work to actually change the view? Well, from here on out is going to be implementation specific, but the code for Svelte 5 is, again, very straight forward and you should be able to translate it into whatever tool you’re using:
That’s it! That’s the whole JS of the router! A few lines of vanilla JS and some blink-and-you’ll-miss-it integrations with a state manager. And with a modern bundler, like Vite, these import
routes will be code split (which is why I’m not using a single, dynamic import statement here), giving you the holy grail (from a performance perspective) of SPA routers–a code-split, asynchronous routing system with 0 external dependencies that only weighs bytes, even before the gzipped and minimized size laundering that we use to describe library impact nowadays. And, like I previously said, it’s universal, bring it with you to any tool, any framework you’re using, and never learn another router again, because here we #UseThePlatform.
Oh, there’s one last bit you need to do: actually render your component. While this will vary from framework to framework, implementation to implementation, one of the other reason I’m particularly happy with Svelte 5 here is, because we’ve told Svelte that View is stat that will change, that gets boiled down to this: