Добавление оглавления к статьям в MODX

В этом теме мы разберём как к статьям и длинным постам на сайте добавить оглавление. Это руководство будет касаться только сайтов, работающих под управлением CMS MODX.
Зачем вообще создавать содержание? Ответ на этот вопрос очень прост. Это нужно для того, чтобы можно было более просто ориентироваться в информации, представленной на той или этой странице сайта, а также очень быстро переходить к интересующим разделам.
Что мы здесь рассмотрим?
- пример сниппета, который будет генерировать оглавление из заголовков;
- плагин, через который будем изменять контент статьи добавляя при его сохранении
id
к заголовкамh2..h4
(если конечно у этих заголовках нетid
); - php-скрипт для массового обновления контента ресурсов, находящихся в указанных родителях (в большинстве случаев этот скрипт нужно запустить всего один раз для каждого родителя, он добавит
id
к заголовкам, находящихся в контенте, у которых их нет).
Сниппет для формирования содержания
Пример простого сниппета, который можно использовать для создания содержания из заголовков <h2>
. Назовём его, например, createTableOfContents
.

Код сниппета createTableOfContents
:
<?php $id = $modx->resource->id; $output = $modx->cacheManager->get($id, array(xPDO::OPT_CACHE_KEY => 'table_of_contents')); if (empty($output)) { $output = ''; $url = $modx->makeUrl($id, '', '', 'abs'); $content = $modx->resource->content; preg_match_all('#<h2.*?>(.*?)<\/h2>#', $content, $matches); foreach ($matches[0] as $index => $header) { preg_match('#.*?id="(.*?)".*?#', $header, $href); $output .= $modx->getChunk('tpl.TableOfContents.row', array( 'index' => $index, 'href' => $url . '#' . $href[1], 'target' => '#' . $href[1], 'text' => $matches[1][$index] )); } $output = $modx->getChunk('tpl.TableOfContents.wrapper', array('output' => $output)); $modx->cacheManager->set($id, $output, 0, array(xPDO::OPT_CACHE_KEY => 'table_of_contents')); } return $output;
Для формирования разметки в этом сниппете используются 2 чанка:
tpl.TableOfContents.wrapper
;tpl.TableOfContents.row
.
Код чанка tpl.TableOfContents.wrapper
:
<div class="table-of-contents"> <div class="table-of-contents__title">Содержание:</div> <div class="table-of-contents__items">[[+output]]</div> </div>
Код чанка tpl.TableOfContents.row
:
<a href="[[+href]]" data-target="[[+target]]" data-index="[[+index]]">[[+text]]</a>
Для создания другой разметки, нужно изменить код этих чанков.
Вызов этого сниппета осуществляется следующим образом:
[[createTableOfContents]]
Данный сниппет при первом выполнении сохраняет созданное содержание страницы в кэш (в папку table_of_contents
), которое не очищается даже при очистке кэша сайта (оно удаляется при сохранении ресурса в админке).
Сниппет createTableOfContents
формирует следующую HTML структуру на странице:
<div class="table-of-contents"> <div class="table-of-contents__title">Содержание:</div> <div class="table-of-contents__items"> <a href="/post-1#zagolovok-1" data-target="#zagolovok-1" data-index="0">Заголовок 1</a> <a href="/post-1#zagolovok-2" data-target="#zagolovok-2" data-index="1">Заголовок 2</a> <a href="/post-1#zagolovok-3" data-target="#zagolovok-3" data-index="2">Заголовок 3</a> ... </div> </div>
Плагин для добавления id к заголовкам
Добавление id
к заголовкам h2 - h4
ресурса при его сохранении выполним посредством плагина. Назовём его, например, addIdToHeaders
.
На вкладке «Системные события» установим галочку напротив OnBeforeDocFormSave
. Это событие, которые данный плагин будет должен отслеживать.
Событие OnBeforeDocFormSave
запускается при нажатии кнопки «Сохранить» в форме редактирования ресурса перед его сохранением.

