SEO или поисковая оптимизация играет важную роль в каждом веб-приложении. Несмотря на то, что Google достаточно умен, вам не нужно быть глупым. Когда дело доходит до SEO в приложении Angular, следует учитывать несколько фундаментальных моментов.

1. Полифиллы ES6

Это самая простая и, тем не менее, самая важная часть. Во-первых, не все поисковые боты умеют читать и отображать JavaScript. На самом деле, по моим наблюдениям, только Google достаточно умен для рендеринга JS-приложения. Ни Bing, ни Яндекс-боты на это не способны. И все же Googlebot не поддерживает стандарт ES6. Другими словами, если вы не закомментируете следующие строки внутри файла polyfils.ts:

import 'core-js/es6/symbol';
import 'core-js/es6/object';
import 'core-js/es6/function';
import 'core-js/es6/parse-int';
import 'core-js/es6/parse-float';
import 'core-js/es6/number';
import 'core-js/es6/math';
import 'core-js/es6/string';
import 'core-js/es6/date';
import 'core-js/es6/array';
import 'core-js/es6/regexp';
import 'core-js/es6/map';
import 'core-js/es6/weak-map';
import 'core-js/es6/set';
import 'core-js/es6/reflect';

Ваше приложение не будет отображаться роботом Googlebot и, следовательно, не будет отображаться в результатах поиска Google.

2. Заголовки страниц

Излишне говорить, что заголовки играют неотъемлемую часть SEO. На самом деле заголовок отражает содержание вашей страницы, и его нужно устанавливать с умом. Другими словами, предварительно задав заголовки, вам необходимо проанализировать поисковые запросы (Google Keyword Planner - это инструмент для анализа популярности поисковых запросов или для поиска относительных поисковых запросов или для определения уровня конкуренции для данного ключевого слова). Эта статья не связана с аспектами SEO, поэтому я не буду акцентировать внимание на этом аспекте.

Для установки заголовков Angular имеет встроенную службу заголовков. Наиболее рациональный подход к настройке заголовков для статических маршрутов - использование объекта данных маршрута.

{
  path: 'contact-us',
  component: ContactComponent,
  data: {
    title: {
      text: 'Contact',
    }
  }
},
{
  path: 'about',
  component: AboutComponent,
  data: {
    title: {
      text: 'About',
    }
  }
}

Я рекомендую написать свой собственный сервис заголовков для вашего проекта. Потому что вам может потребоваться установить собственные префиксы и суффиксы для заголовков. Вы можете использовать эту услугу или изменить ее в соответствии со своими требованиями.

import { Injectable } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router, NavigationEnd } from '@angular/router';
import { environment } from '@environments/environment';
import { filter, map } from 'rxjs/operators';
import { CoreModule } from '@core/core.module';
interface TitleConfig {
  text: string;
  appendSuffix: boolean;
  customSuffix: string;
}
@Injectable({
  providedIn: 'root'
})
export class TitleService {
  constructor(
    protected activatedRoute: ActivatedRoute,
    protected router: Router,
    protected title: Title,
  ) {
    this.router.events
      .pipe(
        filter(event => event instanceof NavigationEnd),
        map(() => {
          let child = this.activatedRoute.firstChild;
          while (child) {
            if (child.firstChild) {
              child = child.firstChild;
            } else if (child.snapshot.data && child.snapshot.data['title']) {
              return child.snapshot.data['title'];
            } else {
              return null;
            }
          }
          return null;
        })
      )
      .subscribe((titleConfig: TitleConfig) => {
        if (titleConfig && titleConfig.text)
          this.setTitle(
            titleConfig.text,
            titleConfig.appendSuffix,
            titleConfig.customSuffix
          );
        else this.setTitle(environment.title.default, false);
      });
  }
  setTitle(
    title: string,
    appendSuffix: boolean = true,
    customSuffix?: string
  ): void {
    if (title) {
      let newTitle: string = title;
      if (appendSuffix === true) {
        if (customSuffix) newTitle += customSuffix;
        else if (environment.title.suffix) newTitle += environment.title.suffix;
      }
      this.title.setTitle(newTitle);
    }
  }
}

