Bevor Sie beginnen, stellen Sie bitte sicher, dass Sie verstehen, was Google benötigt , insbesondere die Verwendung hübscher und hässlicher URLs. Nun sehen wir uns die Implementierung an:
Client-Seite
Auf der Clientseite haben Sie nur eine einzige HTML-Seite, die über AJAX-Aufrufe dynamisch mit dem Server interagiert. Darum geht es bei SPA. Alle a
Tags auf der Clientseite werden dynamisch in meiner Anwendung erstellt. Wir werden später sehen, wie diese Links für Googles Bot auf dem Server sichtbar gemacht werden. Jedes dieser a
Tags muss in der Lage sein, ein pretty URL
im href
Tag zu haben, damit der Google-Bot es crawlen kann. Sie möchten nicht, dass das href
Teil verwendet wird, wenn der Client darauf klickt (obwohl Sie möchten, dass der Server es analysieren kann, werden wir das später sehen), da wir möglicherweise nicht möchten, dass eine neue Seite geladen wird. Nur um einen AJAX-Aufruf zu tätigen und einige Daten in einem Teil der Seite anzuzeigen und die URL über Javascript zu ändern (z. B. mit HTML5 pushstate
oder mit Durandaljs
). Also haben wir beide einehref
Attribut für Google sowie das, auf onclick
das der Job ausgeführt wird, wenn der Benutzer auf den Link klickt. Da push-state
ich jetzt keine #
URL verwenden möchte , kann ein typisches a
Tag folgendermaßen aussehen:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
"Kategorie" und "Unterkategorie" sind wahrscheinlich andere Ausdrücke wie "Kommunikation" und "Telefone" oder "Computer". und "Laptops" für ein Elektrogerätegeschäft. Offensichtlich würde es viele verschiedene Kategorien und Unterkategorien geben. Wie Sie sehen können, befindet sich der Link direkt auf die Kategorie, Unterkategorie und das Produkt, nicht als zusätzliche Parameter für eine bestimmte "Store" -Seite wie z http://www.xyz.com/store/category/subCategory/product111
. Das liegt daran, dass ich kürzere und einfachere Links bevorzuge. Dies bedeutet, dass es keine Kategorie mit demselben Namen wie eine meiner "Seiten" geben wird, dh "
Ich werde nicht darauf eingehen, wie man die Daten über AJAX (das onclick
Teil) lädt , sie auf Google sucht, es gibt viele gute Erklärungen. Das einzig Wichtige, das ich hier erwähnen möchte, ist, dass wenn der Benutzer auf diesen Link klickt, die URL im Browser folgendermaßen aussehen soll:
http://www.xyz.com/category/subCategory/product111
. Und diese URL wird nicht an den Server gesendet! Denken Sie daran, dies ist ein SPA, in dem die gesamte Interaktion zwischen dem Client und dem Server über AJAX erfolgt, überhaupt keine Links! Alle "Seiten" werden auf der Clientseite implementiert, und die unterschiedliche URL ruft den Server nicht auf (der Server muss wissen, wie mit diesen URLs umgegangen wird, wenn sie als externe Links von einer anderen Site zu Ihrer Site verwendet werden.) Wir werden das später auf der Serverseite sehen. Nun, das wird von Durandal wunderbar gehandhabt. Ich kann es nur empfehlen, aber Sie können diesen Teil auch überspringen, wenn Sie andere Technologien bevorzugen. Wenn Sie sich dafür entscheiden und wie ich auch MS Visual Studio Express 2012 für das Web verwenden, können Sie das Durandal Starter Kit installieren und dort etwa Folgendesshell.js
verwenden:
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 });
}
};
});
Hier sind einige wichtige Dinge zu beachten:
- Die erste Route (mit
route:''
) ist für die URL, die keine zusätzlichen Daten enthält, d http://www.xyz.com
. H. Auf dieser Seite laden Sie allgemeine Daten mit AJAX. Auf a
dieser Seite befinden sich möglicherweise überhaupt keine Tags. Sie sollten das folgende Tag hinzufügen, damit der Google-Bot weiß, was damit zu tun ist :
<meta name="fragment" content="!">
. Mit diesem Tag transformiert Googles Bot die URL, zu www.xyz.com?_escaped_fragment_=
der wir später sehen werden.
- Die "Über" -Route ist nur ein Beispiel für einen Link zu anderen "Seiten", die Sie möglicherweise in Ihrer Webanwendung wünschen.
- Der schwierige Teil ist nun, dass es keine Kategorie-Route gibt und es möglicherweise viele verschiedene Kategorien gibt, von denen keine eine vordefinierte Route hat. Hier
mapUnknownRoutes
kommt es ins Spiel. Es ordnet diese unbekannten Routen der 'Store'-Route zu und entfernt auch alle'! ' von der URL, falls es sich um eine pretty URL
von Googles Suchmaschine generierte handelt. Die 'store'-Route nimmt die Informationen in der' fragment'-Eigenschaft und führt den AJAX-Aufruf aus, um die Daten abzurufen, anzuzeigen und die URL lokal zu ändern. In meiner Anwendung lade ich nicht für jeden solchen Aufruf eine andere Seite. Ich ändere nur den Teil der Seite, auf dem diese Daten relevant sind, und ändere auch die URL lokal.
- Beachten Sie,
pushState:true
was Durandal anweist, Push-Status-URLs zu verwenden.
Dies ist alles, was wir auf der Client-Seite brauchen. Es kann auch mit Hash-URLs implementiert werden (in Durandal entfernen Sie einfach die pushState:true
dafür). Der komplexere Teil (zumindest für mich ...) war der Serverteil:
Serverseite
Ich benutze MVC 4.5
auf der Serverseite mit WebAPI
Controllern. Der Server muss tatsächlich drei Arten von URLs verarbeiten: die von Google generierten - sowohl pretty
als ugly
auch eine "einfache" URL mit demselben Format wie die im Browser des Clients angezeigte. Schauen wir uns an, wie das geht:
Hübsche und einfache URLs werden vom Server zunächst so interpretiert, als würde versucht, auf einen nicht vorhandenen Controller zu verweisen. Der Server sieht so etwas wie http://www.xyz.com/category/subCategory/product111
und sucht nach einem Controller namens 'category'. Daher web.config
füge ich die folgende Zeile hinzu, um diese an einen bestimmten Fehlerbehandlungscontroller umzuleiten:
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
Dadurch wird die URL in Folgendes umgewandelt : http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111
. Ich möchte, dass die URL an den Client gesendet wird, der die Daten über AJAX lädt. Der Trick hier besteht darin, den Standard-Index-Controller so aufzurufen, als würde er nicht auf einen Controller verweisen. Dazu füge ich der URL vor allen Parametern 'category' und 'subCategory' einen Hash hinzu . Die Hash-URL erfordert keinen speziellen Controller außer dem Standard-Index-Controller. Die Daten werden an den Client gesendet, der dann den Hash entfernt und die Informationen nach dem Hash verwendet, um die Daten über AJAX zu laden. Hier ist der Code des Fehlerbehandlungs-Controllers:
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;
}
}
}
Aber was ist mit den hässlichen URLs ? Diese werden vom Google-Bot erstellt und sollten einfachen HTML-Code zurückgeben, der alle Daten enthält, die der Benutzer im Browser sieht. Dafür benutze ich Phantomjs . Phantom ist ein kopfloser Browser, der das tut, was der Browser auf der Clientseite tut - aber auf der Serverseite. Mit anderen Worten, Phantom weiß (unter anderem), wie man eine Webseite über eine URL abruft, analysiert, einschließlich des gesamten darin enthaltenen Javascript-Codes (sowie des Abrufs von Daten über AJAX-Aufrufe), und gibt Ihnen den reflektierten HTML-Code zurück das DOM. Wenn Sie MS Visual Studio Express verwenden, möchten viele Phantom über diesen Link installieren .
Aber zuerst, wenn eine hässliche URL an den Server gesendet wird, müssen wir sie abfangen. Zu diesem Zweck habe ich dem Ordner 'App_start' die folgende Datei hinzugefügt:
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;
}
}
}
Dies wird von 'filterConfig.cs' auch in 'App_start' aufgerufen:
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());
}
}
}
Wie Sie sehen können, leitet 'AjaxCrawlableAttribute' hässliche URLs an einen Controller mit dem Namen 'HtmlSnapshot' weiter. Hier ist dieser Controller:
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();
}
}
}
Das zugehörige view
ist sehr einfach, nur eine Codezeile:
@Html.Raw( ViewBag.result )
Wie Sie im Controller sehen können, lädt Phantom eine Javascript-Datei, die createSnapshot.js
unter einem von mir erstellten Ordner namens benannt ist seo
. Hier ist diese Javascript-Datei:
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);
Ich möchte mich zuerst bei Thomas Davis für die Seite bedanken, auf der ich den Basiscode erhalten habe :-).
Sie werden hier etwas Seltsames bemerken: Phantom lädt die Seite so lange neu, bis die checkLoaded()
Funktion true zurückgibt. Warum ist das so? Dies liegt daran, dass mein spezifisches SPA mehrere AJAX-Aufrufe ausführt, um alle Daten abzurufen und im DOM auf meiner Seite zu platzieren. Das Phantom kann nicht wissen, wann alle Aufrufe abgeschlossen sind, bevor es mir die HTML-Reflektion des DOM zurückgibt. Was ich hier getan habe, ist <span id='compositionComplete'></span>
, dass ich nach dem letzten AJAX-Aufruf ein hinzufüge , sodass ich weiß, dass das DOM abgeschlossen ist, wenn dieses Tag vorhanden ist. Ich mache das als Reaktion auf Durandals compositionComplete
Ereignis, siehe hierfür mehr. Wenn dies nicht innerhalb von 10 Sekunden geschieht, gebe ich auf (es sollte höchstens eine Sekunde dauern). Der zurückgegebene HTML-Code enthält alle Links, die der Benutzer im Browser sieht. Das Skript funktioniert nicht ordnungsgemäß, da die <script>
im HTML-Snapshot vorhandenen Tags nicht auf die richtige URL verweisen. Dies kann auch in der Javascript-Phantomdatei geändert werden, aber ich denke nicht, dass dies notwendig ist, da der HTML-Snapshort nur von Google verwendet wird, um die a
Links abzurufen und kein Javascript auszuführen. diese Links tun Bezug ziemlich URL, und wenn die Tat, wenn Sie versuchen , das HTML - Snapshot in einem Browser zu sehen, werden Sie JavaScript - Fehler , aber alle Links bekommen ordnungsgemäß funktionieren und Sie auf den Server direkt wieder mit einem hübschen URL dieses Mal die voll funktionsfähige Seite bekommen.
Das ist es. Jetzt weiß der Server, wie man mit hübschen und hässlichen URLs umgeht, wobei der Push-Status sowohl auf dem Server als auch auf dem Client aktiviert ist. Alle hässlichen URLs werden mit Phantom gleich behandelt, sodass für jeden Anruftyp kein separater Controller erstellt werden muss.
Eine Sache, die Sie vielleicht lieber ändern möchten, ist nicht, einen allgemeinen Aufruf von "Kategorie / Unterkategorie / Produkt" zu tätigen, sondern einen "Shop" hinzuzufügen, damit der Link ungefähr so aussieht : http://www.xyz.com/store/category/subCategory/product111
. Dadurch wird das Problem in meiner Lösung vermieden, dass alle ungültigen URLs so behandelt werden, als würden sie tatsächlich an den 'Index'-Controller aufgerufen, und ich nehme an, dass diese dann innerhalb des' Store'-Controllers behandelt werden können, ohne dass die web.config
oben gezeigte hinzugefügt wird .