Rechercher un thème :
Solutions >   
Toute l'encyclo pratique

Ajax : optimiser le référencement et le crawl d'une application web monopage

Fiche pratique
Ce tutoriel explique en détail comment optimiser le SEO d'une page en AJAX. Il s'appuie sur le framework Durandal côté client, et le langage ASP.NET pour la partie serveur.
 
 
Dernière mise à jour : mars 2017
 
Il est possible de développer un site internet uniquement sur une seule page, avec du contenu chargé grâce à des appels AJAX qui modifient la page. On appelle ces sites des "applications web monopage" (en anglais "single-page application", ou "SPA"). Leurs particularités poussent à procéder à certaines optimisations pour leur référencement. Ce tutorial s'appuie sur le framework Durandal pour la partie client et le langage ASP.NET pour la partie serveur.

Pour développer correctement son site, il est recommandé de suivre les préconisations de Google à propos des SPA. Il faut utiliser deux systèmes d'URL différentes sur son site en fonction du visiteur : d'abord, un système d'URL "jolies", destinées aux visiteurs. Ce sont des URL optimisées pour le référencement qui provoqueront l'affichage du site sur une seule page avec les interactions AJAX. En parallèle, utilisez un système d'URL "moches", c'est-à-dire des URL brutes avec des paramètres. Ces URL seront présentes pour les robots des moteurs de recherche. Elles seront utilisées pour que le site produise une capture en HTML de la page correspondante avec le même contenu que voit le visiteur, mais sans l'utilisation du langage JavaScript. Le robot peut ainsi correctement voir la page. Vous trouverez des informations complémentaires sur un guide officiel de Google pour les développeurs.

Pour la mise en place de ce fonctionnement côté client, utilisez le framework Durandal qui est conçu pour créer des sites monopage. Pour la gestion des liens du site, il faut qu'ils disposent à la fois d'un attribut "href" avec l'URL vue par les robots et d'un code JavaScript qui va charger le contenu de la page correspondante pour les visiteurs. Cette forme de lien est possible grâce à la fonction "pushState" de HTML5, qui manipule l'historique du navigateur. Le framework Durandal est recommandé pour cette fonctionnalité, mais on peut utiliser d'autres frameworks :

<!-- Exemple de lien pour l'application -->
<a href="http://www.monsite.fr/#!/categorie/sousCategorie/produitX" onClick="chargerProduit ('categorie','sousCategorie','produitX')>Voir le produit X</a>

Annonces Google



Le site aura bien entendu plusieurs catégories et sous-catégories. Le but est d'avoir dans ce cas l'URL suivante affichée sur le navigateur :
http://www.monsite.fr/categorie/sousCategorie/produitX

Pour réaliser cette opération, ouvrez le fichier "shell.js" de Durandal et insérez-y le code suivant :
define(['plugins/router', 'durandal/app'], function (router, app) {
    return {
        router: router,
        activate: function () {
            router.map([
                { route: '', title: 'Accueil', moduleId: 'vues/accueil', nav: true },
                { route: 'mentions-legales', moduleId: 'vues/mentionsLegales', nav: true }
            ])
                .buildNavigationModel()
                .mapUnknownRoutes(function (instruction) {
                    instruction.config.moduleId = 'vues/boutique';
                    instruction.fragment = instruction.fragment.replace("!/", ""); // Pour les URL "jolies", le caractère '#' sera déjà supprimé à cause du pushState ; il ne restera donc que le caractère '!'.
                    return instruction;
                });
            return router.activate({ pushState: true });
        }
    };
});


Ce code permet de gérer trois types de routes :
- Une route vide qui correspond à la page d'accueil du site : http://www.monsite.fr/ ;
- Une route spécifique qui indique une page gérée spécialement par l'application (ici les mentions légales du site) ;
- Une route inconnue. C'est ce cas qui est utilisé pour gérer les catégories et sous-catégories du site internet. La partie de l'URL non connue est récupérée dans le paramètre "fragment" et l'application détermine alors avec des appels AJAX le contenu à afficher.

Notez que les URL sont correctement affichées pour la partie client grâce au paramètre "pushState: true", qui indique à Durandal d'utiliser la fonction pushState. Dans votre application, pensez à ajouter sur la page d'arrivée la balise suivante :
<meta name="fragment" content="!">

