Fixing the SEO problem of Angular

Angular 2 or simply Angular and .NET Core are two great modular frameworks. They allow developers to achieve a lot with less code. In this article we will look at why Angular is good and bad. After that we will see how to add some traditional HTML in front of our Angular app so that we do not have to sacrifice SEO or modern web development. Also, just to spice things up, we have included some tips on how to combine Angular with .NET Core.

Angular

Feel free to skip to the next section if you are familiar with how Angular works. This framework benefits from TypeScript – a superset of JavaScript , it allows us to work with static typing, classes and interfaces. As developers typescript gives us the ability to work with object-oriented programming. We can really structure our code with TypeScript and Angular. Lets look at the following example.

Here we will ask a server to get the visitor’s country by the IP address.

TS

import { Component, OnInit } from '@angular/core';
import { DataService } from './shared/dataService';
import { Router } from '@angular/router';
import { CountdownService } from './shared/timer';
import { DataService } from './shared/dataService';


@Component({
    selector: 'my-start,
    templateUrl: "start.component.html"

})


export class StartPage implements OnInit {

    constructor(private data: DataService, private router: Router,
private countdownService: CountdownService) {
    }

    //App StratUp

    ngOnInit(): void {

        this.data.getClientCountry()
        .subscribe(success => {
            if (success) {

               
                if (this.data.country.countryCode == "US" ||
this.data.country.countryCode == "GB") {
                    this.data.selectedLanguage = 1;
                }
                if (this.data.country.countryCode == "RU") {
                    this.data.selectedLanguage = 2;
                }
                if (this.data.country.countryCode == "NL") {
                    this.data.selectedLanguage = 3;
                }
                this.router.navigate(["login"])
                

            }
        }, err => {
                this.data.selectedLanguage = 1;
                this.router.navigate(["login"]);
    });

    }

}

Angular apps are spread over separate files called components. In our StartPage component we have imported a class called DataService from the dataService component. We can access the functions of DataService with the prefix this.data. Here we use this.data.getClientCountry() to call the getClientCountry() function.

TS

import { HttpClient, HttpHeaders, HttpParams} from "@angular/common/http";
import { Injectable } from "@angular/core";
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { Observable } from "rxjs"

import { ClientIP, ClientCountry } from "./circle";


@Injectable()

export class DataService {

    storeIP: ClientIP = new ClientIP();
    country: ClientCountry = new ClientCountry();


    public getClientCountry(): Observable<boolean> {
        return this.http.get("/api/circledmmsg/getcountry", {
            params: new HttpParams().set('Ip', this.storeIP.ip)
            
        })
            .map((data: any) => {
                this.country = data;
                return true;
            });
    }

The getClientCountry() function sends a HTTP GET request to the server. It uses two imported classes from the .circle component to store data. The class storeIP: ClientIP stores the client IP address, in this example the IP address is set by another function. The country: ClientCountry will store the result of the HTTP GET call. Here we can see the two classes in the .circle component.

TS

export class ClientIP {
    ip: string;
}

export class ClientCountry {
    id: number;
    countryCode: string;
    country: string;
}

Usually you would use an interface instead of a class here, but in our app we want to initialize new instances of the ClientIP and the ClientCountry so we set them as classes.

Back into our DataService class the getClientCountry function uses an rxjs observable to tell our first function back in the StartPage component whether the call to the server was successful or not. With the this.data prefix the StartPage class has access in the DataService class to the instance of the ClientCountry class. Based on the contents of the instance of the ClientCountry class the StartPage class can implement some logic and call other functions.

We have pure object-orientated programming working here, we can structure our code and reuse it repeatedly across the app. Also, Angular comes with plenty of features, modules and with npm we can add numerous third party modules to our project as well.

The Fix

Angular is spotless and easy to work with, but very bad when it comes to SEO. When you deploy your angular app it is compiled into a few large JavaScript files. Search engines are bad in indexing JavaScript. Currently, Google is trying to develop a way of indexing JavaScript pages but the result of their efforts is light years away from the SEO performance of a HTML5 page.

There is a way to code with Angular and to have great SEO performance. The basic idea is to put plain HTML pages in front of the components of the Angular app.

Using a View .cshtml file in .NET Core as a fix

If you are not using .NET Core feel free to skip to the next section. You can integrate your Angular app in the directory of your .NET CORE app. In that case you also need to specify in your Startup.cs file that you want to use SPA in your middleware. Just add to your Configure class the following code. Note that order here is important. So app.UseSpaStaticFiles() and app.UseStaticFiles() go before app.UseMvc(). Also, app.UseSpa() goes after app.UseMvc().

C#

app.UseStaticFiles();
app.UseSpaStaticFiles();


app.UseMvc(cfg =>
{
	cfg.MapRoute("Default", 
		"{controller}/{action}/{id?}",
		new { controller = "App", Action = "Index" });
	cfg.MapSeoRoutes();

});


app.UseSpa(spa =>
{

	spa.Options.SourcePath = "ClientApp";

	if (env.IsDevelopment())
	{
		spa.UseAngularCliServer(npmScript: "start");
	}

});

We will look at the file structure of a .NET Core + Angular app further down this article.

You can select one of your Views to host the Angular app. We need to add links to the .js files of the compiled Angular app in our View .cshtml file. The following example can give you an idea on how to do that. The compiled Angular files are located in the dist folder that is located in the clientapp folder on your system.

HTML

@section Scripts{
    <script src="~/clientapp/dist/runtime.js"></script>
    <script src="~/clientapp/dist/polyfills-es5.js"></script>
    <script src="~/clientapp/dist/polyfills.js"></script>
    <script src="~/clientapp/dist/main.js"></script>
}


<div id="Info" class="text-center">

