В статье «DokuWiki как платформа для технической документации: наш опыт» мы рассказали, почему ведем документацию на DokuWiki, и пообещали отдельно разобрать собственные плагины. А как вики выглядит для читателя, мы показали в статье «Как устроена Wiki VAS Experts». Теперь выполняем обещание. Всего на нашей вики работает около 35 плагинов, 10 из них мы написали сами. Ниже расскажем, какую задачу решает каждый, как он устроен и что из этого получилось.
Коротко о вики и задачах
Документация ведется на двух языках и двух площадках: около 1460 страниц на русской вики и 654 на английской. Читают ее инженеры провайдеров и операторов, которые приходят за конкретным параметром или командой и хотят найти их быстро.
Из этого выросли задачи, которые готовые плагины не закрывали. Наши плагины можно разделить на четыре группы:
Два языка — поддерживать RU‑ и EN‑версии в актуальном состоянии и показывать в поиске только страницы на выбранном языке;
Обратная связь — собирать отзывы прямо со страницы, узнавать, что читатель не нашел;
Удобство чтения — сделать чтение длинных технических страниц и копирование кода удобнее;
Данные из продукта — автоматически синхронизировать список протоколов в вики с VAS Cloud.
Все десять плагинов в одной таблице:
Плагин | Группа | Что делает | На что опирается |
translationcompare | Два языка | Оценивает, актуален ли английский перевод | IO_WIKIPAGE_WRITE, JSON‑RPC к модели |
langsearchfilter | Два языка | Оставляет в поиске страницы на языке интерфейса | SEARCH_QUERY_FULLPAGE, SEARCH_QUERY_PAGELOOKUP |
pagefeedback | Обратная связь | Форма «Была ли полезна эта информация?» | TPL_ACT_RENDER, AJAX_CALL_UNKNOWN |
reportproblem | Обратная связь | Кнопка «Не нашли нужную страницу?» | AJAX_CALL_UNKNOWN |
copycode | Удобство чтения | Кнопка копирования у блоков кода | TPL_METAHEADER_OUTPUT |
scrollsidebar | Удобство чтения | Закрепляет боковую панель с оглавлением | Только style.css |
cleansnippet | Удобство чтения | Убирает разметку из сниппетов поиска | FULLTEXT_SNIPPET_CREATE |
syncprotocols | Данные из продукта | Синхронизирует список протоколов из VAS Cloud | Консольная команда, тег |
aihelper | AI | Отвечает на вопросы на странице поиска | JSON‑RPC к модели |
aitranslation | AI | Готовит черновик английской страницы | JSON‑RPC к модели |
Два языка: translationcompare и langsearchfilter
translationcompare: актуален ли перевод
Задача. Русская страница обновляется после каждого релиза, а английская может отставать на несколько версий. Уследить за этим вручную на сотнях пар страниц невозможно. Сравнивать даты правок бессмысленно: исправленная опечатка в RU не делает EN устаревшей. Обычный diff тоже не поможет, ведь тексты написаны на разных языках.
Как устроен. Плагин слушает событие IO_WIKIPAGE_WRITE, то есть срабатывает при каждом сохранении страницы. Пару «оригинал — перевод» он находит через хелпер стороннего плагина translation: у нас английские страницы лежат в пространстве имен en:. Оба текста плагин отправляет по JSON‑RPC в наш сервис на основе нейросети, а тот возвращает оценку соответствия в процентах.
$data = [ "jsonrpc" => "2.0", "method" => "match_score_handle", "params" => [ "russian_text" => $russian, "english_text" => $english, ], "id" => rand(1, 99999) ]; // ... $status = ($percent >= 95) ? 'current' : 'outdated';
По этой оценке пара получает статус: current, если соответствие 95% и выше, outdated, если ниже, и missing, если перевода нет. Статусы хранятся в SQLite. В админке есть таблица всех пар, отсортированная по проценту соответствия — сверху самые отстающие переводы.
Когда плагин заработал на всей документации, мы внесли еще пару изменений, которые оказались необходимы:
Ручная отметка «актуально». Бывает, что английская версия страницы сознательно сокращена. Модель видит расхождение с русским текстом и раз за разом помечает пару как устаревшую, хотя с переводом все в порядке. Для таких случаев мы добавили ручную отметку «актуально». Редактор ставит галочку, и плагин больше не пересчитывает эту пару.
Полный пересчет в фоне. Пройти по тысяче страниц за один PHP‑запрос не получится, он упрется в тайм‑аут. Поэтому кнопка полного пересчета в админке ничего не считает сама, а только создает файл‑флаг. Скрипт, который запускается по cron, видит флаг, проходит по всем страницам и удаляет флаг.
При первом полном прогоне плагин нашел десятки устаревших переводов.

