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


※ 当ページには【広告/PR】を含む場合があります。
2020/04/21
2022/08/10
【SEOのGoogleアナリティクス(GA4)対応】シンプルなJavascript/HtmlのスクリプトでGoogleタグ(gtag.js)からイベントトラッキングを操作する




細かいところですが、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以降にて動作確認させております。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集

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 値を操作してみましょう。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集

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 値を前もって除去する必要があります。


合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集

古い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 7Angular Title Service and Canonical URL
合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集

【おまけ】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メインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログ【効果的学習法レポート・2025年最新】Angular(JSフレームワーク)をこれから学びたい人のためのオススメ書籍&教材特集