Модифицируем 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
15
1 743
+18
Поблагодарить автора Отправить деньги

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

Aborrol
13 октября 2019, 20:49
+1
Насколько этот вариант быстрее чем 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
    0
    Сравнительных тестов не проводил. Вряд ли быстрее чем нативный класс. Основное удобство библиотеки — более простой доступ к выборке нужных узлов дерева
      Aborrol
      13 октября 2019, 21:05
      +1
      Да выборка гораздо проще, чем через xpath, вообщем достаточно интересно.
      Спасибо за статью
    srs
    srs
    13 октября 2019, 22:17
    +2
    я бы еще добавил что-то вроде:
    if($modx->resource->content_type != 3) { // или 3 или 2, тут надо проверять
        $output = preg_replace('|\s+|', ' ', $output);
    }
    Чтоб не минифицировать докуметы формата txt, по типу robots.txt.
      Николай Савин
      14 октября 2019, 07:13
      0
      Да все что угодно можно написать, любые условия конечно. Я лишь показал что такая библиотека есть и мы можем ее использовать
      mngatoff
      13 октября 2019, 23:07
      +4
      тэкс, плагиатец и копипаста :)

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

      function is_external_url($url)
      {
          return (bool)preg_match('/^(http:|https:|ftp:|\/\/)/i', $url);
      }
        Николай Савин
        14 октября 2019, 07:12
        +1
        Да эти строчки я у тебя позаимствовал года два назад. Ты только сильно громко не кричи, а то еще и ребята из sourceforge прибегут — начнут возмущаться что я их документацию скопипастил.
          mngatoff
          14 октября 2019, 11:35
          0
          при чем тут «не кричи». ты функцию используешь, которой нет в библиотеке, не заведется у людей
        Николай
        14 октября 2019, 08:28
        +2
        Я тоже часто пользуюсь этим событием, чтобы обработать код перед отправкой, но мне больше нравится 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
          +2
          В прошлом примере пропустил строку в самом начале:
          $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
          +4
          использую библиотеку PHP Simple HTML DOM Parser
          Это-ж старьё, которое хз когда в последний раз обновлялось. Не слышал про DomCrawler?
            mngatoff
            14 октября 2019, 11:34
            +1
            в этом году обновилась
              Павел Гвоздь
              14 октября 2019, 11:36
              0
              На три года вперёд?
                mngatoff
                14 октября 2019, 11:40
                0
                собственно, какая разница?) работает хорошо и легкая
              Николай Савин
              14 октября 2019, 12:03
              0
              Да я не то, чтобы прямо часто пользуюсь подобным. Не стояла задача найти лучшую либу, иба та что есть вполне справляется с задачами. Про DomCrawler не слышал. Как нибудь на досуге изучу, спасибо.
              Юрий
              14 октября 2019, 12:15
              0
              Спасибо за статью! И, да присоединяюсь к просьбе по поводу парсера страниц. Было бы интересно.
                Pavel Zarubin
                14 октября 2019, 15:49
                0
                modx.pro/development/16940
                Но где то тут же я упоминал что гораздо более грамотно было бы делать это на событие сохранения ресурса, не гоже жертвовать скоростью отрисовки страницы ради служебных нужд. Возможно при одном посетителе это не заметно, но представь эти же самые операции когда одновременных посетителей 10 и более
                  Aborrol
                  14 октября 2019, 21:17
                  +1
                  Но это же совершенно другой функционал, ведь у ТС помимо содержимого ресурса, плагин действует ещё и на все остальное.
                  Sergey
                  18 ноября 2019, 11:57
                  0
                  Николай приветствую! А как то можно плагин использовать для amp страниц? Именно для контента, заменять img на amp-img и iframe на amp-iframe?
                    Николай Савин
                    12 декабря 2019, 08:55
                    0
                    Конечно, чего бы нет.
                    amp — это у нас XML страница, генерируемая в MODX верно? А значит для нее так же можно включить данный плагин. Ну а плагину в общем то все равно что обрабатывать, HTML или XML.
                    Кирилл
                    11 декабря 2019, 21:23
                    0
                    Весь этот код заменяет 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
                      +1
                      Оператор конкатенации просто добавь, должно сработать
                      $img->class .= ' lazy';
                      Если нет — то сохрани в переменную сначала содержимое атрибута class, затем приконкатенируй свой класс и вставь в атрибут.
                        Кирилл
                        12 декабря 2019, 15:31
                        0
                        Да я по другому решил сделать, чтобы ко всем img добавить class lazy
                        по сути не определяю в контенте это или нет, чтобы добавило класс.
                        Теперь работает так как надо. Спасибо
                      Кирилл
                      09 марта 2020, 12:10
                      0
                      Эта конструкция вырезает удаляет переносы строк
                      $output = $html->save();
                                  $html->clear();
                                  unset($html);
                                  $output = preg_replace('|\s+|', ' ', $output);
                      Но как оказалось она работает в том числе и для документов со всеми типами содержимого, в том числе text

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