web-dev-qa-db-ja.com

SPA SEOをクロール可能にする方法

私はグーグルの instructions に基づいてグーグルがSPAをクロール可能にする方法に取り組んできました。かなり多くの一般的な説明がありますが、実際の例を含むより詳細なステップバイステップのチュートリアルはどこにも見つかりませんでした。これを終えた後、他の人もそれを利用し、場合によってはさらに改善できるように、ソリューションを共有したいと思います。
MVCコントローラーでWebapiを使用しています。サーバー側で Phantomjs を使用し、Push-stateを有効にしてクライアント側で Durandal を使用しています。 Breezejs をクライアントとサーバー間のデータのやり取りにも使用しますが、これらすべてを強くお勧めしますが、他のプラットフォームを使用する人々にも役立つ一般的な十分な説明をしようと思います。

143
beamish

始める前に、Google requires の内容、特に 可愛い そして 醜い URL。実装を見てみましょう:

クライアント側

クライアント側には、AJAX呼び出しを介してサーバーと動的にやり取りする単一のhtmlページのみがあります。それがSPAの目的です。クライアント側のすべてのaタグはアプリケーションで動的に作成されます。これらのリンクをサーバーのgoogleのボットに表示する方法については後で説明します。そのような各aタグは、Googleのボットがクロールできるように、hrefタグにpretty URLを含めることができる必要があります。クライアントがクリックしたときにhref部分が使用されないようにします(サーバーで解析できるようにしたい場合でも、後で確認します)。ロードする新しいページ。ページの一部に表示されるデータを取得するAJAX呼び出しを行い、JavaScriptを使用してURLを変更します(HTML5を使用するpushstateまたはDurandaljs )。したがって、Googleのhref属性と、ユーザーがリンクをクリックしたときにジョブを実行するonclick属性の両方があります。ここで、Push-stateを使用しているため、URLに#が必要ないため、一般的なaタグは次のようになります。
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>

「category」と「subCategory」は、電化製品店の「通信」と「電話」または「コンピューター」と「ラップトップ」など、おそらく他のフレーズでしょう。明らかに、多くの異なるカテゴリとサブカテゴリがあります。ご覧のとおり、リンクはhttp://www.xyz.com/store/category/subCategory/product111などの特定の「ストア」ページへの追加パラメーターとしてではなく、カテゴリー、サブカテゴリー、および製品への直接のリンクです。これは、短くてシンプルなリンクを好むためです。これは、「ページ」の1つと同じ名前、つまり「約」のカテゴリが存在しないことを意味します。
AJAX(onclick部分)を介してデータをロードする方法については説明しません。Googleで検索してください。多くの良い説明があります。ここで言及したい唯一の重要なことは、ユーザーがこのリンクをクリックすると、ブラウザーのURLが次のようになることです。
http://www.xyz.com/category/subCategory/product111。そして、これはURLがサーバーに送信されないことです!これは、クライアントとサーバー間のすべての対話がAJAXを介して行われるSPAであり、リンクはまったくないことを思い出してください!すべての「ページ」はクライアント側に実装され、異なるURLはサーバーを呼び出しません(サーバーは、これらのURLが別のサイトからサイトへの外部リンクとして使用される場合にこれらのURLを処理する方法を知る必要があります。後でサーバー側で確認します)。現在、これはDurandalによって素晴らしく処理されています。強くお勧めしますが、他の技術を好む場合はこの部分をスキップすることもできます。それを選択して、私のようなMS Visual Studio Express 2012 for Webも使用している場合は、 Durandal Starter Kit をインストールし、そこでShell.jsで次のようなものを使用できます:

define(['plugins/router', 'durandal/app'], function (router, app) {
    return {
        router: router,
        activate: function () {
            router.map([
                { route: '', title: 'Store', moduleId: 'viewmodels/store', nav: true },
                { route: 'about', moduleId: 'viewmodels/about', nav: true }
            ])
                .buildNavigationModel()
                .mapUnknownRoutes(function (instruction) {
                    instruction.config.moduleId = 'viewmodels/store';
                    instruction.fragment = instruction.fragment.replace("!/", ""); // for pretty-URLs, '#' already removed because of Push-state, only ! remains
                    return instruction;
                });
            return router.activate({ pushState: true });
        }
    };
});

