先日、SEO 関連の対応で canonical や noindex の設定をやっていました。
ただ、対象サイトが SPA のためちょっと苦労したので、そのあたりをまとめておこうと思います。

前提

アプリケーション自体はよくある構成です。
SPA のコードを S3 に配置し、CloudFront 経由でそこにアクセスしています。
また、すでに長く運用されているアプリケーションのため、なるべく既存処理への影響が出ないように進めていきます。

やったこと

その1・JavaScript で制御

最初は、JavaScript でタグの挿入を試みました。
以下のようなコードです。

// canonical
const canonical = document.createElement("link");
canonical.rel = "canonical";
canonical.href = "https://hoge.example.com/pages/1";
document.head.appendChild(canonical);

// noindex
const robots = document.createElement("meta");
robots.name = "robots";
robots.content = "none";
document.head.appendChild(robots);

開発者ツールでタグが挿入されていることが確認できましたし、Search Console の URL 検査ツールを使った場合でも問題なく認識してくれているようでした。
ただ、いざ運用してみるとこれがうまく認識されていないケースが出てきました。

  • noindex を指定したパスがインデックスに登録されている
  • canonical を設定しているのに正規 URL が「指定なし」と判断されている。

SPA である以上読み込むコードの量は今後も増えていくことが想定されますし、そもそもスクリプトでやるのは限界があると判断しました。

前述の前提から、SPA をやめる以外でどう対策するかを検討しました。
その結果、HTTP レスポンスヘッダーでも canonical や noindex の設定ができることを知ったため、レスポンスヘッダーに付与する方法で進めることにしました。

その2・レスポンスヘッダーポリシー

まず、CloudFront レスポンスヘッダーポリシーの利用を考えました。
ただ、テストサイトのように全体に設定するならこれで良いものの、「特定のパスだけ設定したい」というケースには対応できません。
そこで、個々のパスについてはCloudFront Functions を使って実装することにしました。

その3・CloudFront Functions (ビューワーレスポンス)

ビューワーレスポンスを設定すると、あるパスへのリクエストを受けた際、オリジン(今回の例だと S3)からクライアントに返す間に特定の処理を噛ませることができます。
そこで、以下のようなコードで特定パスへのアクセス時にレスポンスヘッダーを付与するようにしました。

function handler(event) {
  const request = event.request;
  const response = event.response;
  const uri = request.uri;

  // 特定パスへのアクセス時は、レスポンスヘッダーを追加する
  if (/^\/pages\/\d+/.test(uri)) {
    // canonical
    response.headers['link'] = {value: '<https://' + request.headers.host.value + uri + '>; rel="canonical"'};
    // noindex
    response.headers['x-robots-tag'] = {value: 'none'};
  }

  return response;
}

ところが、何度該当パスにアクセスしてもレスポンスヘッダーが付与されません。
色々調べたところ、SPA のようにオリジンが 403 や 404 を返すケースだと、ビューワーレスポンスに紐づけた関数は実行されないようです。
SPA ではアクセスしたパスにはファイルが存在しないため、エラーページに index.html を設定するケースが多いのですが、今回もこの設定がされていました。
そこで別の手段を考えることにしました。

その4・CloudFront Functions (ビューワーリクエスト・ビューワーレスポンス)

ビューワーレスポンスだけだとどうにもならないので、ビューワーリクエストも利用することにしました。
ビューワーリクエストを設定すると、オリジンにリクエストする前に特定の処理を噛ませることができます。
そこで、以下のようなコードで特定パスへのアクセス時にリクエスト先を変更するようにしました。

function handler(event) {
  var request = event.request;
  var uri = request.uri;
  const request = event.request;
  const uri = request.uri;

  // 特定パスアクセス時、オリジンのリクエスト先を index.html に書き換えておく
  if (/^\/pages\/\d+/.test(uri)) {
    // request.uri を書き換えることで viewer-response で対象パスかどうかを判定できなくなってしまうため、
    // カスタムリクエストヘッダーを追加しておく
    request.headers["x-original-uri"] = { value: uri }
    request.uri = "/index.html";
  }

  return request;
}

これに伴い、ビューワーレスポンス側のコードも修正しました。

function handler(event) {
  const request = event.request;
  const response = event.response;
  const uri = request.uri;
  const originalUri = request.headers['x-original-uri'] ? request.headers['x-original-uri'].value : null;

  // 特定パスへのアクセス時は、レスポンスヘッダーを追加する
  // viewer-request でリクエストヘッダーを追加しているので、そこから判断する
  if (uri === '/index.html' && /^\/pages\/\d+/.test(originalUri)) {
    // canonical
    response.headers['link'] = {value: '<https://' + request.headers.host.value + originalUri + '>; rel="canonical"'};
    // noindex
    response.headers['x-robots-tag'] = {value: 'none'};
  }

  return response;
}

これで期待する動作をしてくれました。
Search Console の URL 検査ツールでも、問題なく認識されていました。

まとめ

結局、以下のような方法で実現することになりました。

  1. ビューワーリクエストで、特定パスの場合オリジンのリクエスト先を変更する
  2. ビューワーレスポンスで、特定パスならレスポンスヘッダーを付与する

他にもスマートなやり方はありそうなので、ご存じの方がいれば教えていただけると嬉しいです。

SEO の要否も検討してアーキテクチャを選定すべきと言ってしまえばそれまでですが、環境の変化で要否は変わってくるので難しいところではありますね。
とはいえ明日は我が身、自分が検討する際も意識しておこうと思い知らされた今回の件でした。