Привет! Меня зовут Владимир, но вы можете звать меня просто Иннокентий Алексеевич. Я люблю эксперименты. Сегодня я расскажу, как можно улучшить навигационное меню на сайте документации, сократить время сборки и размер сайта больше чем в два раза. В качестве примера возьму сайт документации, собранный при помощи Antora.


Кому будет полезен материал: техническим писателям, разработчикам сайтов документации и просто любителям опенсорса и красивых вещей.


Antora — генератор статических HTML сайтов из исходных AsciiDoc файлов. Antora бесплатная и имеет открытый исходный код.


Магия


Во-первых, зачем вообще нужен сайт документации? Можно пойти дальше и спросить, зачем вообще нужна документация? Этим вопросам посвящено много статей, в том числе и на хабре. Документация повышает привлекательность вашего продукта для потенциальных клиентов. Сайт документации облегчает доступ к этой документации, соответственно, делает продукт ещё более привлекательным.


Ранее я уже писал несколько статей про документацию и AsciiDoc: как я переносил документацию из DITA в AsciiDoc+Antora (1 часть и 2 часть), сравнивал возможности DITA и AsciiDoc.


О чём пойдёт речь?


  • Зачем улучшать навигационное меню
  • Расширения Antora, расширение Antora Navigator
  • Работа с шаблонами .hbs, изменение UI
  • Работа с CSS-стилями навигации
  • Украшаем навигационное меню иконками
  • Ковыряемся в SVG и пытаемся наконец всунуть эти иконки

Зачем улучшать навигационное меню


Лучшее — враг хорошего. Зачем вообще пытаться улучшить то, что и так работает?


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


Старый вид навигационного меню:


Старый вид навигационного меню


Несмотря на всю простоту такого подхода, у него есть существенные недостатки:


  • Пользователю нужно прокручивать основное навигационное меню при просмотре текущего компонента и раскрывать второе меню, чтобы выбрать другой компонент или версию. Много нажатий, много прокручиваний.
  • Самый главный недостаток — навигационное меню создаётся для каждой страницы и прописывается в её исходный HTML. Это увеличивает размер страницы, размер сайта, время на сборку. И вообще зачем делать одно и то же сотни или даже тысячи раз, когда можно не делать?

Расширения Antora


В Antora версии 3.0 появилась возможность использовать пользовательские расширения. Раньше для реализации любой фичи требовалось делать форк Анторы:


  • Форк, чтобы прикрутить поиск Lunr.
  • Форк, чтобы создавать pdf и т.д..

Благодаря расширениям любые изменения стали значительно проще. Теперь менять исходный код Анторы не нужно, достаточно просто установить расширение в репозиторий playbook одной командой:


npm i @antora/pdf-extension


И зарегистрировать его в antora-playbook.yml:


antora:
  extensions:
  — '@antora/pdf-extension'

При необходимости можно выполнить персонализацию расширения. Обычно это выполняется по приложенной к расширению инструкции.


Расширение Antora Navigator


С расширением Навигатора немного сложнее. Это расширение ещё не опубликовано официально в виде npm-пакета, все ресурсы для него находятся в репозитории на GitLab, поэтому нужно действовать по следующему алгоритму:


  1. В репозитории antora-playbook найти файл package.json и добавить зависимость от репозитория Навигатора:
    "antora-navigator-extension": "git+https://gitlab.com/opendevise/oss/antora-navigator-extension"
  2. Затем добавить требование этого расширения в файл Antora-playbook.yml:
    antora:
     extensions:
     — require: antora-navigator-extension
  3. При необходимости можно настроить файл antora-navigator.yml. Как это сделать, я расписал в README репозитория Навигатора, но мой merge request пока ещё не был одобрен.
  4. Выполнить необходимые изменения в UI.

Работа с дополнительными файлами UI


