How to: Deep link into an Angular SPA on IIS
When you create a single page application (SPA) with Angular (or other SPA-frameworks, for the matter), you might want to leverage the power of the web and allow a deep link directly to a certain route within your application.
For most, the easy way is to address routes in your application using the so called hash location strategy. This means the URL within your app is separated by a hash and will look like this: https://your.domain/app#route/to/component
. However, the hash in the URL actually has a different meaning and should position the browser to a specific anchor (or fragment, as it is called in Angular) on the currently shown page.
[caption id=“attachment_700” align=“aligncenter” width=“500”] Fragment in routing links[/caption]
In the optimal case we mostly would like to use an semantically more correct URL like this https://your.domain/app/route/to/component#fragment
.
This, however, brings up a different issue.
Why do we need the # for a deep link in the first place?
Now, why do we need to resort to the hash in the url at all?
The reason is, that the part in front of the hash is treated as the path to the actual page, and the browser will request exactly that. Our example URL would request the file app/route/to/component
from the web server, and this will usually be answered with a HTTP 404 (Not found) status code: the web server does not have a file at this location to deliver. In fact, this route only exists within our client-side application, which is not yet available.
For the application to be available, the browser first needs to load the entry point of our SPA (the index.html), then load the required JavaScript and other required resources, start up the application and finally evaluate the URL to show the correct component to the user.
The solution for an easy deep link
The solution to this is actually pretty simple: We tell the web server to deliver the actual application entry point (our index.html) to the web browser.
The first idea is sending a redirect to the index.html file (using HTTP 301 or 302 responses). This would, however, a) require a second round trip and b) actually change the URL in the browser. If we did this, the SPA would not know of the route in the URL anymore, and we would miss our goal.
Instead, we tell our web server to directly deliver the contents of the index.html file instead of a 404.
Configure IIS to make it so
In our case we host the static files of our SPA on an IIS (Internet Information Server). We need to configure our IIS to deliver the index.html whenever it receives a request it cannot serve otherwise.
For this to happen, we install the URL Rewrite 2.0 extension into IIS. Follow the link and download the installer. It will use the Web Platform Installer to download and install the module into IIS.
Create the basic rule
Now, we need to configure the URL rewrite module to do what we want. In IIS Manager, go to the web site where the SPA is served from and double click on URL rewrite.
We want to ‘Add Rule(s)…’ and create a new rule for the redirect. I called mine ‘redirect all requests’, but you can use whatever name you like to.
In the ‘Match URL’ box, we want to redirect to the index.html whenever we hit any URL. So the ‘Requested URL’ should be set to ‘Matches the Pattern’ and we want to use regular expressions. The pattern we want to use is ^(.*)$
, which just means: Match anything from the beginning to the end. We also check the ‘Ignore case’ checkbox.
[caption id=“attachment_688” align=“aligncenter” width=“678”] Match URL box[/caption]
For the moment we skip the ‘Conditions’ and ‘Server Variables’ box and go right to the end to the ‘Action’ box.
We set the ‘Action type’ to ‘Rewrite’ and the ‘Rewrite URL’ to /index.html
. We also check the box ‘Append query string’ to preserve the rest of the URL and then check the box ‘Stop processing of subsequent rules’ to prevent the IIS from doing too much work.
[caption id=“attachment_691” align=“aligncenter” width=“673”] Action box[/caption]
Tweak the rule for real work usage
Now, this configuration will simply rewrite all requests to our web application and will deliver the contents of our ‘index.html’ file every time. This is not what we want though, as the index.html file will reference some other JavaScript files, images, CSS files etc. from our web server, and we should at least deliver these files ;-)
For that, we go back to the ‘Conditions’ box.
[caption id=“attachment_693” align=“aligncenter” width=“672”] Conditions box[/caption]
First, we ‘Add’ a new condition.
The ‘Condition input’ should be set to {REQUEST_FILENAME}
, which is a IIS variable and points to the actual requested file. We set the ‘Check if input string’ drop down to ‘Is Not a File’ and save this condition.
[caption id=“attachment_695” align=“aligncenter” width=“477”] File exclusion[/caption]
This will prevent IIS from rewriting a URL that points to an existing file, which is exactly what we want: Deliver the existing file instead of the index.html.
Exclude certain paths
In my special case I configured a sub-application in my web site to serve the backend API from the folder ‘/api’. So I do not want the IIS to rewrite requests to the ‘/api’ folder, too.
For that, I added another condition rule that says if the ‘Condition input’ {REQUEST_URI}
‘Does Not Match the Pattern’ /api(.*)$
(with ignore case). This will prevent all requests that start with ‘/api’ from being rewritten.
[caption id=“attachment_694” align=“aligncenter” width=“477”] API exclusion[/caption]
Since I now have multiple rules, I set the ‘Logical grouping’ in the ‘Conditions’ box to ‘Match all’.
Make the rules deployable
We do not want to configure this every time we add a new web site. Also, ISS is a nice player and writes all rules into the ‘web.config’ file. Therefore my use case ended up resulting in this little file of elegant IIS rewrite magic:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<rewrite>
<rewriteMaps>
<rewriteMap name="^(.*)$" />
</rewriteMaps>
<rules>
<rule
name="redirect all requests"
stopProcessing="true"
>
<match url="^(.*)$" />
<conditions logicalGrouping="MatchAll">
<add
input="{REQUEST_URI}"
pattern="/api(.*)$"
negate="true"
/>
<add
input="{REQUEST_FILENAME}"
matchType="IsFile"
negate="true"
/>
</conditions>
<action type="Rewrite" url="/index.html" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
Finally we can leverage this to simply make this web.config file a part of our Angular web application. My Angular application uses the angular-cli tooling, and so I just added the ‘web.config’ file to my .angular-cli.json
file in the app.assets
array.
Conclusion
If you want to deep link into your Angular SPA, you can avoid using the hash location strategy. By configuring your web server to deliver the application entry point file instead of a 404 Not found it is possible to bootstrap your application at any route within your application.
This post shows how to achieve that using the URL rewrite module in IIS, but the same concept applies to other web servers capable of rewriting urls.