Одностраничные приложения на AngularJS всем хорошои, кроме того, что поисковые системы пока что не могут (или не хотят) нормально индексировать их страницы. Часть ботов не умеет яваскрипт полностью, другие - в очень ограниченном объёме.

В наличии имеется AJAX сайт за nginx на CentOS 6, хочется обойтись минимальными телодвижениями для обеспечения его индексации. Основная идея - исполнять приложение в каком-нибудь headless браузере и отдавать результат поисковику.

Подготовка к сканированию

В секцию head главной страницы вставляем <meta name=”fragment” content=”!” />. Увидев такое, поисковики понимают, что на этой странице есть содержимое, которое генерируется яваскриптом, но при этом доступное по запросу, сформированному специальным образом. Подробности и полная спецификация есть у гугла и яндекса.

Вкратце, происходит вот что:

  • Поисковик заходит на главную страницу сайта и видит мета-тэг 'fragment'
  • Теперь все сссылки на этой странице, которые начинаются c #! будут заменениы на ?_escaped_fragment=. То есть, было #!/movies/1 становится ?_escaped_fragment=/movies/1. по спецификации еще положено, что значение параметра будет подвергнуто urlencode, но на практике роботы не всегда так делают
  • Поисковик запрашивает у сервера данные по ссылкам в новом формате и сервер должен отдавать такой же html, какой видит пользователь в браузере

Рендеринг HTML

Для получения хтмл будем использовать PhantomJS. Проще всего его установить через npm, у меня он уже был установлен, если у вас нет, то yum install npm или что-то этом роде, а потом

npm install phantomjs

PhantomJS управляется яваскриптом и к тому же имеет встроенный веб-сервер, чем мы и воспользуемся. Вот простейший скрипт для отрисовки страниц:

var server = require('webserver').create();
var port = 19003;

var getPage = function(url, callback) {
    var page = require('webpage').create();

    // запрашиваем страницу, ждём 200 мс, чтобы ангуляр успел завершить работу
    // можно попросить сайт отмечать готовность самостоятельно, но это потребует изменения приложения
    // а у нас все-таки простое решение
    page.open(url, function() {
        setTimeout(function() {
            page.evaluate(function() {
                // удаляем мета-тэг фрагмента, иначе контент не будет проиндексирован
                // заодно удалим скрипты, потому что отдаём готовый html
                $('meta[name=fragment], script').remove()
            });

            callback(page.content);
            page.close();
        }, 200);
    });
};

// запускаем встроенный в phantomjs веб-сервер
server.listen(port, function(request, response) {
    response.headers = {
        'Content-Type': 'text/html'
    };

    var regexp = /_escaped_fragment_=(.*)$/;
    var fragment = request.url.match(regexp);

    var url = 'http://localhost:19002/#!' + decodeURIComponent(fragment[1]);

    // получаем хтмл и отдаём роботу
    getPage(url, function(content) {
        response.statusCode = 200;
        response.write(content);
        response.close();
    })
});

Запускаем скрипт

phantomjs renderbot.js

Проверяем результат работы

wget http://localhost:19003/?_escaped_fragment_=/movies/1

Можно и из браузера. Должен вернуться html полноценной страницы.

Отправлем решение на живой сервер

В nginx внесем небольшие изменения

if ($args ~* _escaped_fragment_) {
    proxy_pass http://localhost:19003; #phantomjs
}

proxy_pass http://127.0.0.1:19002; #оригинальный сайт

В общем, алогритм такой:

  • Nginx обрабатывает все запросы как обычно и передаёт их бэкэнду на порт 19002
  • До тех пор, пока не встречает в урл _escaped_fragment_. очевидно, что это запрос от робота поисковика (ну или от кого-то очень любопытного), такой запрос nginx передаёт серверу phantomjs на порт 19003
  • Наш скрипт для phantomjs вычленяет все содержимое урл от _escaped_fragment_ до конца строки, раскодирует его и делает запрос уже с хэштэгом к бэкэнду, даёт неторое время на выполенения яваскрипта на странице и возвращает полученный хтмл обратно поисковому роботу

Что дальше?

В посте представлено решение в общих чертах, его уже можно применять в продакшене. Но вожможно, потребуется добавить кэширование отрисованных страниц и обработку ошибок (таймауты и тому подобное) в скрипте phantomjs. Можно сделать это самому, а можно воспользоваться различными сервисами, которые все сделают сами за некоторое вознаграждение, например, prerender.io сотоварищи. It's up to you.