ここで注意すべき重要な点がいくつかあります。

  1. 最初のルート(route:''を使用)は、余分なデータを持たないURL、つまりhttp://www.xyz.comです。このページでは、AJAXを使用して一般データをロードします。このページにはaタグが実際にはまったくない場合があります。次のタグを追加して、Googleのボットがそれをどう処理するかを認識できるようにします。
    <meta name="fragment" content="!">。このタグにより、GoogleのボットはURLをwww.xyz.com?_escaped_fragment_=に変換します。これについては後で説明します。
  2. 「約」ルートは、Webアプリケーションに必要な他の「ページ」へのリンクの単なる例です。
  3. さて、トリッキーな部分は、「カテゴリ」ルートがなく、多くの異なるカテゴリが存在する可能性があるということです。事前定義されたルートはありません。これがmapUnknownRoutesの出番です。これらの不明なルートを「ストア」ルートにマップし、「!」も削除します。 Googleの検索エンジンによって生成されたpretty URLの場合はURLから。 'store'ルートは 'fragment'プロパティの情報を取得し、AJAX呼び出しを行ってデータを取得し、表示し、URLをローカルで変更します。私のアプリケーションでは、そのような呼び出しごとに異なるページをロードしません。このデータが関連するページの部分のみを変更し、URLもローカルで変更します。
  4. Durandalにプッシュ状態URLを使用するよう指示するpushState:trueに注目してください。

クライアント側で必要なのはこれだけです。ハッシュ化されたURLでも実装できます(Durandalでは、そのためにpushState:trueを削除するだけです)。より複雑な部分(少なくとも私にとっては...)はサーバー部分でした:

サーバ側

サーバー側でWebAPIコントローラーを使用してMVC 4.5を使用しています。サーバーは実際に3種類のURLを処理する必要があります。Googleによって生成されたもの-prettyuglyの両方、およびクライアントのブラウザーに表示されるものと同じ形式の「​​単純な」URLこれを行う方法を見てみましょう。

プリティURLと「シンプルな」URLは、存在しないコントローラを参照しようとしているかのようにサーバーによって最初に解釈されます。サーバーはhttp://www.xyz.com/category/subCategory/product111のようなものを見て、「category」という名前のコントローラーを探します。したがって、web.configに次の行を追加して、これらを特定のエラー処理コントローラーにリダイレクトします。

<customErrors mode="On" defaultRedirect="Error">
    <error statusCode="404" redirect="Error" />
</customErrors><br/>

これで、URLがhttp://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111のようなものに変換されます。 AJAX経由でデータをロードするクライアントにURLを送信したいので、ここでのトリックは、コントローラーを参照していないかのようにデフォルトの「インデックス」コントローラーを呼び出すことです。私はそれをする 追加 すべての 'category'および 'subCategory'パラメーターの前のURLへのハッシュ。ハッシュされたURLは、デフォルトの「インデックス」コントローラーを除く特別なコントローラーを必要とせず、データはクライアントに送信され、クライアントはハッシュを削除し、ハッシュ後の情報を使用してAJAX経由でデータをロードします。エラーハンドラコントローラのコードは次のとおりです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

using System.Web.Routing;

namespace eShop.Controllers
{
    public class ErrorController : ApiController
    {
        [HttpGet, HttpPost, HttpPut, HttpDelete, HttpHead, HttpOptions, AcceptVerbs("PATCH"), AllowAnonymous]
        public HttpResponseMessage Handle404()
        {
            string [] parts = Request.RequestUri.OriginalString.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries);
            string parameters = parts[ 1 ].Replace("aspxerrorpath=","");
            var response = Request.CreateResponse(HttpStatusCode.Redirect);
            response.Headers.Location = new Uri(parts[0].Replace("Error","") + string.Format("#{0}", parameters));
            return response;
        }
    }
}


