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


143

私は、Googleの指示に基づいて、SPAをGoogleがクロールできるようにする方法について取り組んでいます。かなりの数の一般的な説明がありますが、実際の例を使用したより詳細な段階的なチュートリアルはどこにもありませんでした。これを終えた後、他の人もそれを利用し、おそらくさらに改善できるように、私の解決策を共有したいと思います。コントローラー
を使用MVCし、サーバー側でPhantomjs、クライアント側でDurandalを有効にして使用しています。私はクライアントとサーバーのデータのやり取りにもBreezejsを使用しています。これらすべてを強くお勧めしますが、他のプラットフォームを使用しているユーザーにも役立つ一般的で十分な説明を提供しようと思います。Webapipush-state


40
「オフトピック」について-Webアプリのプログラマーは、自分のアプリをSEOのためにクロール可能にする方法を見つける必要があります。これは、Webの基本的な要件です。これを行うことは、それ自体プログラミングに関するものではありませんが、stackoverflow.com/help/on-topicで説明されている「プログラミングの専門家に固有の実用的で答えられる問題」の主題に関連しています。これは、Web全体で明確な解決策を持たない多くのプログラマにとって問題です。私は他の人を助けることを望んでおり、ここでそれを説明するだけに何時間も費やしました。否定的なポイントを得ることは確かに私が再び助ける動機にはなりません。
はつらつ

3
ヘビーオイル/シークレットソースのSEOブードゥー/スパムではなく、プログラミングに重点が置かれている場合、それは完全に話題性があります。また、将来の読者にとって長期的に役立つ可能性がある自己回答も気に入っています。この質問と回答のペアは、両方のテストに合格したようです。(背景の詳細​​の一部は、回答で紹介されるよりも、質問をより具体的にすることができますが、それはかなりマイナーです)
Flexo

6
投票を減らすために+1。q / aがブログ投稿として適しているかどうかに関係なく、質問はDurandalに関連しており、回答は十分に調査されています。
RainerAtSpirit 2013

2
今日のSEOは開発者の日常生活の重要な部分であり、stackoverflowのトピックとして間違いなく検討する必要があることに同意します。
Kim D.

プロセス全体を自分で実装する以外に、基本的にこの問題をサービスとして解決するSnapSearch snapsearch.ioを試すことができます。
CMCDragonkai 2014

回答:


121

開始する前に、Google が必要とするものを理解していることを確認してください、特に使用かわいい醜い URLを。実装を見てみましょう:

クライアント側

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

「category」と「subCategory」は、おそらく「communication」と「phones」または「computers」などの他のフレーズになります。と電化製品店の「ラップトップ」。明らかに、多くの異なるカテゴリーとサブカテゴリーがあります。ご覧のとおり、リンクはカテゴリ、サブカテゴリ、および製品への直接のリンクであり、などの特定の「ストア」ページへの追加パラメータではありません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のボットに、www.xyz.com?_escaped_fragment_=後で表示するURLを変換させます。
  2. 「about」ルートは、Webアプリケーションに必要な他の「ページ」へのリンクの例にすぎません。
  3. ここで注意が必要なのは、「カテゴリ」ルートがなく、多くの異なるカテゴリが存在する可能性があることです。いずれにも事前定義されたルートはありません。これが登場mapUnknownRoutesです。これらの不明なルートを「ストア」ルートにマッピングし、「!」も削除します。pretty URLgoogleの検索エンジンで生成された場合は、URLから。「store」ルートは「fragment」プロパティの情報を受け取り、AJAX呼び出しを行ってデータを取得し、表示して、URLをローカルで変更します。私のアプリケーションでは、そのような呼び出しごとに別のページをロードしません。このデータに関連するページの部分のみを変更し、ローカルでURLも変更します。
  4. pushState:trueDurandalにプッシュ状態のURLを使用するように指示することに注意してください。

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

サーバ側

MVC 4.5サーバー側でWebAPIコントローラーを使用しています。サーバーは実際には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;
        }
    }
}