Что касается динамических страниц - это страницы, содержимое которых извлекается асинхронно. Затем просто вставьте эту услугу в свой компонент и соответствующим образом установите заголовок.

export class ProductComponent implements OnInit{
  constructor(private titleService: TitleService) {}
  ngOnInit() {
    // Get data asynchronously and then
    // Set Title
    .then(result => {
      this.titleService.setTitle(result.title);
    })
  }
}

3. Карта сайта и robot.txt

Поисковые системы очень хороши в определении ссылок, когда дело доходит до статических html-страниц, но не очень хороши для одностраничных приложений. На самом деле робот Googlebot очень хорош в получении ссылок на статические маршруты, маршруты, которые определяются вручную внутри RouterModule, то есть:

const routes: Routes = [
  {
    path: 'first-page',
    component: FirstPageComponent,
  },
  {
    path: 'second-page',
    component: SecondPageComponent,
  }
];
@NgModule({
  imports: [
    RouterModule.forRoot(routes)
  ],
  exports: [RouterModule]
})

Но не очень хорошо с динамическими маршрутами, то есть:

<li *ngFor="let category of categories">
  <a [routerLink]="['blog', category.id]">{{ category.name }}</a>
</li>

В результате вы получите статические страницы в результатах поиска.

Файл Sitemap поможет вам решить эту проблему. Просто создайте ссылки на свои динамические страницы и загрузите этот файл в поисковую систему. Например, вы можете создать облачную функцию, которая автоматически генерирует файл карты сайта. Для firebase это можно настроить следующим образом:

{
  "hosting": {
    ...
    "rewrites": [
      {
        "source": "/sitemap",
        "destination": "/sitemap.xml" // Static Sitemap File
      },
      {
        "source": "/sitemap-blog",
        "function": "sitemap-blog" // Dynamic Sitemap Function
      },
      ...
    ]
  }
}

Чтобы передать файл карты сайта в Google, вы можете:

  1. Разместите ссылку на карту сайта в файле robot.txt. Предпочтительный метод, потому что он применим к другим поисковым системам, а также к сканеру facebook.
  2. Дайте ссылку на этот файл в Google Search Console. Может использоваться вместе с robot.txt

Содержимое robot.txt может выглядеть следующим образом:

# Sitemaps
Sitemap: https://my-app.com/sitemap-categories
Sitemap: https://my-app.com/sitemap-products
# Configs
User-agent: *
Crawl-delay: 10
# Pages excluded from indexing
Disallow: /user/*

К счастью, вы можете предоставить несколько карт сайта, их проще создавать по отдельности на основе содержимого.

Автоматически сгенерированное содержимое карты сайта для динамических страниц может выглядеть следующим образом:

<?xml version='1.0' encoding='UTF-8'?>
<urlset xmlns='http://www.sitemaps.org/schemas/sitemap/0.9'>
  <url><loc>https://my-app.com/blog/1</loc></url>
  <url><loc>https://my-app.com/blog/2</loc></url>
  <url><loc>https://my-app.com/blog/3</loc></url>
</urlset>

4. Канонические ссылки

Каноническая ссылка - это элемент HTML, который помогает поисковым системам предотвращать проблемы с дублированием контента. Для этого он указывает «канонический URL», «предпочтительную» версию страницы.

Поисковые системы обычно сами определяют предпочтительные URL-адреса, но вы можете помочь им в этом, вручную установив канонические ссылки для всех страниц, они находятся внутри блока заголовка вашего html и обычно выглядят следующим образом:

<link rel="canonical" href="https://my-app.com/product/slug">

Я настоятельно рекомендую вам установить канонические значения, потому что, если у вас много страниц, боты поисковых систем иногда могут исключать некоторые из них из поискового индекса, предполагая дублированный контент. Это может сделать очень простая функция:

updateCanonicalLink() {
  const link =
    <HTMLLinkElement>(
      this.document.head.querySelector("link[rel='canonical']")
    ) || this.document.head.appendChild(this.document.createElement('link'));
  link.rel = 'canonical';
  link.href = this.document.URL;
}

5. Предварительный рендеринг

Хотя бот Google достаточно умен, я могу гарантировать, что он никогда не будет на 100% правильным, когда дело доходит до рендеринга одностраничного приложения. Кроме того, существуют различные поисковые системы, которые вообще не могут отображать JS-приложения, но игнорировать потенциальных посетителей среди них нерационально. И, наконец, не забывайте о социальных ботах.

Когда дело доходит до предварительного рендеринга, есть 3 решения:

  1. Angular Universal: рендеринг на стороне сервера
  2. Rendertron: безголовое решение для рендеринга в Chrome
  3. Услуги предварительной обработки: https://prerender.io, https://www.prerender.cloud и т. Д.

Угловой универсальный

Я не буду описывать, как это работает, вы можете просто обратиться к официальной документации. Это довольно понятно и понятно. Мне это не очень нравится.

Rendertron

Это отличное решение. Хотя есть пара моментов, которые стоит учесть.

Рендертрон состоит из 3-х частей:

  1. Скрипт промежуточного программного обеспечения (обязательно). Скрипт, определяющий поисковых и социальных ботов.
  2. Сервер рендеринга (обязательно). Это может быть физический / облачный Hetzner или Digital Ocean или AWS EC2 / Google Cloud.
  3. Веб-кэш (необязательно). Varnish Cache, nginx и т. Д.

По цене все зависит от размера вашего приложения. Мы рассчитали на основе следующих требований (~ 10 тыс. Страниц, скорость рендеринга 5 секунд, еженедельные обновления кеша, масштабируемость), и это выглядит следующим образом:

  1. Хетцнер. Примерно 50 $ в месяц
  2. Digital Ocean: около 80 долларов в месяц
  3. Google Cloud: 100 долларов в месяц
  4. AWS EC2 180 $ в месяц.

Вы можете игнорировать веб-кеш, но тогда ваше приложение будет очень упрямым, представьте, что каждый раз, когда GoogleBot посещает вашу страницу, ваш сервер рендеринга будет читать и отображать вашу страницу (это не только вызывает большой дополнительный трафик, но и приводит к потере ресурсов сервера хостинга).

В целом rendertron - отличное решение для приложений среднего и большого размера. Но если у вас ограниченный бюджет и у вас небольшое приложение, вам лучше рассмотреть следующее решение.

Услуги предварительной обработки

Я рекомендую вам использовать этот https://prerender.io, потому что он имеет отличную цену (до 20 000 страниц стоит всего 15 долларов в месяц), но также предоставляет встроенные решения для автоматического кэширования и промежуточное ПО для ExpressJS, Rails, Nginx, Apache и т. Д. Его очень легко настроить. И, наконец, вы можете предварительно кэшировать все свои страницы, предоставив карту сайта.

Это подход, который я использовал для настройки Firebase.

содержимое firebase.json:

{
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [
      {
        "source": "**",
        "function": "prerender"
      }
    ]
  }
}

Облачная функция:

import * as functions from 'firebase-functions';
import fetch from 'node-fetch';
import * as express from "express";
export const app = express();
app.use(require('prerender-node').set('prerenderToken', functions.config().prerender.token));
app.get('*', (req, res) => {
  fetch(`https://${functions.config().prerender.app_url}/${functions.config().app.index}`)
      .then(fetched => fetched.text())
      .then(body => {
        res.send(body.toString());
      })
      .catch(err => {
        console.log(err);
      });
});
exports.prerender = functions.https.onRequest(app);

Резюме

После этих небольших усилий ваше присутствие в поиске приложения Angular и результаты будут радикально улучшены.