Кейс: перенос сайта aganar.ru с версии Drupal 7 на более современную версию Drupal 11

Кирилл Миченус
Web-разработчик

Производитель бытовок и модульных зданий Аганар обратился к нам за продвижением сайта в поиске (SEO). Причем на момент обращения его сайт потонул и затерялся на большой глубине Яндекса и Google по двум интересным причинам:

  1. Неоплаченный в течение долгого времени домен.
  2. Использование этого домена для раздачи ссылок на не очень хорошие сайты.

После запуска работ по продвижению, в Яндексе сайт начал активно подниматься, а вот с зарубежным Google дела шли намного медленнее. Но причина также была в том, что сам сайт был довольно "старым" по функционалу и внутрянке. Поэтому настояли на обновлении версии и ускорению, так как оба момента оказывали плохое влияние на SEO. А после проделанной работы, появились заметные движения даже в Google.

Далее, разработчик компании Зекслер рассказывает, как технически происходило обновление движка Drupal с 7 версии до 11.

Задача: перенести все данные из Drupal 7 в 11 с сохранением материалов, представления, путей, метаданных и оформления. Причем для анонимных пользователей при просмотре сайта не должно быть никаких визуальных отличий. В качестве данных у нас была база и доступ к сайту.

Инструменты для обновления версии сайта

В качестве инструментов использовали: PHP 8.4, MYSQL 8.0, предустановленная последняя версия Drupal 11 и модули миграции, Drush.

Перед началом переноса данных из одной версии сайта в другую нужно было учитывать, что в Drupal 11 используется совершенной другой подход как в модулях, так и в шаблонизаторе в теме сайта. Все файлы PHP следовало переделать самостоятельно в twig шаблона. Недостающие переменные нужно было добавлять через preprocess hook нужного шаблона. При необходимости добавлять suggetions для различных типов материалов, блоков.

Материалы при переносе через Migrate Api должны были иметь те же ID, что и на основном сайте. Значит, можем спокойно делать свои модули переноса.

В данном случае после основной миграции материалов, которая включает в себя создание типов материалов и их полей, нужно было реализовать перенос метатегов из старой базы в новую. Для этого был написан модуль metatag_migrate. Почему? А потому, что метатеги в новой версии нужно добавлять через поле, которого нет в седьмой версии друпала. Зная то, что все метатеги из основной базы хранятся в отдельной таблице metatag и ID материалов совпадают, спокойно можем с помощью подключения через $d7_connection = Database::getConnection('default', 'migrate'); подключать и переносить данные, загрузив их по ID.

// Load entity
   $entity = NULL;
   if ($entity_type === 'node') {
     $entity = Node::load($entity_id);
   } elseif ($entity_type === 'taxonomy_term') {
     $entity = Term::load($entity_id);
   }
   
   if (!$entity) {
     throw new \Exception("Entity not found: $entity_type $entity_id");
   }
   
   // Set metatag field value
   if ($entity->hasField('field_metatag')) {
     // In Drupal 11, metatags are stored in serialized format
     $entity->get('field_metatag')->setValue([
       'value' => serialize($metatag_data),
     ]);
     
     // Save entity
     $entity->save();
   }
 }

Но перед этим нужно прогнать всю миграцию, доступную нам. Учитывая, что Migrate Api работает ещё и с Drupal 6 версии, нам нужно было выполнить основные команды и отключить всё, что связано с Drupal 6. 

Основные команды:

drush migrate:upgrade --legacy-db-key=migrate --configure-only
drush migrate:import --all

В нашем случае нужно было отключить следующие миграции:

