При написании одностраничных приложений легко и естественно увлечься попытками создать идеальный опыт для наиболее распространенного типа пользователей - других людей, таких как мы. Такое агрессивное внимание к одному типу посетителей нашего сайта часто может оставить без внимания другую важную группу - сканеров и ботов, используемых поисковыми системами, такими как Google. Это руководство покажет, как некоторые простые в реализации передовые практики и переход к рендерингу на стороне сервера могут дать вашему приложению лучшее из обоих миров, когда дело касается взаимодействия с пользователем SPA и SEO.

Предпосылки

Предполагается рабочее знание Angular 5+. Некоторые части руководства относятся к Angular 6, но знание этого строго не требуется.

Веб-сайт против веб-приложения

Многие непреднамеренные ошибки SEO, которые мы совершаем, происходят из-за того, что мы создаем веб приложения, а не веб сайты. Какая разница? Это субъективное различие, но я бы сказал с точки зрения сосредоточения усилий:

  • Веб приложения ориентированы на естественное и интуитивно понятное взаимодействие с пользователями.
  • Веб-сайты стремятся сделать информацию общедоступной.

Но эти две концепции не обязательно должны быть взаимоисключающими! Просто вернувшись к истокам правил разработки веб-сайтов, мы можем поддерживать привлекательный внешний вид и удобство использования SPA и размещать информацию во всех нужных местах, чтобы сделать веб-сайт идеальным для поисковых роботов.

Не скрывайте контент за взаимодействиями

Один принцип, о котором следует подумать при разработке компонентов, заключается в том, что поисковые роботы в некотором роде глупы. Они будут нажимать на ваши привязки, но они не будут случайным образом перемещаться по элементам или нажимать на div только потому, что в его содержимом написано «Подробнее». Это приводит к разногласиям с Angular, где обычная практика скрытия информации - это «* нет». И во многих случаях это имеет смысл! Мы используем эту практику, чтобы повысить производительность приложения, не располагая потенциально тяжелые компоненты просто в невидимой части страницы.

Однако это означает, что если вы скрываете контент на своей странице с помощью умных взаимодействий, есть вероятность, что сканер никогда не увидит этот контент. Вы можете смягчить это, просто используя CSS, а не * ngif , чтобы скрыть такой контент. Конечно, умные сканеры заметят, что текст скрыт, и, скорее всего, он будет оценен как менее важный, чем видимый текст. Но это лучший результат, чем отсутствие доступа к тексту в DOM. Пример такого подхода выглядит так:

Не создавайте «виртуальные якоря»

Компонент ниже показывает антипаттерн, который я часто вижу в приложениях Angular, который я называю «виртуальным якорем»:

В основном происходит то, что обработчик кликов прикреплен к чему-то вроде тега ‹button› или ‹div›, и этот обработчик будет выполнять некоторую логику, а затем использовать импортированный Angular Router для перехода на другую страницу. Это проблематично по двум причинам:

  1. Скорее всего, поисковые роботы не будут нажимать на такие элементы, и даже если они это сделают, они не будут устанавливать связь между исходной и целевой страницей.
  2. Это предотвращает очень удобную функцию «Открыть в новой вкладке», которую браузеры предоставляют для реальных тегов привязки.

Вместо использования виртуальных якорей используйте фактический тег ‹a› с директивой routerlink. Если вам нужно выполнить дополнительную логику перед навигацией, вы все равно можете добавить обработчик кликов к тегу привязки.

Не забывайте о заголовках

Один из принципов хорошего SEO - определение относительной важности различного текста на странице. Важным инструментом для этого в комплекте веб-разработчика являются заголовки. При разработке иерархии компонентов приложения Angular часто полностью забывают о заголовках; Включены они или нет, визуально не влияет на конечный продукт. Но это то, что вам нужно учитывать, чтобы убедиться, что поисковые роботы сосредотачиваются на правильных частях вашей информации. Поэтому рассмотрите возможность использования тегов заголовков там, где это имеет смысл. Однако убедитесь, что компоненты, которые включают теги заголовков, не могут быть расположены таким образом, чтобы ‹h1› появлялся внутри ‹h2›.

Сделайте «Страницы результатов поиска» доступными для ссылок

