image

Всем мир!

Содержание статьи:

1. Что такое CRAWL
2. Динамический CRAWL
3. Задачи, инструменты, решение
4. Почитать
5. Выводы



1. Что такое CRAWL



Это сканирование страниц сайта поисковыми системами с целью получить необходимую информацию. Результат выполнения данного сканирования является html представление в конечной точке (у каждой поисковой системы свои настройки, а именно грузить или нет js (с запуском или без), css, img и т.д.) или как это ещё называют «снимок» сайта.

2. Динамический CRAWL



Здесь речь будет идти о динамическом CRAWL страницы, а именно когда у вас сайт имеет динамический контент (или как это называют Ajax контент). У меня проект с использованием Angular.js + HTML5 Router (это когда без domain.ru#!path, а вот так domain.ru/path ), весь контент меняется в <ng-view></ng-view>и один единственный index.php и специальные настройки .htaccess, для того, чтобы после обновления страницы всё отобразилось как надо.

Это прописано в настройках роутера angular:
 $locationProvider.html5Mode({
            enabled: true,
            requireBase: false
        });


Это прописано в .htaccess:
RewriteEngine on

        # Don't rewrite files or directories
        RewriteCond %{REQUEST_FILENAME} -f [OR]
        RewriteCond %{REQUEST_FILENAME} -d
        RewriteRule ^ - [L]

        # Rewrite everything else to index.html to allow html5 state links
        RewriteRule ^ index.php [L]


3. Задачи, инструменты, решение



Задачи:

1. Отдавать динамический контент страницы такой, какой он становится после окончания рендеринга и инициализации приложения
2. Формируем, Оптимизируем и Сжимаем html снимок страницы
3. Отдаём поисковой системе html снимок

Инструменты:

1. Установленная NPM (npm — это пакетный менеджер node.js. С его помощью можно управлять модулями и зависимостями.)
2. Установленный модуль html-snapshots с помощью команды:
 npm install html-snapshots

3. Правильная конфигурация

Решение:

Для быстродействия рекомендую выполнять «кравлинг» на localhost (локальном веб-сервере)

Для начала нужно добавить в главный index.php в meta tag в head:
<meta name="fragment" content="!">


Пример sitemap.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!--	created with www.mysitemapgenerator.com	-->
<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
    <url>
        <loc>http://localhost/domain.ru/www/product/30</loc>
        <lastmod>2016-07-22T19:47:25+01:00</lastmod>
        <priority>1.0</priority>
    </url>
</urlser>


Конфигурация server.js:

var fs = require("fs");
var path = require("path");
var util = require("util");
var assert = require("assert");
var htmlSnapshots = require("html-snapshots");
var minify = require('html-minifier').minify;

htmlSnapshots.run({

    //#1 С использованием SITEMAP
    //input: "sitemap",
    //source: "sitemap_localhost.xml",

    //#2 Массив ссылок на страницы сайта
    input: "array",
    source: ["http://localhost/domain.ru/www/product/30"],
    //protocol: "https",

    // setup and manage the output
    outputDir: path.join(__dirname, "./tmp"),

    //Чистит директорию перед сохранением новых копий
    outputDirClean: false,
    // Селектор любого блока, который находится внутри <ng-view></ng-view> и отображается после инициализации приложения
    selector: "#product",
    //Ограничить время загрузки до 12 секунд, а можно и больше
    timeout: 120000,
    //Настройки помогающие отображать контент быстрее для CRAWL
    phantomjsOptions: [
        "--ssl-protocol=any",
        "--ignore-ssl-errors=true",
        "--load-images=false"
    ]

}, function (err, snapshotsCompleted) {

    var body;

    console.log("completed snapshots:");

    assert.ifError(err);

    snapshotsCompleted.forEach(function(snapshotFile) {

        body = fs.readFileSync(snapshotFile, { encoding: "utf8"});

        //Убираем стили и их содержание
        var regExp = /<style[^>]*?>.*?<\/style>/ig;
        var clearBody = body.replace(regExp, '');

        //Производим замену доменного имени
        var domain = /http:\/\/localhost\/domain.ru\/www/ig;
        clearBody = clearBody.replace(domain, '//domain.ru');

        //Производим оптимизацию html файла
        clearBody = minify(clearBody, {
            conservativeCollapse: true,
            removeComments: true,
            removeEmptyAttributes: true,
            removeEmptyElements: true,
            collapseWhitespace: true
        });
        //Записываем в файл
        fs.open(snapshotFile, 'w', function(e, fd) {
            if (e) return;
            fs.write(fd, clearBody);
        });

    });

});

console.log('FINISH');



Запуск командой:
node server


Понимание алгоритма:

1. Сначала он «кравлит» все страницы
2. Создаёт файлы и называет папки согласно вашему url: product/30/index.hmtl (index.html, а можно и product/30.html кому как удобней на скорость не влияет)
3. После этого вызывает callback -> snapshotsCompleted, где производит оптимизацию каждого index.html снимка вашей страницы

Снимки вашего сайта подготовлены, осталось отдать их поисковому боту при заходе:

index.php
if (isset($_GET['_escaped_fragment_'])) {
    if ($_GET['_escaped_fragment_'] != ''){
        $val = $_GET['_escaped_fragment_'];
        include_once "snapshots" . $val . '/index.html';
    }else{
        $url = "https://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
        $arrUrl = parse_url($url);
        $val = $arrUrl['path'];
        include_once "snapshots" . $val . '/index.html';
    }
}else {
    include_once('pages/home.php');
}


Объяснение:

1. html5 push state
If you use html5 push state (recommended):
Just add this meta tag to the head of your pages
<meta name="fragment" content="!"> 


If your URLs look like this:
www.example.com/user/1
Then access your URLs like this:
www.example.com/user/1?_escaped_fragment_=

2. hashbang
If you use the hashbang (#!):
If your URLs look like this:
www.example.com/#!/user/1

Then access your URLs like this:
www.example.com/?_escaped_fragment_=/user/1

Дополнительно, для тех у кого есть снимки, но нет оптимизации:

var fs = require("fs");
var minify = require('html-minifier').minify;
var path = require("path");
var util = require("util");
var assert = require("assert");
var htmlSnapshots = require("html-snapshots");

//Получение списка папок
var myPath = path.join(__dirname, "./tmp/domain.ru/www/");

function getFiles (dir, files_){
    files_ = files_ || [];
    var files = fs.readdirSync(dir);
    for (var i in files){
        var name = dir + '/' + files[i];
        if (fs.statSync(name).isDirectory()){
            getFiles(name, files_);
        } else {
            files_.push(name);
        }
    }
    return files_;
}

var allFiles = getFiles(myPath);
//var allFiles = [ 'C:\\xampp\\htdocs\\nodejs\\crawler\\tmp\\domain.ru\\www\\/product/30/index.html' ];
var body;
allFiles.forEach(function(snapshotFile){

    body = fs.readFileSync(snapshotFile, { encoding: "utf8"});

    var regExp = /<style[^>]*?>.*?<\/style>/ig;
    var clearBody = body.replace(regExp, '');

    var domain = /http:\/\/localhost\/domain.ru\/www/ig;
    clearBody = clearBody.replace(domain, '//domain.ru');

    clearBody = minify(clearBody, {
        conservativeCollapse: true,
        removeComments: true,
        removeEmptyAttributes: true,
        removeEmptyElements: true,
        collapseWhitespace: true
    });

    var social = /<ul class=\"social-links\">.*?<\/ul>/ig;
    clearBody = clearBody.replace(social, '');

    fs.open(snapshotFile, 'w', function(e, fd) {
        if (e) return;
        fs.write(fd, clearBody);
    });

});

console.log('COMPLETE');


4. Почитать



stackoverflow.com/questions/2727167/getting-all-filenames-in-a-directory-with-node-js — работа с файлами в node.js
github.com/localnerve/html-snapshots — snapshots module doc
perfectionkills.com/experimenting-with-html-minifier — options snapshots module doc

yandex.ru/support/webmaster/robot-workings/ajax-indexing.xml — yandex crawler info
developers.google.com/webmasters/ajax-crawling/docs/specification — google crawler info

www.ng-newsletter.com/posts/serious-angular-seo.html — article
prerender.io/js-seo/angularjs-seo-get-your-site-indexed-and-to-the-top-of-the-search-results — article
prerender.io/documentation — article

regexr.com — regexr
stackoverflow.com/questions/15618005/jquery-regexp-selecting-and-removeclass — regexr

5. Выводы



Теперь смело можно писать любые SPA приложения не беспокоясь за их «кравлинг» поисковым ботом, также вы можете подобрать под свой набор инструментов нужную конфигурацию как для «сервера», так и для «клиента»!

Всем профессиональных успехов!
Оценить:

Проголосовало 23 человека. Воздержалось 42 человека.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Поделиться с друзьями
-->

Комментарии (9)


  1. yarkov
    28.07.2016 23:26
    -2

    Вот бы еще описали, как это к express.js прикрутить — было бы вообще круто.
    Про prerender.io в курсе, но хочется, так сказать, без сторонних сервисов.


    1. liderman
      29.07.2016 00:26

      Можно использовать prerender перенаправляя трафик от поисковых ботов на него. В этом случае будет не важно какой фреймворк вы используете. Сервис можно развернуть у себя на сервере или в Docker-контейнере.


  1. symbix
    29.07.2016 03:59

    Гугл уже умеет JS:

    «We are no longer recommending the AJAX crawling proposal we made back in 2009.

    In 2009, we made a proposal to make AJAX pages crawlable. Back then, our systems were not able to render and understand pages that use JavaScript to present content to users. Because «crawlers … [were] not able to see any content … created dynamically,» we proposed a set of practices that webmasters can follow in order to ensure that their AJAX-based applications are indexed by search engines.

    Times have changed. Today, as long as you're not blocking Googlebot from crawling your JavaScript or CSS files, we are generally able to render and understand your web pages like modern browsers. To reflect this improvement, we recently updated our technical Webmaster Guidelines to recommend against disallowing Googlebot from crawling your site's CSS or JS files.»

    https://webmasters.googleblog.com/2015/10/deprecating-our-ajax-crawling-scheme.html


    1. Akuma
      29.07.2016 09:53

      Где-то на хабре была статья, где человек экспериментировал с этим «умеет ajax». В итоге откатился обратно, т.к. умения ajax довольно скудные и позиции в выдаче падают.


      1. DmitryMV
        29.07.2016 23:09

        У google есть схема JSON-LD, там можно прописывать детали для разного типа страниц


    1. DmitryMV
      29.07.2016 23:13

      Это всё хорошо, но у меня сайт забирает JSON данные 3-5 мб, я пробывал в google search console, посмотреть как это видит Google-bot в итоге он некорректно отображает, а с помощью снимков всё замечательно!


  1. savostin
    29.07.2016 22:42

    if (isset($_GET['_escaped_fragment_'])) {
        if ($_GET['_escaped_fragment_'] != ''){
            $val = $_GET['_escaped_fragment_'];
            include_once "snapshots" . $val . '/index.html';
        }else{
            $url = "https://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
            $arrUrl = parse_url($url);
            $val = $arrUrl['path'];
            include_once "snapshots" . $val . '/index.html';
        }
    

    Отдавать статичный html через php, да еще через include — то еще извращение.
    Пусть этим занимается nginx:
    location / {
    if ( $args ~ "_escaped_fragment_=(.+)" ) {
      set $real_url $1;
      rewrite ^ /snapshots/$real_url.html? break;
      }
    try_files $uri $uri/ =404;
    }
    


    1. DmitryMV
      29.07.2016 23:08

      Include было на скорую руку для примера, за пример спасибо!


    1. symbix
      30.07.2016 03:56

      Можно и без регулярок, $arg__escaped_fragment_

      Главное, с underscore-ами не запутаться. :)