vendor/bin/drush migrate:disable \
 d6_filter_format \
 d6_custom_block \
 d6_node_type \
 d6_taxonomy_vocabulary \
 d6_user_role \
 d6_block \
 d6_file \
 d6_user_picture_file \
 d6_user \
 d6_upload_field \
 d6_language_content_settings \
 d6_language_negotiation_settings \
 d6_language_types \
 d6_language_content_taxonomy_vocabular \
 d6_language_content_menu_settings \
 d6_node_setting_promote \
 d6_node_setting_status \
 d6_node_setting_sticky \
 d6_vocabulary_field \
 d6_vocabulary_field_instance \
 d6_vocabulary_entity_display \
 d6_taxonomy_term \
 d6_vocabulary_entity_form_display \
 d6_user_contact_settings \
 d6_views_migration

Чтобы посмотреть всё, что нужно отключать, можно выполнить команды:

drush migrate:status | grep "d6_"
  • Все это время мы работаем в папке с установленным Drupal 11 и Drush модулями.

После отключения всего, что связано с Drupal 6, нужно пройтись и посмотреть все включенные модули вручную, перенесены ли они в Drupal 11. Основные модули вроде View View UI, Block и т.д. уже включены в ядро Drupal 11. За остальными идем на сайт Drupal и смотрим, есть ли для этих модулей обновление (поддерживают ли они их). 

В нашем случае нужно было переделать несколько модулей, посмотреть, работают ли модули с 10 версии в 11 (вручную скачав и добавив в папку web/modules/custom добавив в info.yml модуля core_version_requirement: ^10 || ^11).

После того, как все модули были проверены и включены (т.к. некоторые могут и вообще отсутствовать и придется сделать аналог, либо отказаться от этого модуля), можем выполнить миграцию. Во время переноса, к сожалению, могут возникать ошибки, связанные, например, с меню, что приведет к дублированию пунктов меню. Все ошибки нужно рассматривать отдельно и самостоятельно решать в зависимости от типа ошибки.

Перенос файлов

Отдельно перенесли файлы и раболи с миграцией d7_file. Для удачного выполнения миграции файлов нужно знать путь к файлам старой версии, и перенести его с помощью команды:

drush migrate:reset d7_file
drush migrate:configure d7_file --source-base-path=/var/www/d7/web/sites/default/files

и дальше:

drush migrate:import d7_file --update

Можно спокойно проверить парочку файлов с помощью команды:

test -e /web/sites/default/files/05_aganar_final_rgb_0010009_kopiya.jpg && echo file exists || echo file not found

Выполненной из папки там, где лежит composer.json.

Вот ещё несколько команд для работы с файлами:

ls -la /var/www/d7/sites/default/files/05_aganar_final_rgb_0010009_kopiya.jpg
ls -la /var/www/d7/web/sites/default/files/05_aganar_final_rgb_0010014_kopiya.jpgtest -e  /var/www/d7/web/sites/default/files/05_aganar_final_rgb_0010009_kopiya.jpg && echo file exists || echo file not found
/sites/default/files/05_aganar_final_rgb_0010014_kopiya.jpg
vendor/bin/drush config:get migrate_plus.migration.d7_file source.constants.source_base_path

Где ls проверяет права доступа к файлам и места, где находятся файлы – тут искали, где конкретно лежат файлы относительно наших двух сайтов, лежащих на одном хостинге.

Внимание! Пришлось залезть в код ядра и выполнить следующее: 

в файле web/core/modules/file/src/Plugin/migrate/source/d7/File.php тут же дописываем:

$this->publicPath = '/var/www/d7/web/sites/default/files';!!!

иначе перенос файлов совсем не будет работать! 

При установке модулей устанавливали следующие модули:

composer require 'drupal/better_exposed_filters:^7.0'
composer require 'drupal/views_slideshow:^5.0'
composer require 'drupal/honeypot:^2.2'
composer require 'drupal/image_url_formatter:^2.0'
composer require 'drupal/menu_block:^1.14'
composer require 'drupal/plupload:^2.2'
composer require 'drupal/xmlsitemap:^2.0'
composer require 'drupal/geophp:^1.2'
composer require drupal/pathauto
composer require drupal/yandex_maps

Модуль Яндекс Карт пришлось доделывать. Это отдельный модуль, который перехватывает js подключенный файл (который в модуле называется main).