Пока расширение в ранней стадии разработки, нужно ещё немного повозиться с UI.


  1. Во-первых, требуется добавить дополнительные файлы пользовательского интерфейса (supplemental-ui) из репозитория Навигатора в antora-playbook.yml:


    supplemental_files: ./node_modules/antora-navigator-extension/data

  2. Если вы используете UI, производный от дефолтного, то, вероятно, у вас уже есть а) корпоративный стиль документации и б) дополнительные (supplemental) файлы.


    У Анторы на момент написания материала есть ограничение, которое не позволяет добавлять более одной папки в supplemental UI. Соответственно, если добавить указанную выше строку в antora-playbook.yml, весь существующий supplemental-ui потеряется. Решение — перенести весь ваш supplemental UI в основной. Например, вот здесь располагается основной UI сайта документации Docsvision.


  3. Обычно файлы в папке supplemental-ui — это какая-нибудь автоматически сгенерированная шушера. Если такой файл просто закинуть в соответствующую папку основного UI, сборка пакета пользовательского интерфейса сломается.


    Всё из-за линтера, который строго блюстит за стилем в .css и .js файлах. Сборка может упасть просто из-за того, что где-то в файле есть лишний пробел или его, наоборот, нет.


    Для таких файлов необходимо отключить линтер, добавив в начало комментарий.


    Для файлов .css:


    /* stylelint-disable */

    Для файлов .js:


    /* eslint-disable */

  4. А ещё такие файлы лучше помещать в подпапку /vendor/, чтобы они сохранили свои названия при добавлении на сайт. Файлы не из папки /vendor/ не попадут на сайт, если они не импортированы в site.css — ограничение UI Анторы.


    Сложно воспринять всё в тексте, поэтому вот наглядная иллюстрация файловой структуры пользовательского интерфейса нашего сайта документации:


    - antora-ui-default
     - src/
       - css/
         - base.css
         - body.css
         - main.css
         - nav.css
         - ...
         - site.css
         - vendor/
           - docsearch.min.css
           - docsearch.override.css
           - ...
       - helpers/
       - img/
       - js/
         - vendor/
           - docsearch.min.js
           - medium-zoom.bundle.js
           - highlight.bundle.js
           - ...
       - layouts/
       - partials/

    Я перенёс файлы docsearch.min.css и docsearch.min.js из supplemental-ui в основной и отключил для них линтер.



Работа с шаблонами UI


Шаблоны UI — это .hbs файлы, описывающие структуру страниц сайта. Необходимо внести несколько изменений в эти файлы, чтобы новая навигация заработала правильно.


Вдохновение можно почерпнуть в репозитории Навигатора, в папке example/supplemental-ui/partials.


  1. Меняем файл footer-scripts.hbs, добавляем первые два скрипта, которые необходимы для работы навигации:


    <script src="{{{siteRootPath}}}/site-navigation-data.js"></script>
    <script src="{{{uiRootPath}}}/js/nav.js"></script>
    <script src="{{{uiRootPath}}}/js/site.js"></script>

    Первый скрипт будет сформирован при сборке сайта, а второй мы добавляли через supplemental-ui ранее.


    При необходимости аналогичным образом редактируем файлы head-meta.hbs и nav.hbs — возьмите их из той же папки в репозитории Навигатора, и перенесите в свой UI отсутствующие строки.



Локализация элементов навигационного меню


Иностранные слова


В навигации есть несколько иностранных слов и фраз: домашняя группа, подсказки к версиям продукта. Это нужно перевести.


Один из вариантов — сделать форк репозитория с расширением и отредактировать nav.js. Но тогда придётся поддерживать форк и отслеживать все изменения.


Решение с форком может оказаться трудозатратным. Поэтому лучше выбрать второй вариант — локализация нативными средствами расширения. Пока это ещё не релизовано, см. issue #2. Будет предусмотрен атрибут для тега javascript:


<script src="{{{uiRootPath}}}/js/nav.js"
  data-t-home="Page d´accueil"
  data-t-current-version="Version actuelle"
  data-t-previous-versions="Versions précédentes"
  data-t-prerelease-versions="Versions préliminaires"></script>

Зочем?


Если вы дочитали до этого места с мыслями "Да ну его в пень, слишком запарно". Да, работёнка выглядит запарной, но результат того стоит. Посмотрите на новый вид навигационного меню и сравните его со старым:


Новый:


Новый вид навигационного меню


Старый:


Старый вид навигационного меню


