Одностраничный сайт. Создаем сайтмап своими руками

июнь 2 , 2017
Метки:
Предыдущая статья Следующая статья Демо Исходники

Так, продолжаем работать над одностраничным сайтом. Сегодня мы сделаем еще один шаг к тому, чтобы подружить наш сайт с поисковиками. А именно, создадим сайтмап.

Казалось бы, что его создавать? Создали sitemap.xml и скопипастили туда содержимое чужого сайтмапа, заменив пути на свои. Но это не наш метод, мы же программисты, нам неинтересно врукопашную набивать содержимое файла. Все должно строиться и обновляться самостоятельно.

Другой вариант. Почему бы не воспользоваться готовыми решениями, особенно если мы делаем одностраничник на базе какой-то CMS? Да, но тогда как мы узнаем, как самим создать xml-файл, да еще и разными способами? Как создать общие конфиги для клиента и сервера, а под маской xml-файла выполнять php-код? Как вычитать даты на php и что такое sitemap changefreq?

Если Вас заинтересовали вышеозначенные темы, то читайте статью дальше. Будет много интересного, а может, даже и полезного :-)


Делаем общий конфиг для клиента и сервера

Давайте вспомним предыдущий урок. Там, где мы создали сайт и разобрались с History API - вот эта статья. Наш конфиг из main.js занимал добрую половину всего javascript-кода и выглядел так

    var config = {
        siteTitle: 'Webdevkin SPA',
        mainPage: 'main',
        pages: {
            main: {
                title: 'Главная',
                menu: 'main'
            },
            // ...
        }
    };

Смотрим на это и понимаем, что означенные данные пригодятся не только на клиенте, но и на сервере. Здесь есть заголовок, главная страница и описание всех страниц сайта. При формировании сайтмапа нам пригодятся как минимум пути к страницам. Но скажу по секрету (а в следующей статье уже официально), что все поля этого конфига нам понадобятся как на клиенте, так и на сервере. Поэтому первое, что мы сделаем, это вынесем целиком конфиг в отдельный json-файл. Таким образом, мы легко сможем работать с ним в javascript, загрузив его предварительно с сервера. И так же легко с json-файлами работает php. Но до этого чуть позже, а пока изменения на клиенте.

Создаем папку data в корне проекта и в ней файл config.json с таким содержимым

    {
        "siteTitle": "Webdevkin SPA",
        "mainPage": "main",
        "pages": {
            "main": {
                "title": "Главная",
                "menu": "main"
            },
            "about": {
                "title": "О проекте",
                "menu": "about"
            },
            "blog": {
                "title": "Блог Webdevkin-a",
                "menu": "blog"
            },
            "simpple": {
                "title": "Проект Simpple",
                "menu": "simpple"
            },
            "contacts": {
                "title": "Контакты",
                "menu": "contacts"
            },
            "shop": {
                "title": "Интернет-магазины",
                "menu": "blog"
            },
            "frontend": {
                "title": "Статьи о фронтенде",
                "menu": "blog"
            },
            "mysql": {
                "title": "База данных Mysql",
                "menu": "blog"
            },
            "widgets": {
                "title": "Встраиваемые javascipt-виджеты",
                "menu": "blog"
            }
        }
    }

Пока ничего умного мы не сделали, лишь скопировали js-объект в отдельный файл. И конечно, не забыли расставить двойные кавычки у ключей, как того требует спецификация json. Дальше нужно избавиться от этих же данных в файле main.js и загружать их из data/config.json.

В самом начале main.js вместо var config = {/* Здесь большой объект с настройками */}; напишем просто var config = {};

Вот так мы сразу сократили main.js на половину - намного лучше. Теперь нужно чуть переделать запуск приложения - функцию init. Вспомним, как она выглядит

    // Инициализация приложения
    function init() {
        var page = document.location.pathname.substr(1) || config.mainPage;
        _loadPage(page, false);

        _bindHandlers();
    }

