SPA для поисковиков. Рендер на стороне сервера, robots.txt и страница 404
В первых двух уроках про SPA мы создали одностраничный сайт и соорудили для него sitemap.xml.
Пройдя по любой ссылке в сайтмапе, посетитель сайта увидит правильно отрендеренный контент, заголовок страницы и прочее. Беда в том, что этот контент не видят поисковики. Они не умеют выполнять javascript и видят только то, что отдает сервер. Поэтому все страницы сайта для них будут совершенно одинаковые, что не только полезно, а еще и очень вредно :-)
Наша задача состоит в том, чтобы научить поисковые роботы видеть то же, что и мы с вами. Другими словами, заставить сервер отрисовывать ровно ту же html-разметку, что образуется после отработки javascript-кода в браузере.
Обращаю внимание: это никоим образом не означает отход от концепции одностраничных сайтов. Для наших посетителей при навигации по сайту страницы будут грузиться как и раньше: через ajax-загрузку контента и с History API. Изменится лишь первоначальная загрузка страницы, когда мы сразу отдадим в браузер нужный контент. Как это сделать, а также про небольшие хитрости с robots.txt и страницей 404 Not Found, читайте ниже в статье.
Подумаем, как это можно сделать
Итак, что у нас есть сейчас? html-шаблон, заготовка любой страницы сайта. В шаблоне имеется пустой тег title, пустой контейнер для содержимого страницы и еще пара мест, которые динамически меняются в зависимости от текущей страницы. Эти данные клиент подгружает с сервера из конфига json. Напомню, что именно: заголовок сайта, заголовок страницы, пункт меню, который нужно выделить, и самое главное - содержимое страницы.
То есть получается, что у нас есть html-каркас, в который мы подставляем некоторые переменные-плейсхолдеры. Но если все эти переменные javascript может вытащить из конфиг-файла config.json, то почему это не может сделать php? А ни почему. Серверный код легко вытащит те же данные, что и клиент. Так давайте напишем же его.
Вытаскиваем данные для рендера страницы
// Вытаскиваем конфиг в ассоциативный массив $jsonString = file_get_contents(__DIR__ . '/data/config.json'); $config = json_decode($jsonString, true); // Определяем текущую страницу $page = trim($_SERVER['REQUEST_URI'], '/'); // Если $page == '', то есть REQUEST_URI = '/', то эта страница главная и берем ее из конфига if ($page == '') { $page = $config['mainPage']; } // Заголовок сайта $siteTitle = $config['siteTitle']; // Заголовок и меню страницы $pageData = $config['pages'][$page]; $pageTitle = $pageData['title']; $pageMenu = $pageData['menu']; // Загружаем содержимое страницы из html-файла в папке /pages $content = file_get_contents(__DIR__ . '/pages/' . $page . '.html'); // Подключаем шаблон страницы и выводим ее содержимое include_once __DIR__ . '/tpl/index.php';
Немного поясню. В переменную $config попадает все содержимое файла config.json. $_SERVER['REQUEST_URI'] нам понадобится для определения текущей страницы. Например, для https://spa.webdevkin.ru/about REQUEST_URI выдаст /about. Поэтому нам нужно отбросить слэш в начале.
Если мы зашли на главную страницу https://spa.webdevkin.ru, то REQUEST_URI определится как просто /. Так мы поймем, что это главная страница сайта. Но у нас договоренность, что для удобства главная страница не обязательно main - она хранится в том же конфиге в ключе mainPage.
Далее определяем заголовок сайта и данные по конкретной странице. Из массива pages конфига вынимаем $pageData - данные по конкретной странице, а уже из $pageData заголовок и меню самой страницы. В переменную $content записываем основной контент.
И последняя строка подключает шаблон страницы. Сейчас мы его и создадим. А пока сообщу, что вышеприведенный код - это все содержимое файла index.php! Больше там нет ни единой строки разметки. index.php теперь делает то, что и должен порядочный php-файл: вытаскивает нужные данные из конфигов, проводит какие-то вычисления и подключает внешний шаблон с html-разметкой. Практически MVC, можно похвастаться, что мы успешно отделили данные от представления.
Кстати, а куда делось это представление? То есть та разметка, которая была в index.php. А она вся, до единой строки перекочевала в новый файл tpl/index.php - тот самый html-шаблон.
Шаблон с html-разметкой tpl/index.php
Почему же файл php, а не html, спросите Вы. Потому что в html-разметке будут вставки php-кода для вывода полученных выше переменных. Я не буду приводить весь код шаблона, потому как это просто копипаста из прежнего index.html. Вы найдете этот код в первой статье про SPA или в исходниках.
Но пусть и не весь код, но все-таки отличия от того файла я обозначу: как раз те отличия, где появились php-вставки. Во-первых, вместо пустого тега title напишем такое: склеим заголовок страницы и сайта (то же самое, что делает javascript)
< ?php echo $pageTitle . ' | ' . $siteTitle ?>
Затем в h2 - заголовке страницы напишем аналогично
< ?php echo $pageTitle ?>
Там, где был пустой контейнер id="content", будет следующее (только без стилей, извините)
< ?php echo $content ?>
И осталось не забыть еще момент: нужно выделить в меню активную страницу.
То есть навесить соответствующему пункту класс active.
Немного поправим тег menu. Парсер кода выдает совершеннейший бред, поэтому придется картинкой :-)
Впрочем, не так уж и плохо получилось заскриншотить, к тому же в Sublime неплохая подсветка кода :-)
И это все, что нам нужно для рендера разметки на стороне сервера. Теперь поисковики могут зайти на любую страницу и увидеть не пустую заготовку, а полноценную разметку с контентом. Можете убедиться в этом сами, зайдя на любую страницу, например, https://spa.webdevkin.ru/blog, отключив предварительно javascript. Покликайте по ссылкам и убедитесь, что сайт адекватно работает, только каждый раз перезагружая страницу. Это нормально, так как js отключен.
Или же просто откройте view-source для страницы blog (ctrl + U или cmd + U). Там видно, что все динамические части страницы заполнились правильно. А именно это и нужно для поисковых роботов.
Сравнение скорости загрузки: разница очевидна
И все-таки отключите javascript, если еще не успели.
У Вас будет возможность убедиться в том, что одностраничник мы делаем не зря.
Насколько шустрее грузятся страницы, когда работает ajax-подгрузка контента.
И насколько больше времени занимает полная перезагрузка при отключенном javascript.
Даже на нашем крошечном сайте это так заметно, а на реальных больших страницах разница будет еще больше.
P.S. Это к вопросу, который я задавал в самом начале первого урока: зачем вообще делать одностраничные сайты для простых корпоративных визиток?
Скорость.
И кстати про скорость. Вспомним, что при начальной загрузке страницы javascript загружает контент с сервера.
// Старт приложения: загрузка страницы и привязка событий function _start() { var page = document.location.pathname.substr(1) || config.mainPage; _loadPage(page, false); _bindHandlers(); }
Сейчас нам этого не нужно, потому как сервер уже отдает готовый контент. Поэтому поправим этот код на такой. Уберем вызов _loadPage, то есть не будем лишний раз нагружать браузер.
// Старт приложения: привязка событий function _start() { _bindHandlers(); }
robots.txt
Все ссылки в нашем сайтмапе рабочие и готовы предстать перед поисковиками. Порядочные роботы и сами найдут sitemap.xml, но лучше указать его по-человечески - через robots.txt. Создадим в корне проекта файл robots.txt с таким содержимым
User-agent: * Allow: / Disallow: /data/ Disallow: /pages/ Disallow: /tpl/ Host: https://spa.webdevkin.ru Sitemap: https://spa.webdevkin.ru/sitemap.xml
В первых двух строках разрешаем индексацию сайта всем поисковым роботам. Дальше закрываем 3 папки, там хранятся служебные файлы, поисковикам знать о них ни к чему. И в конце указываем хост и сайтмап - стандартный набор для robots.txt.
По идее, сайт готов к индексации, кроме одного момента: а что если в сайтмапе попадется неправильная ссылка?
Делаем страницу 404 Not Found
С точки зрения интернетов, ничего страшного в этом нет. Ссылки устаревают, да и никто не запретит пользователю руками вбить адрес какой угодно страницы. Какой-нибудь https://spa.webdevkin.ru/wrong-link - почему бы и нет?
До этого урока на такие левые ссылки у нас загружалась пустая заготовка. javascript просто ругался в консоль, говоря, что такой страницы в конфиге нет. Сейчас же выскочит php-ошибка, что намного хуже. Нужно научиться обрабатывать такие случаи. Хорошо, что в этом нет ничего сложного. В index.php сразу после определения $siteTitle добавьте код
// Если страница не существует, возвращаем 404 Not Found if (!isset($config['pages'][$page])) { // Отдаем код 404 header('HTTP/1.0 404 Not Found'); // Подключаем шаблон 404 страницы include_once __DIR__ . '/tpl/404.php'; die; }
Здесь проверяем, есть такая страница в конфиге и если нет, делаем 2 вещи. Первое, отдаем в заголовках ответа сервера код 404 - это для поисковиков. И второе, подключаем шаблон "Страница не найдена" - а это для людей. Базовую страницу tpl/index.php мы определили в отдельный шаблон, а чем 404 хуже? Вот ее содержимое, а точнее тега body
Страница не найдена
К сожалению, страница, которую Вы искали, больше не существует.
Воспользуйтесь меню или перейдите на главную страницу сайта.
Обратите внимание на некоторые отличия. Во-первых, не подключены js-файлы - они нам здесь не нужны. Во-вторых, ссылки в меню указаны без ajax-атрибутов. Это значит, что клик по любой ссылке (уже правильной) приведет к перезагрузке страницы. Предлагаю сделать так, потому как тащить на 404 скрипты, да еще и заботиться о том, чтобы ничего не поломалось, смысла не имеет. И на этом очередной урок про одностраничные сайты закончен.
Итоги урока
Как видим, рендерить контент на стороне сервера оказалось довольно просто. Все, что мы делали на javascript, продублировали на php. Заодно вынесли html-код в отдельный шаблон и тем самым отделили логику от представления. А также создали robots.txt и не забыли про страницу 404 Not Found.
Ссылки на демо-сайт и на исходники чуть ниже - большие синие кнопки (или фиолетовые? никак не могу решить).
Первые статьи по теме SPA здесь: раз и два. Делитесь своими соображениями по поводу одностраничных сайтов и задавайте вопросы в комментариях. И далеко не расходитесь, ведь это еще не последняя статья на одностраничную тему :-)
Истории из жизни айти и обсуждение кода.