Le contenu de cette balise correspond à ce qui suit l'URL du site. De cette manière, un robot transformera automatiquement l'URL "http://www.monsite.fr/!/categorie/sousCategorie/produitX" en "http://www. monsite.fr?fragment=categorie/sousCategorie/produitX".

Pour la partie serveur, on utilise le modèle MVC et le contrôleur "WebAPI". Il y a à ce moment-là trois types d'URL dans l'application : les URL "moches" pour les robots, les URL "jolies" qui vont être affichées dans le moteur de recherche, et les URL simples qui seront affichées dans le navigateur de l'internaute au moment où il a affiché une page du site.

Pour les URL simples et "jolies", le serveur va les référencer comme des URL menant vers un contrôleur qui n'existe pas. Dans le fichier "web.config", insérez les lignes suivantes pour que les erreurs soient redirigées vers un contrôleur spécial qui va les gérer :
<customErrors mode="On" defaultRedirect="Error">
    <error statusCode="404" redirect="Error" />
</customErrors><br/>

Ce contrôleur découpe l'URL pour insérer le caractère "#" avant la catégorie et la sous-catégorie. Il redirige ensuite sur cette nouvelle URL. Le contrôleur par défaut du site est alors appelé. Il lit le contenu situé après le caractère "#" et affiche la page correspondante grâce à des appels AJAX. Voici le code du contrôleur de gestion des 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()
        {
            //On découpe l'URL.
            string [] parts = Request.RequestUri.OriginalString.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries);
            string parameters = parts[ 1 ].Replace("aspxerrorpath=","");
            var response = Request.CreateResponse(HttpStatusCode.Redirect);
            //On ajoute ici le caractère "#" en reconstituant l'URL.
            response.Headers.Location = new Uri(parts[0].Replace("Error","") + string.Format("#", parameters));
            //On redirige sur cette nouvelle URL.
            return response;
        }
    }
}


Pour gérer les URL "moches" utilisées par les robots, on utilise la bibliothèque PhantomJS. PhantomJS se comporte comme un navigateur sans affichage. Il fonctionne de la même manière qu'un navigateur classique mais sur un serveur. Il génère le contenu d'une page en exécutant les appels AJAX et obtient ainsi le code source de la page générée. On peut ensuite envoyer ce code au robot. Pour que l'application reconnaisse les URL "moches", ajoutez le script suivant dans le dossier "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("?fragment=", "#");
                filterContext.Result = new RedirectToRouteResult(
                    new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
            }
            return;
        }
    }
}



Ce script est appelé dans le fichier "filterconfig.cs" situé également dans le dossier "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());
        }
    }
}

Le contrôleur "HtmlSnapshot" va afficher les vues au format HTML :
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(" ", Path.Combine(appRoot, "seocreateSnapshot.js"), url),
                FileName = Path.Combine(appRoot, "binphantomjs.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();
        }
    }
}


La vue associée au contrôleur ne comporte qu'une seule ligne de code :
@Html.Raw( ViewBag.result )

Dans le contrôleur, la bibliothèque PhantomJS charge un fichier "createsnapshot.js". Ce fichier devra comporter le code suivant :
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;
}
// Ouvre la page
page.open(system.args[1], function () { });
var checkComplete = function () {
    // Le chargement doit prendre moins de 5 secondes, sinon on retourne une erreur.
    if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount)


 
 
Copyright Benchmark Group Envoyer à un ami | Imprimer  
 

 
 
 

