sharpmachine
sharpmachine

Reputation: 3893

Angular 2+ - Set base href dynamically

We have an enterprise app that uses Angular 2 for the client. Each of our customers has their own unique url, ex: https://our.app.com/customer-one and https://our.app.com/customer-two. Currently we are able to set the <base href...> dynamically using document.location. So user hits https://our.app.com/customer-two and <base href...> gets set to /customer-two...perfect!

The problem is if the user is for example at https://our.app.com/customer-two/another-page and they refresh the page or try to hit that url directly, the <base href...> gets set to /customer-two/another-page and the resources can't be found.

We've tried the following to no avail:

<!doctype html>
<html>

<head>
  <script type='text/javascript'>
    var base = document.location.pathname.split('/')[1];
    document.write('<base href="/' + base + '" />');
  </script>

...

Unfortunately we are fresh out of ideas. Any help would be amazing.

Upvotes: 180

Views: 232161

Answers (17)

Ivan Shatsky
Ivan Shatsky

Reputation: 15687

I want to first cite @MaximeMorin comment:

My experience with ./ has been very different. It works until the user navigates to a route and hit the browser's refresh button or keys. At that point, our rewrites handle the call, serves the index page, but the browser assumes that ./ is the current route not the web app's root folder. We solved the OP's issue with a dynamic (.aspx, .php, .jsp, etc) index which sets the base href.

This aligns with my experience. Using a ./ base href breaks page reloading when nested routes are used. However, at least for Apache and NGINX web servers, there is the SSI (Server Side Includes) option to use, which avoids the need for additional script engines.

Apache configuration sample

.htaccess (assuming the folder for angular app is <webroot>/prefix):

Options +Includes
<Files "index.html">
AddOutputFilter INCLUDES html
</Files>
<IfModule mod_rewrite.c>
RewriteEngine on
RewriteBase /prefix/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [L]
</IfModule>

index.html (ap_expr syntax):

<head>
<!--#if expr="v('SCRIPT_NAME') =~ m#^(.*/)index\.html$# && $1 =~ /^(.*)$/" -->
<!--#set var="base" value="$0" -->
<!--#else -->
<!--#set var="base" value="/" -->
<!--#endif -->
<base href="<!--#echo var="base" -->">
...

index.html (legacy apache 2.2.x syntax):

<head>
<!--#if expr="$SCRIPT_NAME = /^(.*\/)index\.html$/" -->
<!--#set var="base" value="$1" -->
<!--#else -->
<!--#set var="base" value="/" -->
<!--#endif -->
<base href="<!--#echo var="base" -->">
...

NGINX configuration sample

nginx.conf snippet (assuming the folder for angular app is <webroot>/prefix):

location /prefix/ {
    ssi on;
    try_files $uri /prefix/index.html;
}

index.html:

<head>
<!--#if expr="$uri = /^(.*\/)index\.html$/" -->
<!--#set var="base" value="$1" -->
<!--#else -->
<!--#set var="base" value="/" -->
<!--#endif -->
<base href="<!--# echo var="base" -->">
...

Upvotes: 0

Richard Turner
Richard Turner

Reputation: 441

This has been a massive headache, #location strategy doesnt seem to do what is says on the tin (seemed like it was an obvios fix but sadly not) so here is what worked for me. Thanks to all for the pointers!

Note I am calling php pages from within my app that are relative to the application path, this was going wrong on a refresh because the folder was one level too high. This is addressed in the in2 section of the config below.

Create a web.config file and put it in your /src folder

<configuration>
<system.webServer>
  <rewrite>

<rules>
            <clear />
            <rule name="in2" stopProcessing="true">
                <match url="(.*)(.php)" />
                  <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
                    <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                    <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                </conditions>
                <action type="Rewrite" url="../{R:0}" />
            </rule>
            <rule name="Angular Routes" stopProcessing="true">
                <match url=".*" />
                <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
                    <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
                    <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
                </conditions>
                <action type="Rewrite" url="../myappname/" />
            </rule>

</rules>
  </rewrite>
</system.webServer>

Create a myappname.html file and put it in the /src folder Add both to the Angular.json build section under assets

