К содержанию

Модифицируем HTML на лету

Привет друзья. Хочу поделиться методикой, которую иногда использую в тех случаях, когда мне нужно подменить или каким то образом модифицировать на лету готовый HTML. Причем сделать это на уровне сервера, а не через JS.

Начнем с того, что определимся, каким образом вообще используя MODX и его технику сбора конечного кода страницы из множества разных частей (шаблонов, чанков, сниппетов), можно получить доступ к уже готовой конечной версии DOM дерева.

Для этого нам на помощь приходит событие для плагина OnWebPagePrerender — оно срабатывает именно в тот момент, когда веб страница собрана и готова к выводу. Единственное, что здесь не получится учесть — это код, выполняемый при помощи JS в момент рендера страницы браузером.

switch ($modx->event->name) {
    case 'OnWebPagePrerender':
        //Получаем доступ к готовому DOM  дереву
        $output = &$modx->resource->_output;
        //Модифицируем  как хотим
        $output = preg_replace('|\s+|', ' ', $output);
        break;
}
Мы можем модифицировать HTML код нативными средствами PHP — для этого у нас есть preg_replace, str_replace и другие методы. Также мы можем использовать PHP Класс DOMDocument дающий более обширные возможности. Можем подключить и использовать и дополнительные библиотеки.

Исторически сложилось, что я для подобных задач использую библиотеку PHP Simple HTML DOM Parser, которая дает возможность перемещаться по DOM дереву и осуществлять перебор и подбор нужных элементов CSS или Jquery подобным способом.

// Find all element which id=foo
$ret = $html->find('#foo');

// Find all element which class=foo
$ret = $html->find('.foo');

// Find all element has attribute id
$ret = $html->find('*[id]');

// Find all anchors and images
$ret = $html->find('a, img');

// Find all anchors and images with the "title" attribute
$ret = $html->find('a[title], img[title]');
Немного модифицируем плагин и получаем вот такую историю

switch ($modx->event->name) {
    // Стартуем плагин
    case 'OnWebPagePrerender':
        // Получаю доступ к  DOM дереву
        $output = &$modx->resource->_output;
        // Здесь доступны все поля ресурса. 
        // Определяю с каким шаблоном имею дело. 
        $template = $modx->resource->template;

        
        //Подключаю библиотеку
        include_once MODX_CORE_PATH . 'components/simple_html_dom/vendor/simple_html_dom.php';

        if (class_exists('simple_html_dom')) {
            $html = new simple_html_dom;
            $html->load($output);
            switch ($template) {                
                case 17:
            // Внутри конкретного шаблона Ищу все изображения внутри div.content
            foreach ($html->find('.content img') as $img) {
                /**
                 * @var simple_html_dom_node $img
                 */
                // Хочу активировать Lazy load для этого 
                // Сохраняю ссылку на изображение
                $src = $img->src;
                if ($src) {      
                    // Удаляю атрибут src              
                    $img->src = null;
                    // Добавляю атрибут data-src
                    $img->{'data-src'} = $src;                    
                   // Добавляю класс lazy
                    $img->class = 'lazy';
                //  Далее картинки с нужным классом и дата атрибутом подхватит js плагин lazy load и сайт прилично ускорит свою работу
                }
            }

             break;
            }
            $output = $html->save();
            $html->clear();
            unset($html);
            $output = preg_replace('|\s+|', ' ', $output);
        }
        break;
}
Для удобства я упаковал всю описанную историю в компонент, который скоро будет доступен в наших репозитариях.
В состав компонент входит свежая версия библиотеки и плагин-болванка.

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

Очищаем контент от лишнего мусора в атрибутах
foreach ($html->find('.product_page table') as $table) {
                $table->border = null;
                $table->cellpadding = null;
                $table->cellspacing = null;
                $table->style = null;
                $table->width = null;
                $table->class = 'table table-responsive';
            }

            foreach ($html->find('.product_page table tr') as $tr) {
                $tr->style = null;
            }
Добавляем обертку noindex для всех фреймов
foreach ($html->find('iframe') as $iframe) {
                $iframe->outertext = '<!--noindex-->' . $iframe->outertext . '<!--/noindex-->';
            }
Добавляем обертку noindex для внешних ссылок
foreach ($html->find('link') as $link) {
                if (is_external_url($link->href) && stripos($link->href, $site_url) !== 0) {
                    $link->outertext = '<!--noindex-->' . $link->outertext . '<!--/noindex-->';
                }
            }
Добавляем rel=nofollow и target=_blank для внешних ссылок
foreach ($html->find('link') as $link) {
                if (is_external_url($link->href) && stripos($link->href, $site_url) !== 0) {
                    $link->rel = 'nofollow';
                    $link->target = '_blank';
                }
            }