Возвращаясь к принципу того, насколько тупые краулеры - рассмотрим поисковую страницу для виджета компании. Сканер не увидит ввод текста в форме и наберет что-то вроде «Виджеты Торонто». По сути, чтобы сделать результаты поиска доступными для сканеров, необходимо сделать следующее:

  1. Необходимо настроить страницу поиска, которая принимает параметры поиска через путь и / или запрос.
  2. Ссылки на конкретные поисковые запросы, которые, по вашему мнению, могут быть интересны сканеру, должны быть добавлены в карту сайта или в качестве якорных ссылок на других страницах сайта.

Стратегия вокруг пункта 2 выходит за рамки данной статьи (некоторые полезные ресурсы: https://yoast.com/internal-linking-for-seo-why-and-how/ и https: // moz. ru / learn / seo / internal-link ). Важно то, что компоненты и страницы поиска должны разрабатываться с учетом пункта №1, чтобы у вас была гибкость для создания ссылки на любой возможный вид поиска, позволяя вводить ее в любом месте. Это означает импорт ActivatedRoute и реагирование на его изменения в пути и параметрах запроса для получения результатов поиска на вашей странице вместо того, чтобы полагаться исключительно на ваш запрос на странице и компоненты фильтрации.

Сделать пагинацию связанной

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

Чтобы повторить предыдущие пункты: не используйте «виртуальные привязки» для ссылок «следующая», «предыдущая» и «номер страницы». Если сканер не видит в них якоря, он может никогда не смотреть на что-либо, кроме вашей первой страницы. Используйте для этого настоящие теги ‹a› с R outerLink. Кроме того, включите разбиение на страницы как необязательную часть URL-адресов поиска, на которые можно ссылаться - это часто имеет форму page = параметр запроса.

Вы можете предоставить поисковым роботам дополнительные подсказки о разбивке на страницы вашего сайта, добавив относительные теги prev / next ‹link›. Объяснение того, почему они могут быть полезны, можно найти по адресу: https://webmasters.googleblog.com/2011/09/pagination-with-relnext-and-relprev.html. Вот пример службы, которая может автоматически управлять этими тегами ‹link› удобным для Angular способом:

Включить динамические метаданные

Первое, что мы делаем с новым приложением Angular, - это вносим изменения в файл index.html - устанавливаем значок, добавляем отзывчивые метатеги и, скорее всего, устанавливаем содержимое заголовка ‹. › и ‹ meta name = ”description” › на некоторые разумные значения по умолчанию для вашего приложения. Но если вам важно, как ваши страницы отображаются в результатах поиска, вы не можете останавливаться на достигнутом. На каждом маршруте для вашего приложения вы должны динамически устанавливать теги title и description в соответствии с содержанием страницы. Это не только поможет сканерам, но и поможет пользователям, поскольку они смогут видеть информативные заголовки вкладок браузера, закладки и информацию для предварительного просмотра, когда они делятся ссылкой в ​​социальных сетях. В приведенном ниже фрагменте показано, как вы можете обновить их удобным для Angular способом, используя классы Meta и Title:

Проверьте, не нарушают ли сканеры ваш код

Некоторые сторонние библиотеки или SDK либо закрываются, либо не могут быть загружены со своего хостинг-провайдера при обнаружении пользовательских агентов, принадлежащих сканерам поисковых систем. Если какая-то часть вашей функциональности зависит от этих зависимостей, вам следует предоставить запасной вариант для зависимостей, которые запрещают поисковые роботы. По крайней мере, ваше приложение должно корректно деградировать в этих случаях, а не приводить к сбою процесса рендеринга клиента. . Отличным инструментом для проверки взаимодействия вашего кода со сканерами является тестовая страница Google Mobile Friendly: https://search.google.com/test/mobile-friendly. Обратите внимание на такой вывод, который означает, что поисковому роботу заблокирован доступ к SDK:

Уменьшение размера пакета с помощью Angular 6

Размер пакета в приложениях Angular - хорошо известная проблема, но разработчик может внести множество оптимизаций, чтобы смягчить ее, включая использование сборок AOT и консервативное включение сторонних библиотек. Однако, чтобы получить минимально возможные пакеты Angular сегодня, требуется обновление до Angular 6. Причиной этого является необходимое параллельное обновление до RXJS 6, которое предлагает значительные улучшения его способности встряхивания дерева. Чтобы добиться этого улучшения, к вашему приложению предъявляются жесткие требования:

  • Удалите библиотеку rxjs-compat (которая добавляется по умолчанию в процессе обновления Angular 6) - эта библиотека делает ваш код обратно совместимым с RXJS 5, но отменяет улучшения, связанные с встряхиванием дерева.
  • Убедитесь, что все зависимости ссылаются на Angular 6, и не используйте библиотеку rxjs-compat.
  • Импортируйте операторы RXJS по одному, а не оптом, чтобы гарантировать, что встряхивание дерева выполнит свою работу. См. Https://github.com/ReactiveX/rxjs/blob/master/docs_app/content/guide/v6/migration.md для получения полного руководства по миграции.

Серверный рендеринг

Даже после выполнения всех приведенных выше рекомендаций вы можете обнаружить, что ваш веб-сайт на Angular не имеет такого высокого рейтинга, как вам хотелось бы. Одна из возможных причин этого - один из фундаментальных недостатков фреймворков SPA в контексте SEO - они полагаются на Javascript для рендеринга страницы. Эта проблема может проявляться двумя способами:

  1. Робот Googlebot может выполнять Javascript, но не каждый сканер. Тем, кто этого не делает, все ваши страницы будут казаться им пустыми.
  2. Чтобы страница показывала полезный контент, сканер должен будет дождаться загрузки пакетов Javascript, обработки их анализатором, кода для запуска и возврата любых внешних XHR - тогда будет контент в DOM. По сравнению с более традиционными языками рендеринга на сервере, где информация становится доступной в DOM, как только документ попадает в браузер, SPA, вероятно, подвергнется здесь некоторому штрафу.

К счастью, у Angular есть решение этой проблемы, которое позволяет обслуживать приложение в форме отрисовки на сервере: Angular Universal (https://github.com/angular/universal). Типичная реализация этого решения выглядит так:

  1. Клиент запрашивает конкретный URL-адрес на вашем сервере приложений.
  2. Сервер передает запрос в службу рендеринга, которая представляет собой ваше приложение Angular, работающее в контейнере Node.js. Эта служба может находиться (но не обязательно) на том же компьютере, что и сервер приложений.
  3. Серверная версия приложения отображает полные HTML и CSS для запрошенного пути и запроса, включая теги ‹script› для загрузки клиентского приложения Angular.
  4. Браузер получает страницу и может сразу отображать контент. Клиентское приложение загружается асинхронно и, когда оно будет готово, повторно визуализирует текущую страницу и заменяет статический HTML-код, отображаемый сервером. Теперь веб-сайт ведет себя как SPA для любого дальнейшего взаимодействия. Этот процесс должен быть беспрепятственным для пользователя, просматривающего сайт.

Однако эта магия не дается бесплатно. Пару раз в этом руководстве я упоминал, как делать что-то дружественным к Angular способом. На самом деле я имел в виду Angular дружественный к серверному рендерингу. Все лучшие практики, которые вы читали об Angular, такие как не касаться DOM напрямую или ограничение использования setTimeout, вернутся, чтобы укусить вас, если вы не следовали им - в в виде медленной загрузки или даже полностью битых страниц. Обширный список универсальных ошибок можно найти по адресу: https://github.com/angular/universal/blob/master/docs/gotchas.md

Привет, сервер

Есть несколько разных вариантов запуска проекта с Universal:

  • Для проектов Angular 5 вы можете запустить следующую команду в существующем проекте:
    ng generate universal server
  • Для проектов Angular 6 пока нет официальной команды CLI для создания рабочего универсального проекта с клиентом и сервером. Вы можете запустить следующую стороннюю команду в существующем проекте:
    ng add @ng-toolkit/universal
  • Вы также можете клонировать этот репозиторий, чтобы использовать его в качестве отправной точки для своего проекта или объединить с существующим: https://github.com/angular/universal-starter

Внедрение зависимости - ваш друг (сервер)

В типичной настройке Angular Universal у вас будет три разных модуля приложений: модуль только для браузера, модуль только для сервера и общий модуль. Мы можем использовать это в своих интересах, создавая абстрактные службы, которые внедряются нашими компонентами, и предоставляя клиентские и серверные реализации в каждом модуле. Рассмотрим следующий пример службы, которая может устанавливать фокус на элемент: мы определить абстрактные реализации службы, клиента и сервера, предоставить их в соответствующих модулях и импортировать абстрактную службу в компонентах.

Исправление зависимостей, враждебных серверу

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

Вы можете * ngIf, если на сервере не работают компоненты, вызывающие нарушение. Простой способ сделать это - создать директиву, которая может определять, будет ли элемент отображаться в зависимости от текущей платформы:

Некоторые библиотеки более проблематичны; сам процесс импорта кода может попытаться использовать зависимости только для браузера, что приведет к сбою рендеринга сервера. Примером может служить любая библиотека, которая импортирует jquery как зависимость npm, а не ожидает, что у потребителя будет jquery, доступный в глобальной области. Чтобы убедиться, что эти библиотеки не нарушают работу сервера, мы должны как * ngIf исключить проблемный компонент, , так и удалить зависимую библиотеку из webpack. Предполагая, что библиотека, которая импортирует jquery, называется jquery-funpicker, мы можем написать правило веб-пакета, подобное приведенному ниже, чтобы исключить его из сборки сервера:

Для этого также необходимо разместить файл с содержимым {} по адресу webpack / empty.json в структуре вашего проекта. В результате библиотека получит пустую реализацию для своего оператора импорта jquery-funpicker, но это не имеет значения, потому что мы удалили этот компонент повсюду в серверном приложении с помощью нашей новой директивы.

Повысьте производительность браузера - не повторяйте XHR

Часть дизайна Universal заключается в том, что клиентская версия приложения будет повторно запускать всю логику, которая была запущена на сервере для создания клиентского представления, включая выполнение тех же вызовов XHR к вашей серверной части, которые уже были выполнены серверным рендерингом! Это создает дополнительную нагрузку на серверную часть и создает у поисковых роботов ощущение, что страница все еще загружает контент, даже если она, вероятно, будет показывать ту же информацию после возврата этих XHR. Если нет проблем с устареванием данных, вам следует запретить клиентскому приложению дублировать XHR, которые уже были созданы сервером. TransferHttpCacheModule от Angular - удобный модуль, который может в этом помочь: https://github.com/angular/universal/blob/master/docs/transfer-http.md

Под капотом TransferHttpCacheModule использует класс TransferState, который можно использовать для любой передачи состояния общего назначения от сервера к клиенту:

Предварительный рендеринг для перемещения времени до первого байта в сторону нуля

Одна вещь, которую следует учитывать при использовании Universal (или даже сторонней службы визуализации, такой как https://prerender.io/), заключается в том, что страница, отображаемая сервером , будет иметь больше времени до того, как первый байт попадет в браузер , чем страница, отображаемая клиентом. Это должно иметь смысл, если учесть, что для того, чтобы сервер доставил страницу, отображаемую клиентом, по сути, ему просто нужно доставить статическую страницу index.html. Universal не завершит рендеринг, пока приложение не будет признано стабильным. Стабильность в контексте Angular сложна, но двумя основными факторами, способствующими задержке стабильности, вероятно, будут:

  • Выдающиеся XHR
  • Выдающиеся вызовы setTimeout

Если у вас нет возможности оптимизировать вышеупомянутое, вариант для сокращения времени до первого байта - это просто предварительный рендеринг некоторых или всех страниц вашего приложения и их обслуживание из кеш. Стартовый репозиторий Angular Universal, ссылка на который приведен ранее в этом руководстве, поставляется с реализацией для предварительного рендеринга. Если у вас есть предварительно обработанные страницы, в зависимости от вашей архитектуры, решение для кеширования может быть чем-то вроде Varnish, Redis, CDN или сочетанием технологий. Удалив время отрисовки из пути ответа от сервера к клиенту, вы можете обеспечить чрезвычайно быструю начальную загрузку страницы для поисковых роботов и пользователей вашего приложения.

Заключение

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

По мере развития Интернета я надеюсь, что мы увидим день, когда поисковые роботы и серверы захвата экрана будут взаимодействовать с веб-сайтами таким образом, который больше соответствует тому, как пользователи взаимодействуют на своих устройствах, - отличая веб-приложения от старых веб-сайтов. вынужден подражать. А пока, как разработчики, мы должны продолжать поддерживать старый мир.