"assets":{ ["projects/myapp/src/myappname.html","projects/toolbar2/src/web.config"
]

In the index.html

 <script type='text/javascript'>
function configExists(url) {
  var req = new XMLHttpRequest();
  req.open('GET', url, false); 
  req.send();
  return req.status==200;
}

var probePath = 'myappname.html';
var origin = document.location.origin;
var pathSegments = document.location.pathname.split('/');

var basePath = '/'
var configFound = false;
for (var i = 0; i < pathSegments.length; i++) {
  var segment = pathSegments[i];
  if (segment.length > 0) {
    basePath = basePath + segment + '/';
  }
  var fullPath = origin + basePath + probePath;
  configFound = configExists(fullPath);
  if (configFound) {
    break;
  }
}

document.write("<base href='" + (configFound ? basePath : '/') + "' />");

Upvotes: 0

morcibacsi
morcibacsi

Reputation: 65

I am late for the show, but you can use a rewrite rule if you are deploying to IIS (I am sure it can be done with other webservers as well).

You can modify the content of the outgoing response when the index.html is requested and replace the base href dynamically. (url rewrite module must be installed into IIS)

The following example assumes you have <base href="/angularapp/"> in your index.html

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.webServer>
        <rewrite>
            <outboundRules>
                <rule name="Modify base href to current path">
                    <match filterByTags="Base" pattern="^(.*)(/angularapp/)(.*)" />
                    <conditions logicalGrouping="MatchAll" trackAllCaptures="true">
                        <add input="{REQUEST_URI}" pattern="^(.*)/index.html$"/>
                    </conditions>
                    <action type="Rewrite" value="{C:1}/" />
                </rule>
            </outboundRules>
        </rewrite>
    </system.webServer>
</configuration>

Upvotes: 0

Nick Gallimore
Nick Gallimore

Reputation: 1263

You can set the base href dynamically in the app.module.ts file using this: https://angular.io/api/common/APP_BASE_HREF

import {Component, NgModule} from '@angular/core';
import {APP_BASE_HREF} from '@angular/common';

@NgModule({
  providers: [{provide: APP_BASE_HREF, useValue: '/my/app'}]
})
class AppModule {}

Upvotes: 2

Jarek Wichrowski
Jarek Wichrowski

Reputation: 256

In package.json set flag --base-href to relative path:

"script": {
    "build": "ng build --base-href ./"
}

Upvotes: 0

Huy - Logarit
Huy - Logarit

Reputation: 686

I just changed:

  <base href="/">

to this:

  <base href="/something.html/">

don't forget ending with /

Upvotes: -15

Ronald Korze
Ronald Korze

Reputation: 828

Had a similar problem, actually I ended up in probing the existence of some file within my web.

probePath would be the relative URL to the file you want to check for (kind of a marker if you're now at the correct location), e.g. 'assets/images/thisImageAlwaysExists.png'

<script type='text/javascript'>
  function configExists(url) {
    var req = new XMLHttpRequest();
    req.open('GET', url, false); 
    req.send();
    return req.status==200;
  }

  var probePath = '...(some file that must exist)...';
  var origin = document.location.origin;
  var pathSegments = document.location.pathname.split('/');

  var basePath = '/'
  var configFound = false;
  for (var i = 0; i < pathSegments.length; i++) {
    var segment = pathSegments[i];
    if (segment.length > 0) {
      basePath = basePath + segment + '/';
    }
    var fullPath = origin + basePath + probePath;
    configFound = configExists(fullPath);
    if (configFound) {
      break;
    }
  }

  document.write("<base href='" + (configFound ? basePath : '/') + "' />");
</script>

Upvotes: 6

3rdi
3rdi

Reputation: 485

Add-ons:If you want for eg: /users as application base for router and /public as base for assets use

$ ng build -prod --base-href /users --deploy-url /public

Upvotes: 3

Zze
Zze

Reputation: 18865

The marked answer here did not solve my issue, possibly different angular versions. I was able to achieve the desired outcome with the following angular cli command in terminal / shell:

ng build --base-href /myUrl/

ng build --bh /myUrl/ or ng build --prod --bh /myUrl/

This changes the <base href="/"> to <base href="/myUrl/"> in the built version only which was perfect for our change in environment between development and production. The best part was no code base requires changing using this method.


To summarise, leave your index.html base href as: <base href="/"> then run ng build --bh ./ with angular cli to make it a relative path, or replace the ./ with whatever you require.


Update:

As the example above shows how to do it from command line, here is how to add it to your angular.json configuration file.

This will be used for all ng serving for local development

"architect": {
    "build": {
      "builder": "@angular-devkit/build-angular:browser",
      "options": {
        "baseHref": "/testurl/",

This is the config specific for configuration builds such as prod:

"configurations": {
   "Prod": {
     "fileReplacements": [
        {
          "replace": src/environments/environment.ts",
          "with": src/environments/environment.prod.ts"
        }
      ],
      "baseHref": "./productionurl/",

The official angular-cli documentation referring to usage.

Upvotes: 216

Aviw
Aviw

Reputation: 1095

Frankly speaking, your case works fine for me!

I'm using Angular 5.

<script type='text/javascript'>
    var base = window.location.href.substring(0, window.location.href.toLowerCase().indexOf('index.aspx'))
    document.write('<base href="' + base + '" />');
</script>

Upvotes: -1

Katlu
Katlu

Reputation: 506

If you are using Angular CLI 6, you can use the deployUrl/baseHref options in angular.json (projects > xxx > architect > build > configurations > production). In this way, you can easily specify the baseUrl per project.

Check that your settings are correct by looking at the index.html of the built app.

Upvotes: 5

Vince
Vince

Reputation: 650

In angular4, you can also configure the baseHref in .angular-cli.json apps.

in .angular-cli.json

{   ....
 "apps": [
        {
            "name": "myapp1"
            "baseHref" : "MY_APP_BASE_HREF_1"
         },
        {
            "name": "myapp2"
            "baseHref" : "MY_APP_BASE_HREF_2"
         },
    ],
}

this will update base href in index.html to MY_APP_BASE_HREF_1

ng build --app myapp1

Upvotes: 8

Avikar
Avikar

Reputation: 349

This work fine for me in prod environment

<base href="/" id="baseHref">
<script>
  (function() {
    document.getElementById('baseHref').href = '/' + window.location.pathname.split('/')[1] + "/";
  })();
</script>

Upvotes: 14

Karanvir Kang
Karanvir Kang

Reputation: 2239

Simplification of the existing answer by @sharpmachine.

import { APP_BASE_HREF } from '@angular/common';
import { NgModule } from '@angular/core';

@NgModule({
    providers: [
        {
            provide: APP_BASE_HREF,
            useValue: '/' + (window.location.pathname.split('/')[1] || '')
        }
    ]
})

export class AppModule { }

You do not have to specify a base tag in your index.html, if you are providing value for APP_BASE_HREF opaque token.

Upvotes: 20

Krishnan
Krishnan

Reputation: 1504

Based on your (@sharpmachine) answer, I was able to further refactor it a little, so that we don't have to hack out a function in index.html.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { APP_BASE_HREF, Location } from '@angular/common';

import { AppComponent } from './';
import { getBaseLocation } from './shared/common-functions.util';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    HttpModule,
  ],
  bootstrap: [AppComponent],
  providers: [
    appRoutingProviders,
    {
        provide: APP_BASE_HREF,
        useFactory: getBaseLocation
    },
  ]
})
export class AppModule { }

And, ./shared/common-functions.util.ts contains:

export function getBaseLocation() {
    let paths: string[] = location.pathname.split('/').splice(1, 1);
    let basePath: string = (paths && paths[0]) || 'my-account'; // Default: my-account
    return '/' + basePath;
}

Upvotes: 45

Daerik
Daerik

Reputation: 4277

I use the current working directory ./ when building several apps off the same domain:

<base href="./">

On a side note, I use .htaccess to assist with my routing on page reload:

RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule .* index.html [L]

Upvotes: 24

sharpmachine
sharpmachine

Reputation: 3893

Here's what we ended up doing.

Add this to index.html. It should be the first thing in the <head> section

<base href="/">
<script>
  (function() {
    window['_app_base'] = '/' + window.location.pathname.split('/')[1];
  })();
</script>

Then in the app.module.ts file, add { provide: APP_BASE_HREF, useValue: window['_app_base'] || '/' } to the list of providers, like so:

import { NgModule, enableProdMode, provide } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { APP_BASE_HREF, Location } from '@angular/common';


import { AppComponent, routing, appRoutingProviders, environment } from './';

if (environment.production) {
  enableProdMode();
}

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    HttpModule,
    routing],
  bootstrap: [AppComponent],
  providers: [
    appRoutingProviders,
    { provide: APP_BASE_HREF, useValue: window['_app_base'] || '/' },
  ]
})
export class AppModule { }

Upvotes: 77

Related Questions