it-swarm-fr.com

Comment faire un SPA SEO explorable?

Je travaille sur la façon de rendre un SPA crawlable par Google à partir des instructions de Google. Même s'il existe de nombreuses explications générales, je ne trouvais nulle part un tutoriel pas à pas plus approfondi avec des exemples concrets. Après cela, je voudrais partager ma solution afin que d’autres puissent également l’utiliser et éventuellement l’améliorer. 
J'utilise MVC avec les contrôleurs Webapi et Phantomjs du côté serveur et Durandal du côté client avec Push-state activé; J'utilise également Breezejs pour l'interaction client-serveur, ce que je recommande fortement, mais je vais essayer de donner une explication suffisamment générale pour aider également les utilisateurs d'autres plates-formes.

142
beamish

Avant de commencer, assurez-vous de bien comprendre ce que Google requiert , en particulier l’utilisation de jolie et laid URL. Voyons maintenant l'implémentation:

Côté client

Du côté client, vous n'avez qu'une seule page HTML qui interagit avec le serveur de manière dynamique via des appels AJAX. C'est ce que propose SPA. Toutes les balises a du côté client sont créées dynamiquement dans mon application. Nous verrons plus tard comment rendre ces liens visibles pour le bot de Google sur le serveur. Chacune de ces balises a doit pouvoir avoir un pretty URL dans la balise href afin que le bot de Google l'exploite. Vous ne voulez pas que la partie href soit utilisée lorsque le client clique dessus (même si vous voulez que le serveur puisse l'analyser, nous le verrons plus tard), car nous ne voulons peut-être pas nouvelle page à charger, uniquement pour passer un appel AJAX, obtenir l'affichage de certaines données dans une partie de la page et modifier l'URL via javascript (par exemple, à l'aide de HTML5 pushstate ou avec Durandaljs ). Nous avons donc à la fois un attribut href pour Google ainsi que sur onclick, qui effectue le travail lorsque l'utilisateur clique sur le lien. Maintenant, depuis que j'utilise Push-state, je ne veux pas de # sur l'URL, donc une balise typique a peut ressembler à ceci:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>

"catégorie" et "sous-catégorie" seraient probablement d'autres expressions, telles que "communication" et "téléphones" ou "ordinateurs" et "ordinateurs portables" pour un magasin d'appareils électriques. De toute évidence, il y aurait de nombreuses catégories et sous-catégories différentes. Comme vous pouvez le constater, le lien renvoie directement à la catégorie, à la sous-catégorie et au produit, et non en tant que paramètres supplémentaires à une page de magasin spécifique telle que http://www.xyz.com/store/category/subCategory/product111. C'est parce que je préfère des liens plus courts et plus simples. Cela implique qu’il n’y aura pas une catégorie du même nom que l’une de mes pages, c’est-à-dire "à propos de".
Je n’expliquerai pas comment charger les données via AJAX (la partie onclick), recherchez-le sur Google, il existe de nombreuses explications intéressantes. La seule chose importante ici que je veux mentionner est que lorsque l'utilisateur clique sur ce lien, je souhaite que l'URL du navigateur ressemble à ceci:
http://www.xyz.com/category/subCategory/product111. Et cette URL n'est pas envoyée au serveur! Rappelez-vous, il s’agit d’un SPA où toutes les interactions entre le client et le serveur se font via AJAX, pas de liens du tout! toutes les pages sont implémentées côté client et les différentes URL ne font pas appel au serveur (le serveur doit savoir comment gérer ces URL au cas où elles seraient utilisées comme liens externes depuis un autre site vers votre site, nous verrons cela plus tard dans la partie serveur). Maintenant, cela est géré à merveille par Durandal. Je le recommande vivement, mais vous pouvez également ignorer cette partie si vous préférez d'autres technologies. Si vous le choisissez et que vous utilisez également MS Visual Studio Express 2012 pour le Web, vous pouvez installer le Durandal Starter Kit , et là, dans Shell.js, utilisez quelque chose comme: ce:

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