    <table border="0" cellpadding="0" cellspacing="0" width="0" 
style="table-layout: auto; width:100%;">

        <tr>
            <td>
               <p>Some plain html here.</p>
            </td>
        </tr>

	</table>

</div>


<my-start></my-start>

You also need to add the selector of the angular component you want to display in your View .cshtml file. In this example we have added the “my-start” selector that corresponds to the StartPage component of our Angular app.

It is up to you what you want to add to your View .cshtm for SEO. A meta description, title tag and some Open Graph meta tags are always a good place to start, throw in whatever you need.

In the above scenario the purpose of the .NET Core app is to perform server side operations and to hold your View .cshtml files that link to components in the Angular app. The .NET Core app is a skeleton for the great client side development we can do with our Angular app.

Using a sub domain or a virtual directory as a fix

You can deploy your angular app to a sub domain or a different domain entirely and use some simple urls with parameters to get your app to do whatever you want. Of course you can use something as simple as a window.location.href = “” to build your urls. You send urls from different HTML pages with some JavaScript and have a blast. HTML pages are great for SEO and your angular app gives you the modern web development you need.

Simple example. We have a link called Gallery in a plain html page.

HTML

<a href="https://example.com/#search?group=gallery">Gallery</a>

The link points to the domain that holds our Angular app – https://example.com. Everything after the hash # symbol in the url is ignored by the server, however the angular app reads it. So we tell our Angular app that we want to navigate to our Search component. We send over the url parameter “group” with the value “gallery” and we want our app to read it.

In order for this work we open the app.module.ts file located in the app folder that is located in our ClientApp folder. We add the following code to our imports:[] statement.

TS

RouterModule.forRoot(routes, {
	useHash: true,
	enableTracing: false // for Debugging of the Routes
})

For our navigation we add the “search” path in our route options. I set the route options right before the @NgModule({}) , but that is just my personal preference.

TS

let routes = [
    { path: "", component: AppComponent },
    { path: "search", component: Search }
]

Finally, we add the Search component to declarations: [].

TS

declarations: [
	AppComponent,
	Search

],

Let’s go to our search component. Now it can be navigated with our url, so let’s see how to extract the url parameters.

TS

import { Component, OnInit } from '@angular/core';
import { DataService } from '../shared/dataService';
import { Router, ActivatedRoute } from '@angular/router';
import { CountdownService } from '../shared/timer';


@Component({
    selector: 'search',
    templateUrl: "search.component.html",
    styleUrls: ["search.component.css"],

})


export class Search implements OnInit {

	constructor(private data: DataService, private router: Router,
private countdownService: CountdownService, private route: ActivatedRoute) {}
		
    ngOnInit(): void {

        this.route.queryParams.subscribe(params => {
          
            let group = params['group'];
            if (group != null) {
                
               	//Some function here
				if(group == "Bananas"){
					//Some function there
				}
            }

        });

    }

}

We have imported the ActivatedRoute module from our @angular/router and we have set it up in our constructor as route. We use this.route.queryParams.subscribe(params => {}) to get the parameters from the url. Inside the function we set “group” to be equal to the group parameter from our url. We use some simple boolean to check which of these parameters are populated, we call some functions accordingly and have a blast.

Why use .NET Core (with Angular)?

.Net Core is a suitable framework for the backend of your project. It fully supports C# and you can do some pretty advanced stuff with this language. If you are hosting your project on Azure or a Windows Server deploying your project from Visual Studio is a breeze. Not to mention that with NET Core 2.2+ you get in-process hosting for your web app which gives it some awesome performance. (see this blog post from Rick Strahl on in-process hosting)

Microsoft made an effort here to make .NET Core modular. You have a lot of functionality ready for you. With the Nuget package manager you can add modules from Microsoft or numerous third party developers. Not to mention that .NET Core is an open source and an entirely cross-platform framework. Why not combine it with Angular and have modular development across your entire project? One reason to combine it with Angular is that .NET Core is not as nearly as good as Angular when it comes to client side development.

Example file structure of a .NET Core + Angular app.

Image containing computer code

Our angular.json, package-lock.json, package.json and tsconfig.json are located in the very root of our project, the ClientApp folder which is the source folder for our Angular app is also located in the root.

In the following screenshot we can see that the output of our Angular app is in the wwwroot folder. We can specify the location of the Angular app output folder in our Angular.json file.

The SEO performance of Angular today is very bad, but with some traditional HTML in front of our app we do not need to sacrifice SEO or modern web development.