Google Analytics et SEO
Fiches pratiques
 Activer l'URL Rewriting avec Drupal 7 Ajax : optimiser le référencement et le crawl d'une application web monopage Ajouter le code de Google Analytics à un site PHP
 Android : comment utiliser Google Analytics pour mesurer des fragments API Google Analytics : comment régler le problème de login ? Bien faire apparaître un événement dans Universal Analytics : un clic par exemple
 Bien installer Google Analytics version Universal sur un site en AJAX Bien mettre en place le User ID d'Universal Analytics Ce que veulent dire les chiffres dans les cookies _ga d'Universal Analytics
 Comment ajouter Google Analytics à Drupal ? Comment ajouter l'ID de suivi de Google Analytics aux pages GitHub ? Comment avoir les rapports "Données démographiques" et "Centres d'intérêt" dans Google Analytics
 Comment combiner les statistiques de plusieurs sites dans Google Analytics ? Comment comprendre les informations Google Click ID (gclid) et les extraire ? Comment connaître le nombre d'impressions des pages avec Google Analytics ?
 Comment indiquer un en-tête "Vary: Accept-Encoding" dans .htaccess ? Conseils pour améliorer le SEO d'une application web monopage Empêcher l'indexation des paramètres d'URL avec robots.txt
 Empêcher l'indexation d'un site miroir (un sous-domaine) via le fichier robots.txt Faire fonctionner Google Analytics avec Rails 4 Générer des URL optimisées pour le SEO avec PHP [URL Rewriting]
 Gérer le SEO pour escaped_fragment (AJAX) Google Analytics API : résoudre le problème de permission (erreur 403) Google Analytics : ce que veut dire
 Google Analytics : comment connaître le nombre de visiteurs sur une page précise ? Google Analytics : comment supprimer une propriété Google Analytics : comment supprimer une vue
 Google Analytics : comment supprimer un site Google Analytics : faire marcher la configuration avancée des événements (avec onClick et jQuery) Google Analytics pour iOS : comment mesurer les écrans avec GAITrackedViewController ?
 Google Analytics : pourquoi la fonctionnalité "analyse des pages web" ne marche pas ? Google Analytics : résoudre le problème du code de suivi non installé .htaccess : mettre en place une page 404 personnalisée
 .htaccess : mettre en place une redirection sans changer l'URL Indexer de l'AJAX et du contenu généré dynamiquement Installer Google Analytics sur un site d'une seule page (ou "monopage")
 Installer Google Analytics sur un sous-domaine Liens : quand utiliser rel="external" ou rel="nofollow" ? Mesurer des sous-domaines avec Universal Analytics et Google Tag Manager
 Mesurer le remplissage des formulaires avec Google Analytics (via les événements) ? Mesurer les URL dans les iframes avec Google Analytics  Mettre en place les variables personnalisées avec Universal Analytics et analytics.js
 Migrer vers Universal Analytics : comment suivre les pages vues et les dimensions personnalisées ? Obtenir les statistiques Google Analytics avec Ruby on Rails OpenCart : comment enlever "index.php?route=common/home" ?
 Peut-on exporter un historique complet de données Google Analytics ? Pourquoi certains chiffres de Google Analytics peuvent différer de ceux de Flurry Analytics Pourquoi Google Analytics n'arrive pas à mesurer les vues sur iOS ?
 Pourquoi un site n'apparaît pas dans Google ? Quel code ajouter pour suivre les événements dans Google Analytics ? Quel est le code Google Analytics permettant de mesurer les événements OnClick ?
 Quelle différence entre "Temps moyen passé sur la page" et "Durée moyenne des sessions" dans Google Analytics ?  Quelles différences entre ga ou _gaq.push pour analyser des événements dans Google Analytics ? Quelle URL du sitemap mettre dans le robots.txt ?
 Rediriger des pages .php vers .html via .htaccess Rich Snippet : faut-il utiliser plusieurs attributs itemprop pour un seul élément ? SEO : empêcher l'indexation de parties de pages
 SEO : empêcher l'indexation de parties de pages SEO et Opencart : résoudre un problème d'URL mal optimisée  SEO : les avantages de WordPress sur des pages HTML écrites soi-même
 SEO : optimiser les URL des pages catégories et des pages produits dans Magento Site ASP.net MVC : comment rediriger toutes les pages sans www vers les pages avec www ? Sites référents dans Google Analytics : comment bloquer le trafic venant de certains sites via .htaccess ?
 Suivre les commentaires sur un blog avec Universal Analytics Suivre un appel Ajax avec Google Analytics (version Universal Analytics) Universal Analytics : comment suivre les transactions sur Magento ?
 Universal Analytics : quel est le client ID pour envoyer des mesures à Google Analytics ? Utiliser display:none est-il sans danger pour le SEO ?  Utiliser $_GET pour avoir de bonnes URL pour le SEO
 Variables personnalisées : les différences entre Universal Analytics et Google Analytics