Кроме единого навигационного меню без лишних панелей, расширение Навигатора даёт и другие преимущества:


  • Удобную навигацию по версиям через выпадающий список с подписями "Последняя версия", "Предрелизная версия".
  • Общий размер сайта сокращается более чем на 50% из-за того, что навигация формируется не при помощи HTML, а с помощью JavaScript.
  • Появляется возможность добавить красивые иконки для компонентов в меню (об иконках чуть позже, сперва расскажу про стили).

Работа с CSS-стилями навигации


По умолчанию строки навигационного меню выделяются подчёркиванием при наведении и всё. Я подумал, что стоит прикрутить что-то более отзывчивое при помощи стилей. Я убрал подчёркивание при наведении, вместо него сделал выделение цветом.


Это легко сделать всего несколькими строками CSS:


a.nav-text:hover {
    text-decoration: none;
    color: #4c22f7;
}

a.nav-text:active {
    text-decoration: none;
    color: #4c22f7;
}

А ещё хорошо бы выделять текущую страницу чуть ярче, чем просто жирным:


.nav-list [aria-current=page] {
    color: #4c22f7;
    font-weight: normal;
    -webkit-text-stroke-width: 0.02em;
}

Но есть одна проблема — стили навигации определены в supplemental-ui, который в репозитории Навигатора.


Решение — переопределить стили в другом файле.


Создаём один файл со всеми перечисленными стилями (например, nav-override.css) и импортируем его в site.css. Так как этот файл будет частью общего .css сайта, мы сможем даже использовать общие переменные для цветов:


nav-override.css:


.nav-item:hover > .nav-title .nav-icon {
  filter: grayscale(0);
  opacity: 1;
  transition: 0.3s;
}

.nav-item:active > .nav-title .nav-icon {
  filter: grayscale(0);
  opacity: 1;
}

a.nav-text:hover {
  text-decoration: none;
  color: var(--link_hover-font-color);
}

a.nav-text:active {
  text-decoration: none;
  color: var(--link_hover-font-color);
}

a.nav-text:focus {
  text-decoration: none;
  color: var(--link-font-color);
  -webkit-text-stroke-width: 0.02em;
}

.nav-list [aria-current=page] {
  color: var(--link-font-color) !important;
}

Самые внимательные могут найти в коде стили со словом "icon", но я так и не рассказал про то, откуда берутся иконки. Время это исправить.


Украшаем навигационное меню иконками


Когда я представил новое навигационное меню коллегам на демо, первый вопрос был "А можно изменить иконки на собственные?"