Модуля хлебных крошек Path Breadcrumbs нет в Drupal 11 на момент публикации статьи, так что пришлось делать модуль site_breadcrumbs, который в коде выполняет те же функции. Для этого, например, он перехватывает с помощью кода:

$term = $route_match->getParameter('taxonomy_term');
        return $term instanceof TermInterface && $term->bundle() === 'catalog';

и заполняет массив $breadcrumb = new Breadcrumb(); нужными позициями. Для работы этого модуля используется site_breadcrumbs.services.yml. Например:

services:
 site_breadcrumbs.breadcrumb_builder:
   class: Drupal\site_breadcrumbs\CustomBreadcrumbBuilder
   arguments: ['@entity_type.manager', '@current_route_match']
   tags:
     - { name: breadcrumb_builder, priority: 2000 }

Он добавляет вложенность в хлебные крошки независимо от того, как отображается URL сейчас. Точно так же, как это делает модуль Easy Breadcrumb, который строго зависит от URL!

Перенос URL 

Осуществляется с помощью модуля Pathauto. Перенос нужно было настроить самостоятельно и вручную. Важно было перенести с основного сайта все пути, которые были. Все пути таксономий нужно было перенести с помощью пересоздания путей, у нодов обычных типов ссылки перенеслись по большей части правильно, за исключением продуктов. Т.е. обязательно проверяйте что перенесла система Migrate API, а что не перенесла.

Перенос темы

Всю тему переносили вручную. Все CSS и зависимости стилей переносятся легко – достаточно скопировать в новую тему все стили и прописать в файле site.libraries.yml нужные пути. А вот js нужно проверять, смотреть зависимости и прочее, так как в Drupal 11 используется Jquery 4, а значит многие функции могут быть выкинуты и не работать на новом сайте. Пример:

$('.sm5').click(function(){
       $.scrollTo('.user-8',500);
   });

нужно переделать в:

$('.sm5').on('click', function(){
       $.scrollTo('.user-8',500);
   });

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

Drupal.behaviors.myModuleMagic = {
 attach: function (context, settings) {
      $('.webform-component-phone .form-control', context).mask('+7 (999) 999-9999');
}
}

В данном примере в контексте применяется маска телефона и при Ajax перезагрузки любого элемента (включая вызова форм при клике) маска не потеряется! 

Остальные шаблоны нужно вручную переводить в twig, добавлять suggetions и переменные в шаблон. При переносе нужно смотреть верстку старого сайта и практически полностью повторять её. Тут пришлось сделать следующее:

Добавить suggestions для контейнеров – в 11 версии в некоторых местах появляется контейнер обертка элементов:

  /**
* Implements hook_theme_suggestions_container_alter().
*/
function site_theme_suggestions_container_alter(array &$suggestions, array $variables) {
 // Проверяем, что это контейнер внутри блока
 if (
     isset($variables['element']['#type']) && $variables['element']['#type'] == 'view' &&
     isset($variables['element']['#view_id'])
 ) {
   $suggestions[] = 'container__view__' . $variables['element']['#view_id'];
 }
   // Проверяем, что это контейнер с действиями формы редактирования
 if (isset($variables['element']['#type']) && $variables['element']['#type'] === 'actions' && $variables['element']['#theme'] === 'webform_actions') {
   //dump($variables['element']);
     $suggestions[] = 'container__edit_actions'; // Добавляем suggestion
 }
}

Тут добавляем suggestions наименования для типов страниц и таксономий.

