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.
Why not configure a 404 path to index.html (in Static web site blade in portal > Error document path)- that’s much simpler (cheaper too) solution and SEO friendly too?
I wonder, for SPAs (static contents) does WAF really adds a lot of values trading off the cost App Gateway brings? APIs must be WAFed – no args there.
CDN could still be a good consideration.
If WAF must be present for compliance I think Azure Front Door serves this scenario better than App Gateway.
Good questions, thanks.
As for the 404: we considered that as well, but that results in, well, 404 status codes. Not ideal if you’re monitoring your logs for suspicious HTTP status codes. After all, it’s actually not a 404, and I don’t like to abuse status codes because they tend to become meaningless if you make a habit out of that. But yes, as a poor man’s solution, that actually gets the job done fastest and cheapest.
And as mentioned: we already have that WAF in place, so the costs for adding this particular app are negligible. Futhermore: on the same hostname we also have a /api path which serves out the API, so there will need to be a WAF sitting in front of this URL anyway.
As for Azure Front Door: yes, that may be a good choice too in some scenarios (although it also leads to a bit of trouble when configuring the required URL rewriting if I recall correctly). But again: we have that App Gateway sitting there already – and we need it, too, because it integrates with our VNET, which Front Door does not. Not required for this SPA, but for some VM-hosted workloads. Results in more narrow IP whitelists on the Storage Account as well.
And a CDN is always a good consideration – if you’re looking for global content distribution. The point for me was that I want to be free to select the optimal CDN if and when that becomes a requirement, instead of having to limit myself to the subset that included some specific URL rewriting capabilities.
But yes, as always, there are more ways to Rome, and what works for us may not be the optimal solution for others. Thanks for commenting!