Радикально ничего меняться не будет. Нам нужно выполнить те же самые действия, но предварительно загрузив содержимое data/config.json в пока что пустой объект config. Сделаем 2 правки. Во-первых, вынесем 3 строчки кода из init в отдельную приватную функцию _start.

    // Старт приложения: загрузка страницы и привязка событий
    function _start() {
        var page = document.location.pathname.substr(1) || config.mainPage;
        _loadPage(page, false);

        _bindHandlers();
    }

Фактически просто переименовали init в _start и все. А новый init будет теперь такой

    // Инициализация приложения: загрузка конфига и старт
    function init() {
        $.getJSON('/data/config.json', function(data) {
            config = data;
            _start();
        });
    }

Тоже все просто: получили содержимое config.json в переменную data и закинули все ее содержимое в глобальный (для модуля app) объект config. И с клиентской частью на сегодня все. Переходим непосредственно к теме сайтмапов.


Создаем тестовый сайтмап. Как превратить php в xml?

Как выглядит сайтмап, думаю, особых вопросов нет. Ничего хитрого, вот, например, сайтмап моего блога - webdevkin.ru/sitemap.xml. Этот сайтмап создан с помощью плагина Modx Revo GoogleSiteMap, который встречается, насколько мне известно, на самых разных Modx-ресурсах. Именно по его образу мы и создадим сайтмап для одностраничного сайта.

Но минутку. Вот сейчас встает один небольшой, но хитрый вопрос: а как нам формировать sitemap.xml? Создать его руками проблем нет, но это же безобразие и расстройство. Мы знаем php и можем создать sitemap.php, но это будет php, а не xml. Может, каким-то поисковым ботам и можно подсунуть php-файл в качестве сайтмапа, но хотите ли Вы это проверять? Нет, нам определенно нужен именно sitemap.xml.

Можем пойти другим путем: создать php-скрипт, который будет генерировать sitemap.xml. То есть создавать отдельный файл - это несложно. Но опять вопрос: а когда запускать этот скрипт? Раз в день руками? Забудем, да и страницы на сайте могут обновляться чаще. Каждый раз при заходе на главную? Слишком шикарно для служебной процедуры.

Нет, нам нужен именно sitemap.xml, при открытии которого сайтмап строится динамически из конфига data/config.json.

И для этого нужно сделать всего 2 вещи. Первое, создать файл sitemap.php, который сгенерирует xml-строку динамически из конфигов и укажет нужный тип файла. И второе, написать правило в .htaccess, согласно которому при запросе ресурса sitemap.xml веб-сервер перекинет запрос на файл sitemap.xml. Вот и вся идея.

И реализация этой идеи. Сначала создадим в корне проекта файл sitemap.php (не xml, нет) со следующим содержимым. Сначала 2 строки php-кода

    header('Content-type: text/xml');
    echo '';

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

    
        
            https://spa.webdevkin.ru
            2017-05-10
            monthly
            0.25
        
        
            https://spa.webdevkin.ru/about
            2017-05-25
            weekly
            0.75
        
    

Первой строкой функцией header мы говорим, что хоть файл и php, но он отдает содержимое xml-файла. Теперь поисковики и, например, браузеры, понимают, как читать и парсить наш файл.

Дальше выводим строку "xml version тра-та-та" и два узла url на пробу. Пока руками, нам нужно просто убедиться, что сайтмап создан правильно, о наполнении позаботимся позже. А пока откройте sitemap.php в браузере, они отлично справляются с парсингом xml-файлов. Если все сделали правильно, то браузер поймет, что на самом деле выводится xml-файл и все отлично распарсит. Если нет, то сверьтесь с исходниками и все будет хорошо.

Напомню, что наша задача - отдавать не sitemap.php, а sitemap.xml. Для этого в .htaccess мы добавим простую строчку (в конец файла)

    RewriteRule ^sitemap\.xml$ sitemap.php [L]

Откроем sitemap.xml. Странно, открылся сам одностраничник с верхним меню и ошибками в консоли. Но это логичное поведение из-за такой строки в .htaccess

    RewriteRule ^(.+)$ index.php?q=$1 [L,QSA]