Код плагина addIdToHeaders
:
<?php switch ($modx->event->name) { case 'OnBeforeDocFormSave': if ($resource->get('class_key') !== 'Ticket') { return; } $content = $resource->get('content'); $newContent = preg_replace_callback('#<h[2-4].*?>(.*?)<\/h[2-4]>#', function($matches){ preg_match('#.*?id="(.*?)".*?#', $matches[0], $header); if (!empty($header)) { return $matches[0]; } // убираем HTML-теги $header = strip_tags($matches[1]); // убираем перевод каретки $header = str_replace(array('\n', '\r'), ' ', $header); // удаляем повторяющие пробелы $header = preg_replace('#\s+#', ' ', $header); // убираем пробелы в начале и конце строки, а также справа символы :!.?; $header = trim(rtrim($header, ':!.?;')); // переводим строку в нижний регистр $anchor = function_exists('mb_strtolower') ? mb_strtolower($header) : strtolower($header); $anchor = strtr($anchor, array('а'=>'a','б'=>'b','в'=>'v','г'=>'g','д'=>'d','е'=>'e','ё'=>'e','ж'=>'j','з'=>'z','и'=>'i','й'=>'y','к'=>'k','л'=>'l','м'=>'m','н'=>'n','о'=>'o','п'=>'p','р'=>'r','с'=>'s','т'=>'t','у'=>'u','ф'=>'f','х'=>'h','ц'=>'c','ч'=>'ch','ш'=>'sh','щ'=>'shch','ы'=>'y','э'=>'e','ю'=>'yu','я'=>'ya','ъ'=>'','ь'=>'')); // очищаем строку от недопустимых символов $anchor = preg_replace('#[^0-9a-z-_ ]#i', '', $anchor); // заменяем пробелы знаком минус $anchor = str_replace(' ', '-', $anchor); return '<h'. $matches[0][2] .' id="' . $anchor . '">' . $header . '</h' . $matches[0][2] . '>'; }, $content); $resource->set('content', $newContent); // дополнительно - очистка кеша, содержащего содержание ресурса (например, если кеш храниться в table_of_contents) $output = $modx->cacheManager->get($id,array(xPDO::OPT_CACHE_KEY=>'table_of_contents')); if (!empty($output)) { $modx->cacheManager->delete($id,array(xPDO::OPT_CACHE_KEY=>'table_of_contents')); } break; }
Как этот плагин работает?
При сохранении ресурса (в этом примере он выполняет это только для тикетов, т.е. ресурсов у которых class_key
имеет значение Ticket
) данный плагин получает его контент ($resource->get('content')
) и ищет в нём заголовки h2 - h4
, которые не имеют id
. Далее формирует для каждого из них определённое значение, которое затем устанавливает в качестве id
. После этого текущий контент заменяет новым контентом, в котором установлены id
для заголовков.
Скрипт на PHP для добавления id к заголовкам ресурсов
Для добавления id
к заголовкам h2 - h4
большого количества ресурсов удобно использовать следующий скрипт на PHP:
<?php require_once 'config.core.php'; require_once MODX_CORE_PATH . 'model/modx/modx.class.php'; $modx = new modX(); $modx->initialize('web'); $modx->getService('error', 'error.modError', '', ''); $pdoTools = $modx->getService('pdoTools'); // массив родителей (их id), ресурсы которых нужно обработать (здесь: 5) $parent = array(5); $query = $modx->newQuery('modResource', array( 'parent:IN' => $parent )); $resources = $modx->getCollection('modResource', $query); foreach ($resources as $resource) { if ($resource->get('class_key') !== 'Ticket') { continue; } $id = $resource->get('id'); $pagetitle = $resource->get('pagetitle'); $content = $resource->get('content'); // какие были заголовки preg_match_all('#<h[2-4].*?>(.*?)<\/h[2-4]>#', $content, $matches); echo '<div style="font-weight: bold;">' . $id . ' - ' . $pagetitle . '</div>'; echo '<pre>'; echo str_replace('>', '>', str_replace('<', '<', print_r($matches[0], true))); echo '</pre>'; if (empty($matches[0])) { continue; } $newContent = preg_replace_callback('#<h[2-4].*?>(.*?)<\/h[2-4]>#', function ($matches) { preg_match('#.*?id="(.*?)".*?#', $matches[0], $header); if (!empty($header)) { return $matches[0]; } // убираем HTML-теги $header = strip_tags($matches[1]); // убираем перевод каретки $header = str_replace(array('\n', '\r'), ' ', $header); // удаляем повторяющие пробелы $header = preg_replace('#\s+#', ' ', $header); // убираем пробелы в начале и конце строки, а также справа символы :!.?; $header = trim(rtrim($header, ':!.?;')); // переводим строку в нижний регистр (иногда надо задать локаль) $anchor = function_exists('mb_strtolower') ? mb_strtolower($header) : strtolower($header); $anchor = strtr($anchor, array('а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'e', 'ж' => 'j', 'з' => 'z', 'и' => 'i', 'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c', 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'shch', 'ы' => 'y', 'э' => 'e', 'ю' => 'yu', 'я' => 'ya', 'ъ' => '', 'ь' => '')); // очищаем строку от недопустимых символов $anchor = preg_replace('#[^0-9a-z-_ ]#i', '', $anchor); // заменяем пробелы знаком минус $anchor = str_replace(' ', '-', $anchor); return '<h'. $matches[0][2] .' id="' . $anchor . '">' . $header . '</h' . $matches[0][2] . '>'; }, $content); // какие стали заголовки preg_match_all('#<h[2-4].*?>(.*?)<\/h[2-4]>#', $newContent, $matches); echo '<div style="font-weight: bold;">'; echo '<pre>'; echo str_replace('>', '>', str_replace('<', '<', print_r($matches[0], true))); echo '</pre>'; echo '</div>'; // сохраняем $resource->set('content', $newContent); $resource->save(); } exit();
В этом примере, он выполняет работу только для ресурсов, расположенных в id = 5
:
$parent = array(5);
Измените это значение на нужное или укажите несколько значений через запятую тех родителей, ресурсы которых нужно обработать (добавить к заголовкам id
).
Запустить этот php-скрипт можно просто указав в адресной строке браузера URL к нему.
Результат своей работы он выведет следующи образом на страницу:

Комментарии: 7
Александр как сделать так чтоб список вышел после тега например <hr> внутри [[*content]] в Tickets
Это можно сделать прямо в коде плагина
addIdToHeaders
. После того какid
добавлены к заголовкам, вы можете уже проанализировать контент$newContent
и сформировать, используя его HTML-код заголовков. А потом их просто добавить перед$newContent
:Открываю в Console, ошибка Fatal error: require_once(): Failed opening required 'config.core.php' (include_path='.:/usr/share/php') in /home/b/barano17/lorzdrav.ru/public_html/core/components/console/processors/exec.class.php(24): eval()'d code on line 2
Осуществляется это перед сохранением ресурса с помощью плагина. Если id не добавляется, значит заголовок не соответствует указанному регулярному выражению.
Сниппет составляет оглавление только из h2. Если нужно по-другому, то нужно в сниппете это изменить.
Заголовки, доставшиеся от старого верстальщика работали:
А мои, нет)
Спасибо за помощь!