IT

SPA SEO를 크롤링 할 수있게 만드는 방법은 무엇입니까?

lottoking 2020. 6. 19. 07:53
반응형

SPA SEO를 크롤링 할 수있게 만드는 방법은 무엇입니까?


Google의 지침 에 따라 Google이 SPA를 크롤링 할 수 있도록 만드는 방법을 연구하고 있습니다. 비록 몇 가지 일반적인 설명이 있지만 실제 예제를 사용하여보다 철저한 단계별 자습서를 찾을 수 없었습니다. 이 작업을 마친 후 다른 사람들이 솔루션을 사용하고 더 향상시킬 수 있도록 솔루션을 공유하고 싶습니다.
내가 사용하고 MVCWebapi컨트롤러 및 Phantomjs 서버 측 및 Durandal 와 클라이언트 측에서 push-state사용할 수; 또한 클라이언트-서버 데이터 상호 작용을 위해 Breezejs사용 합니다.이 모두를 강력히 권장하지만, 다른 플랫폼을 사용하는 사람들에게도 도움이 될만한 충분한 설명을 제공하려고합니다.


시작하기 전에, 당신이 구글에서 무엇을 이해하도록하십시오 필요 , 특히 사용을 하고 추한 URL을. 이제 구현을 보자.

고객 입장에서

클라이언트 측에는 AJAX 호출을 통해 서버와 동적으로 상호 작용하는 단일 html 페이지 만 있습니다. 그것이 바로 SPA의 문제입니다. a클라이언트 측의 모든 태그는 내 응용 프로그램에서 동적으로 만들어지며 나중에 서버에서 Google 로봇에 이러한 링크를 표시하는 방법을 살펴 보겠습니다. 이러한 각 a태그의 요구는이 할 수 pretty URLhref구글의 로봇이 크롤링 있도록 태그입니다. 당신은 원하지 않는 href우리가 부하에 새 페이지를 원하지 않을 수 있기 때문에, (우리가 나중에 보자, 서버가 구문 분석 할 수 있도록하려는에도 불구하고) 부분이 그것의 클라이언트 클릭 할 때 사용되는 AJAX 호출을 통해 일부 데이터가 페이지의 일부로 표시되도록하고 자바 스크립트를 통해 (예 : HTML5 사용 pushstate또는 Durandaljs) URL을 변경합니다 . 그래서, 우리는 둘 다hrefonclick사용자가 링크를 클릭 할 때 작업을 수행 할 뿐만 아니라 Google의 속성입니다 . 이제는 URL에 push-state아무것도 원하지 #않으므로 일반적인 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. 더 짧고 간단한 링크를 선호하기 때문입니다. 내 '페이지'중 하나와 같은 이름을 가진 카테고리, 즉 'about'이 없다는 것을 의미합니다.
AJAX ( onclick부분) 를 통해 데이터를로드하는 방법에 대해서는 설명하지 않고 Google에서 검색하면 많은 좋은 설명이 있습니다. 여기서 언급하고 싶은 유일한 중요한 것은 사용자가이 링크를 클릭하면 브라우저의 URL이 다음과 같이 표시되기를 원한다는 것입니다.
http://www.xyz.com/category/subCategory/product111. 그리고 이것은 URL이 서버로 전송되지 않습니다! 이것은 클라이언트와 서버 간의 모든 상호 작용이 AJAX를 통해 이루어지고 전혀 링크가없는 SPA입니다. 모든 '페이지'는 클라이언트 측에서 구현되며 다른 URL은 서버를 호출하지 않습니다 (서버는 이러한 URL을 다른 사이트에서 사이트로 외부 링크로 사용하는 경우 이러한 URL을 처리하는 방법을 알아야합니다. 나중에 서버 측에서 볼 수 있습니다). 이제 이것은 Durandal에 의해 훌륭하게 처리됩니다. 나는 그것을 강력히 추천하지만 다른 기술을 선호한다면이 부분을 건너 뛸 수도 있습니다. 당신이 그것을 선택하고 나처럼 웹용 MS Visual Studio Express 2012를 사용 하고 있다면 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. '정보'경로는 웹 애플리케이션에서 원하는 다른 '페이지'링크에 대한 예일뿐입니다.
  3. 이제 까다로운 부분은 '범주'경로가 없으며 여러 가지 범주가있을 수 있다는 것입니다. 사전 정의 된 경로는 없습니다. 이것은 mapUnknownRoutes알려지지 않은 경로를 'store'경로에 매핑하고 '!'를 제거합니다. pretty URLGoogle의 검색 엔진 에서 생성 된 경우 URL에서 'store'경로는 'fragment'속성의 정보를 가져와 AJAX 호출을 통해 데이터를 가져 와서 표시하고 URL을 로컬로 변경합니다. 내 응용 프로그램에서는 모든 호출에 대해 다른 페이지를로드하지 않습니다. 이 데이터와 관련된 페이지 부분 만 변경하고 URL을 로컬로 변경합니다.
  4. pushState:trueDurandal이 푸시 상태 URL을 사용하도록 지시 하는 것을 주목하십시오 .

