source

SPA SEO를 크롤 가능한 상태로 만드는 방법

ittop 2023. 3. 18. 09:20
반응형

SPA SEO를 크롤 가능한 상태로 만드는 방법

저는 구글의 지시에 따라 SPA를 구글에서 크롤링할 수 있도록 만드는 방법을 연구해 왔습니다.일반적인 설명도 꽤 있지만, 실제 예시를 포함한 단계별 튜토리얼은 어디에서도 찾을 수 없었습니다.이것을 마치면, 다른 사람도 활용할 수 있도록, 한층 더 개선해 나갈 수 있도록, 제 솔루션을 공유해 나가고 싶습니다.
하고 .MVCWebapi서버 측에서는 컨트롤러, 서버 측에서는 Phantomj, 클라이언트 측에서는 Durandal,push-state또한 클라이언트와 서버의 데이터 상호작용에도 Breezejs를 사용하고 있습니다.이 모든 것을 강력히 추천합니다만, 다른 플랫폼을 사용하는 사람에게도 도움이 되는 일반적인 설명을 하도록 하겠습니다.

시작하기 전에 구글이 요구하는 것, 특히 예쁘고 못생긴 URL을 사용하는 것을 이해하시기 바랍니다.이제 실장을 살펴보겠습니다.

측 " " "

의미입니다SPA spa 、 [ SPA ] ★★★★★★★★★★★★★★★★★.a클라이언트 측 태그는 응용 프로그램에서 동적으로 생성됩니다. 이러한 링크를 서버의 Google 봇에 표시하는 방법은 나중에 알아보겠습니다.의 그러한 「」는, 「 」, 「 」, 「 」, 「 」.a는 그음음음음음음음음음음음 tag tag tag tag tag tag tag tag tag 가 있어야 합니다.pretty URL href구글의 봇이 기어갈 수 있도록 태그를 붙입니다. 건 없어요.href HTML5 html using using using서서 using5 ) 。새로운 페이지를 로드하는 것을 원하지 않을 수 있기 때문에 페이지의 일부에 데이터를 표시하는 AJAX 콜을 발신하고 Javascript를 사용하여 URL을 변경합니다(예를 들어 HTML5 사용).pushstate ★★★★★★★★★★★★★★★★★★★★★★.Durandaljs 둘 다 .href및의 onclick사용자가 링크를 클릭하면 작업이 수행됩니다. 이제 ,, 제를 사용하니까push-state는 아무것도 않는다.#인 「URL」이 됩니다.a는 이렇게 수 요.
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>

category과 ''computers와 'computers'는 'computers'와 'computers'입니다분명히 많은 다른 카테고리와 하위 카테고리가 있을 것이다. 및, '스토어': '스토어' 페이지), '' 페이지에는 추가 되어 있지 않습니다.http://www.xyz.com/store/category/subCategory/product111짧고 단순한 링크를 선호하기 때문입니다.와 이름이 즉 'about 'about', 'about', 'about', 'about', 'about', 'about', 'about', 'about', 'about', 'about', 'about', 'about', 'about', 'about', 'about', 'about', 'about', 'about'의는 없을 예요.
AJAX)를.onclick많이 .여기서 유일하게 언급하고 싶은 것은 사용자가 이 링크를 클릭했을 때 브라우저의 URL을 다음과 같이 하고 싶다는 것입니다.
http://www.xyz.com/category/subCategory/product111그리고 이것은 URL이 서버로 전송되지 않습니다!이것은 클라이언트와 서버 간의 모든 상호작용이 AJAX를 통해 이루어지는 SPA입니다.링크는 전혀 없습니다.모든 '페이지'는 클라이언트 측에서 구현됩니다.또, 다른 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:''는 추가 )에 대한 것입니다http://www.xyz.com이 페이지에서는 AJAX를 사용하여 일반 데이터를 로드합니다. 수도 .a태그는 이 페이지에서 전혀 표시되지 않습니다.이 이 태그를 사용하여 알 수 다음 . will will 、 will 、 will 、 will 、 음 、 음 、 음 、 음 、 음 、 will 、 will 、 will will will 。
    <meta name="fragment" content="!">을 URL로 www.xyz.com?_escaped_fragment_=나중에 보게 될 거야
  2. 'about' 경로는 웹 응용 프로그램에서 원하는 다른 '페이지' 링크의 예에 불과합니다.
  3. 여기서 어려운 점은 '카테고리' 루트가 없고 다양한 카테고리가 있을 수 있다는 것입니다.이들 중 어느 것도 사전에 정의된 루트가 없습니다.여기가 바로 그 장소입니다mapUnknownRoutes,스토어」루트인 경우「!' 를 삭제합니다.pretty URL시치「store」루트는, 「fragment」속성의 정보를 취득해, 데이터를 취득해 표시해, 로컬로 URL 를 변경하는 AJAX 콜을 발신합니다.어플리케이션에서는 이러한 콜마다 다른 페이지를 로드하지 않습니다.이 데이터와 관련된 페이지의 일부만 변경하고 URL도 로컬로 변경합니다.
  4. 점에 주의:pushState:trueDurandal URL입니다.

