The mysterious Angular couple, base-href and deploy-url

This post talks about an ASP.NET Core web application with Angular 8 frontend. The application I use as an example was based on a skeleton application that Visual Studio 2019 creates from a template for an ASP.NET Core web application with Angular frontend. The .NET Core backend is not significant for the problems and solutions discussed here, but the Visual Studio publishing (deployment) process is.

This post talks about an ASP.NET Core web application with Angular 8 frontend. The application I use as an example was based on a skeleton application that Visual Studio 2019 creates from a template for an ASP.NET Core web application with Angular frontend.

I host my .NET stack programming projects at a Windows hosting provider, and I’m cheap. I use my provider’s cheapest plan, which allows me to have just one website. But under that website I can have multiple directories (haven’t checked if there is an upper limit). Which means that all of my .NET applications must be deployed it in their own subdirectories. (I can deploy only one application at root level.)

That’s different from the development server — IIS Express — that you typically use for debugging, in which your application lives at the root level. So, let’s say, on my webhost I deploy my application to directory /foo. My application may be doing a number of things that are dependent on its path. That means that the paths in production will be different than those in the development environment.

One such path will probably be its backend API path. In the skeleton example of ASP.NET Core-WebAPI-with-Angular web application that Visual Studio 2019 creates, it creates an Angular component fetch-data. This component fetches the data from a backend API via a controller called WeatherForecastController. The route for fetching data is weatherforecast . This route is relative to your domain name, which in the development server is localhost://<port_number> . However, when it is deployed to a production server’s subdirectory foo , this route would have to be /foo/weatherforecast

How do we make it happen?

That’s where our good old friend APP_BASE_HREF comes into play. Often APP_BASE_HREF is statically encoded in the head element of index.html:

<base href="/">

We need to make it configurable, so that we could prefix our paths with APP_BASE_HREF. It is, as the name implies, the base URL of our application. In production it will be not / but /foo. Making it configurable would mean that the right APP_BASE_HREF will be automatically selected by the application depending on the environment it’s running in.

Now, if this was a “normal” Angular application — e. g., not coupled to .NET backend and not built with Visual Studio, I would build it for production with this command:

ng build --prod --base-href /foo --deploy-url foo/

