Bringing SEO to Angular Applications

Andres Rutnik
Andres Rutnik

Follow

Aug 9, 2018 – 12 min read

シングルページのアプリケーションを書くとき、最も一般的なタイプのユーザー、つまり自分たちのような他の人間に対して理想的な体験を作ろうとしてとらわれるのは簡単かつ自然なことです。 このように、ある種のサイト訪問者に積極的に焦点を当てると、Google などの検索エンジンが使用するクローラーやボットなど、別の重要なグループが冷遇されることがよくあります。 このガイドでは、SPA のユーザー エクスペリエンスと SEO に関して、いくつかの簡単に実装できるベスト プラクティスとサーバー サイド レンダリングへの移行が、どのようにアプリケーションに両方の利点を与えることができるかを紹介します。 このガイドの一部は Angular 6 を扱っていますが、その知識は厳密には必要ありません。

私たちが犯す不注意な SEO ミスの多くは、Web サイトではなく Web アプリケーションを構築するという考え方に由来しています。 その違いは何でしょうか。 341>

  • Web アプリケーションは、ユーザーにとって自然で直感的なインタラクションに重点を置いています
  • Web サイトは、情報を一般に公開することに重点を置いています

しかし、これら 2 つのコンセプトは相互に排他的である必要はありません! Web サイト開発のルーツに戻るだけで、SPA の洗練されたルック アンド フィールを維持しながら、情報を適切な場所に配置し、クローラーにとって理想的な Web サイトを作成できます。

コンテンツをインタラクションに隠さない

コンポーネントを設計するときに考えるべき原則は、クローラーはある種の馬鹿であることです。 アンカーをクリックすることはあっても、要素をランダムにスワイプしたり、コンテンツに「続きを読む」と書いてあるからといって div をクリックしたりすることはないでしょう。 これはAngularと相反するもので、情報を隠すための一般的な方法は「*ngif it out」です。 そして、多くの場合、これは理にかなっています。

しかし、これは、巧妙なインタラクションによってページ上のコンテンツを隠すと、クローラーがそのコンテンツを決して見ることができない可能性があることを意味します。 この種のコンテンツを隠すために *ngif ではなく CSS を使用するだけで、これを緩和することができます。 もちろん、賢いクローラーはテキストが隠されていることに気づき、見えるテキストよりも重要度が低いと判断する可能性があります。 しかし、DOM上でテキストにまったくアクセスできないよりは、よい結果です。 このアプローチの例は次のようになります。

Don’t create “Virtual Anchors”

以下のコンポーネントは、私が「仮想アンカー」と呼ぶ、Angular アプリケーションでよく目にするアンチパターンを示しています。