이치노할 수도 을 삭제합니다).pushState:true( (...) :) ((적 ( ( ( ( (

서버측

하고 MVC 4.5의 「」를 사용해 .WebAPI컨트롤러입니다.은 둘 다입니다.-둘다다다 URL - 다다다pretty ★★★★★★★★★★★★★★★★★」ugly또한 클라이언트의 브라우저에 표시되는 것과 같은 형식의 '단순' URL도 있습니다. 볼까요?

예쁜 URL과 '간단한' URL은 존재하지 않는 컨트롤러를 참조하려는 것처럼 서버에 의해 먼저 해석됩니다.에서는 이런 게 요.http://www.xyz.com/category/subCategory/product111 'categorycategory'라는 이름의 . 래서에서...web.config다음 행을 추가하여 특정 오류 처리 컨트롤러로 수정합니다.

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

하면 이 'URL'로 http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111AJAX를 통해 데이터를 로드하는 클라이언트에 URL을 전송하고 싶습니다.따라서 여기서 트릭은 기본 'index' 컨트롤러를 참조하지 않는 것처럼 호출하는 것입니다.모든 '카테고리' 파라미터와 'subCategory' 파라미터 앞에 해시를 추가하여 URL을 실행합니다.해시된 URL에는 기본 'index'컨트롤러를 제외하고 특별한 컨트롤러가 필요하지 않으며 데이터가 클라이언트에 전송되어 해시가 삭제됩니다.해시 후에 정보를 사용하여 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;
        }
    }
}


하지만 Ugly URL은 어떨까요?이것들은 구글의 봇에 의해 생성되며 사용자가 브라우저에서 보는 모든 데이터를 포함하는 플레인 HTML을 반환해야 합니다.이걸 위해 pantomjs를 사용합니다.Phantom은 브라우저가 클라이언트 측에서 수행하는 작업을 서버 측에서 수행하는 헤드리스 브라우저입니다.즉, 팬텀은 URL을 통해 웹 페이지를 가져오는 방법, 웹 페이지 내의 모든 Javascript 코드를 실행하는 방법(AJAX 호출을 통해 데이터를 얻는 방법)과 DOM을 반영하는 HTML을 사용자에게 제공하는 방법을 알고 있습니다.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 )
볼 수a as as as named named named named named named named named named named named named named named named named named named named named named파일을 로드합니다.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()사실, 의 AJAX 해, 모든 에, 은, DOM에, 했는지 알 수입니다.이것은, 특정의 SPA가 복수의 AJAX 콜을 발신해, 모든 데이터를 취득해, 페이지의 DOM 에 배치하기 때문에, 팬텀은, DOM 의 HTML 리플렉션을 되돌리기 전에, 모든 콜이 언제 완료했는지 알 수 없기 때문입니다.서 한 AJAX 콜에 AJAX를 입니다.<span id='compositionComplete'></span>이 태그가 존재하면 DOM이 완료되었음을 알 수 있습니다.의 ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★.compositionComplete자세한 내용은 여기를 참조하십시오.10초 동안 이 문제가 발생하지 않으면 포기합니다(최대한 1초밖에 걸리지 않습니다).반환되는 HTML에는 사용자가 브라우저에 표시하는 모든 링크가 포함되어 있습니다.스크립트는 정상적으로 동작하지 않습니다.<script>HTML 스냅샷에 존재하는 태그는 올바른 URL을 참조하지 않습니다.할 수 , 에서 javascript를 만 사용되기 ajavascript를 실행하지 않는 링크입니다.이 링크는 예쁜 URL을 참조합니다.사실 브라우저에서 HTML 스냅숏을 보려고 하면 javascript 오류가 발생하지만 모든 링크는 정상적으로 동작하며 이번에는 예쁜 URL을 사용하여 다시 한 번 서버로 안내합니다.
서버와하게 되어 과 못생긴 URL을 모두 할 수 .이제 서버는 서버와 클라이언트 모두에서 push-state를 활성화하여 예쁜 URL과 못생긴 URL을 모두 처리하는 방법을 알게 되었습니다.모든 추악한 URL은 팬텀을 사용하여 동일하게 처리되므로 콜 유형별로 별도의 컨트롤러를 작성할 필요가 없습니다.
' '스토어하여 '스토어'를입니다.「 」 「 」 / 「 」 / 「 」 / 「 」 / 「 」 / 「 」 / 「 」 / 「 」 / 「 」 http://www.xyz.com/store/category/subCategory/product111, 무효인 는 할 수 은 「」에되지 않고, 「할 수 입니다.이러한 URL은 '스토어' 컨트롤러 내에서 처리할 수 있습니다.web.config위에 보여드렸어요.

이제 Google은 SPA 페이지를 렌더링할 수 있습니다.AJAX 크롤링 스킴 폐지

8월 14일에 런던에서 주최한 Ember.js 트레이닝 클래스의 스크린 캐스트 녹화 링크입니다.클라이언트 측 애플리케이션과 서버 측 애플리케이션 모두에 대한 전략을 개략적으로 설명하고 있으며, 이러한 기능을 구현하면 JavaScript가 꺼진 사용자라도 JavaScript Single-Page-App에 정상적인 성능 저하를 가져올 수 있음을 라이브 데모를 제공합니다.

팬텀을 사용웹 사이트를 탐색하는 데 도움이 되는 JS입니다.

즉, 필요한 순서는 다음과 같습니다.

  • 크롤링할 웹 응용 프로그램의 호스트 버전이 있어야 합니다. 이 사이트에는 실제 가동 중인 모든 데이터가 있어야 합니다.
  • JavaScript 응용 프로그램 작성(Phantom)JS 스크립트)를 사용하여 웹 사이트를 로드합니다.
  • URL index.html('/') 입니다.
    • 크롤 목록에 추가된 첫 번째 URL을 팝합니다.
    • 페이지를 로드하고 해당 DOM을 렌더링합니다.
    • 로드된 페이지에서 자신의 사이트로 연결되는 링크를 찾습니다(URL 필터링).
    • 아직 크롤링되지 않은 경우 이 링크를 "크롤링 가능한" URL 목록에 추가합니다.
    • 렌더링된 DOM을 파일 시스템의 파일에 저장하되 모든 스크립트 태그를 먼저 제거합니다.
    • 마지막에 크롤링된 URL을 사용하여 Sitemap.xml 파일을 만듭니다.

이 절차가 완료되면 HTML의 정적 버전을 해당 페이지의 noscript-tag의 일부로 제공하는 것은 사용자의 백엔드에 달려 있습니다.이를 통해 Google과 다른 검색 엔진은 원래 앱이 한 페이지 앱일지라도 웹 사이트의 모든 페이지를 탐색할 수 있습니다.

자세한 내용이 포함된 스크린캐스트 링크:

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

하였습니다.RendertronSEO의 를 ASP.net core클라이언트 측에서는 Angular로 크롤러 또는 클라이언트에 따라 요구를 구별하는 미들웨어이기 때문에 크롤러 측에서 요구가 있을 경우 짧고 신속하게 응답합니다.

  • 일반 클라이언트의 렌더링된 사이트:

  • 크롤러를 위해 렌더링된 사이트:

»Startup.cs

렌더트론 서비스 구성:

public void ConfigureServices(IServiceCollection services)
{
    // Add rendertron services
    services.AddRendertron(options =>
    {
        // rendertron service url
        options.RendertronUrl = "http://rendertron:3000/render/";

        // proxy url for application
        options.AppProxyUrl = "http://webapplication";

        // prerender for firefox
        //options.UserAgents.Add("firefox");

        // inject shady dom
        options.InjectShadyDom = true;
        
        // use http compression
        options.AcceptCompression = true;
    });
}

크롤러에 특화된 콘텐츠를 제작하기 위해서는 짧은 코드가 필요한 것이 사실이지만, CMS나 포털 사이트 등 소규모 프로젝트에 유용하다.

은 대부분의 측 예: 측 프레임워크)에서 실행할 수 있습니다.ASP.net core,Python (Django),Express.js,Firebase.

소스 및 자세한 내용은http://https://github.com/GoogleChrome/rendertron 를 참조해 주세요.

2021년 갱신

  • SPA는 SEO에 친화적이기 위해 History API를 사용해야 합니다.

    SPA를 통해 .history.pushState(path)다음 단계는 프레임워크에 따라 달라집니다.가 React를 합니다.history "React"/"React"에 설정된 React 를 표시합니다.path

  • 단순한 SPA로 SEO를 실현하는 것은 간단합니다.

  • 보다 고도의 SPA(퍼포먼스 향상을 위해 선택적 프리렌더링을 사용)의 SEO를 실현하는 것은 기사에 나타나 있듯이 더욱 중요합니다.제가 작가입니다.

프리렌더라고 불리는 서비스를 사용하여 SPA 프리렌더에 자체 서비스를 사용하거나 작성할 수 있습니다.그의 웹사이트 prerender.io과 그의 github 프로젝트에서 확인할 수 있습니다(팬텀 사용).JS가 웹 사이트를 렌더링합니다).

시작은 아주 쉽다.크롤러 요청을 서비스로 리디렉션하기만 하면 렌더링된 html을 받을 수 있습니다.

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

반응형