Blindflug im Web

SEO für Angular-Anwendungen: Tipps und Tricks
Keine Kommentare

Suchmaschinenoptimierung (Search Engine Optimization, SEO) ist ein sehr umfangreiches Thema. In diesem Artikel möchte ich beleuchten, wie die Indizierung von Google funktioniert, warum Google keine Seiten einer Angular-Anwendung crawlen kann und wie wir dieses essenzielle Problem angehen können.

SEO besteht aus einer Reihe von Techniken, die für mehr Traffic auf der eigenen Webseite, dem Blog oder in Bezug auf eine App sorgen sollen. Dafür wird versucht, das eigene Ranking in den Suchergebnissen bekannter Suchmaschinen wie Google, Yahoo usw. zu verbessern. Um die Techniken ein wenig besser zu verstehen, schauen wir uns zunächst an, wie eine Suchmaschine eigentlich funktioniert.

Suchmaschinen lassen konstant Programme über sämtliche Webseiten im Internet laufen, sogenannte Spider. Diese durchsuchen alle Teile dieser Seiten nach Informationen, die für eine Suche ggf. von Interesse sein könnten, und speichern sie in einer speziellen Datenbank, dem Suchindex. Den Spidern von Google kann man dabei auch den Zugriff verwehren, indem man die Datei robots.txt entsprechend anpasst.

Gibt man einen Suchbegriff etwa in Google ein, erhält der Suchindex zunächst alle Seiten, die mit irgendeinem der eingegebenen Wörter einen Treffer liefern. Durch individuelle Algorithmen, die sich von Suchmaschine zu Suchmaschine unterscheiden, wird dann geprüft, wo die Suchbegriffe in den entsprechenden Seiten enthalten sind, also bspw. im Seitentitel oder in der Metabeschreibung. Der Algorithmus kann zudem prüfen, ob alle Wörter auf der Seite enthalten sind, in welcher Reihenfolge sie vorkommen und so weiter. Nach etlichen Berechnungen gibt die Suchmaschine dann im besten Fall die relevantesten Webseiten an den Suchenden zurück.

Trivia

Google hat seinerzeit einen berühmten Algorithmus entwickelt, der darauf basiert, wie viele andere Seiten auf eine bestimmte Webseite verlinken. Die Idee dahinter war, dass eine Seite besonders relevant sein muss, wenn möglichst viele andere Webseiten das augenscheinlich ebenfalls so sehen und auf diese Seite verlinken.

Die Algorithmen von Suchmaschinen werden selbstverständlich ständigen Verbesserungen unterzogen, um den Nutzern die bestmöglichen Ergebnisse präsentieren zu können. Die meisten modernen Algorithmen machen dabei auch immer mehr Gebrauch von Machine Learning, um die Suchergebnisse zu verbessern.

Wie finden Suchmaschinen meine Seite?

Es gibt einige grundlegende Dinge, die man tun kann, um der Suchmaschine mitzuteilen, welche Informationen man bereitstellt:

  • Fokussieren auf den Inhalt: Das wichtigste Element ist der Content. Keine Tricks und Kniffe können eine Seite gut ranken lassen, wenn der Inhalt nicht gut ist. Daraus ergibt sich: Guter Content sorgt für mehr Besucher auf der Webseite, dies wiederum sorgt für eine höhere Click-Through-Rate und damit für ein besseres Ranking in Suchmaschinen.
  • Angeben von Titel und Meta Tags: Natürlich wird die Angabe eines Seitentitels und das Einstellen von Meta Tags keine Webseite auf magische Weise ganz nach oben in den Suchergebnissen katapultieren. Dennoch sind diese beiden Dinge für die Click-Through-Rate sehr wichtig und ermutigen andere Leute, eher auf die eigene als auf eine Konkurrenzseite zu verlinken.
  • Nutzen von Canonical Tags
  • Erstellen von crawlbaren URLs
  • Bereitstellen einer Sitemap für den Google Bot
  • Hochhalten der Geschwindigkeit einer Seite

