Avant de commencer, assurez-vous de comprendre ce que google exige , en particulier l'utilisation d' URL jolies et laides . Voyons maintenant l'implémentation:
Côté client
Du côté client, vous n'avez qu'une seule page html qui interagit dynamiquement avec le serveur via des appels AJAX. c'est ça le SPA. Toutes les a
balises côté client sont créées dynamiquement dans mon application, nous verrons plus tard comment rendre ces liens visibles au bot de google sur le serveur. Chacun de ces a
besoins d'étiquette pour être en mesure d'avoir un pretty URL
dans la href
balise si le bot de Google va explorer. Vous ne voulez pas que la href
partie soit utilisée lorsque le client clique dessus (même si vous voulez que le serveur puisse l'analyser, nous verrons cela plus tard), car nous ne voulons peut-être pas qu'une nouvelle page se charge, uniquement pour faire un appel AJAX pour obtenir des données à afficher dans une partie de la page et changer l'URL via javascript (par exemple en utilisant HTML5 pushstate
ou avec Durandaljs
). Donc, nous avons à la fois unhref
attribut pour google ainsi que sur onclick
lequel fait le travail lorsque l'utilisateur clique sur le lien. Maintenant, puisque j'utilise push-state
je n'en veux pas #
sur l'URL, donc une a
balise typique 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 des «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 voir, le lien est directement vers la catégorie, la sous-catégorie et le produit, et non comme des paramètres supplémentaires vers 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 de catégorie avec le même nom qu'une de mes 'pages', c'est à dire '
Je n'entrerai pas dans la façon de charger les données via AJAX (la onclick
partie), recherchez-les sur google, il y a beaucoup de bonnes explications. La seule chose importante que je veux mentionner ici est que lorsque l'utilisateur clique sur ce lien, je veux que l'URL du navigateur ressemble à ceci:
http://www.xyz.com/category/subCategory/product111
. Et c'est l'URL n'est pas envoyée au serveur! rappelez-vous, c'est un SPA où toute l'interaction entre le client et le serveur se fait via AJAX, pas de liens du tout! toutes les `` pages '' sont implémentées côté client et les différentes URL n'appellent pas le serveur (le serveur a besoin de savoir comment gérer ces URL au cas où elles seraient utilisées comme liens externes d'un autre site vers votre site, nous verrons cela plus tard sur la partie côté serveur). Maintenant, cela est géré à merveille par Durandal. Je le recommande fortement, mais vous pouvez également sauter 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 comme moi, vous pouvez installer le kit de démarrage Durandal , et là, dans shell.js
, utilisez quelque chose comme ceci:
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:
- La première route (avec
route:''
) est pour l'URL qui ne contient pas de données supplémentaires, c'est-à-dire http://www.xyz.com
. Dans cette page, vous chargez des données générales à l'aide d'AJAX. Il se peut qu'il n'y ait en fait aucune a
balise sur cette page. Vous voulez ajouter la balise suivante si le bot de Google saura quoi faire avec elle:
<meta name="fragment" content="!">
. Cette balise permettra au bot de Google de transformer l'URL vers www.xyz.com?_escaped_fragment_=
laquelle nous verrons plus tard.
- La route «à propos» n'est qu'un exemple d'un lien vers d'autres «pages» que vous voudrez peut-être sur votre application Web.
- Maintenant, la partie délicate est qu'il n'y a pas de route de «catégorie», et il peut y avoir de nombreuses catégories différentes - dont aucune n'a une route prédéfinie. C'est là
mapUnknownRoutes
qu'intervient. Il mappe ces routes inconnues à la route «magasin» et supprime également tout «! à partir de l'URL au cas où elle pretty URL
serait générée par le moteur de recherche de Google. La route '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 change également l'URL localement.
- Notez le
pushState:true
qui indique à Durandal d'utiliser les 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
côté serveur avec des WebAPI
contrôleurs. Le serveur doit en fait gérer 3 types d'URL: celles générées par Google - à la fois pretty
et ugly
aussi une URL «simple» avec le même format que celle qui apparaît dans le navigateur du client. Voyons comment procéder:
Les jolies URL et les «simples» sont d'abord interprétées par le serveur comme s'il 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 gestion des erreurs spécifique:
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
Maintenant, cela transforme l'URL à 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, donc l'astuce ici est d'appeler le contrôleur «index» par défaut comme s'il ne faisait référence à aucun contrôleur; Je fais cela en ajoutant 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 «index» par défaut 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 des URL laides ? Ceux-ci sont créés 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 faisant ce que le navigateur fait du côté client - mais du côté serveur. En d'autres termes, phantom sait (entre autres) comment obtenir une page Web via une URL, l'analyser, y compris exécuter tout le code javascript qu'elle contient (ainsi que récupérer des données via des appels AJAX), et vous rendre le HTML qui reflète le DOM. Si vous utilisez MS Visual Studio Express, vous souhaitez souvent installer phantom via ce lien .
Mais d'abord, lorsqu'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 est appelé depuis 'filterConfig.cs' également 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 voir, 'AjaxCrawlableAttribute' achemine les 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();
}
}
}
L'associé view
est très simple, juste une ligne de code:
@Html.Raw( ViewBag.result )
Comme vous pouvez le voir dans le contrôleur, phantom charge un fichier javascript nommé createSnapshot.js
sous un dossier que j'ai créé appelé 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 tiens d'abord à remercier Thomas Davis pour la page où j'ai obtenu le code de base :-).
Vous remarquerez quelque chose d'étrange ici: phantom continue de recharger la page jusqu'à ce que la checkLoaded()
fonction retourne true. Pourquoi donc? c'est parce que mon SPA spécifique fait plusieurs appels AJAX pour obtenir toutes les données et les placer dans le DOM sur ma page, et phantom ne peut pas savoir quand tous les appels sont terminés avant de me renvoyer le reflet HTML du DOM. Ce que j'ai fait ici, c'est après le dernier appel AJAX, j'ajoute un <span id='compositionComplete'></span>
, de sorte que si cette balise existe, je sache que le DOM est terminé. Je fais cela en réponse à l' compositionComplete
événement de Durandal , voir icipour plus. Si cela ne se produit pas dans les 10 secondes, j'abandonne (cela ne devrait prendre qu'une seconde au maximum). Le HTML renvoyé contient tous les liens que l'utilisateur voit dans le navigateur. Le script ne fonctionnera pas correctement car les <script>
balises qui existent dans l'instantané HTML ne référencent pas la bonne URL. Cela peut également être modifié dans le fichier fantôme javascript, mais je ne pense pas que ce soit nécessaire car le snapshort HTML est uniquement utilisé par Google pour obtenir les a
liens et non pour exécuter javascript; ces liens font référence à une jolie URL, et si en 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 dirigeront à nouveau vers le serveur avec une jolie URL cette fois obtenir la page entièrement fonctionnelle.
Ça y est. Maintenant, le serveur sait comment gérer à la fois les URL jolies et laides, avec l'état push activé sur le serveur et le client. Toutes les URL laides sont traitées de la même manière en utilisant un 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 le changement est de ne pas faire une « catégorie / sous / produit » appel général , mais d'ajouter un « magasin » de sorte que le lien ressemblera à quelque chose comme: http://www.xyz.com/store/category/subCategory/product111
. Cela évitera le problème dans ma solution que toutes les URL invalides sont traitées comme si elles étaient en fait des appels au contrôleur `` index '', et je suppose que celles-ci peuvent être traitées ensuite dans le contrôleur `` magasin '' sans l'ajout de celui que web.config
j'ai montré ci-dessus .