SPA’s and deep linking
One of the challenges with single page apps is how to handle deep linking. In other words: if the user starts neatly at http://hostname.com and navigates from there, the SPA framework will handle the routing and ensure that http://hostname.com/path/to/content will trigger the loading of a view. But when a user bookmarks that link and uses it directly, the server will try to look for some file that sits at that location – and fail, because the app is actually contained in that single page (typically index.html).
This was no different for us, so we needed some URL rewriting-like mechanism to ensure that a request to http://hostname.com/path/to/content would re-route to http://hostname.com (or http://hostname.com/index.html). That way, index.html would be served to the user for every request to load the SPA app, which in turn loads the requested view.
Obviously, a static website in Blob Storage doesn’t provide this out of the box, so we considered using Azure CDN which offers some tiers that support URL rewriting options capable of this. But for me, it seemed wrong to require the use of a CDN to do URL rewriting: it negatively affects both the cost-effectiveness and the low-maintenance properties that made us host it in Blob Storage. And, maybe even more important: that’s not what a CDN is for.
Besides, for all our other outward-facing applications, we’re using Azure Application Gateway already anyway, to do load-balancing and firewalling. So I preferred handling this in Application Gateway through path-based routing rules, and a HTTP setting with the ‘Override backend path’ set.
The first attempt was to simply set the backend path to
/index.html, in the hopes that all requests would end up at http://hostname.com/index.html. This didn’t work, however: what this setting does is basically prepending the path override to the requested path. So the full URL would read: http://hostname.com/index.html/path/to/content. And that will not serve up index.html.
On the second attempt, I tried
/index.html/# as the override backend path. The resulting URL would be http://hostname.com/index.html/#/path/to/content, and I hoped that this would work, but for some reason that’s still unclear to me, it doesn’t.
Third time’s a charm
The third attempt was a winner however, even though in my mind it’s just a variation on the fragment identifier-approach: once I set it to
/index.html?path=, it all started working like a charm. That made sense to me, since the resulting URL would now be http://hostname.com/index.html?path=/path/to/content, i.e. a URL that points to index.html. Again, I fail to see why the URI fragment approach did not work, so if someone can shed some light, please do!
An alternative approach
After all was said and done, however, we landed on a different solution altogether. Having a dependency on Application Gateway is not too big of an issue for us since we’re using it anyway, but having no dependency at all is still preferable. So we simply changed the routing in the app itself to use a fragment identifier. So instead of expecting http://hostname.com/path/to/content, the app now expects http://hostname.com/#/path/to/content. That way, index.html is always being served by default, without URL rewriting and without using the ‘Override backend path’ setting. This may negatively affect indexation by search engines, but since our app sits behind a login anyway, this doesn’t matter for us.
But since search engine indexation may matter to some, I figured I’d share my initial approach anyway, for everyone who has a need to host an SPA behind Application Gateway and retain search engine-friendly deep linking support.