しかし、どうですか UいURL?これらはgoogleのボットによって作成され、ユーザーがブラウザーに表示するすべてのデータを含むプレーンHTMLを返す必要があります。これには phantomjs を使用します。 Phantomは、ブラウザーがクライアント側で行っていることを実行するヘッドレスブラウザーです。ただし、サーバー側で実行されます。言い換えれば、ファントムは(とりわけ)URL経由でWebページを取得し、その中のすべてのjavascriptコードを実行することを含めて解析し(同様にAJAX呼び出し経由でデータを取得する)、 DOMを反映するHTMLをバックアップします。 MS Visual Studio Expressを使用している場合、多くの場合、この link を使用してファントムをインストールする必要があります。
しかし、最初に、いURLがサーバーに送信されると、それをキャッチする必要があります。このため、「App_start」フォルダーに次のファイルを追加しました。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace eShop.App_Start
{
    public class AjaxCrawlableAttribute : ActionFilterAttribute
    {
        private const string Fragment = "_escaped_fragment_";

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.RequestContext.HttpContext.Request;

            if (request.QueryString[Fragment] != null)
            {

                var url = request.Url.ToString().Replace("?_escaped_fragment_=", "#");

                filterContext.Result = new RedirectToRouteResult(
                    new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
            }
            return;
        }
    }
}

これは、「App_start」の「filterConfig.cs」からも呼び出されます。

using System.Web.Mvc;
using eShop.App_Start;

namespace eShop
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new AjaxCrawlableAttribute());
        }
    }
}

ご覧のとおり、「AjaxCrawlableAttribute」はいURLを「HtmlSnapshot」という名前のコントローラーにルーティングします。このコントローラーは次のとおりです。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace eShop.Controllers
{
    public class HtmlSnapshotController : Controller
    {
        public ActionResult returnHTML(string url)
        {
            string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);

            var startInfo = new ProcessStartInfo
            {
                Arguments = String.Format("{0} {1}", Path.Combine(appRoot, "seo\\createSnapshot.js"), url),
                FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true,
                StandardOutputEncoding = System.Text.Encoding.UTF8
            };
            var p = new Process();
            p.StartInfo = startInfo;
            p.Start();
            string output = p.StandardOutput.ReadToEnd();
            p.WaitForExit();
            ViewData["result"] = output;
            return View();
        }

    }
}

関連するviewは非常に単純で、1行のコードのみです。
@Html.Raw( ViewBag.result )
コントローラーで確認できるように、ファントムは、seoという名前で作成したフォルダーの下にcreateSnapshot.jsという名前のjavascriptファイルをロードします。このjavascriptファイルは次のとおりです。

var page = require('webpage').create();
var system = require('system');

var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();

page.onResourceReceived = function (response) {
    if (requestIds.indexOf(response.id) !== -1) {
        lastReceived = new Date().getTime();
        responseCount++;
        requestIds[requestIds.indexOf(response.id)] = null;
    }
};
page.onResourceRequested = function (request) {
    if (requestIds.indexOf(request.id) === -1) {
        requestIds.Push(request.id);
        requestCount++;
    }
};

function checkLoaded() {
    return page.evaluate(function () {
        return document.all["compositionComplete"];
    }) != null;
}
// Open the page
page.open(system.args[1], function () { });

var checkComplete = function () {
    // We don't allow it to take longer than 5 seconds but
    // don't return until all requests are finished
    if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
        clearInterval(checkCompleteInterval);
        var result = page.content;
        //result = result.substring(0, 10000);
        console.log(result);
        //console.log(results);
        phantom.exit();
    }
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);