Да, можно. Но чтобы понять, как это делается, мне пришлось долго курить исходники. Это всё, конечно же, просто потому что я недалёкий.


  1. Чтобы приделать иконки, нужно сперва вспомнить, что такое компоненты Antora, и где определены их названия. Компоненты — это базовая единица документации Antora. Для лучшего восприятия можно назвать их, например, томами. Названия компонентов задаются в файлах antora.yml. Также имена компонентов можно узнать в файле antora-navigator.yml.


  2. Затем необходимо создать файл icondefs.js, где каждая иконка будет привязана к имени компонента:


    ;(function () {
     /* eslint-disable max-len */
     // prettier-ignore
     var defs = [
       {
         id: 'icon-nav-component', 
         /* Корневая навигационная группа, а также любой другой компонент, для которого не указана специальная иконка. */
         viewBox: '0 0 30 30',
         path: { d: 'M19.9035 16.7957L22.0559 24.8282C19.8893 25.7545 17.5053 26.2702 15 26.2702C12.4947 26.2702 10.1107 25.7545 7.94414 24.8282L10.0965 16.7957C7.16563 15.0998 5.19232 11.9329 5.19232 8.30324C5.19232 6.65115 5.6026 5.09557 6.32406 3.72984L12.9574 13.2033H17.0426L23.676 3.72984C24.3974 5.09557 24.8077 6.65115 24.8077 8.30324C24.8077 11.9329 22.8344 15.0998 19.9035 16.7957Z', fill: '#00A2DF' }, 
         /* Иконка для корневой навигационной группы [в формате SVG path](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths). */
       },
       {
         id: 'icon-nav-component-ROOT', <.>
         /* Компонент с именем ROOT (`name: ROOT`) в файле `antora.yml`. */
         viewBox: '0 0 14 15',
         paths: [
           { d: 'M1.36278 0L1.36787 3.94183L7.40742 3.95558L7.40236 0.0376971L8.73519 0.00265382L8.6981 5.27362L0 5.28486L0.0369378 0.0348597L1.36278 0Z', transform: 'translate(2.8598 6.64626)', fill: '#00A3E0' },
           { d: 'M6.00319 0L11.7143 4.23063V5.86337L5.97677 1.61317L0 5.5623L0.137638 3.91056L6.00319 0Z', transform: 'translate(1.198 1.32681) scale(0.999808) rotate(-1.50596)', fill: '#087299' }, 
           /* Иконка для компонента с именем ROOT [в формате SVG path](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths). */
         ],
       },
     ]
     var icondefs = Object.assign(document.createElement('div'), { id: 'icondefs', hidden: true })
     icondefs.appendChild(
       defs.reduce(function (parent, icondef) {
         var symbol = Object.assign(document.createElementNS('http://www.w3.org/2000/svg', 'symbol'), { id: icondef.id })
         symbol.setAttribute('viewBox', icondef.viewBox)
         var contents = icondef.contents || icondef.paths || [icondef.path]
         if (Array.isArray(contents)) {
           contents.forEach(function (props) {
             symbol.appendChild(
               Object.entries(props).reduce(function (tag, prop) {
                 tag.setAttribute(prop[0], prop[1])
                 return tag
               }, document.createElementNS('http://www.w3.org/2000/svg', 'path'))
             )
           })
         } else {
           symbol.innerHTML = contents
         }
         parent.appendChild(symbol)
         return parent
       }, document.createElementNS('http://www.w3.org/2000/svg', 'svg'))
     )
     document.body.appendChild(icondefs)
    })()

    Также есть несколько служебных иконок навигации:


    {
      id: 'icon-nav-item-toggle',
      /* Иконка для разворачивающихся заголовков. */
      viewBox: '0 0 16 16',
      paths: [
        { d: 'm5.345 3.22a0.75 0.75 0 0 1 1.06 0l4.25 4.25a0.75 0.75 0 0 1 0 1.06l-4.25 4.25a0.75 0.75 0 0 1-1.06-1.06l3.72-3.72-3.72-3.72a0.75 0.75 0 0 1 0-1.06z', 'fill-rule': 'evenodd' },
      ],
    },
    {
      id: 'icon-nav-version',
      /* Иконка рядом с элементом выбора версии */
      viewBox: '0 0 16 16',
      paths: [
        { d: 'm12.78 5.345a0.75 0.75 0 0 1 0 1.06l-4.25 4.25a0.75 0.75 0 0 1-1.06 0l-4.25-4.25a0.75 0.75 0 0 1 1.06-1.06l3.72 3.72 3.72-3.72a0.75 0.75 0 0 1 1.06 0z' },
      ],
    },

    Вот эти иконки на скрине:


    Служебные иконки


  3. Файл icondefs.js нужно добавить в свой проект пользовательского интерфейса, в папку src/js/vendor/.


  4. Затем его нужно зарегистрировать в шаблоне footer-scripts.hbs следующим образом:


    <script src="{{siteRootPath}}/site-navigation-data.js"></script>
    <script src="{{uiRootPath}}/js/vendor/icondefs.js"></script>

    Файл icondefs.js должен быть вторым в списке после site-navigation-data.js, иначе иконок не будет видно!



Ковыряемся в SVG и пытаемся наконец всунуть эти иконки


Обычно SVG имеют исходный код следующего вида:


<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
    <rect x="0.5" y="0.5" width="31" height="31" rx="15.5" fill="#F18A00" stroke="white" />
    <rect x="14.5638" y="9" width="1.51276" height="12.2702" fill="white" />
    <path
        fill-rule="evenodd"
        clip-rule="evenodd"
        d="M11.4542 21.2702C13.9143 21.2702 15.9085 19.276 15.9085 16.8159C15.9085 14.3559 13.9143 12.3617 11.4542 12.3617C8.99423 12.3617 7 14.3559 7 16.8159C7 19.276 8.99423 21.2702 11.4542 21.2702ZM11.5383 19.9255C13.2093 19.9255 14.5639 18.5709 14.5639 16.9C14.5639 15.229 13.2093 13.8744 11.5383 13.8744C9.8674 13.8744 8.51282 15.229 8.51282 16.9C8.51282 18.5709 9.8674 19.9255 11.5383 19.9255Z"
        fill="white"
    />
    <path
        fill-rule="evenodd"
        clip-rule="evenodd"
        d="M24.3128 12.5298H26.1617L22.632 21.2702H22.6319L22.6319 21.2702H20.783L17.2532 12.5298H19.1021L21.7075 18.9811L24.3128 12.5298Z"
        fill="white"
    />
</svg>

Нельзя просто так взять и


Нельзя просто так взять и вставить это в icondefs, потому что здесь сочетаются объекты (фигуры, линии) и пути, а также присутствуют fill-rule и clip-rule.


Из следующего фрагмента кода можно понять, как приделать fill-rule и clip-rule, но объекты и пути так сочетать не получится:


    {
      id: 'icon-nav-component-mgmtconsole',
      viewBox: '0 0 32 32',
      paths: [
        { d: 'M11.4542 21.2702C13.9143 21.2702 15.9085 19.276 15.9085 16.8159C15.9085 14.3559 13.9143 12.3617 11.4542 12.3617C8.99423 12.3617 7 14.3559 7 16.8159C7 19.276 8.99423 21.2702 11.4542 21.2702ZM11.5383 19.9255C13.2093 19.9255 14.5639 18.5709 14.5639 16.9C14.5639 15.229 13.2093 13.8744 11.5383 13.8744C9.8674 13.8744 8.51282 15.229 8.51282 16.9C8.51282 18.5709 9.8674 19.9255 11.5383 19.9255Z', fill: 'white', 'fill-rule': 'evenodd', 'clip-rule': 'evenodd' },
        { d: 'M24.3128 12.5298H26.1617L22.632 21.2702H22.6319L22.6319 21.2702H20.783L17.2532 12.5298H19.1021L21.7075 18.9811L24.3128 12.5298Z', fill: '#271F47', 'fill-rule': 'evenodd', 'clip-rule': 'evenodd' },
        { d: 'M2.21876 7.91919L15.4448 13.3055V28.996L2.21876 22.79V7.91919Z', fill: '#00ADBB' },
        { d: 'M16.1337 2.59143L2.63208 7.04097L16.1337 12.3687L29.4286 7.04097L16.1337 2.59143Z', fill: '#BBD02D' },
      ],
      contents:
        '<rect x="0.5" y="0.5" width="31" height="31" rx="15.5" fill="#F18A00" stroke="white"/>' +
        '<rect x="14.5638" y="9" width="1.51276" height="12.2702" fill="white"/>',
    },

Выйти из ситуации можно при помощи Adobe Illustrator или Inkscape. Преобразовать все объекты в пути за два клика по инструкции.


В результате получится что-то такое:


    {
      id: 'icon-nav-component-mgmtconsole',
      viewBox: '0 0 32 32',
      paths: [
        { d: 'M 16,0.5 C 24.587,0.5 31.5,7.413 31.5,16 31.5,24.587 24.587,31.5 16,31.5 7.413,31.5 0.5,24.587 0.5,16 0.5,7.413 7.413,0.5 16,0.5 Z', fill: '#f18a00', stroke: '#ffffff' },
        { d: 'm 14.5638,9 h 1.51276 V 21.2702 H 14.5638 Z', fill: '#ffffff' },
        { d: 'M11.4542 21.2702C13.9143 21.2702 15.9085 19.276 15.9085 16.8159C15.9085 14.3559 13.9143 12.3617 11.4542 12.3617C8.99423 12.3617 7 14.3559 7 16.8159C7 19.276 8.99423 21.2702 11.4542 21.2702ZM11.5383 19.9255C13.2093 19.9255 14.5639 18.5709 14.5639 16.9C14.5639 15.229 13.2093 13.8744 11.5383 13.8744C9.8674 13.8744 8.51282 15.229 8.51282 16.9C8.51282 18.5709 9.8674 19.9255 11.5383 19.9255Z', fill: 'white', 'fill-rule': 'evenodd', 'clip-rule': 'evenodd' },
        { d: 'M24.3128 12.5298H26.1617L22.632 21.2702H22.6319L22.6319 21.2702H20.783L17.2532 12.5298H19.1021L21.7075 18.9811L24.3128 12.5298Z', fill: 'white', 'fill-rule': 'evenodd', 'clip-rule': 'evenodd' },
      ],
    },

