SEO med AngularJS

Traditionellt sett så har arbetet med SEO främst handlat om att optimera sitt innehåll så mycket som möjligt för att få Google, Bing och Yahoo (om man nu ska räkna de största i västvärlden) att ranka just din sida så högt som möjligt. Det finns ett flertal tekniker som bör tillämpas för att knipa de åtråvärda platserna på första sidan för en specifik sökning, dessa är dock inte fokus i detta inlägg. Istället ska vi kolla lite närmre på hur du kan arbeta med SEO och AngularJS.

Översikt

Framfarten för såkallade ”one-page-applications” som inte använder sig av klassiska sidladdningar utan istället laddar in allt innehåll med AJAX medför en del nya utmaningar relaterade till just SEO. Utöver det som tidigare nämnts handlar det nu även om att faktiskt visa något innehåll för sökmotorerna överhuvudtaget. När en sökmotor, eller crawler som det brukar kallas, ska indexera en sida ber den servern att skicka över en färdig HTML-sida som sedan skannas igenom efter innehåll, länkar, meta-taggar osv. Problemen är ganska uppenbara när man tänker på det faktum att en JavaScript-baserad ”one-page-app” i många fall renderas i webbläsaren, inte av servern.

Den HTML som servern skickar och som en crawler ska indexera kan t.ex se ut såhär innan webbläsaren har hunnit läsa in JavaScriptet och själv renderat allt innehåll (som ofta laddas in med AJAX):


...
<body ng-app="minapp">
    <div ng-view></div>
</body>
...

Ovanstående är ett exempel på en mycket enkel applikation byggd med ramverket AngularJS kan se ut innan webbläsaren har hunnit läsa in all JavaScript och renderat sidan. Inte mycket att indexera där, eller hur? Hur löser vi då det här? Det finns ett antal metoder att tillgripa för att lösa problemet men vi tänker främst fokusera på att upptäcka om det är en crawler som frågar efter en sida eller ej, och om så är fallet, skicka en helt färdig ”avbild” av hur sidan ser ut när browsern har fått rendera allt.

Angular

Det första vi behöver göra är att förbereda vår AngularJS-applikation för att kunna hantera scenariot att det är en crawler som besöker sidan. Om man studerar mer noggrant hur en crawler hanterar URLer så upptäcker man att tecknen ’#!’ omvandlas till ’?_escaped_fragment_=’ av sökmotorerna. Tänk er följande exempel:


http://www.minapp.com/#!/information/om-oss
// Blir istället
http://www.minapp.com/?_escaped_fragment_=/information/om-oss

Det här är en standard som introducerades för att just kunna indexera innehåll som inte renderas direkt på servern. Vad har vi för att nytta av det här då? Jo, genom att säga till vår AngularJS-applikation att använda sig av ett utropstecken i samband med hashtagen i URLen kan vi sedan på servern enkelt upptäcka om ’_escaped_fragment_’ finns med i vår URL, och därmed avgöra om det är en crawler som frågar efter en sida. Vi börjar med att säga åt vår applikation att faktiskt använda ’#!’, såkallat hashbang:


angular.module('minApp', []).config(['$locationProvider', function($locationProvider) {
    $locationProvider.hashPrefix('!');
}]);

På detta sätt lägger AngularJS automatiskt till ett utropstecken efter varje hash (observera dock att detta förutsätter att man inte använder sig av $locationProvider.html5Mode(true), tillvägagångsättet blir något annorlunda då).

Servern

I vårt exempel använder vi oss av en simpel webserver i NodeJS för att se om vi fått en förfrågan från en crawler eller ej, man kan dock tillämpa liknande strategier för andra typer av webservrar/språk. Utöver det använder vi oss av ett plugin till den populära ”task-management”-modulen GruntJS, nämligen grunt-htmlSnapshots.

För att upptäcka vår crawler använder vi oss av följande middleware för alla inkommande requests till NodeJS. Det här läggs vanligtvis till när man konfigurerar sin app med minApp.configure():


minApp.configure(function() {
    // Mer konfiguration här...

    // Vår middleware för att upptäcka crawlers
    minApp.use(function(req, res, next) {

        var crawler_url = req.query._escaped_fragment_;

        // Gå vidare om vi inte har något escaped fragment i vår querystring
        if(!crawler_url) next();

        // Om vi bara har '/' eller tom sträng, skicka snapshot av index-sidan
        if(crawler_url.length === 0 || crawler_url === '/') {
            crawler_url = 'index.html';
        }

        // Lägg till .html om det inte redan finns
        crawler_url = crawler_url.indexOf('.html') === -1 ? crawler_url += '.html' : crawler_url;

        // Ta bort det första slashet om vi har det
        crawler_url = crawler_url.charAt(0) === '/' ? crawler_url.substr(1) : crawler_url;


        // Och skicka filen
        try {

            var fileToSend = path.resolve(__dirname + '../snapshots/' + crawler_url);
            res.sendfile(fileToSend);

        } catch(e) {
            res.send(404, e);
        }

    });
});

Vad som händer här är att vi för varje inkommande förfrågning kollar om vi har en parameter i ”query-stringen” som är ’_escaped_fragment_’. Om vi har det extraherar vi den route som faktiskt frågades efter och skickar den korrekta avbilden(snapshoten) av hur sidan hade sett ut om webbläsaren hade fått rendera den. Smidigt!
 

GruntJS – html-snapshots

Hur skapar vi dessa snapshots som vi skickar till crawlern då? Jo, med hjälp av grunt som är ett fantastiskt verktyg vilket automatiserar flertalet saker som annars kan vara tråkiga och enformiga att göra manuellt. Exempel på detta är minifiering av script/css, testning och att göra din applikation redo för distribution. Det finns även ett flertal nyttiga plugin till GruntJS, och ett av dem är html-snapshots vilket låter dig automatisera processen av att skapa upp färdiga html-sidor av din JavaScript-applikation.

Nedanstående kod visar hur du kan sätta upp en uppgift i Grunt som tar ”snapshots” av de routes du önskar. Dessa sparas sedan i en separat mapp och skickas istället för den egentliga JavaScript-applikationen om servern upptäcker att det är en crawler som frågar efter sidan:


htmlSnapshot: {
    dev: {
        options: {
            snapshotPath: 'snapshots/',
            sitePath: 'http://minapp:dev/',
            msWaitForPages: 1000,
            urls: [
                '/',
                '/information/om-oss'
            ]
        }
    }
}

// Och registrera
grunt.registerTask('snapshots', ['htmlSnapshot:dev']);

Det finns mer att läsa om de olika alternativen som kan användas för att ta snapshots här: grunt-html-snapshots

Summering

Sådär, nu har vi en fullt fungerande uppsättning som kan skapa avbilder (snapshots) av din AngularJS-applikation och visa dessa, fulla med innehåll, för sökmotorer som vill indexera din ”one-page-applikation”.

Skrivet den 14 februari av

Vilhelm Josander

Skriv en kommentar

  • XHTML: Du kan använda följande taggar: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

2 kommentarer

  1. Martin skriver:

    Om man vill generera sidorna i skarp miljö så är https://github.com/prerender/prerender-node fett!

  2. Absolut, vi har kikat på det en del också Martin. Googles crawler verkar dock få mer och mer stöd för att rendera sidor helt baserade på JS: http://googlewebmastercentral.blogspot.se/2014/05/understanding-web-pages-better.html. Förhoppningsvis kommer sådana här workarounds vara ett minne blott snart!