In 2011, a new part of the HTML5 specification known as the History API was introduced, which has transformed the ability for fully Client-Side Rendered (CSR) applications to support multi-page behaviours. I’m using the term CSR application here to mean a web application that runs entirely in the browser, such that nothing but static content is served from a web server or CDN.
The History API allows client-side scripts to programmatically modify the entire URL path—not just the URL fragment—while suppressing the page reload that would normally occur when the URL path changes. As its name suggests, the History API integrates with the browser’s history. This allows CSR applications to make the browser’s history work as expected when they change the URL, including use of the Back and Forward buttons.
The History API is great, but it doesn’t provide support for all the multi-page behaviours on its own. In order to illustrate this, I’ve built a simple demo application, inspired by a demo from a well known tutorial—but with a few important differences.
Note: When you try the demo application, certain actions result in HTTP 404 (Not found) errors—this is intentional. I want to show the limitations of the History API when used on its own.
In the next post I’ll describe how to fix those Not found errors—but before that, let’s take a look at what the History API can do on its own.
Here’s a screenshot which links to the demo application:
The demo is a fully CSR application, so it runs in entirely in the browser. It displays three randomly-coloured blocks, which are hyperlinks to separate logical “pages” within the application. When you click on a link, a new “page” opens—but a page reload does not occur. The application changes the URL path to reflect the hexadecimal value of the colour that was clicked, and then renders the page background in that colour.
Using the demo application, you can immediately tell when a page reload is occurring:
- If the three link colours stay the same: The page did not reload, the DOM structure was preserved, and client-side code was able to retain arbitrary data structures in memory.
- If the three link colours are randomized: A full page reload occurred, the entire DOM structure was rebuilt scratch, and any data structures held in memory by client-side code were discarded and (if necessary) recreated.
If you try the demo application, you’ll notice that when you click on a link, the current page changes along with the URL—but the three link colours stay the same! This shows that the page is not reloading as you navigate.
I’ll explain how this is achieved later. First, let’s see which multi-page behaviours this application supports and which it does not, in its current form. I encourage you to try the demo application and see for yourself what works and what doesn’t.
Evaluation against the multi-page behaviours
Despite the page not reloading when you click on a link, the entire URL path in the browser’s address bar changes.1 This is the magic of the History API, and it means you can “share” your current location by using the browser’s native Share button or copying the URL in the address bar.
Therefore we can say that the shareable behaviour is supported.
However, you can’t actually open a shared URL, since that needs a different behaviour: deep linkable. Let’s look at that next.
Deep linkable ✗
What happens if you take a previously shared or bookmarked link, and try and open it?
You can try this with any valid URL, but let’s try it with https://egalo.com/hist/27d98d (“Shamrock”), for example.
You get a HTTP 404 Not Found error. I’ll explain why this happens and how to fix it later—for now, let’s just say that the deep linkable behaviour is not supported by default using the History API.
What happens if you open the application home page, navigate to any colour-specific page, and then click the browser’s Reload button?
Another HTTP 404 Not Found error appears. The refreshable behaviour is also not supported by default.
History traversable ✓
What happens if you click Back or Forward?
You retrace your navigational steps in the application, just as you’d expect. You can also choose arbitrary pages from your browser’s history by long-clicking the Back or Forward buttons (though not by selecting from the browser’s main History menu, since that again requires the deep linkable behaviour).
In a real-life application, making the browser history work as expected can take considerable effort, especially if there is a lot of complex state to manage. For now, let’s just say that the history traversable behaviour works at a basic level using the History API.
Link operable ✓
If you right-click on one of the three coloured links, all the standard browser menu options for hyperlinks appear: Open Link in New Tab, Open Link in New Window, Copy Link Address, and so on.
Rejoice! Hallelujah! These are genuine, regular, honest-to-God hyperlinks you can right click. They are not those super-annoying fake hyperlinks you often get in modern web applications that can only be primary clicked.2
This is another great thing about the History API: it works with regular hyperlinks. By “regular hyperlink”, I mean an anchor element with an
As a result, we can say that the link operable behaviour is fully supported using the History API.
After right-clicking a link, actually opening it successfully in a new browser tab is a different story. That again requires the deep linkable behaviour—which as we’ve seen, is not supported by default.
Try opening the application home page in one browser tab, and then navigate to a colour-specific page. Then in a second browser tab, open the home page again and navigate to a different colour-specific page.
Now you have two browser tabs open with the same application. You can navigate independently in each tab and switch between the tabs at will.
As before, you cannot right-click a link and open it in a new browser tab because the deep linkable behaviour is not supported. Furthermore, if this was an authenticated application, then the developer might have to do extra work to ensure the user’s session is active in both tabs.
But the demo application is an unauthenticated application. And it works in multiple tabs. So I’m going to say that the multi-tabbable behaviour is supported by default. It’s up to the developer not to break this behaviour when they add extra functionality.
Let’s summarize what we’ve found so far using the HTML5 History API on its own.
|Behaviour||Works by default?|
What’s going on under the hood to produce this behaviour?
A look under the hood
The History API allows you to attach an event listener to a hyperlink element (or one of its DOM parents), like this:
linkRowElement.addEventListener('click', colourLinkClickHandler, false);
In the demo application there’s one listener attached to a div element which is a parent to all three hyperlinks.
colourLinkClickHandler). Your web framework might do this automatically.
- It calls the event’s
preventDefault()method to suppress the page reload that would normally occur when a link is clicked.
- It calls browser API method
history.pushState(). This pushes the link’s destination URL into the browser’s history and simultaneously updates the address bar to show the new URL (the browser history works like a stack: you push items on, and pop them off). Alternatively, if you want to change the current URL without adding a new entry to the browser history, you can use
- It runs whatever client-side code is necessary to update the page using the DOM to reflect the new location.
The third item is important. If you leave that out, the URL will change but the page content will stay the same.
You don’t have to redraw the whole page, though. You only have to apply the delta—that is, figure out which parts of the DOM need to change for this particular page transition, and only update them. In more advanced applications, this is the most complex part to get right.
The key thing to understand is that the DOM structure from the previous page is preserved by default, and it’s the application’s responsibility to make any changes required. Any data structures the code holds in memory are preserved throughout the page transition.
Another event listener is attached to the browser’s history:
When the user clicks Back or Forward (or chooses an arbitrary page from their browser history by long-clicking the Back or Forward buttons), the chosen URL is restored to the browser’s address bar, and control is handed off to another function which you need to supply (in this case,
historyHandler). This time, the function only needs to perform item 3 in the list above—update the DOM. Items 1 or 2 happen automatically. Again, a full page reload does not occur.
And that’s it! In its most basic form, that’s how the History API works.
Why are some multi-page behaviours failing?
Of the six multi-page behaviours, four work fine: shareable, history traversable, link operable, and multi-tabbable; while two fail: deep linkable and refreshable.
The result is that if you perform any of the following actions, a HTTP 404 Not Found error appears:
- Click Reload (on any page except the home page)
- Right-click a link and open it in a new browser tab or window
- Open a previously-shared deep link or bookmark
- Pick an arbitrary page from the browser’s full History menu
- Hack the URL directly in the address bar
Why are we getting all these Not found errors?
The reason is that those actions involve interrogating the web server for pages that were effectively synthesized in the browser. When we generated those URLs, no-one told the web server they exist!
From the web server’s point of view, there’s only one web page: the application’s home page. If you ask it about any other pages—Not found.
Therefore, if you use the History API without doing any extra work on your web server, two of the multi-page behaviours remain unsupported: deep linkable and refreshable.
How can we fix it?
How can we fix these errors? The solution seems obvious, right? Just make a separate copy of the application’s
index.html file for each valid URL for this application, and place the copies in folder paths corresponding to those URL paths. Easy!
Not so fast! The demo application generates random URLs corresponding to all the colours in the 24-bit colourspace. There are 16,777,216 colours in the 24-bit colourspace. So for this application you’d need to host 16,777,217
index.html files (including the home page).
I don’t want to do that—and I especially don’t want to host 16 million identical copies of the same
Surely there’s a better way? Well, there is—and in the next post I’ll describe it.
Some browsers hide the URL path in the address bar by default, but can optionally be configured to show it. For example using the desktop version of Safari, the entire URL can be shown using Preferences… → Advanced → Smart Search Field → Show full website address. ↩
These fake hyperlinks often these display ‘
about:blank#blocked’, a blank string, or some other infuriating nonsense when you right-click them. ↩