Запарно, конечно. Самое интересное, что можно было просто взять чистый код SVG и вставить в src/partials/footer-scripts.hbs вот так:


    <symbol id="icon-nav-component-mgmtconsole" viewBox="0 0 16 16">
      <path d="m5.345 3.22a0.75 0.75 0 0 1 1.06 0l4.25 4.25a0.75 0.75 0 0 1 0 1.06l-4.25 4.25a0.75 0.75 0 0 1-1.06-1.06l3.72-3.72-3.72-3.72a0.75 0.75 0 0 1 0-1.06z" fill-rule="evenodd"/>
    </symbol>

Это достаточно очевидный путь, потому что именно так определяются служебные иконки (позже я их удалил из footer-scripts.hbs и перенёс в icondefs.js, чтобы все яйца лежали в одной корзине):


    <symbol id="icon-nav-group" viewBox="0 0 16 16">
      <path d="m12.78 5.345a0.75 0.75 0 0 1 0 1.06l-4.25 4.25a0.75 0.75 0 0 1-1.06 0l-4.25-4.25a0.75 0.75 0 0 1 1.06-1.06l3.72 3.72 3.72-3.72a0.75 0.75 0 0 1 1.06 0z"/>
    </symbol>

Но до этого метода я почему-то додумался позднее всего. К тому же это слишком просто.


Вот так в результате всех описанных стараний и страданий выглядит финальная навигация:


Навигация с красивыми иконками


По умолчанию иконки серые, но окрашиваются в цвет при наведении на текст. Причём делают это постепенно, в течение 0.3 секунд.


Это изменение я придумал в последний момент, вспомнив интересное свойство CSS — transition:


.nav-item:hover > .nav-title .nav-icon {
  filter: grayscale(0);
  opacity: 1;
  transition: .3s;
}

.nav-item:active > .nav-title .nav-icon {
  filter: grayscale(0);
  opacity: 1;
}

Теперь у нашего сайта документации есть роскошное меню навигации с красивыми иконками, а ещё на сборку уходит в два раза меньше времени. Порой было сложно и непонятно, но результат того стоит.


Ещё один момент, который я забыл отметить в статье: расширение выдвигает определённые требования к исходному файлу навигации для компонента, то есть условному файлу nav.adoc. Чтобы выяснить эти требования, мне тоже пришлось поломать мозги. Я подробно расписал об этом в упомянутом выше merge request. Только его ещё нужно дополнить и немного изменить в связи с тем, что я узнал. Что-то я смог понять без посторонней помощи. В чём-то разобрался сам, опираясь на исходники сайта Mulesoft. На сайте Mulesoft так же используется расширение Навигатора. В чём-то я разобрался с помощью подсказок от Дэна Аллена — создателя AsciDoc, Анторы и данного расширения.


А где сам сайт? Ну, он ещё не опубликован в широкий доступ. Точнее, опубликован, но там старая навигация. К счастью, специально для читателей я записал небольшое демо, его можно посмотреть по ссылке: https://vimeo.com/758052662/fce5ecd0d8


Как ещё можно улучшить навигацию по сайту? Делитесь лайфхаками в комментариях.

Комментарии (2)


  1. little-brother
    17.10.2022 18:21
    +2

    Вы боитесь, что пользователи забудут про какую систему они читают документацию (или они ее не читают от слова "совсем"?). Держите картинку из Paint "на подумать" :)

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


    1. Grolribasi Автор
      18.10.2022 09:09

      Спишем это на SEO оптимизацию. Если юзер введёт в поисковую систему "как установить Docsvision", он получит нужный результат. Ну и давайте по-честному, это документация для нашего продукта, мы можем развивать так, как считаем нужным.

      Метрики будут обязательно.