Multi-site architecture in Angular

Sahil Purav
Cactus Tech Blog
Published in
8 min readJan 1, 2020
Angular multi-site architecture

With over 20+ locale-specific domains sharing the same features, it is mandatory for me to set up a scalable architecture that supports faster build time, quick deployments, ability to have a different theme and easy switching of features. This article shares my thoughts and solution to the common problems that you may face while developing multi-site architecture.

Let’s talk about the problem statement:

Since we’ve multiple sites sharing the same features, creating 20+ applications in angular.json will result in 20+ builds every time you add / update features in project. Thus, reducing the overall deployment time.

Angular CLI uses “Webpack” internally to construct the build that is surely customizable. Historically, it required ng eject which in turn ejects the default webpack configuration in the project but it moves us away from the beautiful features of Angular CLI like “Schematics”. With the newest version of Angular, it is possible to “extend” webpack configuration with something called “Angular CLI builders”.

We will go in a much simpler solution that will help us to achieve faster deployment and scalable architecture.

I’m going to assume a few basic requirements:

  • All sites may have different themes (colors and fonts).
  • A particular feature may be shared across all sites.
  • A particular feature may be available only in a limited number of sites. E.g., you want to get the early feedback of a subscription model in one site first and once it is successful then implement it across all sites.

Initial Setup

We need to start by fetching the configuration for each site. The minimum configuration will have the following parameters:

  • Theme — Name of a theme
  • Domain — URI of site
  • Features — List of all features.
  • Additionally, you can also think about fetching common domain-specific properties.

The above data should be fetched from API. The example API should look like this: GET /configuration?domain=www.xyz.com

Remember, we’re building multi-sites hence, the response of API can be different based on the “domain” parameter you pass in the configuration.

The example response can be:

{
"theme": "dark",
"domain": {
"uri": "www.xyz.com",
"phone": 1234567890,
"support": "eg@xyz.com"
},
"features": [
"dashboard",
"profile",
"subscription"
]
}

Store all of the above properties in an Angular State at the boot time so that, they’re available across modules. Although this can be achieved with plain Angular Services, for scalability and easy maintenance, I recommend the “NGRX Entity”.

NGRX is a reactive state management tool for Angular. NGRX Entity is used to store a collection (mainly JavaScript objects) and is highly recommended when you want to share the data across modules / components.

The setup of NGRX is beyond the scope of this article but there are plenty of tutorials available on the internet to help install this great tool. Also, to simplify the solution, I will use standard state management methods in examples.

Hi, thanks for reading my post. I work at CACTUS and we are a technology company accelerating scientific advancement, I am always in the looking out for highly skilled tech enthusiasts who love solving problems. Interested in joining the team, then hit me up!

Theme Architecture

So we’ve got the theme configuration from API and we’ve stored it in Angular state. Next is we’re going to load the theme for the active site.

We use Tailwind CSS with SCSS wrapper in our Project. For setting up Tailwind CSS in Angular, please read this article (credits Tamás Bajzát). Angular by default comes up with default “styles.scss” file which is loaded along with the application. We also want to load a theme file specific to sites that should be lazy-loaded after receiving the response from the “configuration API”.

  • “style.scss” will act as a global CSS. Anything added here will apply to all 20+ sites. It can be used to import common libraries like Bootstrap, FontAwesome, Normalize.css, etc.
  • Site themes (like dark, light and so on…) shall be lazy-loaded based on the active domain. It should contain site-specific properties like “primary-color”, “secondary-color”, “font sizes for the body, headers and other HTML elements”.
  • Each site may have a different variation of components per the theme that can be handled through site themes but the recommended way is to encapsulate it in component-specific styles which I will cover later in the “Component Architecture” section of this article.

While writing in the themes file, make sure you put a generic “CSS class name”. Imagine a situation where we have two sites sharing same component in the “Site A”, we use red as primary color and green as a secondary color in the “Site B”, we use black as primary color and grey as a secondary color.

Consider this to be a GOOD example, where we write more generic class names:

.primary-color {
color: red;
}
.secondary-color {
color: green;
}

The above nomenclature helps us to keep the class name consistent in the component’s template (HTML) file and the same component can be used in multiple sites with a different value of say “primary-color” and “secondary-color”.

Consider this to be a BAD example

.red {
color: red;
}
.green {
color: green;
}

Once you’ve written the two themes, we want to lazy load the CSS and the decision has to be taken at the Angular runtime. This can be achieved by adding the following lines in the build section of angular.json:

"styles": [
"src/styles.scss",
{
"input": "src/assets/scss/themes/dark/theme.scss",
"bundleName": "dark",
"lazy": true
},
{
"input": "src/assets/scss/themes/light/theme.scss",
"bundleName": "light",
"lazy": true
}
]

“lazy” property generates CSS in production build (ng build) and JS in development build (ng serve) but it doesn’t inject inside the HTML.

Next, we need to write a code to load these generated files on runtime. Since we use both development and production builds and the above method generates two types of files, our code should handle both JS and CSS versions of a theme.

Create a theme-loader.service.ts with the following code:

