SEO for angular apps

TL;DR

Angular is great for building single-page applications (SPA) that provides seamless transition between pages which enabled better navigation experiences for users. The way that SPA navigates to a different page is by rewrite the content in the index.html file (the only html file) with the content of the new page, unlikely traditionally we store each page as individual HTML file and serve to users on requests. One immediate disadvantage of SPAs is they are not SEO (search engine optimisation) friendly.

Since there is only one HTML file it makes difficult for search engines to crawl the metadata information of other parts of the website other than the home page, a common solution is to rewrite each url’s website metadata such as page title, author, description, and many more metadata defined in the Open Graph Protocol. Each url should at least have the title and the description metadata inside the head tag:

<meta name="title" content="SEO for angular apps" />
<meta
  name="description"
  content="Write a description so that potential visitors have better understanding of this page."
/>

Although Scully (a static site generator for Angular) generates index.html files per page/route which helps search engines to crawl the contents, we still need a way to change the meta and the title programmatically for each page.

I have discovered that there aren’t many off-the-shelf SEO packages on NPM available for Angular and packages like ngx-seo was published a year ago. On the other hand, I have also found that Angular is providing the platform-browser API and unsurprisingly it is the corner stone of these SEO packages. The platform-browser API has two classes Meta and Title which allows us to manage HTML meta tags and the title of a current HTML document. Sounds like a solution for what we try to achieve right?

SEO service and component

To keep things organised and reusable in the future, let’s create a new service for our SEO related functions. I have used the command ng g service shared/services/seo --skip-tests but you can create it anywhere to suit your need. In addition, I created a dedicated SEO component so that it can be reused across different pages:

ng g module shared/components/seo
ng g component shared/components/seo/ -m seo

Meta tags

Beside the title and the description, we also need to find out important meta tags to be included in each page. I have selected the following tags but you can find out more on Open Graph Protocol and MDN.

  • title (and universal title template)
  • description
  • keywords
  • author
  • og:title
  • og:description
  • og:type
  • og:image
  • og:url
  • og:site_name (title if blog)
  • twitter:card
  • twitter:site
  • twitter:creator
  • twitter:title
  • twitter:description
  • twitter:image
  • twitter:image:alt

SEO service

The SEO service basically is a wrapper around the Title and Meta services provided in @angular/platform-browser, we keep everything that interact with these services here to hide implementation details. To keep things simple I have use one function for multiple meta tags which could share the same meta. For example, I can use the blog description for metas description, og:description, and twitter:description.`

import { Injectable } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root',
})
export class SeoService {
  constructor(private titleService: Title, private metaService: Meta) {}

  setPageMetas(metas: Metas): void {
    this.updateTitle(metas.title);
    this.updateDescription(metas.description);
    this.updateAuthors(metas.authors);
    this.updateKeywords(metas.keywords);
    this.updateOgMetas(metas.type, metas.site_name, metas.url);
    this.updateImage(metas.image);
    this.updateTwitterMetas(metas.twitterCard, metas.twitterCreator);
  }

  updateTitle(title: string | undefined): void {
    this.titleService.setTitle(title ?? 'yldweng');

    for (let meta of ['og:title', 'twitter:title']) {
      this.metaService.updateTag({
        name: meta,
        content: title ? `${title}${environment.titleTemplate}` : 'yldweng'
      });
    }
  }

  updateDescription(description: string | undefined): void {
    const metaArr = ['description', 'og:description', 'og:image:alt', 'twitter:description'];

    for (let meta of metaArr) {
      this.metaService.updateTag({
        name: meta,
        content: description ?? environment.description
      });
    }
  }

  ... skip ...

  updateImage(image: string | undefined): void {
    for (let meta of ['image', 'og:image', 'twitter:image']) {
      this.metaService.updateTag({
        name: meta,
        content: image ? `${environment.url}${image}` : `${environment.url}${environment.image}`
      });
    }
  }

  updateTwitterMetas(
    card: string | undefined,
    creator: string | undefined
  ): void {
    this.metaService.updateTag({
      name: 'twitter:card',
      content: card ?? environment.twitterCard
    });
    this.metaService.updateTag({
      name: 'twitter:creator',
      content: creator ?? environment.twitterID
    })
  }
}

export interface Metas {
  title?: string,
  description?: string,
  authors?: any[] | string,
  keywords?: any[] | string,
  type?: string,
  site_name?: string,
  url?: string,
  image?: string,
  twitterCard?: string,
  twitterCreator?: string
}

With this service, other services and components can call the setPageMetas function to create/update page metas through dependency injection.

SEO component

Although we could reuse the SEO service across other parts of the application, say, I have a blog post component and import the service to do whatever is needed. However, it is recommended to create a dedicated component for each feature whenever possible and delegate the application logic to services. This separation between components (user faced building block) and services allows you to produce maintainable application which is easy to refactor in the future.

import {
  Component,
  Input,
  OnInit,
  OnChanges,
  SimpleChanges,
} from '@angular/core';
import { environment } from 'src/environments/environment';
import { SeoService } from '@shared/services/seo.service';

@Component({
  selector: 'app-seo',
  templateUrl: './seo.component.html',
  styleUrls: ['./seo.component.less'],
})
export class SeoComponent implements OnInit, OnChanges {
  @Input() title: string | undefined = 'yldweng';
  @Input() description: string | undefined = environment.description;
  @Input() keywords: string[] | string | undefined = environment.keywords;
  @Input() authors: string[] | string | undefined = environment.author;
  @Input() image: string | undefined = environment.image;
  @Input() twitterCard: string | undefined = environment.twitterCard;
  @Input() type: string | undefined = 'articles';

  constructor(private seoService: SeoService) {}

  ngOnInit(): void {
    this.seoService.setPageMetas({
      title: this.title,
      description: this.description,
      keywords: this.keywords,
      authors: this.authors,
      image: this.image,
      twitterCard: this.twitterCard,
      type: this.type,
    });
  }

  /**
   * Execute when there are changes to Inputs
   * @param changes
   */
  ngOnChanges(changes: SimpleChanges): void {}
}

Now we can import this component into other components to update metas! I have used the blog post component as an example:

<app-seo
  *ngIf="currentPost$ | async as currentPost"
  [title]="currentPost.title"
  [description]="currentPost['description']"
  [authors]="currentPost['authors']"
  [image]="currentPost['thumbnail']"
  [keywords]="currentPost['tags']"
  type="Blog"
>
</app-seo>
<!-- Other parts of the blog-post component -->

Open the dev tools in the browser and I can see metas has been generated for my current blog page!

Result

Reference