Il y a quelques points importants à noter ici:

  1. La première route (avec route:'') est destinée à l'URL qui ne contient aucune donnée supplémentaire, c'est-à-dire http://www.xyz.com. Dans cette page, vous chargez des données générales avec AJAX. En fait, il peut ne pas y avoir de balises a sur cette page. Vous voudrez ajouter la balise suivante pour que le bot de Google sache quoi en faire:
    <meta name="fragment" content="!">. Cette balise fera que le robot de Google transforme l'URL en www.xyz.com?_escaped_fragment_= que nous verrons plus tard.
  2. L'itinéraire "à propos de" n'est qu'un exemple de lien vers d'autres "pages" que vous voudrez peut-être sur votre application Web.
  3. La difficulté réside dans le fait qu’il n’existe pas d’itinéraire "catégorie" et qu’il peut exister de nombreuses catégories différentes, dont aucune n’a un itinéraire prédéfini. C’est là que mapUnknownRoutes entre en jeu. Il mappe ces itinéraires inconnus sur l’itinéraire "magasin" et supprime également tout "!". à partir de l'URL au cas où il s'agirait d'un pretty URL généré par le moteur de recherche de Google. L'itinéraire 'store' prend les informations dans la propriété 'fragment' et effectue l'appel AJAX pour obtenir les données, les afficher et modifier l'URL localement. Dans mon application, je ne charge pas une page différente pour chaque appel de ce type; Je ne change que la partie de la page où ces données sont pertinentes et modifie également l'URL localement.
  4. Notez le pushState:true qui donne à Durandal l’utilisation des URL d’état Push.

C'est tout ce dont nous avons besoin du côté client. Il peut également être implémenté avec des URL hachées (dans Durandal, vous supprimez simplement le pushState:true pour cela). La partie la plus complexe (du moins pour moi ...) était la partie serveur:

Du côté serveur

J'utilise MVC 4.5 du côté serveur avec les contrôleurs WebAPI. Le serveur doit en réalité gérer trois types d'URL: ceux générés par Google, à la fois pretty et ugly, ainsi qu'une URL "simple" avec le même format que celui qui apparaît dans le navigateur du client. Regardons comment faire cela:

Les jolies URL et les "simples" sont d'abord interprétées par le serveur comme si l'on essayait de référencer un contrôleur inexistant. Le serveur voit quelque chose comme http://www.xyz.com/category/subCategory/product111 et recherche un contrôleur nommé 'catégorie'. Donc, dans web.config, j'ajoute la ligne suivante pour les rediriger vers un contrôleur de traitement d'erreur spécifique:

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

Maintenant, cela transforme l'URL en quelque chose comme: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111. Je veux que l'URL soit envoyée au client qui chargera les données via AJAX. L'astuce consiste donc à appeler le contrôleur 'index' par défaut comme s'il ne faisait référence à aucun contrôleur. Je fais ça par ajouter un hachage à l'URL avant tous les paramètres 'category' et 'subCategory'; l'URL hachée ne nécessite aucun contrôleur spécial à l'exception du contrôleur par défaut 'index' et les données sont envoyées au client qui supprime ensuite le hachage et utilise les informations après le hachage pour charger les données via AJAX. Voici le code du contrôleur du gestionnaire d'erreurs:

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


Mais qu'en est-il de la URL laides? Celles-ci sont créées par le bot de Google et doivent renvoyer du HTML brut contenant toutes les données que l'utilisateur voit dans le navigateur. Pour cela, j'utilise phantomjs . Phantom est un navigateur sans tête qui fait ce que le navigateur fait côté client, mais côté serveur. En d’autres termes, fantôme sait (entre autres choses) comment obtenir une page Web via une URL, l’analyser, y compris l’exécution de tout le code javascript qui y est contenu (ainsi que l’obtention de données via AJAX appels), et donner vous sauvegardez le code HTML qui reflète le DOM. Si vous utilisez MS Visual Studio Express, vous voudrez peut-être installer fantôme via ceci lien .
Mais d’abord, quand une URL laide est envoyée au serveur, nous devons l’attraper; Pour cela, j'ai ajouté au dossier 'App_start' le fichier suivant:

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

Ceci s'appelle de 'filterConfig.cs' aussi dans '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());
        }
    }
}

Comme vous pouvez le constater, 'AjaxCrawlableAttribute' achemine des URL laides vers un contrôleur nommé 'HtmlSnapshot', et voici ce contrôleur:

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

    }
}

Le view associé est très simple, une seule ligne de code:
@Html.Raw( ViewBag.result )
Comme vous pouvez le voir dans le contrôleur, Phantom charge un fichier javascript nommé createSnapshot.js dans un dossier que j'ai créé et nommé seo. Voici ce fichier 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);

