[Angular] head要素内に配置したlink要素でCanonical Urlを書き換えるサービスクラスの実装方法


2020/04/21

細かいところですが、WebページのSEO対策でURLアドレスにCanonical名を設定することで、クローラーが正しくページを読み込んでインデックスを作成してくれるなどの利点が挙げられます。

Angularでウェブベージをフルスクラッチして作成する場合、なんとも悩ましいのはrouterのページ遷移とhead要素のcanonicalの動的変更の方法です。

基本的にはページが変われば、canonical urlの値は適切に変わる必要があります。

AngularのようにSPAに特化したフレームワークでは、そのままではこの値を変えることはありません。

            
            ...
<head>
    <link rel="canonical" href="http://example.com/">
</head>
...
        
今回は上記のように動的にcanonical urlの値を更新するサービスを記述します。

ちなみに、何故ページごとに書き換えないと駄目なのかは、以下のページが詳しいので、そちらをご覧ください。

canonicalとは〜URLの正規化でSEOのマイナス評価を避けよう

なお、記事内のプロジェクトは、angular7以降にて動作確認させております。


DOMにアクセスさせる

まずは、CanonicalLinkServiceという名のサービスを新規作成し、直接DOM操作をできるようにDOCUMENTというDIトークンを使います。

この
DOCUMENT@angular/commonから呼び出すことで利用できます。

また使う場合には、
InjectデコレーターでDIするのが作法のようです。

            
            import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Injectable({
    providedIn: 'root'
})

export class CanonicalLinkService {
    constructor(
        @Inject(DOCUMENT) private dom
    ) { }

    ...
        
これで、直にhtmlのhead要素を操作できるようになります。

それでは以降で、
<link>タグのcanonical値を操作してみましょう。


canonical値を追加する関数を実装

ではまず、html内に存在する<link>タグを検出し、その値を任意のurl名に書き換えられるようなcreateCanonicalUrlメソッドをCanonicalLinkServiceへ追加します。

            
            import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Injectable({
    providedIn: 'root'
})

export class CanonicalLinkService {
    //...中略
    private createCanonicalUrl(url?: string) {
        const canURL = url === undefined ? this.dom.URL : url;
        const link: HTMLLinkElement = this.dom.createElement('link');
        link.setAttribute('rel', 'canonical');
        this.dom.head.appendChild(link);
        link.setAttribute('href', canURL);
    }
    //...以下略
        
設定したいurl値があったら、新しく作成したlink要素にrel="canonical"href="urlの値"を加えて、設定しているメソッドになります。

これで無事、ページごとに
canonical値を書き換えられるサービスができたのですが、出来上がったhtmlのソースコードをよく見ると、昔の<link>タグに既に設定済みのcanonical値が残っていることがあります。

同じ一つのページに2つ以上の
canonicalをもったlinkタグがあっては、全く意味をなさないので、昔のcanonical値を前もって除去する必要があります。


古いcanonical値を消す方法

上でも説明したように、このままcreateCanonicalUrl関数を呼び出すだけだと、head要素にいくつものcanonical値を持ったlink要素が増えていきます。

よって、古いものは残さないのように消去してくれる
refreshCanonicalUrl関数を作成しましょう。

            
            import { Injectable, Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';

@Injectable({
    providedIn: 'root'
})

export class CanonicalLinkService {
    //...中略
    private refreshCanonicalUrl() {
        const links = this.dom.head.getElementsByTagName('link');
        for (const link of links) {
            if (link.getAttribute('rel') === 'canonical') {
                this.dom.head.removeChild(link);
            }
        }
    }

    private createCanonicalUrl(url?: string) {

        this.refreshCanonicalUrl() // Refresh links from the head element

        const canURL = url === undefined ? this.dom.URL : url;
        const link: HTMLLinkElement = this.dom.createElement('link');
        link.setAttribute('rel', 'canonical');
        this.dom.head.appendChild(link);
        link.setAttribute('href', canURL);
    }
    //...以下略
        
このrefreshCanonicalUrlを呼び出した後に、改めて一意に決まるcanonical値を設定することで、適切なheadの内容のページ遷移が可能になります。

参考1

How to set Canonical URL in Angular 7

Angular Title Service and Canonical URL


【おまけ】HTMLCollection(getElementsByTagNameの返り値)をIterableにする

お手元の開発環境によっては、利用しているtypescriptcore-jsのバーションを上げて言った結果、上記で利用した以下のコード部分、

            
            //...
const links = this.dom.head.getElementsByTagName('link');
for (const link of links) { //👈ここのイテレーションでエラー
    if (link.getAttribute('rel') === 'canonical') {
        this.dom.head.removeChild(link);
    }
}
        
not iteralbe等のコンパイルエラーで引っかかるようになってくるかも知れません。

これはバグではなく、
HTMLCollectionクラスの扱いに仕様変更がなされたためのようです。

要は、最近の
HTMLCollectionクラスのインスタンスは配列としては扱えないようです。

そこで
Array.prototype.slice.callのようなプロトタイプ関数で配列を返すようにすることが考えられます。

            
            //...
const links = Array.prototype.slice.call(this.dom.head.getElementsByTagName('link')); //Array化
for (const link of links) {
    if (link.getAttribute('rel') === 'canonical') {
        this.dom.head.removeChild(link);
    }
}
        
またモダンなtslintではシンタックス警告がでるのですが、以下のように単純なforループも使えます...。

            
            //...
const links = this.dom.head.getElementsByTagName('link');
for (let i = 0; i < links.length ; i++) {
    if (links[i].getAttribute('rel') === 'canonical') {
        this.dom.head.removeChild(links[i]);
    }
}
        
以上、HTMLCollectionのイテレーションの扱いにはご注意ください。

参考2

HTMLCollectionをiterableにする

記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。