langsearchfilter: поиск на своем языке
Задача. Русские и английские страницы лежат в одной вики, поэтому встроенный поиск выдает оба языка вперемешку. По запросу dpdk_engine, например, читатель получал два набора одинаковых страниц.
Как устроен. Весь плагин занимает всего 24 строки. Он вешается на полнотекстовый поиск (SEARCH_QUERY_FULLPAGE) и на поиск по названию страницы (SEARCH_QUERY_PAGELOOKUP) и оставляет в выдаче только страницы на языке интерфейса.
public function register(Doku_Event_Handler $controller) { $controller->register_hook('SEARCH_QUERY_FULLPAGE', 'AFTER', $this, 'filter_by_language_namespace'); $controller->register_hook('SEARCH_QUERY_PAGELOOKUP', 'AFTER', $this, 'filter_by_language_namespace'); } public function filter_by_language_namespace(Doku_Event $event, $param) { global $conf; $lang = $conf['lang']; if (empty($event->result) || !is_array($event->result)) return; $event->result = array_filter($event->result, function ($page_id) use ($lang) { if ($lang === 'en') return strpos($page_id, 'en:') === 0; return strpos($page_id, 'en:') !== 0; }, ARRAY_FILTER_USE_KEY); }
На этом плагине хорошо видно, за что мы ценим DokuWiki. Почти каждое действие движок сопровождает именованным событием, а плагин подписывается на нужное и встраивается до или после него. Поэтому менять ядро поиска не нужно: достаточно поправить результат, который оно уже выдало.
Обратная связь: pagefeedback и reportproblem
Для обратной связи у нас есть два плагина. pagefeedback собирает отзывы о странице, которую человек нашел, а reportproblem — запросы о том, чего в документации нет.
Как формы выглядят для читателя, мы уже показывали в блоге, поэтому здесь речь пойдет о том, как они устроены внутри.
pagefeedback: «Была ли полезна эта информация?»
Задача. Понять, какие статьи работают, какие нет и почему. Одного ответа «да/нет» для этого мало, а длинную форму никто заполнять не станет.
Как устроен. Плагин добавляет блок под текстом статьи. Для этого он подписан на событие TPL_ACT_RENDER и показывает форму только в режиме просмотра, а не при редактировании или поиске. Форма раскрывается по шагам:
Читатель нажимает «Да» или «Нет».
Появляются галочки с причинами. Для «Нет» это «Недостаточно информации», «Информация устарела», «Нет ответа на вопрос», «Рекомендации не помогли» и «Другое».
Текстовое поле появляется, только если выбрано «Другое».
Ответ уходит AJAX‑запросом в стандартный обработчик lib/exe/ajax.php. Вызовы с незнакомым именем DokuWiki передает плагинам через событие AJAX_CALL_UNKNOWN, и плагин перехватывает свой вызов по имени. Голоса пишутся в SQLite через плагин sqlite. Один человек может проголосовать за страницу только один раз: повторный голос отсекается по IP или по cookie.
$ip = $_SERVER['REMOTE_ADDR']; $cookie = $_COOKIE['pfid'] ?? ''; if (!$cookie) { $cookie = md5(uniqid()); setcookie('pfid', $cookie, time() + (365*86400), DOKU_REL); } $already = $db->hasVoted($page, $ip, $cookie); if ($already) { echo json_encode(['status' => 'already']); return; } $db->saveVote($page, $vote, $details, $checkboxes, $ip, $cookie);
В админке редактор видит таблицу с датой, страницей, оценкой, причинами и комментарием.