しかし、醜いURLはどうですか?これらはグーグルのボットによって作成され、ユーザーがブラウザに表示するすべてのデータを含むプレーンHTMLを返す必要があります。これにはphantomjsを使用します。Phantomは、クライアント側でブラウザーが実行していることを実行するヘッドレスブラウザーですが、サーバー側で実行します。言い換えると、ファントムは(とりわけ)URLを介してWebページを取得し、その中ですべてのJavaScriptコードの実行(およびAJAX呼び出しを介してデータを取得)を含めて解析し、反映するHTMLを返す方法を知っています。 DOM。MS Visual Studio Expressを使用している場合、このリンクからファントムをインストールしたいと考える人がたくさんいます。
しかし、最初に、醜い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 )
コントローラーで確認できるように、ファントムは、createSnapshot.js私が作成したフォルダーの下にという名前のjavascriptファイルをロードしますseo。これがこの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がすべてのデータを取得してページのDOMに配置するためにAJAX呼び出しをいくつか行っており、ファントムはDOMのHTMLリフレクションを返す前にすべての呼び出しが完了したことを認識できないためです。ここで行ったのは、最後のAJAX呼び出しの後にを追加した<span id='compositionComplete'></span>ため、このタグが存在する場合、DOMが完了したことがわかります。DurandalのcompositionCompleteイベントに応じてこれを行います。ここを参照してください多くのための。これが10秒以内に起こらない場合、私はあきらめます(1秒で終わります)。返されるHTMLには、ユーザーがブラウザに表示するすべてのリンクが含まれています。<script>HTMLスナップショットに存在するタグが正しいURLを参照していないため、スクリプトは正しく機能しません。これはJavaScriptファントムファイルでも変更できますが、HTMLスナップショットはaリンクを取得してJavaScriptを実行するためではなく、Googleでのみ使用されるため、これが必要であるとは思いません。これらのリンクかなりのURLを参照しています。実際、ブラウザーでHTMLスナップショットを表示しようとすると、JavaScriptエラーが発生しますが、すべてのリンクは正しく機能し、今回はかなりのURLを使用してサーバーに再度アクセスします。完全に機能するページを取得します。
これだよ。これで、サーバーはきれいなURLと醜いURLの両方を処理する方法を認識し、サーバーとクライアントの両方でプッシュ状態が有効になります。すべての醜いURLはファントムを使用して同じ方法で処理されるため、呼び出しのタイプごとに個別のコントローラーを作成する必要はありません。
変更することをお勧めするのは、一般的な「category / subCategory / product」呼び出しを行うのではなく、「store」を追加してリンクが次のようになるようにすることですhttp://www.xyz.com/store/category/subCategory/product111。これにより、すべての無効なURLが実際に「インデックス」コントローラーへの呼び出しであるかのように扱われるという私のソリューションの問題が回避されます。これらは、web.config上記で示したものに追加することなく、「ストア」コントローラー内で処理できると思います。


簡単な質問がありますが、これでうまくいったと思いますが、自分のサイトをグーグルに送信してグーグルやサイトマップなどへのリンクを与えると、グーグルmysite.com/#を与える必要があります!または、mysite.comとgoogle だけで、escaped_fragmentが追加されます。
ccorrin 2013

ccorrin-私の知る限り、グーグルに何も与える必要はありません。グーグルのボットはあなたのサイトを見つけて、かなりのURLを探します(URLが含まれていない可能性があるため、ホームページにもメタタグを追加することを忘れないでください)。escaped_fragmentを含む醜いURLは、常にgoogleによってのみ追加されます-HTML内に自分で配置することはできません。そしてサポートに感謝します:-)
ビームイッシュ

BjornとSandraに感謝します:-)このドキュメントのより良いバージョンに取り組んでいます。これには、ページをキャッシュしてプロセスを高速化し、URLに含まれる一般的な用途でそれを実行する方法に関する情報も含まれますコントローラの名前。準備ができ次第、投稿します
派手な2013

これは素晴らしい説明です!!。私はそれを実装し、私のlocalhost devboxの魅力のように動作します。問題は、Azure Webサイトにデプロイするときです。これは、サイトがフリーズし、しばらくすると502エラーが発生するためです。あなたはアズールにphantomjsを展開する方法についてのアイデアを持っている?? ...おかげで(testypv.azurewebsites.net/?_escaped_fragment_=home/about
yagopv

Azure Webサイトの経験はありませんが、おそらく、ページが完全に読み込まれるためのチェックプロセスが実行されない可能性があるため、サーバーは何度も何度もページをリロードしようとしますが、成功しません。おそらくそれが問題です(これらのチェックには時間制限があるため、そこにない場合があります)?'return true;'を入れてみてください。「checkLoaded()」の最初の行として、違いがあるかどうかを確認します。
ビームッシュな2013


4

これは、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/#


0

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

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


2
このリンクで質問に答えることができますが、回答の重要な部分をここに含め、参照用のリンクを提供することをお勧めします。リンクされたページが変更されると、リンクのみの回答が無効になる可能性があります。- レビューから
timgeb

2
あなたが正しいです。コメントを更新しました...より正確になったと思います。
gabrielperales 2016年

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.