(check out this article for an explanation: https://shekhargulati.com/2017/07/06/angular-4-use-of-base-href-and-deploy-url-build-options)

and that would do it. But it is a .NET core project. And Microsoft does things its own way. I deploy the app to production from Visual Studio by right-clicking on the project name in Solutions Explorer, and selecting “Publish”. Before the app is published — i. e. uploaded to a folder on the remote server — it is built for production. I don’t control the build command; it’s probably somewhere in the arcane depths of the various Visual Studio project files with strange extensions. I looked for it briefly without success, and then decided to look for more productive venues to solve this problem.

In app.module.ts, add this object to the providers: array:

{ provide: APP_BASE_HREF, useValue: environment.baseHref }

If your app.module.ts didn’t have any providers until now, it will now have a line

providers: [{ provide: APP_BASE_HREF, useValue: environment.baseHref }],

It means APP_BASE_HREF will come from the environment. An out-of-the-box .NET Core-with-Angular web app has two environment files under ClientApp/src/environments: environment.ts and environment.prod.ts. You should add a proper value of baseHref to both of them.

baseHref<.code> to both of them.

environment.ts:

export const environment = {
  production: false,
  baseHref: '/'
};

 

environment.prod.ts:

export const environment = {
   production: true,
   baseHref: '/foo/'
};

 

And this is how we get baseHref from the environment and use it in our code.

As an aside (which is important for my next article), I thought that meant that I should remove the above-mentioned line from index.html:

<base href="/">

 

because the hardcoded baseHref would “interfere” with the one injected. That turned out to be a mistake. I’ll explain why in a separate blog article.

Configuring production directory from which JS and CSS files are loaded

But that’s only half of the job. The paths from which JS scripts and CSS files are loaded in index.html will also have to change, for the following reasons.

When I build my app in the development environment, the index.html that gets generated in the dist folder includes JS scripts and CSS files. And they are referred to like this:

<script src="runtime-es2015.js" type="module"></script>
<script src="runtime-es5.js" nomodule defer></script>
<script src="polyfills-es5.js" nomodule defer></script>
<script src="polyfills-es2015.js" type="module"></script>
<script src="vendor-es2015.js" type="module"></script>
<script src="vendor-es5.js" nomodule defer></script>
<script src="main-es2015.js" type="module"></script>
<script src="main-es5.js" nomodule defer></script>

 

But in the production environment — recall that the application directory in production is /foo — they have to be referred to like this:

<script src="/foo/runtime-es2015.9dc158bbff4fb6dd0447.js" type="module"></script>
<script src="/foo/runtime-es5.9dc158bbff4fb6dd0447.js" nomodule defer></script>
<script src="/foo/polyfills-es5.8e50a9832860f7cf804a.js" nomodule defer></script>
<script src="/foo/polyfills-es2015.5b10b8fd823b6392f1fd.js" type="module"></script>
<script src="/foo/main-es2015.f08fe2adb0eb37b1e1e0.js" type="module"></script>
<script src="/foo/main-es5.f08fe2adb0eb37b1e1e0.js" nomodule defer></script>

 

The most obvious difference is all the JS file names are different — that’s because they are built for production, so they contain JS code that’s been minified and bundled from Angular files and third-party libraries. But the difference that interests us is that every script file has to be prefixed with /foo/. If we didn’t prefix them, this snippet would look like this:


<script src="runtime-es2015.9dc158bbff4fb6dd0447.js" type="module"></script>
<script src="runtime-es5.9dc158bbff4fb6dd0447.js" nomodule defer></script>
<script src="polyfills-es5.8e50a9832860f7cf804a.js" nomodule defer></script>
<script src="polyfills-es2015.5b10b8fd823b6392f1fd.js" type="module"></script>
<script src="main-es2015.f08fe2adb0eb37b1e1e0.js" type="module"></script>
<script src="main-es5.f08fe2adb0eb37b1e1e0.js" nomodule defer></script>

and for some reason that would cause index.html to look for .js files in the web server’s root directory. It would fail, since they are not in the root but in foo directory. Why can’t the browser look for those files in the same directory where index.html is located? After all, those files are right there in the same directory. Beats me. I always thought that the browser assumes a JS or CSS file path is relative to the file that references it (unless the path starts with a /, in that case it’s relative to the root directory). For example, if you wrote a static index.html file, not involving any JS framework or any backend, and added a <script src="myscript.js"/> to it, the browser would look for myscript.js in the same directory as index.html. Why doesn’t it do it when it’s an Angular app? I don’t know. Maybe it’s all explained in Angular docs and I just haven’t stumbled upon that page yet.

But then again, when I think about it… A .NET Core backend / Angular frontend app has both backend routes and frontend routes, as does an Angular app with any routed backend, whether it’s Node.js, Flask, or numerous others. So how does the server recognize which route is a backend route and which is a frontend route? At least the ones I’m familiar with — .NET Core, Node.js (Express.js) and Flask — try to match a route to its backend routes, and if it doesn’t match, it sends back the index.html . Then index.html loads the Angular files (really, just Javascript files at that point), and Javascript tries to find a frontend route to match. So perhaps in all that shuffle the browser forgets what its “working directory” is (if indeed it has such a concept), or maybe it can’t be reliably determined, so the creators of Angular decided that the browser should not even try to determine it? And that’s why we need to prefix all those .js and .css file names with /foo/? Just grasping at straws here.

But whatever the reason is, we do indeed need to prefix all those .js and .css file names with the directory name relative to the web root, such as /foo/, if our application runs in a subdirectory.

The question is, how do we do it, if we can’t use the ng build --prod --base-href /foo --deploy-url foo/ command.

Configure it in angular.json

Well, our Angular app has an angular.json file. And deeply nested in its nestings, there is an object

projects.<your_app_name>.architect.build.options

or in other words


"projects": {
	"": {
		"architect": {
			"build": {
				"options": {
				...
				}
			...
			}
		...
		}
	...
	}
}  

You can add to it this option:

"deployUrl": "/foo/",

 

So the whole thing will look like this:


"projects": {
	"": {
		"architect": {
			"build": {
				"options": {
					"deployUrl": "/foo/",
				...
				}
			...
			}
		...
		}
	...
	}
}  

Now when you publish to production via Visual Studio, it will prepend /foo/ to all the .js and .css files referenced in index.html.

But this means you have to comment this line out when you are testing your application locally, and uncomment it when you want to publish it. That’s a manual step, and as with any manual step, it’s easy to forget to do it once in a while. Still, Dear Reader, I settled for it. Why? Googling didn’t lead me to any clues on how to automate it. If I was doing this for work, I would leave no stone unturned, but I’m doing it for a personal project. And with personal projects I have to be careful not to expend too much effort on tasks that don’t, subjectively, provide me satisfaction. Every evening I have at most half an hour to work on my projects, and it usually happens when I’m faceplanting into the keyboard from tiredness. So I need to carefully spend the dregs of my energy on tasks that actually move my product forward, i. e. implement new, exciting-to-me features. If I spend a week’s worth of evenings on a task I consider housekeeping — such as automating a manual step — I’ll lose interest in my own project. That’s why I settled on this compromise.

That’s the power of the base-href and deploy-url duo.