С лета 2025 года читатели оставили 194 отзыва, 64 из них (около трети) — «Нет». Чаще всего читателям не хватало информации.
reportproblem: «Не нашли нужную страницу?»
Задача. Самый важный сигнал для технического писателя — что читатель искал и не нашел. pagefeedback такие запросы не собирает, ведь форма есть только на существующих страницах.
Как устроен. Сервер выводит на страницу скрытый блок с кнопкой «Сообщить нам», а место для него выбирает скрипт. Скрипт проверяет три места — страницу «ничего не найдено», боковую панель и верх результатов полнотекстового поиска — и показывает блок там, где нашел нужный элемент. Если подходящих мест на странице несколько, блок окажется в последнем из проверенных. Так один плагин работает в разных частях шаблона без правки самого шаблона.
const content = document.getElementsByClassName("nothing")[0]; if (content) { content.append(block); block.style.display = "block"; } const aside = document.getElementsByClassName("aside")[0]; if (aside) { aside.appendChild(block); block.style.display = "block"; } const search = document.getElementsByClassName("search_fulltextresult")[0]; if (search) { search.prepend(block); block.style.display = "block"; }
Кнопка открывает модальное окно с двумя полями: email и «Что вы хотели найти?». Email обязателен, чтобы можно было ответить человеку. Дальше все устроено так же, как в pagefeedback: AJAX‑запрос, SQLite и таблица в админке.
Примерно раз в месяц сообщения разбирает технический писатель.

Мелочи для читателя: copycode, scrollsidebar, cleansnippet
Следующие три плагина сделаны специально для читателя. В каждом всего по 20–50 строк, но именно они избавляют от мелких неудобств.
copycode: кнопка «Скопировать» у блока кода
Задача. Наши читатели постоянно копируют команды и конфиги в консоль. Выделять длинный блок мышью неудобно, и легко потерять первую или последнюю строку. Казалось бы, кнопка «Скопировать» у блока кода — давно привычная вещь, однако в стандартной поставке DokuWiki ее нет.
Как устроен. PHP‑часть только подключает скрипт и стили (TPL_METAHEADER_OUTPUT). Скрипт находит на странице каждый блок pre.code, оборачивает его и добавляет в угол кнопку. Перед копированием блок клонируется, из клона удаляется сама кнопка, и в буфер попадает чистый текст.
Самое интересное здесь — запасной вариант. Clipboard API работает не везде, поэтому при ошибке плагин копирует текст по‑старому — через скрытый textarea:
if (navigator.clipboard && navigator.clipboard.writeText) { try { await navigator.clipboard.writeText(text); showCopied(btn); return; } catch (err) } const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.left = '-9999px'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy');
После копирования иконка на 2 секунды меняется на галочку.
scrollsidebar: оглавление всегда под рукой
Задача. Страницы с описанием параметров бывают очень длинными, и меню разделов в боковой панели тоже. В стандартном шаблоне панель прокручивается вместе со страницей: дочитал до середины — меню уехало, и чтобы перейти в соседний раздел, приходится возвращаться наверх.
Как устроен. В этом плагине нет ни строки PHP, ни строки JavaScript, только стили. DokuWiki сама подключает style.css из папки плагина, поэтому достаточно положить файл. Панель «прилипает» к верху экрана и получает собственную прокрутку:
#dokuwiki__aside { position: sticky !important; top: 0 !important; align-self: flex-start !important; } .pad.aside.include.group { max-height: 100vh; overflow-y: auto; }
На экранах уже 1200 px панель фиксируется, чтобы не съедать место у текста.
Эти два десятка строк мы оформили как плагин, а не как правку шаблона. Поэтому шаблон можно обновить, и стили не потеряются.
cleansnippet: чистые сниппеты в поиске
Задача. DokuWiki строит фрагмент текста в результатах поиска из исходной разметки страницы. Вместо обычного текста читатель видел [[dpi:...|...]], {{...}}, ** и ====.
Так выглядела выдача без плагина:

А так — с ним:

Как устроен. Плагин перехватывает событие FULLTEXT_SNIPPET_CREATE и прогоняет текст через набор регулярных выражений. Он убирает вставки медиа, из ссылок оставляет подпись, снимает форматирование и заголовки:
$text = preg_replace('/\{\{.*?\}\}/s', '', $text); // медиа $text = preg_replace('/\[\[(?:[^\|\]]*\|)?([^\]]+)\]\]/', '$1', $text); // [[id|текст]] → текст $text = preg_replace('/\*\*(.*?)\*\*/s', '$1', $text); // жирный $text = preg_replace('/={2,}\s*(.*?)\s*={2,}/', '$1', $text); // заголовки
Администраторам сниппет показывается без очистки: редактору иногда нужно видеть, в какой разметке нашлось слово.
Данные из продукта: syncprotocols
Задача. В вики нужен список протоколов, которые распознает СКАТ: по нему клиент проверяет, поддерживается ли нужный ему протокол. Список ведется в VAS Cloud и постоянно пополняется, потому что облачные протоколы добавляются без выхода новой версии СКАТ. Поддерживать такую таблицу в вики вручную, да еще на двух языках, невозможно.
Как устроен. Плагин состоит из трех частей.
Синхронизация. Команда регистрируется через событие
CLI_PLUGIN_REGISTER. Она скачивает из VAS Cloud два сжатых списка — протоколы и группы — и раскладывает их по таблицам SQLite. Каждая синхронизация пересоздает таблицы с нуля, так что в вики всегда ровно то, что лежит в облаке.Представление для вики. Поверх таблиц создается SQL‑view, в котором данные уже подготовлены для читателя: названия групп на русском и английском, дата обновления или пометка «По необходимости», тип протокола (встроенный или облачный). Тип определяется по диапазону номеров.
Вывод на страницу. Синтаксический плагин добавляет в разметку DokuWiki тег
<sql>. Редактор пишет на странице запрос к view и получает таблицу с поиском по всем колонкам и постраничной навигацией. Русская и английская страницы отличаются только выбранными колонками (_ruили_en).
Частота синхронизации настраивается в админке: раз в час, день или неделю. Задание в crontab плагин переписывает сам. Там же есть кнопка «Принудительная синхронизация» на случай, когда новый протокол нужен на странице прямо сейчас.
Сейчас синхронизация идет раз в день, а в таблице уже больше 6000 протоколов.

А что с AI
Еще два наших плагина работают с собственной моделью через тот же JSON‑RPC, что и translationcompare:
aihelper отвечает на вопрос прямо на странице поиска и дает ссылки на источники. Кроме того, у него есть отдельная страница‑чат «Режим ИИ».
aitranslation открывает новую английскую страницу в редакторе уже с черновиком перевода русской версии, вместе со всей разметкой DokuWiki.
Про саму модель, ее обучение и качество ответов расскажем в отдельной статье.
Что мы из этого вынесли
Самый большой из наших плагинов, syncprotocols, занимает около 550 строк, а самые маленькие — 20–25. Компактными их удалось сделать благодаря нескольким принципам:
Только события, никаких правок ядра. Плагины цепляются к стандартным событиям DokuWiki (
IO_WIKIPAGE_WRITE,SEARCH_QUERY_FULLPAGE,FULLTEXT_SNIPPET_CREATE,TPL_ACT_RENDER,AJAX_CALL_UNKNOWN), поэтому обновления DokuWiki их не ломают.Один шаблон для форм. pagefeedback, reportproblem и translationcompare устроены одинаково: своя база в SQLite через плагин sqlite и таблица в админке. Каждый следующий плагин писать было быстрее предыдущего.
Тяжелая логика вынесена отдельно. Плагины не сравнивают и не переводят тексты сами, а отправляют запрос в сервис по JSON‑RPC. Модель можно улучшать, не трогая вики.
Вместо заключения
Код плагинов мы не выкладываем: они написаны под наши конкретные задачи и под объем нашей вики. Но надеемся, что сами подходы пригодятся тем, кто тоже ведет документацию на DokuWiki.
А какие плагины дописывали вы? Как отслеживаете актуальность переводов и собираете отзывы читателей? Расскажите в комментариях.
Ранее в блоге:
DokuWiki как платформа для технической документации: наш опыт
Как устроена Wiki VAS Experts