Je veux d’abord remercier Thomas Davis pour la page où j’ai obtenu le code de base de :-).
Vous remarquerez quelque chose d'étrange ici: phantom continue à recharger la page jusqu'à ce que la fonction checkLoaded() renvoie la valeur true. Pourquoi donc? ceci est dû au fait que mon SPA effectue plusieurs AJAX appels pour obtenir toutes les données et les placer dans le DOM sur ma page, et le fantôme ne peut pas savoir quand tous les appels sont terminés avant de me renvoyer le reflet HTML du message. DOM. Ce que j'ai fait ici, c'est après l'appel final AJAX, j'ajoute un <span id='compositionComplete'></span>, de sorte que si cette balise existe, je sais que le DOM est terminé. Je le fais en réponse à l'événement compositionComplete de Durandal, voir ici pour plus d'informations. Si cela ne se produit pas dans 10 secondes, j'abandonne (cela ne devrait prendre qu'une seconde au maximum). Le code HTML renvoyé contient tous les liens que l'utilisateur voit dans le navigateur. Le script ne fonctionnera pas correctement car les balises <script> qui existent dans l’instantané HTML ne font pas référence à la bonne URL. Cela peut aussi être changé dans le fichier fantôme javascript, mais je ne pense pas que cela soit nécessaire, car le snapshort HTML n'est utilisé que par Google pour obtenir les liens a et non pour exécuter javascript; ces liens faire référencer une jolie URL, et si fait, si vous essayez de voir l’instantané HTML dans un navigateur, vous obtiendrez des erreurs javascript mais tous les liens fonctionneront correctement et vous redirigera vers le serveur avec une jolie URL cette fois page de travail.
Ça y est. Maintenant, le serveur sait comment gérer à la fois les jolies et les très mauvaises URL, avec l’option Push-state activée sur le serveur et le client. Toutes les URL laides sont traitées de la même manière avec fantôme. Il n'est donc pas nécessaire de créer un contrôleur distinct pour chaque type d'appel.
Une chose que vous pourriez préférer changer n’est pas de faire un appel général 'catégorie/sous-catégorie/produit' mais d’ajouter un 'magasin' afin que le lien ressemble à quelque chose comme: http://www.xyz.com/store/category/subCategory/product111. Cela évitera le problème de ma solution selon lequel toutes les URL non valides sont traitées comme si elles étaient en réalité des appels du contrôleur 'index', et je suppose qu'elles peuvent ensuite être gérées dans le contrôleur 'store' sans l'ajout au web.config J'ai montré ci-dessus.

122
beamish

Google peut maintenant afficher les pages SPA: Deprecating our AJAX crawling scheme

32
Edward Olamisan

Voici un lien vers un enregistrement vidéo de ma classe de formation Ember.js que j’avais animée à Londres le 14 août. Il présente une stratégie à la fois pour votre application côté client et pour votre application côté serveur, ainsi qu'une démonstration en direct de la mise en œuvre de ces fonctionnalités pour fournir à votre application JavaScript une page une dégradation progressive même pour les utilisateurs avec JavaScript désactivé. . 

Il utilise PhantomJS pour vous aider à explorer votre site Web. 

En bref, les étapes requises sont les suivantes: 

  • Avoir une version hébergée de l'application Web que vous souhaitez analyser, ce site doit avoir TOUTES les données que vous avez en production
  • Écrivez une application JavaScript (script PhantomJS) pour charger votre site web.
  • Ajoutez index.html (ou “/“) à la liste des URL à analyser
    • Pop la première URL ajoutée à la liste d'analyse
    • Charger la page et rendre son DOM 
    • Recherchez les liens de la page chargée qui pointent vers votre propre site (filtrage d'URL).
    • Ajoutez ce lien à une liste d'URL «explorables», si ce n'est déjà fait.
    • Stocker le DOM rendu dans un fichier sur le système de fichiers, mais enlevez d'abord TOUTES les balises de script
    • A la fin, créez un fichier Sitemap.xml avec les URL analysées.

Une fois cette étape effectuée, il est de votre devoir de fournir la version statique de votre code HTML en tant que partie de la balise noscript sur cette page. Cela permettra à Google et aux autres moteurs de recherche d'explorer chaque page de votre site Web, même si votre application est à l'origine une application d'une seule page. 

Lien vers le screencast avec tous les détails: 

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

4
Joachim H. Skeie

Vous pouvez utiliser http://sparender.com/ qui permet aux applications à page unique d'être analysées correctement.

0
ddtxra

Vous pouvez utiliser ou créer votre propre service pour prérender votre SPA avec le service appelé prérender. Vous pouvez le vérifier sur son site Web prerender.io et sur son projet github (il utilise PhantomJS et rend votre site Web pour vous). 

C'est très facile de commencer. Vous devez seulement rediriger les requêtes des robots d'exploration vers le service et ils recevront le code HTML rendu.

0
gabrielperales