import { Injectable, Renderer2, Inject, RendererFactory2 } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { environment } from '../../../environments/environment';
import { HttpClient } from '@angular/common/http';
/**
* Theme definition
* @author Sahil Purav
*/
interface IThemes {
name: string;
loaded: boolean;
}
export const STORE: IThemes[] = [{
name: 'dark',
loaded: false,
},
{
name: 'light',
loaded: false,
},
];
@Injectable({
providedIn: 'root',
})
export class ThemeLoaderService {
private _themes: IThemes[] = [];
private _renderer: Renderer2;
constructor(rendererFactory: RendererFactory2, @Inject(DOCUMENT) private _document, private _http: HttpClient) {
this._renderer = rendererFactory.createRenderer(null, null);
this._initialize();
}
/**
* Initialize all themes
* @author Sahil Purav
*/
private _initialize() {
STORE.forEach((theme: any) => {
this._themes[theme.name] = {
loaded: false,
name: theme.name
};
});
}
/**
* Loads themes
* @param themes all in memory themes
*/
load(...themes: string[]) {
const promises: any[] = [];
themes.forEach(theme => promises.push(this._loadTheme(theme)));
return Promise.all(promises);
}
/**
* Checks if given theme is already loaded
* @param name name of theme
*/
private _isThemeLoaded(name: string) {
if (this._themes[name].loaded) {
return true;
}
return false;
}
/**
* Attach theme tag through Renderer2
* @param name name of theme
* @author Sahil Purav
*/
private _renderTheme(name: string) {
if (environment.production) {
const style = this._renderer.createElement('link');
style.rel = 'stylesheet';
style.type = 'text/css';
style.href = this._themes[name] + '.css';
return style;
}
const script = this._renderer.createElement('script');
script.type = 'text/javascript';
script.src = this._themes[name] + '.js';
return script;
}
/**
* get resolve params based on themes status
* @param name name of script
* @param status status of script
*/
private _setThemeStatus(name: string, status = true) {
this._themes[name].loaded = status;
return {
name,
loaded: status
};
}
/**
* Loads themes
* @param name name of script
*/
private _loadTheme(name: string) {
return new Promise(resolve => {
if (this._isThemeLoaded(name)) {
return resolve(this._setThemeStatus(name));
}
const theme = this._renderTheme(name);
theme.onload = () => {
resolve(this._setThemeStatus(name));
};
theme.onerror = () => resolve(this._setThemeStatus(name, false));
this._renderer.appendChild(this._document.getElementsByTagName('head')[0], theme);
});
}
/**
* Get theme name API and host
*/
getTheme() {
return this._http.get('configuration?domain=this._document.location.host');
}
}

As you can see, we differentiated the production and development build with an environment variable. We’ve also added additional parameter “loaded” with default “false” value to prevent loading the CSS file on each time theme loader service is invoked (this is optional). Lastly, we have got the “getTheme()” function to hit our configuration API based on currently active host and get theme name.

This theme loader has to be injected in the main AppComponent by adding the following bit of code:

this._themeLoaderService.getTheme().subscribe(response => {
const theme = response.theme;
this._themeLoaderService.load(theme);
});

PS: You will notice some delay in loading the styles on development build as theme is loaded through JS. But this should be a problem on production build as we’re injecting CSS in the HEAD portion.

…and that’s it! You’ve successfully lazy-loaded the theme based on the active domain.

Component Architecture

The previous section illustrates the lazy-loading of a theme. But we may also have a drastic difference in HTML and CSS of a specific component based on the theme. In such cases, it makes sense to create a different component altogether in the codebase but how do we load it based on the active theme?

Take an example of a landing page. Our dark theme has the following structure code in the template file of a landing component:

<div class="landing">
<h1>This is my landing page</h1>
<p>This is subsection</p>
</div>
...
some more stuff (different than light theme) fetched from API

And, we have got a different structure of landing page in light theme:

<div class="landing">
<div class="banner"></div>
<div class="interesting">Some interesting pitch</div>
<h2>This is my landing page</h2>
<p>This is subsection</p>
</div>
...
some more stuff (different than dark theme) fetched from API

To complicate it further, light and dark themes may have different component-specific styles.

While I suggest keeping such instances minimal as it defeats the purpose of choosing multi-site architecture, but in those cases, it makes sense to create separate components.

Some of you may suggest adding “if-else” in the component’s template (HTML) but it is not suitable when components get bigger and it has so many variations in CSS.

We will be utilizing the concept of “Dynamic Component Loader” where we will be creating two versions of components (LandingLightComponent and LandingDarkComponent) and a root component (LandingComponent) that is responsible for deciding which component to load based on the active theme.

Both LandingLightComponent and LandingDarkComponent may extend an abstract class that will hold the required data to render the child component.

LandingComponent

export class LandingComponent implements OnInit {
ngOnInit() {
const componentName = 'Landing' + theme + 'Component'; // Make theme titlecase
const component = resolver.resolveComponentFactory(components[componentName]); // Resolver is ComponentFactoryResolver
container.createComponent(component); // Container is <ng-container>
}

LandingDarkComponent and LandingLightComponent can now have their own version of structure and styles.

Feature Architecture

Remember the response of our configuration API? It had something like this:

{
...,
features: [
'dashboard',
'profile',
'subscription'
]
}

Features can act as a smaller portion of code in big component. As stated earlier, in multi-site architecture, we may have a particular feature available only in a specific set of sites.

Take an example of a “subscription” model. We may have a separate page where clients can come and subscribe to our services and in order to promote it, we may want to show a banner of subscription on our high-traffic landing page. But since the “subscription” feature is available only on specific sites and landing page is present on all sites, we require “if-else” loop.

To make it more dynamic, we put it in site configuration so that, if a “subscription” model is successful in one site it can be easily enabled in other sites.

Here’s how I recommend the approach:

  • Fetch the features object from Configuration API. If it is stored in NGRX Entity, it is easily accessible across services.
  • Create a new Angular service feature.service.ts with isFeatureEnabled(feature: string): boolean function which will simply check if an input is available in features object (fetched from API)
  • Use *ngIf=isFeatureEnabled(‘subscription’) in the Landing component template and show the banner only when it returns true.

That’s all! Hopefully, this clears the bigger picture of setting up a multi-site framework in Angular based application.

--

--

Sahil Purav
Cactus Tech Blog

Associate Director at Cactus Communications, India. I help building highly scalable architecture for Authors and help them to publish their papers