Backbone.js + Rails + AWSでWebサービスを開発したお話(その3:SEO/OGP)

Pocket

弊社では、新サービス「otacco – おたっこ|オトナになったオタク女子のための情報サイト」を先日ローンチいたしました。

otacco

その1では主に弊社のRailsでの開発手法を、その2ではotaccoのAWSおよびミドルウェアの構成に関してご紹介させて頂きましたが、今回はシングルページアプリケーションを開発する際に問題になりやすい、SEOとOGPのノウハウに関して解説させて頂きたいと思います。

SEO

結論から申し上げますと、一般的なAjaxページであれば、ここ数年間のGoogle botの機能向上により、URLにHashbang(#!)を使用してHTMLスナップショットを返すという、Googleが公式ドキュメントで推奨している方式もはや不要になったと考えて良いのではないかと思われます。詳細は下記の記事等をご参照ください。

AjaxやSPAのHTMLスナップショットをSEO向けに作る必要はなし

ただし、あらゆる種類のAjaxページでGoogle botがコンテンツを正しく認識出来るのかどうかは分かりません。Google Search ConsoleのFetch as Google機能を使用した際にコンテンツが正確にレンダリングされるかどうかが一つの目安になりそうな気もしますが、確証はありませんので、こちらはあくまで参考情報であると考えて頂ければと思います。

以下、hash fragment方式、Hashbang方式、そしてpushState(History API)を使用した方式に関して解説いたします。

(なお、この記事では「hash fragment」は「#だけで区切られたURLの形式」、「Hashbang」は「#!で区切られたURLの形式」という前提で説明させて頂きます。SEO用語に関してはこちらの記事が詳しいです)

hash fragment

Backbone.jsのデフォルトである「#だけで区切られたURL」は、Googleのインデックス対象外となります。

ネット上では「hash fragment形式でもGoogleはインデックス化することがある」というような情報も見受けられましたが、これはかなり特殊なケース(もしくはHashbangとの混同?)なようで、少なくともotaccoの場合、#だけで区切られたURLではGoogleはインデックス化してくれませんでした。

よって、Backbone.jsでシングルページアプリケーションを開発する場合、SEOを考慮するのであれば、少なくともhash fragment方式は不可ということになると思われます。

Hashbang

「#!」でURLを区切り、「?escaped_fragment=」というクエリ文字列の付加されたGoogle botからのリクエストに対してHTMLスナップショットを返すという方式は、Googleが公式に推奨している方式ではありますが、過去にこの手法を採用していたFacebookやTwitterが現在ではもうHashbangを使用していないこと、およびこのURL形式がかなり評判が悪い&将来的に廃止される可能性が高そうということもあり、otaccoではこの手法は採用しませんでした。

どうしてもHashbang&HTMLスナップショットで対応したいという場合、Railsでは下記のようなgemを使用する方法もあるようです。(内部でヘッドレスブラウザを使用してAjaxページをレンダリングし、それをGoogle botに返すというような方式になるようです)
google-ajax-crawler

pushState

pushState/popStateは、HTML5でHistory APIに追加された機能ですが、otaccoではこの機能を使用して、URLは全て「/」のみで区切る方式を採用いたしました。

Backbone.jsでのpushStateの使用方法に関しては下記をご参照ください。
試して学ぶ Backbone.js入門5

また、otaccoでは、アンカーがクリックされた際の挙動に関しては下記の記事を参考にいたしました。そのため、URLは全て「/」から開始する方式で統一しています。
Proper Link Handling With PushState

pushStateを使用する場合の大きな問題点は、IE8とIE9、および一部のAndroid標準ブラウザ(version3〜4.1)では動作しないということです。
Can I use history?

otaccoの場合、対象となるユーザー様でIE8やIE9を使用されている方はほぼいらっしゃらないと想定されること、および各ブラウザのシェアや工数等を考慮して上記は動作対象外とさせて頂きましたが、IE8とIE9に関しては、現時点(2015年9月時点)でもまだそれなりのシェアを確保しているため、pushState方式を選択される方はこの点をよくご確認された方がよろしいかと思われます。
Desktop Browser Version Market Share

蛇足ながら、現在IE8もしくはIE9を使用しているユーザーのOSは、既にMicrosoftのサポート対象外となっているXP、もしくは2017年4月でサポートが終了するVistaのいずれかと思われますが、ハードウェアの耐久年数等を考えれば、おそらく順次Windows 10やその他のOSに移行していくものと思われますので、IE8やIE9への対応に大きな工数を割くのは、一般的なWebサービスの場合あまり得策ではないのでは、という気はいたします。
各Windows OSで利用できるIEのバージョンを知る

OGP

FacebookまたはTwitterのbotは、残念ながらAjaxコンテンツを解釈してくれません。つまり、OGP用のタグをJavaScriptで動的に変更しても、Facebook/Twitterのbotはその変更を認識してくれないということになります。

よって、Facebook/Twitterのbotからアクセスされた場合、何らかの手段で「OGPタグがそのページ用にレンダリングされた状態の静的HTML」を返す必要があるということになりますが、otaccoではRails側に「ogpタグ専用のHTMLページ」を出力するためのコントローラを作成し、Facbook/Twitterのbotからアクセスされた場合はそのページ用のHTMLを動的に生成して返す、という方式を採用いたしました。

ちなみに、以下で説明いたしますが、このHTMLページに関しては、最低限のHTMLタグとOGPタグが記述されていれば、コンテンツは一切不要のようです。

titleタグやmeta descriptionタグは一応設定しておりますが、固定の文字列を出力しております。(もしかするとこのタグも不要かもしれません)

HTMLテンプレート

FacebookもしくはTwitterのbotからアクセスされた際に出力するHTMLのテンプレートは下記のようになります。(Twitter Cards用のOGPはページ毎に出力するタグを切り替えています)

# index.html.erb
<!DOCTYPE html>
<html>
<head>
<title>otacco</title>
<meta name="description" content="otacco">
<%= render 記事ページ?(@origin) ? {partial: "twitter_entry", locals: {entry: 記事を取得()}} : {partial: "twitter"}   %>
<meta property="og:locale" content="ja_JP">
<meta property="og:site_name" content="otacco">
<meta property="og:url" content="<%= get_ogp_url @origin %>">
<meta property="og:type" content="<%= get_ogp_type @origin %>">
<meta property="og:title" content="<%= get_ogp_title @origin %>">
<meta property="og:description" content="<%= get_ogp_description @origin %>">
<meta property="og:image" content="<%= get_ogp_image @origin %>">
</head>
<body>
</body>
</html>
# _twitter.html.erb
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="<%= Settings.twitter.account %>">
# _twitter_entry.html.erb
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:site" content="<%= Settings.twitter.account %>">
<meta name="twitter:title" content="<%= entry.title %>" />
<meta name="twitter:description" content="<%= entry.description %>" />
<meta name="twitter:image" content="<%= entry.image.url %>" />

nginxの設定

Facebook/TwitterのUser Agentを判定するために下記のような設定を行っています。(「Facebook/Twitter botからのアクセス」かつ「imagesディレクトリへのアクセスではない」場合に、Railsのbot用コントローラにURLをRewriteするという処理になります)

if ($http_user_agent ~* (facebookexternalhit|Twitterbot)) {
  set $bot A;
}
if ($uri ~* "^/(?!images)") {
  set $bot "${bot}B";
}
if ($bot = AB) {
  rewrite .* /bot?target=$uri break;
}

ご参考までに、フロントエンドが担当するURLと、バックエンドが担当するURLを切り分けるためのnginx設定の一部をご紹介いたします。下記のような設定を行わずに、全てのリクエストが常にunicorn等のアプリケーションサーバに渡されてしまうと、負荷の面で問題がございますのでご注意ください。

location ~* ^/(?!api|bot|index.html) {
  try_files $uri /index.html;
  break;
}

try_files $uri @unicorn;

location @unicorn {
  include conf.d/proxy.conf;
  proxy_pass http://unicorn;
}
正規表現に関する補足

上記の補足になりますが、nginxはPCRE(Perl Compatible Regular Expressions)をサポートしていますので、正規表現においてPerlと同様な構文で「否定先読み」が使用できます。
4. 正規表現

シングルページアプリケーションの場合、「フロントエンド側独自のURL」はどんどん増えていく可能性がありますが、それに合わせてnginxの設定を毎回変更するのは大変です。

これに対して、バックエンド側が担当するURLは滅多に増えませんので、「バックエンド側で担当するURLでない場合」という判定を行っておく方が作業工数を減らせるということになりますが、こういった判定は上記のような「否定先読み構文」を使うことでシンプルに対応出来ます。

以上になります。


「世界NO1のotakuマーケティングカンパニー」を目指す株式会社アリスマティックでは、インフラ・サーバーサイド・ネイティブ・フロントエンドなど、各職種でエンジニアを絶賛募集中です!

採用ページ

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です