Все запросы, идущие на главную страницу, перенаправляются на index.php, который пытается загрузить нужную страницу сайта (вспоминаем первый урок). Нам нужно явно указать, чтобы перенаправлялись все запросы, кроме sitemap.xml. Изменим эту строку так

    RewriteRule ^(?!(.*sitemap\.xml)) index.php?q=$1 [L,QSA]

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


Формируем сайтмап динамически из config.json. Способ первый.

Конфиг json уже готов к тому, чтобы сформировать по нему сайтмап. Но давайте немного расширим его. Возможно, не каждую страницу сайта мы захотим видеть в сайтмапе. Поэтому добавим логический ключ inSitemap в массив pages, с тем, чтобы отличать такие страницы. Я не буду еще раз копипастить содержимое json-файла, просто скажу, что добавил ключ "inSitemap": true во все страницы, кроме about и contacts. А теперь можно и php-код писать.

Сначала пара вспомогательных функций.

    // Получаем базовый url
    function getRootUrl() {
        $protocol = ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443)
            ? "https://"
            : "http://";
        $domain = $_SERVER['SERVER_NAME'];
        return $protocol . $domain;
    }
    
    // Получаем массив страниц из конфига
    function getPages() {
        $jsonString = file_get_contents( __DIR__ . '/data/config.json' );
        $config = json_decode($jsonString, true);
        return $config['pages'];
    }

В сайтмапе все ссылки должны быть абсолютными. getRootUrl поможет сформировать нам такую абсолютную ссылку вида http://site.ru/. Хитрый способ получения протокола нагуглен в интернетах. А вторая функция getPages вытащит содержимое конфига, преобразует его в ассоциативный массив и вернет часть конфига, pages. Остальное нам пока не понадобится.

Теперь основной код. Заменим наши тестовые узлы <url> на следующий php-код

    $pages = getPages();
    $rootUrl = getRootUrl();
    
    // Перебираем все страницы сайта
    foreach($pages as $key => $page) {
        if (!empty($page['inSitemap'])) {
            echo '';
            echo sprintf('%s%s', $rootUrl, $key);
            echo '';
        }
    }

Очень простое решение в лоб. Перебираем массив страниц из конфига, если есть флаг inSitemap, то формируем узлы xml. Пока только <loc>, до остальных доберемся чуть позже.

В общем, вот так, без затей, просто вывод строки через echo. Откроем sitemap.xml и убедимся, что нехитрый код теперь выводит страницы из конфига.

А теперь посмотрим, как избежать такого странного, хоть и рабочего, способа формирования xml-файлов.


Другой способ формирования xml. SimpleXMLElement

В php есть более приятный способ работы с xml - SimpleXMLElement. Вы можете загуглить и подробнее почитать, как работать с этой штукой. А можете посмотреть код ниже, и все самое важное узнаете прямо здесь

    // Отдаем правильный заголовок
    header('Content-type: text/xml');
    
    // Создаем xml-документ
    $xml = new SimpleXMLElement('');
    
    $pages = getPages();
    $rootUrl = getRootUrl();
    
    // Перебираем все страницы сайта
    foreach($pages as $key => $page) {
        if (!empty($page['inSitemap'])) {
            $url = $xml->addChild('url');
            $url->addChild('loc', $rootUrl . $key);
        }
    }
    
    // Создаем dom-элемент
    $dom = new DomDocument();
    $dom->loadXML($xml->asXML());
    $dom->formatOutput = true;
    
    // Выводим xml-строку
    echo $dom->saveXML();

За исключением двух вспомогательных функций getRootUrl и getPages здесь приведен весь код формирования xml-файла. Создали xml-документ через new SimpleXMLElement и добавили нужные узлы через addChild. По сравнению с предыдущим примером кода больше не стало, но выглядит он намного аккуратнее. И теперь это именно чистый php-код, а не страшная мешанина php, xml и вывода строк через echo.