基本的に何が起こっているかというと、クリック ハンドラーが <button> や <div> タグなどの何かにアタッチされていて、そのハンドラーはいくつかのロジックを実行し、その後インポートした Angular ルーターを使って別のページにナビゲートしているということです。 これは 2 つの理由から問題があります。

  1. クローラーはおそらくこれらの種類の要素をクリックしませんし、たとえクリックしたとしても、ソース ページと目的ページの間のリンクは確立されないでしょう。 ナビゲートする前に余分なロジックを実行する必要がある場合、アンカー タグにクリック ハンドラを追加することができます。

    見出しを忘れないでください

    優れた SEO の原則の 1 つは、ページ上の異なるテキストの相対的重要性を確立することです。 Web 開発者のキットにおけるこのための重要なツールは、見出しです。 Angular アプリケーションのコンポーネント階層を設計するときに、見出しのことを完全に忘れてしまうのはよくあることです。 しかし、クローラーが情報の正しい部分に注目するようにするには、この点を考慮する必要があります。 そのため、見出しタグを使用することを検討してください。 しかし、見出しタグを含むコンポーネントは、<h1> が <h2> の中に現れるような配置にはできないようにしてください。

    「検索結果ページ」をリンク可能にする

    クローラーがいかに間抜けかという原理に戻り、あるウィジェット会社の検索ページを考えてみます。 クローラーは、フォームのテキスト入力を見て、「トロントのウィジェット」のようなものを入力することはないでしょう。 341>

    1. パスやクエリを通じて検索パラメータを受け入れる検索ページを設定する必要があります。
    2. クローラーが面白いと思うような特定の検索へのリンクは、サイトマップに追加するか、サイトの他のページにアンカーリンクとして追加しなければならない。 重要なのは、検索コンポーネントとページは、ポイント #1 を念頭に置いて設計することで、可能なあらゆる種類の検索へのリンクを作成し、好きな場所に注入できるようにする柔軟性を持つことです。 これは、ページ上のクエリやフィルタリング コンポーネントだけに頼るのではなく、ActivatedRoute をインポートし、パスやクエリ パラメータの変更に反応してページ上の検索結果を駆動することを意味します。

      ページ送りをリンク可能にする

      検索ページについてですが、クローラーが望めば検索結果のすべてのページをアクセスできるよう、ページ送りが正しく扱われることを確認することが重要です。

      以前のポイントを繰り返しますが、「次」「前」「ページ番号」のリンクに「仮想アンカー」を使用しないでください。 もしクローラーがこれらをアンカーとして見ることができなければ、最初のページ以降を見ることはないでしょう。 これらは、ルーターリンクで実際の<a>タグを使用する。

      相対的な “prev” / “next” <link> タグを追加することにより、サイトのページネーションについてクローラーに追加のヒントを提供することができます。 これらが有用である理由についての説明は、以下を参照してください。 https://webmasters.googleblog.com/2011/09/pagination-with-relnext-and-relprev.html. 以下は、Angular フレンドリーな方法でこれらの <link> タグを自動的に管理するサービスの例です:

      Include dynamic metadata

      新しい Angular アプリケーションで最初に行うことの 1 つは、index.txt に調整を行うことです。ファビコンの設定、レスポンシブな meta タグの追加、およびおそらく <title> と <meta name=”description”> タグのコンテンツをアプリケーション用の適切なデフォルトに設定することなどがあります。 しかし、検索結果でページがどのように表示されるかを気にするのであれば、そこで止めるわけにはいきません。 アプリケーションのすべてのルートで、ページのコンテンツに一致するように title と description タグを動的に設定する必要があります。 これはクローラーに役立つだけでなく、ユーザーがリンクをソーシャルメディアで共有する際に、有益なブラウザのタブタイトル、ブックマーク、プレビュー情報を見ることができるため、ユーザーにも役立ちます。 以下のスニペットは、Meta クラスと Title クラスを使用して Angular フレンドリーな方法でこれらを更新する方法を示しています。

      Test for crawlers breaking your code

      Some third-party libraries or SDKs shutdown or cannot be loaded from their hosting provider when user agents belong to search engine crawlers is detected. 機能の一部がこれらの依存関係に依存している場合、クローラーを許可しない依存関係のフォールバックを提供する必要があります。 少なくとも、このような場合、クライアントのレンダリングプロセスをクラッシュさせるのではなく、アプリケーションを優雅にデグレードさせる必要があります。 クローラーとコードのやりとりをテストするための優れたツールに、Google Mobile Friendlyテストページがあります。 https://search.google.com/test/mobile-friendly. クローラーがSDKへのアクセスをブロックされていることを示す、次のような出力を探してください。

      Reducing bundle size with Angular 6

      Bundle size in Angular applications is a well known issue, but there are a many optimizations that the developer can make mitigation it, including using AOT builds and being conservative with inclusion of third party libraries.ANGGUALSHOCKER はよく知られた課題ですが、これを緩和するには、AOT ビルドを使用することで、保守的に第三者のライブラリを含めるなど、多くの最適化があります。 しかし、今日可能な限り小さなAngularバンドルを得るには、Angular 6にアップグレードする必要があります。 その理由は、RXJS 6への並行アップグレードが必要であり、ツリーシェイク機能が大幅に改善されるからです。 この改善を実際に得るには、アプリケーションにいくつかの難しい要件があります。

      • rxjs-compat ライブラリを削除する (Angular 6 アップグレード プロセスでデフォルトで追加される) – このライブラリはコードを RXJS 5 との下位互換にしますが、ツリーを揺する改善を打ち消すものです。
      • すべての依存関係が Angular 6 を参照しており、rxjs-compat ライブラリを使用していないことを確認する。
      • RXJS 演算子を 1 つずつインポートし、ツリー シェイクが確実に機能するようにするため、ホールセールにしない。 移行に関する完全なガイドは https://github.com/ReactiveX/rxjs/blob/master/docs_app/content/guide/v6/migration.md を参照してください。

      Server Rendering

      前述のベスト プラクティスをすべて実行しても、あなたの Angular Web サイトが希望するほど上位にランキングされないことがあります。 この理由として考えられるのは、SEO の文脈における SPA フレームワークの基本的な欠陥の 1 つである、ページのレンダリングに Javascript を使用していることです。 この問題は 2 つの方法で現れます。

      1. Googlebot は Javascript を実行できますが、すべてのクローラーで実行できるわけではありません。 159>
      2. ページが有用なコンテンツを表示するには、クローラーは、Javascript バンドルのダウンロード、エンジンの解析、コードの実行、および外部 XHR のリターンを待つ必要があり、その後 DOM 内にコンテンツが存在することになります。 ドキュメントがブラウザにヒットするとすぐに DOM で情報が利用できる、より伝統的なサーバー レンダリング言語と比較すると、SPA はここで多少ペナルティを受ける可能性があります。

      幸運にも、Angular にはこの問題に対するソリューションがあり、サーバー レンダリング形式でアプリケーションを提供することができます。 Angular Universal (https://github.com/angular/universal) です。 このソリューションを使用した典型的な実装は次のようになります。

      1. クライアントがアプリケーション サーバーに特定の URL に対する要求を行います。 このサービスは、アプリケーション サーバーと同じマシン上にある (ただし、必ずしもそうではない) 可能性があります。
      2. サーバー バージョンのアプリケーションは、クライアント Angular アプリケーションをダウンロードするための <script> タグを含め、要求されたパスとクエリに対して HTML と CSS を完全にレンダーリングします。 クライアント アプリケーションが非同期にロードされ、準備ができると、現在のページを再レンダリングして、サーバーがレンダリングした静的な HTML を置き換えます。 これで Web サイトは、今後のあらゆるインタラクションに対して SPA のように動作するようになります。

      ただし、このマジックは無料で得られるものではありません。 このガイドで 2 回ほど、「Angular フレンドリー」な方法で物事を行う方法について言及しました。 私が本当に言いたかったのは、「Angular サーバー レンダリング フレンドリー」ということです。 DOMを直接触らない、setTimeoutの使用を制限する、といったAngularに関するベストプラクティスはすべて、それに従わなければ、読み込みが遅くなったり、ページが完全に壊れたりといった形で、あなたに跳ね返ってくるでしょう。 Universalの「gotchas」の広範なリストはこちらで見ることができます。 https://github.com/angular/universal/blob/master/docs/gotchas.md

      Hello Server

      Universal でプロジェクトを実行するには、いくつかの異なるオプションがあります:

      • Angular 5 プロジェクトの場合、既存のプロジェクトで次のコマンドを実行できます:
        ng generate universal server
      • アンギュラー 6 プロジェクトには、クライアントおよびサーバーで動作する Universal プロジェクトを作成できる公式 CLI コマンドがまだ存在しません。 既存のプロジェクトで次のサードパーティ製コマンドを実行できます。
        ng add @ng-toolkit/universal
      • また、このリポジトリをクローンしてプロジェクトの出発点として使用したり、既存のプロジェクトにマージしたりすることも可能です。 https://github.com/angular/universal-starter

      Dependency injection is your (server’s) friend

      典型的な Angular Universal のセットアップでは、3 つの異なるアプリケーション モジュール (ブラウザ専用モジュール、サーバー専用モジュール、共有モジュール) があります。 これを利用して、コンポーネントが注入する抽象的なサービスを作成し、各モジュールでクライアントとサーバー固有の実装を提供することができます。 例えば、ある要素にフォーカスを設定するサービスを考えてみましょう。抽象サービス、クライアント、サーバーの実装を定義し、それぞれのモジュールで提供し、コンポーネントで抽象サービスをインポートします。

      サーバーにとって有害な依存関係を修正する

      Angular ベスト プラクティスに従っていない任意のサード パーティ製コンポーネント (ドキュメントまたはウィンドウを使用するなど) によって、そのコンポーネントを使用する任意のページのサーバー レンダリングがクラッシュする可能性があります。 最良の選択肢は、ライブラリのユニバーサル互換性のある代替品を見つけることです。 これが不可能な場合もありますし、時間の制約で依存関係を置き換えることができない場合もあります。 このような場合、ライブラリが干渉するのを防ぐために 2 つの主なオプションがあります。

      サーバー上で問題のあるコンポーネントを *ngIf out することができます。 これを行う簡単な方法は、現在のプラットフォームに応じて要素がレンダリングされるかどうかを決定できるディレクティブを作成することです。

      一部のライブラリはより問題があります。 たとえば、グローバル スコープで利用可能な jquery を消費者が持つことを期待するのではなく、npm 依存性として jquery をインポートする任意のライブラリです。 これらのライブラリがサーバーをクラッシュさせないようにするには、問題のあるコンポーネントを *ngIf で除外し、依存するライブラリを webpack から除外する必要があります。 jquery をインポートするライブラリが ‘jquery-funpicker’ と呼ばれると仮定すると、サーバー構築からそれを取り除くために、次のような webpack ルールを記述できます:

      このためには、プロジェクト構造内の webpack/empty.json に {} という内容のファイルを配置することも必要とされます。 その結果、ライブラリは ‘jquery-funpicker’ import ステートメントに対して空の実装を取得することになりますが、新しいディレクティブによって、サーバーアプリケーションのあらゆる場所でそのコンポーネントを削除したので、問題ありません。

      ブラウザーのパフォーマンスを向上させる – XHR を繰り返さない

      Universal の設計の一部は、アプリケーションのクライアント バージョンが、クライアント ビューを作成するためにサーバー上で実行されたすべてのロジックを再実行することで、サーバー レンダリングがすでに行ったバックエンドへの同じ XHR 呼び出しを含む! これによって、バックエンドに余分な負荷がかかり、クローラーにはページがまだコンテンツをロードしていると認識されます。 データの陳腐化が懸念されない限り、クライアントアプリケーションがサーバーが既に作成したXHRを重複して実行しないようにすべきです。 AngularのTransferHttpCacheModuleはこれを助けることができる便利なモジュールです。 https://github.com/angular/universal/blob/master/docs/transfer-http.md

      フードの下で、TransferHttpCacheModule は TransferState クラスを使用し、サーバーからクライアントへのあらゆる汎用状態転送に使用することができます。

      Pre render to move time-to-first-byte towards zero

      Universal (または https://prerender.io/ などのサード パーティ レンダリング サービス) の使用時に考慮すべきことは、サーバー レンダー ページはクライアント レンダー ページより最初のバイトがブラウザにヒットするまでに長い時間を有するだろうということです。 これは、サーバーがクライアント レンダリング ページを配信するには、基本的に静的な index.html ページを配信する必要があることを考慮すると、理解できるはずです。 アプリケーションが「安定」していると見なされるまで、Universal はレンダリングを完了しません。 Angular のコンテキストにおける安定性は複雑ですが、安定性の遅延に最も貢献するのはおそらく次の 2 つです:

      • Outstanding XHRs
      • Outstanding setTimeout calls

      上記をさらに最適化する方法がない場合は、アプリケーションの一部またはすべてのページを単に事前レンダリングしキャッシュからそれらを提供することにより time-to-first-byte を減らす選択肢があります。 このガイドの最初のほうでリンクしたAngular Universalのスターターレポには、プリレンダリングの実装が含まれています。 プリレンダリングされたページを取得したら、アーキテクチャに応じて、Varnish、Redis、CDN、あるいは複数の技術を組み合わせたキャッシュソリューションが考えられます。 サーバーからクライアントへのレスポンス パスからレンダリング時間を削除することにより、クローラーやアプリケーションのユーザーに対して非常に高速な初期ページ ロードを提供することが可能になります。 異なるページに対して有益なタブ タイトルを持つという単純なことで、比較的低い実装コストで大きな違いが生まれます。 Web が進化するにつれ、クローラーやスクリーン キャプチャ サーバーが、ユーザーがデバイスで行う操作に沿った方法で Web サイトとやり取りする日が来ることを期待しています。 今のところ、開発者として私たちは古い世界をサポートし続けなければなりません。

コメントする