이것이 클라이언트 측에서 필요한 전부입니다. 해시 된 URL로도 구현할 수 있습니다 (Durandal에서는 간단히 제거하십시오 pushState:true). 더 복잡한 부분 (적어도 나를 위해 ...)은 서버 부분이었습니다.

서버 측

내가 사용하고 MVC 4.5있는 서버 측에서 WebAPI컨트롤러. 서버는 실제로 3의 URL 종류 처리해야합니다 : 구글에 의해 생성 된 것들 - 모두 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을 통해 웹 페이지를 가져 오는 방법과 모든 자바 스크립트 코드 실행 (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;
        }
    }
}

This is called from 'filterConfig.cs' also in 'App_start':

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());
        }
    }
}

As you can see, 'AjaxCrawlableAttribute' routes ugly URLs to a controller named 'HtmlSnapshot', and here is this controller:

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();
        }

    }
}

The associated view is very simple, just one line of code:
@Html.Raw( ViewBag.result )
As you can see in the controller, phantom loads a javascript file named createSnapshot.js under a folder I created called seo. Here is this javascript file:

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);

I first want to thank Thomas Davis for the page where I got the basic code from :-).
You will notice something odd here: phantom keeps re-loading the page until the checkLoaded() function returns true. Why is that? this is because my specific SPA makes several AJAX call to get all the data and place it in the DOM on my page, and phantom cannot know when all the calls have completed before returning me back the HTML reflection of the DOM. What I did here is after the final AJAX call I add a <span id='compositionComplete'></span>, so that if this tag exists I know the DOM is completed. I do this in response to Durandal's compositionComplete event, see here for more. If this does not happen withing 10 seconds I give up (it should take only a second to so the most). The HTML returned contains all the links that the user sees in the browser. The script will not work properly because the <script> tags that do exist in the HTML snapshot do not reference the right URL. This can be changed too in the javascript phantom file, but I don't think this is necassary because the HTML snapshort is only used by google to get the a links and not to run javascript; these links do reference a pretty URL, and if fact, if you try to see the HTML snapshot in a browser, you will get javascript errors but all the links will work properly and direct you to the server once again with a pretty URL this time getting the fully working page.
This is it. Now the server know how to handle both pretty and ugly URLs, with push-state enabled on both server and client. All ugly URLs are treated the same way using phantom so there's no need to create a separate controller for each type of call.
One thing you might prefer to change is not to make a general 'category/subCategory/product' call but to add a 'store' so that the link will look something like: http://www.xyz.com/store/category/subCategory/product111. This will avoid the problem in my solution that all invalid URLs are treated as if they are actually calls to the 'index' controller, and I suppose that these can be handled then within the 'store' controller without the addition to the web.config I showed above.


Google is now able to render SPA pages: Deprecating our AJAX crawling scheme


Here is a link to a screencast-recording from my Ember.js Training class I hosted in London on August 14th. It outlines a strategy for both your client-side application and for you server-side application, as well as gives a live demonstration of how implementing these features will provide your JavaScript Single-Page-App with graceful degradation even for users with JavaScript turned off.

It uses PhantomJS to aid in crawling your website.

In short, the steps required are:

  • Have a hosted version of the web application you want to crawl, this site needs to have ALL of the data you have in production
  • Write a JavaScript application (PhantomJS Script) to load your website
  • Add index.html ( or “/“ ) to the list of URLs to crawl
    • Pop the first URL added to the crawl-list
    • Load page and render its DOM
    • Find any links on the loaded page that links to your own site (URL filtering)
    • Add this link to a list of “crawlable” URLS, if its not already crawled
    • Store the rendered DOM to a file on the file system, but strip away ALL script-tags first
    • At the end, create a Sitemap.xml file with the crawled URLs

Once this step is done, its up to your backend to serve the static-version of your HTML as part of the noscript-tag on that page. This will allow Google and other search engines to crawl every single page on your website, even though your app originally is a single-page-app.

Link to the screencast with the full details:

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


You can use or create your own service for prerender your SPA with the service called prerender. You can check it out on his website prerender.io and on his github project (It uses PhantomJS and it renderize your website for you).

It's very easy to start with. You only have to redirect crawlers requests to the service and they will receive the rendered html.


You can use http://sparender.com/ which enables Single Page Applications to be crawled correctly.

참고URL : https://stackoverflow.com/questions/18530258/how-to-make-a-spa-seo-crawlable

반응형