/**
* Implements hook_theme_suggestions_page_alter().
*/
function site_theme_suggestions_page_alter(array &$suggestions, array $variables) {
 
   // Добавляем suggestions для нод по bundle (типу контента)
   if ($node = \Drupal::routeMatch()->getParameter('node')) {
     if (is_object($node)) {
       $bundle = $node->bundle();
       // $suggestions[] = 'page__node__' . $node->bundle();
       array_splice($suggestions, count($suggestions) - 1, 0, ['page__node__' . $bundle]);
     }
     elseif (is_numeric($node)) {
       $node = \Drupal\node\Entity\Node::load($node);
       $bundle = $node->bundle();
       // $suggestions[] = 'page__node__' . $node->bundle();
       array_splice($suggestions, count($suggestions) - 1, 0, ['page__node__' . $bundle]);
     }
   }
   if($term = \Drupal::routeMatch()->getParameter('taxonomy_term')){
     if ($term instanceof \Drupal\taxonomy\TermInterface) {
       $vocabulary = $term->bundle(); // Машинное имя таксономии (tags, categories и т. д.)
 
       // Базовый suggestion: page--taxonomy--term--[vocabulary].html.twig
       $suggestions[] = 'page__taxonomy__term__' . $vocabulary;
       /** @var \Drupal\taxonomy\Entity\Term $term */
       if ($term->get('parent')->target_id == 0) {
         $suggestions[] = 'page__taxonomy__term__' . $vocabulary . '__' . $term->id();
       }
     }
   }
}

Во многих темах седьмой версии доступна функция arg(), которой нет в 11 версии. Это нужно учитывать и смотреть, для чего она используется. В нашем случае позиция user8 для таксономий и нодов в различных местах была. Значит добавляем page--node--product.html.twig и page--taxonomy--term--catalog.html.twig и смотрим размещение позиции user8 на двух страницах.

Примечательно, что в 7 версии и 11 версии шаблон для полей использует похожие классы, и нам достаточно было в шаблоне field.html.twig добавить в список классов 'field-name-'  ~ field_name|clean_class - чтобы верстка из старой версии применилась в 11 версии сайта.

Перенос webform

Веб-формы в 11 версии – это отдельный модуль, который в нашем случае не сработал и формы переносились тоже вручную. При этом тип материала веб-формы из 7 версии создался в 11 версии. На сайте Аганара 6 форм – каждую переносили и настраивали отдельно. В шаблонах используется уже другой подход вызова формы по клику, блоки с формами тоже расставлялись вручную.

Пример строчки вызова веб-формы в 11 версии и в 7 версии.

Версия 7:

<a class="colorbox-node init-colorbox-node-processed-processed" href="/node/42?width=350&amp;height=430">Расчет проекта</a>
версии 11: 
<a href="/webform/calculation_request" class="use-ajax " data-dialog-type="modal" rel="nofollow">Расчет проекта</a>

Если есть настройки веб-форм в виде Yml, то можно воспользоваться командой:

drush config:import --source=/var/www/d11/config/ymls/webform --partial

Потерянные представления

Вообще все представление должны переноситься в последнюю очередь. Команда Drush migrate:import d7_views_migration должна сработать, как нужно. Но! Нужно проверить все представления вручную – связи, контекстные фильтры и поля могут быть потеряны! Это связано с различием баз данных.

В случае с сайтом Аганар пришлось вручную переводить экспорт конфигурацию из седьмой версии в Yml-файл конфигурации и использовать Drush config:import --source=/var/www/d11/config/install/test --partial - частичный импорт из папки.

Внимание! Сайт может вообще не запуститься из-за неправильного переноса MIgrate API и потерей связи в представлении. Чтобы хотя бы зайти в админку и проверить все представления (вручную) нужно зайти в файл ядра и закомментировать строчку, которая вызывает ошибку (там будет класс вызова ошибки).

Статьи по теме

Подробно рассказываем как неправильно поставленные цели и статистика могут вводить в заблуждение крупные компании, а также разбираем так ли хороши дешевые лиды, как кажется.
Подробно на конкретном примере разбираем, как компании проанализировать работу отдела продаж при помощи BI-аналитики.
Рассказываем о проекте разработки интернет-магазина, смене концепции в процессе, а также о том, как мы работали над удобством сайта для конечного пользователя.
Обсудить проект