Nach dem gleichen Prinzip wie die Suchmaschinen-Crawler funktionieren auch Social Media Crawler auf Plattformen wie Facebook oder Twitter. Wann immer wir einen Link teilen, durchläuft der Crawler der Social-Media-Plattform den Inhalt der Zielseite. Dabei kann es vorkommen, dass von dort ein Bild oder der Seitentitel abgerufen wird, um unser Posting optisch aufzuwerten.

Crawling von Angular-Anwendungen: Das Problem

Man könnte nun annehmen, dass die Berücksichtigung sämtlicher oben genannter Punkte ausreichend ist, damit die Suchmaschinen-Crawler die eigene Seite gut durchsuchen können. So einfach ist es leider nicht. Schaut man sich den Quelltext einer Angular-Anwendung im Browser an, stellt man fest, dass der Code aus Listing 1 das einzige ist, was die Suchmaschine im DOM sehen kann. Der Code enthält außer ein wenig initialem HTML nichts, was für einen Crawler interessant sein könnte. Die eigene Anwendung wird stattdessen dynamisch dem HTML-Element durch den Selektor app-root hinzugefügt, sobald die Datei main.js geladen ist.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>AngularUniversal</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css"></head>
<body>
  <app-root></app-root>
<script type="text/javascript" src="main.js"></script></body>
</html>

Google kann, wie auf der Google I/O 2018 in einem Talk berichtet wurde, JavaScript crawlen. Die Dateien können also genau so ausgelesen werden, wie ein Browser dies täte. Die gecrawlten Webseiten werden dafür durch den Indexer Caffeine geschleust. Neben anderen coolen Features ist die Hauptaufgabe von Caffeine das Rendern von JavaScript-Webseiten. Diese Lösung ist nicht zu 100 Prozent zuverlässig und man muss die eigenen Seiten vorher via „Fetch as Google“-Feature testen. Nur so kann man sicher sein, was Google am Ende wirklich sieht.

Unterstützung für den Crawler

Damit die Suchmaschinen den eigenen Content einer Angular-Anwendung erfassen können, muss der Content irgendwie anders als mit dem Selektor für HTML-Elemente (app-root) gerendert werden. Dafür gibt es zwei Möglichkeiten.

Serverseitiges Rendering

Beim serverseitigen Rendering wird der gesamte Inhalt vom Server kompiliert, der wiederum ein komplettes HTML mit CSS an den Client ausliefert. Diese Anwendungen werden universelle Anwendungen oder isomorphische Anwendungen genannt. Wird eine neue Route angefordert, muss der Server dann den Kompilierungsprozess erneut komplett durchlaufen, um den gewünschten Content auszuliefern. Packages wie Angular Universal erlauben das Kompilieren vorgewählter Routes, sodass man eine vollständige HTML-Datei auf einem statischen Server ablegen kann. Ein Vorteil ist, dass der Nutzer mit der Zielseite schneller interagieren kann, als dies bei einer gewöhnlichen clientseitigen Anwendung der Fall wäre.

JavaScript Days 2019

JavaScript Testing in der Praxis (Teil 1 + 2)

mit Dominik Ehrenberg (Crosscan) und Sebastian Springer (MaibornWolff)

Fortgeschrittene schwarze Magie in TypeScript

mit Peter Kröner (‚Webtechnologie-Erklärbär‘)

Gemeinsam mit dem serverseitig gerenderten HTML wird auch eine normale clientseitige Anwendung ausgeliefert. Diese Angular-Anwendung übernimmt schlussendlich die Kontrolle, und alles fühlt sich wie eine gewöhnliche Single Page Application (SPA) an. Vorteil dieser Lösung ist die Möglichkeit, eine App client- und serverseitig mit dem gleichen Code zu erstellen. Der User kann so sehr schnell auf die individuellen Daten zugreifen. Für unterschiedliche Routes sind zudem unterschiedliche Meta Tags möglich, was bei reinen SPAs eher schwierig durchzusetzen ist. Der große Nachteil ist, dass man einen Server betreiben muss.

Prerendering

