Смоделируем ситуацию: Вы являетесь членом команды веб-разработчиков, занимающихся созданием frontend-части молодого интернет-ресурса на базе React. И вот, когда уже начинает казаться что ваша разработка достигла определенной функциональной, качественной и эстетической кондиции, вы сталкиваетесь с достаточно сложным и не менее интересным вопросом: А что делать с SEO? Как добиться качественной конверсии от поисковых систем? Как сделать так, чтобы о вашем ресурсе узнал весь мир, не вкладывая в это огромного количества денег за платные рекламные компании либо сил в крупномасштабную дополнительную разработку? Как заставить контент вашего Single Page Application работать на вас в поисковых выдачах и приносить клиентов? Интересно? Тогда поехали…
Привет! Меня зовут Антон и я являюсь full-stack-разработчиком со стажем работы более 12 лет. За время своей трудовой деятельности я работал над проектами различной сложности и с разнообразными стеками технологий. Последние 6 лет я сосредоточил scope своего внимания в большей мере на frontend, который стал в итоге профильным направлением, так как привлек у меня больший интерес по сравнению с backend-ом (не хочу при этом сказать, что backend в целом менее интересен. Просто дело вкуса и особенности развития карьеры исключительно в моём личном случае).
В данный момент я являюсь team-лидером команды разработчиков на проекте «Своё.Жильё» — это экосистема Россельхозбанка с помощью которой можно выбрать недвижимость, рассчитать стоимость кредита, подать заявку и получить ответ в режиме онлайн. Уже сейчас в экосистеме есть возможность оформить заявку на ипотечный кредит и выбрать недвижимость из более чем 1,2 млн вариантов жилья. Набор онлайн-сервисов позволяет сократить количество посещений офисов банка до одного – непосредственно для подписания кредитного договора и проведения расчетов с продавцом.
Проект на момент написания данного материала является еще достаточно молодым, но уже начал приносить заметную конверсию в общую бизнес-модель банка.
Используемый стек и преследуемые цели
Не буду заострять внимание на полном стеке frontend технологий, используемых экосистеме «Своё Жилье», а опишу лишь то, что действительно важно для дальнейшего рассказа, чтобы стало понятно, с чем в результате мы имеем дело. А именно:
-
Webpack
-
React
-
Redux
Из вышеперечисленного сразу становится ясно, что на выходе мы получаем Single Page Application на React, что влечет за собой как безграничное количество возможностей и плюсов данного стека, так и ряд подводных камней, связанных с реализацией SEO-friendly ресурса, который сможет в конечном счёте выполнить ряд задач, важных для продвижения в поисковых системах, социальных сетях и так далее:
-
Поисковой робот должен видеть все ссылки на страницы сайта;
-
Контент каждой из страниц должен быть доступен для индексирования поисковым роботом, дабы попасть в итоге в выдачи результатов поиска;
-
Контент страниц для корректной индексации должен содержать все необходимые элементы, важные для создания SEO-оптимизированного сайта.
В данном списке я не затрагиваю задачи, которые необходимо решать на стороне backend (такие как поддержка last-modified, оптимизация скоростных показателей и т.п.), чтобы искусственно не раздувать общий материал и не отвлекаться от основной темы.
Казалось бы, перечень задач не так велик и решался бы практически “из коробки” на любом обычном относительно стандартном интернет ресурсе… Но тут нужно вернуться к тому, что мы имеем дело не с привычным веб-сайтом, у которого все страницы с их контентом отдаются с backend-а, а с Single Page Application, у которого контент страничек рендерится (отрисовывается средствами js) на стороне браузера. А ведь львиная доля поисковых роботов не умеет выполнять js-код при обходе интернет-ресурсов, и поэтому они попросту не увидят наш контент (Google умеет, но делает это пока недостаточно корректно и эффективно. Да и кроме Google, есть еще множество других целевых поисковых систем и кейсов, при которых “голый” SPA не сможет решить поставленные задачи).
Поэтому давайте нальём тёплого чайку и поэтапно пройдемся по целям и тому, что в них должно включаться для понимания и подбора оптимальных решений поставленных задач.
Для того, чтобы поисковые роботы “увидели” страницы сайта, им необходимо иметь возможность прочесть контент страницы, в том числе содержащий ссылки на другие страницы (при этом подход с sitemap я тут описывать не буду, но упомянул, на случай если кому-то станет интересно, можете погуглить. Мы пока обходимся без карт). Из этого вытекает, что поисковой робот всё же должен “по старинке” загрузить html-разметку, содержащую контент страницы с web-сервера. Ну и конечно же, каждая из страниц, запрашиваемых с web-сервера, должна отдавать код 200 (случаи с необходимостью переадресаций с кодом 301 я тут также рассматривать не буду) и приходить в виде стандартного html-документа, содержащего текстовый и медиа-контент данной страницы, а так же ссылки на другие страницы и, конечно же, необходимые для SEO-оптимизации элементы, такие как ряд обязательных meta-тегов, заголовков и так далее. Общий список необходимого “SEO-тюнинга” любого веб-ресурса достаточно велик и про него можно написать отдельный материал и не один. Затронем тут обязательный “план минимум”, который включит в себя следующие пункты:
1 — Каждая из страниц ресурса должна в блоке <head> включать в себя:
-
Meta-тег title (заголовок страницы)
-
Meta-тег description (описание страницы)
-
Meta-тег keywords (перечень ключевых фраз)
2 — Каждая страница должна иметь в блоке <body> основной заголовок внутри html-элемента <h1> расположенный как можно выше перед началом текстового контента.
3 — Каждое изображение, которое присутствует на странице в виде html-элемента <img>, должно иметь атрибут alt, описывающий содержимое данного изображения.
Ну и конечно же, на сайте не должно быть “битых” ссылок отдающих с web-сервера код ошибки 404 (либо иной) или какой-либо пустой контент вместо ожидаемого.
И тут снова вспоминаем, что у нас SPA (Single Page Application) и с backend-а приходит лишь пустая часть разметки html-документа, включающая в себя информацию для загрузки js и css кода, который после загрузки и выполнения отрисует нам контент запрошенной страницы.
Вот тут мы и начнем наши “танцы с бубном”. Перед началом хочется отдельно отметить, что на момент моего прихода в проект, он уже был частично написан с минимальным функционалом. И конечно хотелось реализовать SEO-оптимизацию с минимальным затрагиванием общей архитектуры приложения.
Подбор решения и реализация
Что же делать, если у Вас уже есть готовый SPA, либо просто отточенный архитектурный подход, нарушать который ради SEO было бы кощунством?
Ответ: Реализация пререндеринга, как конечного шага сборки приложения. Пререндеринг – это процесс выполнения js-кода после его основной сборки и сохранение получившихся html-копий отрисованных страниц, которые в последствии и будут отдаваться с web-сервера при соответствующих запросах.
Пререндеринг
Для реализации данного кейса есть ряд готовых решений и инструментов с различными ограничениями, подходящих для разных стеков frontend-технологий и с разным уровнем сложности внедрения.
Конечно есть такие решения как Next.Js и ему подобные (а также другие способы реализации того же SSR). И я не могу не упомянуть об этом. Но изучив их, я пришел к выводу, что они вносят ряд ограничений (либо усложнений) в процесс конфигурации основной сборки и имеют крайне заметное влияние на архитектуру приложения. Также, есть сторонние внешние сервисы пререндеринга, но для закрытой экосистемы банка – лишний внешний инструмент был бы крайне нежелателен (и опять же, зачем перекладывать зону ответственности на внешний инструмент, если можно реализовать его функционал внутри проекта).
После анализа, я пришел к максимально подходящему нам инструменту для покрытия описанных кейсов, реализующему проход по страницам SPA на React (и не только) и создание html-копий страниц.
Выбор пал на React-Snap.
Если в двух словах, то данный инструмент позволяет после сборки осуществить запуск Chromium-а и передать ему получившееся приложение. Затем пройти по всем его страницам, используя пул найденных ссылок так, как сделал бы это поисковой робот. При этом html-копии отрисованных средствами js страниц будут сохранены в выбранный каталог с сохранением иерархии путей, из которых они были получены относительно корня проекта.
Важной отличительной чертой данного подхода и инструмента является то, что для реализации его работы не важна специфика архитектуры самого приложения, так как он абсолютно стандартным образом будет запускать Ваш заранее собранный SPA в практически обыкновенном браузере.
Установка React-Snap не вызывает никаких дополнительных вопросов, так как его пакет доступен для скачивания стандартным образом из npm (и yarn).
Что же касается его настройки, тут, пожалуй, стоит проговорить несколько основных моментов, чтобы упростить быстрый старт тем, кто ранее не сталкивался с данным инструментом.
Конфигурация запуска React-Snap описывается в корневом файле package.json проекта. Давайте рассмотрим пример минимальной конфигурации:
"scripts": { // …Необходимые команды запуска hot-а, сборки и т.п. "build:production": "webpack --mode production && react-snap" // …Другие необходимые команды }, "reactSnap": { "source": "dist", // Каталог собранного приложения "destination": "dist", // Каталог для сохранения html-копий "include": [ // Список энтрипоинтов для обхода страниц "/", "/404", "/500" // …Другие необходимые энрипоинты ] }
На самом деле, полный перечень доступных настроек достаточно широк, но лучше не стану его расписывать, чтобы вы смогли осознанно выбрать только необходимое конкретно в вашем случае.
К примеру, запуск React-Snap можно осуществить, просто добавив в блок “scripts” команду:
"postbuild": "react-snap"
Но тогда он будет запускаться после каждого билда, а в проекте их может быть несколько вариантов (например, для production и тестового стенда, где на последнем нам наоборот, может быть не нужен SEO и какой-либо еще функционал, такой как инструменты аналитики типа Google Analytics и т.п.).
Что касается блока ”include”, его желательно описать, иначе, например, html-копия для странички ошибки (500, либо другая техническая страница, при наличии) не будет создана, так как не на одной из страниц сайта не фигурирует ни одной ссылки на нее. Как следствие, React-Snap не узнает о её наличии. В теории и поисковик на них ходить не должен, но бывают случаи, когда страница создается для распространения ссылки за пределами сайта, а на сайте на нее ссылки может и не быть (к примеру, баннеры для рекламных компаний и тому подобное). Это как раз тот самый случай. Тут стоит проанализировать, нет ли у вас еще каких-то (возможно аналогичных «технических») страничек, на которые прямые ссылки на сайте отсутствуют.
Далее, для нормальной работы самого React-приложения у конечного пользователя, “поверх” DOM который придёт с web-сервера, нам потребуется внести небольшую правку в корневой render:
import { hydrate, render } from "react-dom"; // … Ваш код const rootElement = document.getElementById("root"); // (или ваш id при олтличии) if (rootElement.hasChildNodes()) { // …Если в корневом элементе есть контент, то… hydrate(<App />, rootElement); // …"цепляем" приложение на существующий DOM. } else { // …Иначе рендерим приложение стандартным образом render(<App />, rootElement); }
Вот и всё что нам потребуется для начала (а возможно в ряде случаев и в принципе всё что необходимо будет сделать). Уже можно попробовать выполнить build с созданием html-копий страниц вашего ресурса. На выходе (с приведенным выше конфигом) в каталоге /dist вы получите тот же набор js и css файлов (а также других ресурсов), что и ранее, index.html, плюс файл 200.html и другие html-файлы с копиями контента ваших страниц.
Для полноты картины сразу опишу небольшой подводный камень с пояснением, для понимания, что при таком подходе на реальном production-web-сервере вам нужно будет позаботиться о следующем нюансе…
Ранее у вас скорее всего по умолчанию на любой запрос, если ресурс отсутствует физически на сервере, отдавалась index.html, которая запускала приложение. Далее, в соответствии с запросом из адресной строки, приложение отрисовывало необходимую страницу, либо страницу 404, если не находило соответствие. Теперь же, наш index.html уже не является пустым, а содержит контент главной страницы. Но страничка с пустой html-разметкой для случая попытки запуска страницы без html-копии всё же существует. Это та самая вышеупомянутая 200.html. Таким образом, на web-сервере необходимо перенастроить дефолтный ресурс для случая 404 с index.html на 200.html, чтобы избежать открытия “кривых” страниц (с контентом главной страницы поверх которого будет пытаться запуститься наш SPA) при обращении на страницы, html-копий для которых нет, либо просто при некорректном обращении на несуществующую страницу.
И вот у нас есть готовое приложение, страницы которого доступны для любого поисковика.
Meta-теги, заголовки, описания
Если с вышеописанным разобрались, то перейдем к реализации следующей части нашей задачи, а именно к необходимым meta-тегам и другим SEO-нюансам на страницах.
<!doctype html> <html lang="ru"> <head> <meta charset="utf-8"> <!-- ...и т.д. --> <title>Контент meta-тега Title</title> <meta name="description" content="Контент meta-тега Description"> <meta name="keywords" content="ключевые фразы для meta-тега keywords"> </head> <body> <div id="root"> <div className="content"> <h1>Заголовок страницы H1</h1> <p>Текстовый контент страницы...</p> <img alt="Описание изображения" src="..."> <!-- ...и т.д. --> </div> </div> <script src="/application.js"></script> </body> </html>
На заголовки <h1> и alt-ы для картинок особое внимание заострять не буду. Тут всё просто: идем по существующему js-коду react-компонентов страниц и добавляем там, где этого нет (а также не забываем это делать в дальнейшем для новых компонентов). А вот относительно meta-тегов title, description и keywords стоит немного поговорить отдельно. Они должны быть уникальными для каждой страницы. О том, зачем нужен каждый из них и как его стоит формировать, будет полезнее почитать более профильные материалы по SEO. Для нас же стоит более прагматичная задача – реализовать средствами js изменение контента данных тегов при навигации между страницами (таким образом у каждой html-копии страницы они будут разными как и положено, а при дальнейшей навигации по приложению после его запуска, они так же будут меняться в зависимости от текущей странички, но уже силами js приложения).
В целом, для реализации данного функционала есть готовый инструмент:
Можно использовать его, либо на его основе написать что-то своё. Суть дела не изменится. Предлагаю просто перейти по ссылке и изучить то, что делает данный React-компонент и воспроизвести у себя необходимый функционал с его помощью либо написав собственное решение.
В итоге мы добавили необходимые для эффективного SEO элементы на страницы нашего интернет-ресурса.
Подводные камни, нюансы реализации и советы
Хочу в первую очередь затронуть один нюанс, знать о котором будет полезно при дальнейшей разработке. Как правило, при создании html-копий рано или поздно появятся кейсы, при которых поведение приложения для пререндера должно будет отличаться от поведения приложения в браузере у реального пользователя. Это может касаться случаев, начиная от наличия каких-либо прелоадеров, которые не нужны в html-копиях или статистического функционала, который не должен выполняться на этапе создания html-копий (и возможно даже будет вызывать ошибки при их создании) заканчивая банальным объявлением адреса для API backend-а, который вероятно настроен у вас для hot в разделе devServer webpack-конфига как proxy, а в самом приложении указан как относительный, что не заработает, так как во время работы пререндера hot не запущен и нужно ходить на реальный адрес backend-а. Как вариант, могу также привести пример в виде распространенного окошка, которое сейчас есть практически на любом интернет-ресурсе, говорящее о том, что на сайте используются Cookies. Как правило, окошко отображается на любой страничке, пока пользователь в какой-то момент один раз не закроет его. Но вот беда: пререндер то не знает, что что-то нужно закрыть, а соответственно контент данного окошка будет присутствовать на всех html-копиях, что плохо для SEO.
Но не всё так страшно и решение есть. В таких местах в приложении мы можем использовать условие для запуска тех или иных функций (или использования тех или иных переменных, как в примере с адресом API). Дело в том, что у пререндера есть специфическое имя user-agent-а – “ReactSnap” (кстати через параметры можно задать своё при необходимости). К примеру:
const isPrerender = navigator.userAgent === "ReactSnap";
Как и где это можно объявить и использовать, думаю, не нуждается в дальнейшем объяснении. Полагаю, как минимум пара мест, где это пригодится, найдётся в любом проекте.
Также стоит затронуть случай, с которым с большой долей вероятности вам придется столкнуться. А именно: React-Snap обошел не все страницы, либо, нам стало необходимо, чтобы он не заходил в какие-либо разделы или на определенные страницы и не создавал для них html-копии.
Тут сразу стоит внести понимание в процесс работы пререндера React-Snap. Он запускает наше приложение и осуществляет обход по ссылкам, найденным на страницах. При этом учитываются именно html-элементы <a>. Если пререндер не сохранил html-копию для какой-либо страницы (либо мы намеренно хотим этого добиться), то скорее всего переход на эту страницу сделан (либо вы намеренно можете так сделать) с использованием, к примеру, onClick а не через обязательный атрибут ссылки — href. Тут нужно упомянуть, что стандартные компоненты Link либо NavLink из react-router-dom фактически создают в DOM именно html-элемент <a> с href, так что если не отбиваться от классических подходов, то проблем не будет.
Следующим полезным знанием будет то, что нам обязательно необходимо позаботиться о минификации размеров DOM, который будет содержаться в наших html-копиях, так как большой html-документ будет дольше загружаться с backend-а, съедать больше трафика, да и поисковые роботы могут попросту не добраться до необходимого контента, если, к примеру, у вас в <head> документа все стили заинлайнины, как и все svg-изображения в <body>, что раздует каждую из html-копий до огромных размеров. Для понимания: если логотип вашего ресурса рендерится как inline-svg, то в файле каждой html-копии он будет присутствовать именно в таком виде.
Выход: настроить webpack таким образом, чтобы при сборке все стили складывались в css-файлы, а inline-svg заменить на использование <img> (либо средствами css) для отображения картинок (и то и другое будет загружаться один раз, а далее браться из кеша браузера и, что главное, задублированный контент таких ресурсов будет отсутствовать в html-копиях).
Еще один небольшой совет: общее количество и список всех созданных html-копий страниц, либо ошибок создания и различные вызванные редиректы (к примеру 404), а также прочие проблемные места мы сможем сразу увидеть и проанализировать благодаря достаточно понятному и подробному логу, который будет выводиться в процессе работы пререндера React-Snap. Не стоит забывать смотреть в него после сборки, так как на этом этапе мы всегда сможем увидеть те же проблемы на сайте, что увидит поисковой робот, но при этом у нас будет возможность заблаговременно что-то поправить при необходимости.
Заключение
Пожалуй, вышеописанного будет достаточно, чтобы начать и относительно быстро реализовать SEO-friendly сайт, написанный в виде Single Page Application. Далее всё будет зависеть лишь от особенностей конкретно вашего интернет-ресурса и тех целей, которые вы будете преследовать при его создании. Я постарался описать основные нюансы и подводные камни, с которыми пришлось столкнуться в процессе аналогичной разработки.
Основные плюсы данного подхода заключаются в том, что мы получаем инструментарий, который не требует архитектурных доработок приложения, дополнительного обучения программистов в команде и высоких трудозатрат на реализацию. После внедрения технологии, вы просто продолжаете разрабатывать ваше приложение по всем традициям React и придерживаясь ранее реализованной архитектуры. При этом ваше приложение уже не будет являться обычным SPA в привычном смысле, так как старт любой страницы будет сопровождаться загрузкой копии содержимого данной страницы, а дальнейшая навигация по приложению будет осуществляться уже привычным образом средствами SPA, что позволит поисковым роботам полноценно взаимодействовать с вашим контентом и выводить в результатах поиска актуальные ссылки с соответствующими описаниями страниц, а конечным пользователям как и ранее будет доступен весь спектр функционала Single Page Application.
Ну вот мы и добрались до финала. Сейчас вам удалось познакомиться с реально работающим на продакшн-версии проекта «Своё.Жильё» от Россельхозбанка подходом по реализации SEO-friendly интернет-ресурса на примере React-приложения и рассмотреть основные тезисы и подводные камни процесса создания SEO-эффективного сайта на основе SPA. Надеюсь, что полученные в данном материале знания будут полезны и найдут применение. Спасибо за уделённое на прочтение время.
ссылка на оригинал статьи https://habr.com/ru/company/rshb/blog/529636/
Добавить комментарий