最初に感謝します Thomas Davis から基本コードを入手したページについて:-)。
ここで奇妙なことに気付くでしょう。checkLoaded()関数がtrueを返すまで、ファントムはページの再読み込みを続けます。何故ですか?これは、特定のSPAが複数のAJAX呼び出しを行ってすべてのデータを取得し、それをページ上のDOMに配置し、ファントムがHTMLのリフレクションを返す前にすべての呼び出しが完了したことを知ることができないためですDOM。ここでやったことは、最後のAJAX呼び出しの後に<span id='compositionComplete'></span>を追加することです。このタグが存在する場合、DOMが完了したことがわかります。これはDurandalのcompositionCompleteイベントに応じて行います。詳細については here を参照してください。これが10秒以内に起こらない場合、私はあきらめます(1秒しかかからないはずです)。返されるHTMLには、ユーザーがブラウザーで見るすべてのリンクが含まれます。 HTMLスナップショットに存在する<script>タグは正しいURLを参照しないため、スクリプトは正しく機能しません。これは、javascriptファントムファイルでも変更できますが、HTMLスナップショートはaリンクを取得し、javascriptを実行しないためにGoogleでのみ使用されるため、これが必要だとは思いません。これらのリンク 行う きれいなURLを参照します。実際、ブラウザでHTMLスナップショットを表示しようとすると、javascriptエラーが発生しますが、すべてのリンクが適切に機能し、今回はきれいなURLを使用してもう一度サーバーに移動します作業ページ。
これです。これで、サーバーは、きれいなURLとbothいURLの両方を処理する方法を知っており、サーバーとクライアントの両方でプッシュ状態が有効になっています。すべてのugいURLはファントムを使用して同じように扱われるため、呼び出しのタイプごとに個別のコントローラーを作成する必要はありません。
変更したいことの1つは、一般的な「category/subCategory/product」呼び出しを行うのではなく、リンクがhttp://www.xyz.com/store/category/subCategory/product111のようになるように「store」を追加することです。これにより、すべての無効なURLが実際に「インデックス」コントローラーへの呼び出しであるかのように扱われるというソリューションでの問題を回避できます。また、これらは、web.config上記。

122
beamish

GoogleはSPAページをレンダリングできるようになりました。 AJAXクロールスキームの廃止

32
Edward Olamisan

8月14日にロンドンでホストしたEmber.jsトレーニングクラスのスクリーンキャスト録画へのリンクを次に示します。クライアント側アプリケーションとサーバー側アプリケーションの両方の戦略の概要を説明し、これらの機能を実装することでJavaScriptがオフになっているユーザーでもJavaScriptシングルページアプリがどのようにグレースフルデグラデーションを提供するかを実演します。 。

PhantomJSを使用して、Webサイトのクロールを支援します。

要するに、必要な手順は次のとおりです。

  • クロールするWebアプリケーションのホストバージョンを使用します。このサイトには、運用環境にあるすべてのデータが必要です。
  • JavaScriptアプリケーション(PhantomJSスクリプト)を作成してWebサイトをロードします
  • クロールするURLのリストにindex.html(または「/」)を追加します
    • クロールリストに追加された最初のURLをポップします
    • ページを読み込み、そのDOMをレンダリングします
    • ロードされたページで、自分のサイトにリンクするリンクを見つけます(URLフィルタリング)
    • このリンクを「クロール可能な」URLのリストに追加します(まだクロールされていない場合)
    • レンダリングされたDOMをファイルシステム上のファイルに保存しますが、最初にすべてのスクリプトタグを取り除きます
    • 最後に、クロールされたURLでSitemap.xmlファイルを作成します

このステップが完了すると、バックエンドがHTMLの静的バージョンをそのページのnoscript-tagの一部として提供します。これにより、アプリが元々単一ページのアプリであったとしても、Googleや他の検索エンジンがウェブサイトのすべてのページをクロールできるようになります。

完全な詳細を含むスクリーンキャストへのリンク:

http://www.devcasts.io/p/spas-phantomjs-and-seo/#

4

http://sparender.com/ を使用すると、シングルページアプリケーションを正しくクロールできます。

0
ddtxra

Prerenderと呼ばれるサービスを使用して、SPAをprerenderするための独自のサービスを使用または作成できます。彼のWebサイト prerender.io と彼の githubプロジェクト (PhantomJSを使用し、あなたのWebサイトをレンダリングします)で確認できます。

始めるのはとても簡単です。クローラーのリクエストをサービスにリダイレクトするだけで、レンダリングされたhtmlを受け取ります。

0
gabrielperales