0062 — Условия сборки страниц в Antora

Содержимое страницы

Постановка задачи

Есть два окружения, назовём их prod и dev.

  • prod — продуктовое окружение. Результаты сборки документации показываются пользователям.

    Для сборки используется плейбук antora-playbook-prod.yml.

  • dev — окружение для служебного пользования. Результаты документации доступны только сотрудникам компании. Помимо страниц, доступных пользователям, есть страницы, которые им показывать не надо.

    Для сборки используется плейбук antora-playbook-dev.yml.

Цель: настроить Antora таким образом, чтобы при сборке проекта для окружения prod в него не попадал контент, предназначенный только для служебного пользования.

Решение № 1, наивное

Попробуем решить задачу «в лоб»:

  1. В antora-playbook-dev.yml добавим свойство is-dev:

    ---
    site:
      title: Documentation
    content:
      edit_url: false
      sources:
      # ...
    asciidoc:
      attributes:
        is-dev: true
        # ...
    
  2. В файл навигации nav.adoc добавим условия:

    * xref:index.adoc[]
    * xref:pub-page-1.adoc[]
    * xref:pub-page-2.adoc[]
    ifdef::is-dev[]
    ** xref:private-page-1.adoc[]
    ** xref:pirvate-page-2.adoc[]
    endif::[]
    * xref:pub-page-3.adoc[]
    
  3. Соберём проект:

    make production
    

Проверим результат: страницы private-page-1 и private-page-2 отсутствуют в оглавлении, но по-прежнему доступны по прямым ссылкам. Файлы на диске тоже присутствуют.

Решение № 2, сложное

Это решение взято из обсуждения Issue 1080 в репозитории Antora. Оно дополняет решение, описанное выше, за счёт использования расширений.

  1. В каталоге с проектом Antora создайте подкаталог extensions:

    mkdir extensions
    
  2. Разместите в этом каталоге три указанных файла:

    • page-unpublish-tag-extension.js

      "use strict";
      
      module.exports.register = function() {
        this.on('documentsConverted', ({ contentCatalog }) => {
          contentCatalog.getPages((page) => {
            if (page.out && page.asciidoc?.attributes['page-unpublish'] != null) delete page.out;
          });
        });
      }
      
    • unlisted-pages-extension.js

      module.exports.register = function ({ config }) {
        const { addToNavigation, unlistedPagesHeading = 'Unlisted Pages' } = config
        const logger = this.getLogger('unlisted-pages-extension')
        this
          .on('navigationBuilt', ({ contentCatalog }) => {
            contentCatalog.getComponents().forEach(({ versions }) => {
              versions.forEach(({ name: component, version, navigation: nav, url: defaultUrl }) => {
                const navEntriesByUrl = getNavEntriesByUrl(nav)
                const unlistedPages = contentCatalog
                  .findBy({ component, version, family: 'page' })
                  .filter((page) => page.out)
                  .reduce((collector, page) => {
                    if ((page.pub.url in navEntriesByUrl) || page.pub.url === defaultUrl) return collector
                    logger.warn({ file: page.src, source: page.src.origin }, 'detected unlisted page')
                    return collector.concat(page)
                  }, [])
                if (unlistedPages.length && addToNavigation) {
                  nav.push({
                    content: unlistedPagesHeading,
                    items: unlistedPages.map((page) => {
                      const title = 'navtitle' in page.asciidoc
                        ? page.asciidoc.navtitle
                        : (page.src.module === 'ROOT' ? '' : page.src.module + ':') + page.src.relative
                      return { content: title, url: page.pub.url, urlType: 'internal' }
                    }),
                    root: true,
                  })
                }
              })
            })
          })
      }
      
      function getNavEntriesByUrl (items = [], accum = {}) {
        items.forEach((item) => {
          if (item.urlType === 'internal') accum[item.url.split('#')[0]] = item
          getNavEntriesByUrl(item.items, accum)
        })
        return accum
      }
      
    • unpublish-unlisted-pages-extension.js

      module.exports.register = function ({ config }) {
        this.on('navigationBuilt', ({ contentCatalog }) => {
          contentCatalog.getComponents().forEach(({ versions }) => {
            versions.forEach(({ name: component, version, navigation: nav, url: defaultUrl }) => {
              const navEntriesByUrl = getNavEntriesByUrl(nav)
              const unlistedPages = contentCatalog
                .findBy({ component, version, family: 'page' })
                .filter((page) => page.out)
                .reduce((collector, page) => {
                  if ((page.pub.url in navEntriesByUrl) || page.pub.url === defaultUrl) return collector
                  return collector.concat(page)
                }, [])
              if (unlistedPages.length) unlistedPages.forEach((page) => delete page.out)
            })
          })
        })
      }
      
      function getNavEntriesByUrl (items = [], accum = {}) {
        items.forEach((item) => {
          if (item.urlType === 'internal') accum[item.url.split('#')[0]] = item
          getNavEntriesByUrl(item.items, accum)
        })
        return accum
      }
      
  3. В плейбук добавьте код регистрации указанных расширений:

    # ...
    antora:
      extensions:
        - require: ./extensions/page-unpublish-tag-extension.js
        - require: ./extensions/unlisted-pages-extension.js
        - require: ./extensions/unpublish-unlisted-pages-extension.js
    
  4. В начало страниц, которые нужно скрыть из публичной документации, добавьте код установки атрибута page-unpublish:

    [#private-page-1]
    = Private Page 1
    ifdef::is-dev[]:page-unpublish:endif::is-dev[]
    
    // ...
    
  5. Соберите документацию:

    make clean && make prod
    

    В терминал будет выведено несколько предупреждений о том, что некоторые файлы исключены из результатов сборки. Это именно то, чего мы и добивались.

Заключение

Ждём решения Issue 1080. Пока решения нет, актуальную версию расширений вы можете найти в документации Antora, раздел Extend Antora / Antora Extensions / Extension Use Cases.