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

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



답변

시작하기 전에, 당신이 구글에서 무엇을 이해하도록하십시오 필요 , 특히 사용을 하고 추한 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. 더 짧고 간단한 링크를 선호하기 때문입니다. 내 ‘페이지’중 하나와 이름이 같은 카테고리가 없습니다. 즉 ‘
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;
        }
    }
}

이것은 ‘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은 한 줄의 코드로 매우 간단
@Html.Raw( ViewBag.result )
합니다. 컨트롤러에서 볼 수 있듯이 phantom createSnapshot.js은 내가 만든 폴더 아래에 이름이 지정된 자바 스크립트 파일을로드합니다 seo. 이 자바 스크립트 파일은 다음과 같습니다.

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을 참조하지 않기 때문에 스크립트가 제대로 작동 하지 않습니다. 이것은 자바 스크립트 팬텀 파일에서도 변경 될 수 있지만 HTML snapshort는 a링크 를 가져 오고 자바 스크립트를 실행하지 않기 위해 Google에서만 사용하기 때문에 이것이 불필요하다고 생각하지 않습니다 . 이 링크 예쁜 URL을 참조하며, 사실 브라우저에서 HTML 스냅 샷을 보려고하면 자바 스크립트 오류가 발생하지만 모든 링크가 제대로 작동하고 이번에는 예쁜 URL로 서버로 다시 연결됩니다. 완전히 작동하는 페이지를 얻는 중입니다.
이거 야. 이제 서버는 서버와 클라이언트 모두에서 푸시 상태를 활성화하여 예쁘고 못생긴 URL을 처리하는 방법을 알고 있습니다. 모든 추악한 URL은 팬텀을 사용하여 동일한 방식으로 처리되므로 각 유형의 통화에 대해 별도의 컨트롤러를 만들 필요가 없습니다.
변경하려는 것을 선호하는 한 가지는 일반적인 ‘카테고리 / 서브 카테고리 / 제품’호출을하는 것이 아니라 링크를 다음과 같이 보이도록 ‘저장소’를 추가하는 것 http://www.xyz.com/store/category/subCategory/product111입니다. 이것은 내 솔루션에서 모든 유효하지 않은 URL이 실제로 ‘인덱스’컨트롤러를 호출하는 것처럼 취급된다는 문제를 피할 수 있으며 web.config위에서 언급 한 것 외에도 ‘저장소’컨트롤러 내에서 처리 할 수 ​​있다고 가정합니다. .


답변

Google은 이제 SPA 페이지를 렌더링 할 수 있습니다.
AJAX 크롤링 체계 사용 중단


답변

다음은 8 월 14 일 런던에서 주최 한 Ember.js Training 클래스의 스크린 캐스트 녹화 링크입니다. 클라이언트 측 애플리케이션과 서버 측 애플리케이션 모두에 대한 전략을 설명하고, 이러한 기능을 구현하여 JavaScript 단일 페이지 앱에 JavaScript를 사용하지 않는 사용자에게도 정상적인 성능 저하를 제공하는 방법에 대한 실시간 데모를 제공합니다. .

PhantomJS를 사용하여 웹 사이트를 크롤링합니다.

간단히 말해서 필요한 단계는 다음과 같습니다.

  • 크롤링하려는 웹 응용 프로그램의 호스팅 된 버전이 있어야합니다.이 사이트에는 프로덕션 환경에있는 모든 데이터가 있어야합니다.
  • 웹 사이트를로드 할 JavaScript 응용 프로그램 (PhantomJS 스크립트)을 작성하십시오
  • 크롤링 할 URL 목록에 index.html (또는“/“) 추가
    • 크롤링 목록에 추가 된 첫 번째 URL을 팝
    • 페이지로드 및 DOM 렌더링
    • 로드 된 페이지에서 자신의 사이트로 연결되는 링크를 찾으십시오 (URL 필터링).
    • 크롤링 할 수없는 “크롤링 가능”URL 목록에이 링크를 추가하십시오.
    • 렌더링 된 DOM을 파일 시스템의 파일에 저장하지만 먼저 모든 스크립트 태그를 제거하십시오.
    • 마지막으로 크롤링 된 URL로 Sitemap.xml 파일을 작성하십시오.

이 단계가 완료되면 해당 페이지의 noscript-tag의 일부로 HTML의 정적 버전을 제공하기 위해 백엔드까지 수행합니다. 이렇게하면 앱이 원래 단일 페이지 앱인 경우에도 Google 및 기타 검색 엔진이 웹 사이트의 모든 단일 페이지를 크롤링 할 수 있습니다.

자세한 내용이있는 스크린 캐스트 링크 :

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


답변

프리 렌더라는 서비스를 사용하여 SPA를 프리 렌더링하기위한 고유 한 서비스를 사용하거나 작성할 수 있습니다. 그의 웹 사이트 prerender.io 및 그의 github 프로젝트 에서 확인할 수 있습니다 (PhantomJS를 사용 하고 웹 사이트를 렌더링 합니다).

시작하기가 매우 쉽습니다. 크롤러 요청을 서비스로 리디렉션하기 만하면 렌더링 된 HTML이 수신됩니다.


답변

당신은 사용할 수 있습니다 http://sparender.com/ 제대로 크롤링하는 단일 페이지 응용 프로그램을 가능하게하는합니다.


답변