Für kleinere Anwendungen lohnt sich serverseitiges Rendering womöglich nicht. Hier könnte Prerendering eine geeignete Alternative sein, da es unabhängig vom Server funktioniert. Statisches HTML und CSS kommt zum Einsatz und wird an den Crawler ausgegeben. Für das Prerendering können entsprechende Services wie prerender.io oder ein statisches Hosting mit Prerendering Feature wie netlify oder roast.io genutzt werden. Prerender.io kann man sogar selbst hosten, wenn man das möchte.

SEO-freundliche Angular-Anwendungen mit Angular Universal

Wie man serverseitiges Rendering mit Hilfe von Angular Universal umsetzen kann, möchte ich nun Schritt für Schritt durchgehen. Dafür braucht man zunächst einmal ein initiales Set-up. Man kann hierbei irgendeine existierende Angular-Anwendung nutzen oder die grundlegende Anwendung klonen, die die ersten zehn Elemente des Periodensystems auf der Seite anzeigt:

git clone https://github.com/piyukore06/angular-universal.git

Nach der Installation der Abhängigkeiten kann man das Projekt erstellen. Wichtig ist es sicherzustellen, dass index.html nichts außer dem app-root-Selektor enthält. Das Ziel ist es, die Liste mit Elementen in der index.html während des Build-Prozesses zu befüllen. Auf die Installation der Abhängigkeiten der Anwendung folgen die Abhängigkeiten von Angular Universal via

npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine

Dabei ist darauf zu achten, dass die Abhängigkeit zum Angular-Plattformserver die gleiche Version hat wie die anderen Angular Packages.

Die generelle Unterstützung für Angular Universal bedingt die Implementierung bzw. Erstellung von Dateien, damit die Anwendung später läuft. Auf Seiten des Clients ist damit etwa die Datei app.module gemeint, die mehr oder weniger direkt die Definition aller in der Anwendung genutzten Module beinhaltet. Außerdem enthält app.module das Modul BrowserModule, das einige spezifische Services für die Browserumgebung zur Verfügung stellt. Die Datei main.ts bootstrappt die App mit app.module. Damit das serverseitige Rendering funktioniert, muss die Anwendung allerdings in einer anderen Weise gebootstrappt werden. Hierfür muss das Modul ServerModule hinzugefügt werden, um die Anwendung auf dem Server starten zu können.

@NgModule({
  declarations: [
    AppComponent,
    ElementsComponent
  ],
  imports: [
    // making it compatible with universal
    // my-app is unique identifier on the page
    BrowserModule.withServerTransition({appId: 'my-app'})
  ],
  providers: [],
  bootstrap: [AppComponent]
})

Um app.module.ts (Listing 2) auf einem Server zu starten, muss ein neues Modul erstellt werden (Listing 3).

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

import { AppComponent } from './app.component';
import { AppModule } from './app.module';