В общем как вы видите — довольно простыми манипуляциями можно как угодно модифицировать html разметку страницы без вмешательства в исходных код шаблонов и текста в текстовом редакторе.

Некоторые примеры любезно предоставил @mngatoff в свое время, за что ему большое спасибо.

Более подробно о методах библиотеки можно почитать в официальной документации

Также использую эту библиотеку для парсера страниц. Если интересно — дайте знать, расскажу и покажу как нибудь.
Николай Савин
Николай Савин
13 октября 2019, 20:28
modx.pro
6 342
Реклама

Комментарии: 26

Aborrol
Aborrol Старожил
13 октября 2019, 20:49
Насколько этот вариант быстрее чем DOMDocument
case 'OnWebPagePrerender':
    $content = &$modx->resource->_output;
    $html = $modx->resource->_output;
    $dom = new DOMDocument();
    $dom->encoding = "utf-8";
    @$dom->loadHTML($content);
А то я делал подобное через него, но мне не понравилась производительность
    Николай Савин
    Николай Савин Администратор
    13 октября 2019, 20:55
    Сравнительных тестов не проводил. Вряд ли быстрее чем нативный класс. Основное удобство библиотеки — более простой доступ к выборке нужных узлов дерева
      Aborrol
      Aborrol Старожил
      13 октября 2019, 21:05
      Да выборка гораздо проще, чем через xpath, вообщем достаточно интересно.
      Спасибо за статью
    srs
    srs Проверенный
    13 октября 2019, 22:17
    я бы еще добавил что-то вроде:
    if($modx->resource->content_type != 3) { // или 3 или 2, тут надо проверять
        $output = preg_replace('|\s+|', ' ', $output);
    }
    Чтоб не минифицировать докуметы формата txt, по типу robots.txt.
      Николай Савин
      Николай Савин Администратор
      14 октября 2019, 07:13
      Да все что угодно можно написать, любые условия конечно. Я лишь показал что такая библиотека есть и мы можем ее использовать
      mngatoff
      mngatoff Проверенный
      13 октября 2019, 23:07
      тэкс, плагиатец и копипаста :)

      раз уж на то пошло, функции is_external_url в оригинале библиотеки нет, я ее сам туда дописал, вот она:

      function is_external_url($url)
      {
          return (bool)preg_match('/^(http:|https:|ftp:|\/\/)/i', $url);
      }
        Николай Савин
        Николай Савин Администратор
        14 октября 2019, 07:12
        Да эти строчки я у тебя позаимствовал года два назад. Ты только сильно громко не кричи, а то еще и ребята из sourceforge прибегут — начнут возмущаться что я их документацию скопипастил.
          mngatoff
          mngatoff Проверенный
          14 октября 2019, 11:35
          при чем тут «не кричи». ты функцию используешь, которой нет в библиотеке, не заведется у людей
        Николай
        Николай Проверенный
        14 октября 2019, 08:28
        Я тоже часто пользуюсь этим событием, чтобы обработать код перед отправкой, но мне больше нравится phpQuery, это почти jQuery, только на PHP) К примеру, очистка style в тексте статьи:

        require_once(MODX_CORE_PATH . 'components/phpquery/phpQuery/phpQuery.php');
        $html = phpQuery::newDocumentHTML($output);
        
        $paragraphs = $html->find('.article__txt p');
        
        foreach($paragraphs as $el) {
            $p = pq($el);
            $p->attr('style','');
        }
        
        $output = $html->html();
          Николай
          Николай Проверенный
          14 октября 2019, 08:46
          В прошлом примере пропустил строку в самом начале:
          $output = &$modx->resource->_output;

          Или вот так можно fancybox подцепить:
          $output = &$modx->resource->_output;
          
          require_once(MODX_CORE_PATH . 'components/phpquery/phpQuery/phpQuery.php');
          $html = phpQuery::newDocumentHTML($output);
          
          $images = $html->find('.article__txt img');
          
          foreach($images as $el) {
              $img = pq($el);
          
              $src = $img->attr('src');
              $width = $img->attr('width');
              $height = $img->attr('height');
          
              $options = "w={$width}&h={$height}&zc=1";
          
              $thumb = $modx->runSnippet('phpthumbon', [
                  'input' => $src,
                  'options' => $options
              ]);
          
              $img->attr('src', $thumb);
              $img->attr('data-original', $src);
              $img->wrap("<a href='{$src}' data-fancybox=''></a>");
              $img->removeAttr('width');
              $img->removeAttr('height');
          }
          
          $output = $html->html();

          То есть менеджер добавляет фото к статье как обычно, и может управлять его размерами. А плагин обрезает фото до заданных размеров и оборачивает его ссылкой, кликнув на которую откроется увеличенное изображение с помощью fancybox.
          Павел Гвоздь
          Павел Гвоздь Эксперт
          14 октября 2019, 08:49
          использую библиотеку PHP Simple HTML DOM Parser
          Это-ж старьё, которое хз когда в последний раз обновлялось. Не слышал про DomCrawler?
            mngatoff
            mngatoff Проверенный
            14 октября 2019, 11:34
            в этом году обновилась
              Павел Гвоздь
              Павел Гвоздь Эксперт
              14 октября 2019, 11:36
              На три года вперёд?
                mngatoff
                mngatoff Проверенный
                14 октября 2019, 11:40
                собственно, какая разница?) работает хорошо и легкая
              Николай Савин
              Николай Савин Администратор
              14 октября 2019, 12:03
              Да я не то, чтобы прямо часто пользуюсь подобным. Не стояла задача найти лучшую либу, иба та что есть вполне справляется с задачами. Про DomCrawler не слышал. Как нибудь на досуге изучу, спасибо.
              Юрий
              Юрий Старожил
              14 октября 2019, 12:15
              Спасибо за статью! И, да присоединяюсь к просьбе по поводу парсера страниц. Было бы интересно.
                Pavel Zarubin
                Pavel Zarubin Эксперт
                14 октября 2019, 15:49
                modx.pro/development/16940
                Но где то тут же я упоминал что гораздо более грамотно было бы делать это на событие сохранения ресурса, не гоже жертвовать скоростью отрисовки страницы ради служебных нужд. Возможно при одном посетителе это не заметно, но представь эти же самые операции когда одновременных посетителей 10 и более
                  Aborrol
                  Aborrol Старожил
                  14 октября 2019, 21:17
                  Но это же совершенно другой функционал, ведь у ТС помимо содержимого ресурса, плагин действует ещё и на все остальное.
                  Sergey (Sentinel)
                  Sergey (Sentinel) Проверенный
                  18 ноября 2019, 11:57
                  Николай приветствую! А как то можно плагин использовать для amp страниц? Именно для контента, заменять img на amp-img и iframe на amp-iframe?
                    Николай Савин
                    Николай Савин Администратор
                    12 декабря 2019, 08:55
                    Конечно, чего бы нет.
                    amp — это у нас XML страница, генерируемая в MODX верно? А значит для нее так же можно включить данный плагин. Ну а плагину в общем то все равно что обрабатывать, HTML или XML.
                    Кирилл
                    Кирилл Проверенный
                    11 декабря 2019, 21:23
                    Весь этот код заменяет class был он там или не был
                    $src = $img->src;
                                    if ($src) {
                                        $img->src = null;
                                        $img->{'data-src'} = $src;
                                        $img->class = 'lazy';
                    А как переписать чтобы добавлял class lazy к существующим class'ам при наличии? и при отсутствии просто добавлял class=«lazy»?
                      Николай Савин
                      Николай Савин Администратор
                      12 декабря 2019, 08:58
                      Оператор конкатенации просто добавь, должно сработать
                      $img->class .= ' lazy';
                      Если нет — то сохрани в переменную сначала содержимое атрибута class, затем приконкатенируй свой класс и вставь в атрибут.
                        Кирилл
                        Кирилл Проверенный
                        12 декабря 2019, 15:31
                        Да я по другому решил сделать, чтобы ко всем img добавить class lazy
                        по сути не определяю в контенте это или нет, чтобы добавило класс.
                        Теперь работает так как надо. Спасибо
                      Кирилл
                      Кирилл Проверенный
                      09 марта 2020, 12:10
                      Эта конструкция вырезает удаляет переносы строк
                      $output = $html->save();
                                  $html->clear();
                                  unset($html);
                                  $output = preg_replace('|\s+|', ' ', $output);
                      Но как оказалось она работает в том числе и для документов со всеми типами содержимого, в том числе text

                      Можно ли как-то ограничить действие только для типа содержимого HTML или исключить для text?
                        Николай Савин
                        Николай Савин Администратор
                        09 марта 2020, 12:34
                        У тебя всегда доступен объект $modx->resource
                        Пиши условие, проверяй у него content_type или как там, не помню сейчас на лету.
                          Кирилл
                          Кирилл Проверенный
                          09 марта 2020, 20:29
                          Да, достаточно сделать такую проверку, чтобы срабатывало только для типа содержимого text/html
                          $type = $modx->resource->get('contentType');
                                      if ($type == 'text/html') {
                                          $output = $html->save();
                                          $html->clear();
                                          unset($html);
                                          $output = preg_replace('|\s+|', ' ', $output);
                                      }
                        Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
                        26