По совести говоря, последние 4 строки нужны только для форматирования xml-файла. То есть чтобы при открытии sitemap.xml, например, в блокноте, Вы увидели не сплошную строку, а красиво отформатированную и читаемую. Если же Вам это не нужно, то напишите просто

    echo $xml->asXML();

Поисковики или браузеры прекрасно распарят длинную строку и без дополнительных ухищрений.

Итак, теперь у нас есть полноценный рабочий сайтмап, который уже можно подкладывать поисковикам. Но на приличных сайтах принято кроме параметра loc, адреса страницы, указывать еще 3 штуки: lastmod, changefreq и priority. Читаем дальше, что это и как их добавить в сайтмап.


Расширяем сайтмап

Кратко поясню.
— lastmod - дата последнего обновления страницы.
— changefreq - как часто обновляется страница.
— priority - приоритет, сигнализирует поисковикам, в каком порядке нужно индексировать страницы.

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

Первый параметр - lastmod. Время последнего обновления мы недрогнувшей рукой запишем в тот же config.json. Знаю, по-хорошему время должно храниться в базе данных и обновляться при редактировании контента. Но пока мы до этого не добрались (возможно, только пока), то просто запишем новый параметр в конфиг. Назовем его "updated" и забьем тестовыми датами. В формате "ГГГГ-ММ-ДД". Чтобы не набивать все даты руками, в исходниках найдете все, что нужно.

changefreq - частота обновления, имеет такие допустимые значения: daily, weekly, monthly и какие-то еще.
priority по умолчанию равно 0.5, но никто не запрещает ставить больше или меньше.

Предлагаю оба этих параметра высчитывать на лету. Договоримся о таком принципе. Для страниц, которые обновлялись больше недели назад, зададим changefreq=monthly и priority=0.5. До недели: weekly и 0.75. Сегодня или вчера: daily и 1.

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

    // Возвращает для сайтмапа нужные данные
    function getSitemapData($dateString) {
        // Ищем разницу дат в днях
        $diffDate = date_diff(date_create(), date_create($dateString));
        $diff = $diffDate->days;
    
        // Определяем остальные данные
        if ($diff <= 1) {
            $changefreq = 'daily';
            $priority = 1;
        } else if ($diff <= 7) {
            $changefreq = 'weekly';
            $priority = 0.75;
        } else {
            $changefreq = 'monthly';
            $priority = 0.5;
        }
        return array(
            'lastmod' => $dateString,
            'changefreq' => $changefreq,
            'priority' => $priority
        );
    }

На вход мы подаем строку-дату, взятую прямиком из конфига. Дальше ищем разницу дат в днях - в этом нам поможет date_diff. И дальше в зависимости от разницы дней формируем нужный результат в виде массива. Осталось добавить эти данные непосредственно в сайтмап. Доработаем немного цикл, где формируются узлы, и подселим к loc-у еще 3 пункта.

    // Перебираем все страницы сайта
    foreach($pages as $key => $page) {
        if (!empty($page['inSitemap'])) {
            $url = $xml->addChild('url');
            $data = getSitemapData($page['updated']);
    
            $url->addChild('loc', $rootUrl . $key);
            $url->addChild('lastmod', $data['lastmod']);
            $url->addChild('changefreq', $data['changefreq']);
            $url->addChild('priority', $data['priority']);
        }
    }

Вот и все. Откроем еще раз наш spa.webdevkin.ru/sitemap.xml и увидим правильный сайтмап.


Послесловие

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

Впрочем, пока что для нормальной индексации мы сделали только половину работы. Потому как все страницы сейчас отдают поисковикам одинаковое содержимое. То, что написано в index.php, без контента из файлов pages/*.html. Как научить сервер отдавать такой же контент, который видит пользователь, мы разберем в следующей статье. И напоследок небольшой опрос и ссылки на демо и исходники.


Предыдущая статья Следующая статья Демо Исходники
Метки:
Заходите в группу в контакте - https://vk.com/webdevkin
Анонсы статей, обсуждения интернет-магазинов, vue, фронтенда, php, гита.
Истории из жизни айти и обсуждение кода.
Как Вам статья? Оцените!