@NgModule({
  declarations: [
    // do not forget to remove the declarations for app and element component
    // as they are already included in AppModule
  ],
  imports: [
    // AppModule has to be imported here
    AppModule,
    // ServerModule to run the code on Platform server
    ServerModule,
    // ModuleMapLoaderModule is needed for lazy loading to work
    // since we do not have any routes, it's being commented out
    // ModuleMapLoaderModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppSSRModule { }

Anschließend wird die Datei main.ts durch den Export des Moduls AppSSRModule erstellt:

Export { AppSSRModule } from ' ./app/app-ssr.module ' ;

Da die Anwendung auf andere Weise kompiliert und als Bundle zusammengefasst wird, muss eine separate tsconfig hinzugefügt werden (Listing 4). Dann wird das neue Build-Ziel für das Erstellen des Bundles festgelegt (Listing 5).

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    // changed to commonjs
    "module": "commonjs",
    "types": []
  },
  "exclude": [
    "src/test.ts",
    "**/*.spec.ts"
  ],
  // giving Reference to appSSRModule
  "angularCompilerOptions": {
    "entryModule": "app/app-ssr.module#AppSSRModule"
  }
}

 

"server": {
  "builder": "@angular-devkit/build-angular:server",
  "options": {
    "outputPath": "dist/my-universal-server",
    "main": "src/main-ssr.ts",
    "tsConfig": "src/tsconfig-ssr.json"
  }
}

Das Universal Bundle erhält man, wenn man die Anwendung nun erstellt:

ng run angular-universal:server

Um das Universal Bundle starten zu können, muss es an den Server gesendet werden. Betrachtet man das nach AOT-Prinzip zusammengefasstes Bundle in dist/my-universal-server, sieht man, dass die AppSSRModuleNgFactory bereits exportiert wurde. Im folgenden Beispiel müssen wir es nun an die renderModuleFactory übergeben, was die Anwendung serialisiert, woraufhin das Ergebnis an den Browser zurückgegeben wird.

Im Root-Verzeichnis der App muss nun die Datei server.ts (Listing 6) erstellt werden, die den Code eines Express-Servers enthält. Dieser wird den eingehenden Request abwarten, die Teile der Anwendung bereitstellen und die HTML-Seiten mit dem Aufrufen von renderModuleFactory rendern. Mit dem in Listing 7 angegebenen Code wird eine server.config-Datei erstellt, die auf webpack basiert, anschließend muss die Datei package.json wie in Listing 8 angegeben aktualisiert werden.

// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

import { enableProdMode } from '@angular/core';

import * as express from 'express';
import { join } from 'path';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppSSRModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/my-universal-server/main.js');

// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

app.engine('html', ngExpressEngine({
  bootstrap: AppSSRModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));

// TODO: implement data requests securely
app.get('/api/*', (req, res) => {
  res.status(404).send('data requests are not supported');
});

// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
  res.render('index', { req });
});

// Start up the Node server
app.listen(PORT, () => {
  console.log(`Node server listening on http://localhost:${PORT}`);
});

 

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: { server: './server.ts' },
  resolve: { extensions: ['.js', '.ts'] },
  target: 'node',
  mode: 'none',
  // this makes sure we include node_modules and other 3rd party libraries
  externals: [/node_modules/],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
  },
  plugins: [
    // Temporary Fix for issue: https://github.com/angular/angular/issues/11580
    // for 'WARNING Critical dependency: the request of a dependency is an expression'
    new webpack.ContextReplacementPlugin(
      /(.+)?angular(\\|\/)core(.+)?/,
      path.join(__dirname, 'src'), // location of your src
      {} // a map of your routes
    ),
    new webpack.ContextReplacementPlugin(
      /(.+)?express(\\|\/)(.+)?/,
      path.join(__dirname, 'src'),
      {}
    )
  ]
};

 

"build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
"serve:ssr": "node dist/my-universal-server",
"build:client-and-server-bundles": "ng build --prod && ng run angular-universal:server",
"webpack:server": "webpack --config webpack-ssr.config.js --progress --colors"

Nach dem Erstellen und Starten der App via

Yarn build:ssr && yarn serve:ssr

können wir in der Datei index.html sehen, dass dort die Elemente hinzugefügt wurden.

Fazit

Das Erstellen einer Anwendung mit Angular Universal ist sicher angenehm. Man sollte allerdings nicht vergessen, dass dieser Weg für größere Anwendungen immer komplexer wird. Viele argumentieren für serverseitiges Rendering aus Performancegründen. Das ist oft nur legitim und könnte der Anwendung helfen, allerdings werden in manchen Fällen hiermit nur Symptome, aber nicht die eigentliche Krankheit behandelt. Viele Performanceprobleme auf der Clientseite könnten durch das Weglassen von unnötigem JavaScript in initialen Bundles behoben werden.

Fakt ist allerdings, dass das serverseitige Rendern die Suchmaschinenoptimierung unterstützt, da die Crawler so sämtlichen Content der Seite sicher auslesen können, wodurch sich das Ranking der Webseite positiv beeinflussen lässt.

Entwickler Magazin

Entwickler Magazin abonnierenDieser Artikel ist im Entwickler Magazin erschienen.

Natürlich können Sie das Entwickler Magazin über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist das Entwickler Magazin ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -