Static Site Generator своими руками: готовим из API Документерры, nginx и щепотки bash | Документерра

Static Site Generator своими руками: готовим из API Документерры, nginx и щепотки bash

Эльмира Аббясова
Эльмира АббясоваКонтент-эксперт
Эльмира Аббясова
Эльмира Аббясова
Контент-эксперт

Рассказываю о сложных вещах простым и понятным языком, превращая сложный контент в интересные и полезные материалы для читателей.
15+ лет переводов технических текстов, 5+ лет в сфере технического писательства.

20.03.2026
15 минут

Всем привет! Мы — команда Документерры, платформы для создания сайтов документации и баз знаний, сегодня поговорим про статические сайты и студентов.

Static Site Generator своими руками: готовим из API Документерры, nginx и щепотки bash

Уже несколько лет мы сотрудничаем с Уральским федеральным университетом, предоставляя бесплатную лицензию для учебного процесса. Для нас это важная инвестиция в будущее индустрии: нам хочется, чтобы студенты ещё в вузе осваивали инструменты, которые реально используются в компаниях. Если вы тоже преподаёте документирование или смежные дисциплины и хотите использовать Документерру в обучении — мы с радостью поддержим вашу кафедру и предоставим лицензию, просто напишите нам на success@documenterra.ru.

Итак, к делу — недавно Игорь Олегович Ситников, доцент Департамента информационных технологий и автоматики ИРИТ-РТФ и преподаватель курса по созданию технической документации, поделился с нами интересным кейсом. Он превратил Документерру в подобие Static Site Generator для публикации лабораторных руководств. Иногда привычные инструменты раскрываются с неожиданной стороны, даже для нас. 🙂

Этот опыт пригодится всем, кто хочет перестать обновлять доки руками и ищет способ автоматизировать доставку до читателя. Передаём слово Игорю Олеговичу!

Как Документерра вписалась в наш учебный процесс

Мы в УрФУ уже несколько лет используем Документерру в учебном процессе. Для студентов это прежде всего профессиональная среда, где они на практике осваивают современные подходы к документированию: работу с условиями, использование переменных и, самое главное, технологию единого источника (Single Sourcing). Платформа позволяет им пройти весь цикл — от написания текста до публикации готового портала.
Ситников Игорь Олегович
доцент Департамента информационных технологий и автоматики ИРИТ-РТФ

Долгое время мы использовали систему классическим способом — как готовый хостинг. Студенты создавали свои проекты, вели базу знаний и публиковали документацию прямо на портале Документерры. Это закрывало 99% наших задач. Но недавно я задался вопросом: а можно ли пойти дальше?

Проблема: как обновлять руководства для 30 компьютеров

Одна из задач, которую мне постоянно приходится решать — поддержание актуальности руководств по лабораторным работам. Раньше схема выглядела так: редактирую документ в Word, экспортирую в PDF, затем вручную выкладываю файл на Samba-сервер. В разных лабораторных используются и Windows, и Ubuntu, поэтому выбрал SMB как универсальный протокол. Затем студенты самостоятельно скачивают себе руководства с сервера.

Схема работала, но была неповоротливой. Любое исправление опечатки превращалось в цепочку действий: открыть Word, исправить, экспортировать, скопировать на сервер, убедиться, что  студенты скачали новую версию.

Когда я начал использовать портал Документерры, перенёс туда свои Markdown-файлы через возможность импорта. Сначала по привычке экспортировал в PDF. Потом попробовал экспорт в WebHelp, скачал архив, открыл index.html в браузере — и понял, что это совсем другой уровень. Навигация, поиск, нормальное отображение на любом экране. И главное — обновление занимает секунды, а не десятки минут.

Тогда появилась очевидная идея: поднять статический сайт под nginx.

Шаг 1. Docker + nginx — базовая инфраструктура

Чтобы не возиться с установкой и настройкой nginx на сервере, я поднял контейнер из официального образа на Docker Hub.

Сначала пошёл по простому пути: собрал свой Docker-образ, в который сразу «вшил» папку с руководством из экспортированного WebHelp. Работало отлично, но быстро понял недостаток — каждое обновление документации требовало пересборки образа.

Тогда переделал на volume: HTML-файлы лежат в отдельной папке на хосте, а контейнер просто монтирует её:

docker run -d \
  --name docs-server \
  -p 8080:80 \
  -v /home/docs/webhelp:/usr/share/nginx/html:ro \
  nginx:alpine

Теперь достаточно обновить содержимое папки /home/docs/webhelp — и nginx сразу отдаёт новую версию. Осталось автоматизировать само обновление.

Шаг 2. Изучаем API Документерры

Я погрузился в документацию по API и быстро понял, что всё можно автоматизировать. Отдельно хочу отметить техподдержку Документерры — ребята помогали разбираться в нюансах, оперативно отвечали на вопросы и даже исправили пару неточностей в документации по моим замечаниям. В итоге собрал рабочую цепочку вызовов.

Получение информации о проектах

Первым делом нужно понять, какие проекты и публикации доступны. Метод GET /projects возвращает полный список:

curl -u "login:API_KEY" \
  "https://PORTALNAME.documenterra.net/api/v1/projects"

Можно отфильтровать только публикации, добавив параметр types=publication:

curl -u "login:API_KEY" \
  "https://PORTALNAME.documenterra.net/api/v1/projects?types=publication"

Проверка конкретного проекта

Чтобы получить информацию о конкретном проекте или публикации, используем GET /projects/{externalId}:

curl -u "login:API_KEY" \
  "https://PORTALNAME.documenterra.net/api/v1/projects/lab-manual-project"

Ответ содержит всё необходимое: название, статус, дату последнего изменения.

Создание публикации

Если публикации ещё нет, создаём её методом POST /projects/{project-id}?action=publish:

curl -X POST -u "login:API_KEY" \
  -H "Content-Type: application/json" \
  "https://PORTALNAME.documenterra.net/api/v1/projects/lab-manual-project?action=publish" \
  -d '{
    "pubId": "lab-manual-pub",
    "pubName": "Руководство по лабораторным работам",
    "pubVisibility": "Private"
  }'

Параметры:

  • pubId — идентификатор публикации (латиницей, без пробелов)
  • pubName — человекочитаемое название (оно будет видно читателю в интерфейсе)
  • pubVisibility — видимость: Public, Restricted или Private

Если публикация уже существует, можно обновить её, чтобы не плодить публикации — для этого есть следующие параметры:

  • updatedPubId — ID существующей публикации
  • updateMode — режим обновления, например FullReplace для полной замены

Экспорт в WebHelp

Ключевой метод — POST /projects/{publication-id}?action=export. Он запускает экспорт публикации в нужный формат:

curl -X POST -u "login:API_KEY" \
  -H "Content-Type: application/json" \
  "https://PORTALNAME.documenterra.net/api/v1/projects/lab-manual-pub?action=export" \
  -d '{
    "format": "WebHelp",
    "outputFileName": "Storage/exports/lab-manual.zip"
  }'

Формат указывается в параметре format. Документерра поддерживает: WebHelp, PureHtml, Markdown, Pdf, Docx, Chm, Epub и другие.
Метод возвращает taskKey — идентификатор задачи:

{
  "taskKey": "85413f07c1644b12add45da9df56ec8b"
}

Отслеживание статуса задачи

В Документерре есть операции, которые могут занимать значительное время — экспорт, публикация, импорт. Для больших проектов на тысячи топиков экспорт может длиться десятки минут. Поэтому такие операции выполняются асинхронно: вы запускаете задачу, получаете её идентификатор (taskKey) и периодически проверяете статус.
Для проверки используем

GET /tasks/{task-key}:
curl -u "login:API_KEY" \
  "https://PORTALNAME.documenterra.net/api/v1/tasks/85413f07c1644b12add45da9df56ec8b"

Ответ:

{
  "isSucceeded": true,
  "isWorking": false,
  "maxOverallProgress": 100,
  "overallProgress": 100,
  "statusText": "Export finished.",
  "taskName": "Exporting Publication"
}

Когда isWorking станет false, а isSucceededtrue, архив готов.
Я использовал альтернативный подход — просто проверял доступность файла в определенной папке файлового хранилища портала. Для моей задачи этого было достаточно, так как мне не нужна была информация о прогрессе выполнения.

for i in {1..120}; do 
  if curl -u "login:API_KEY" -f -o /dev/null "${PORTAL}/resources/${EXPORT_FILE}"; then
    echo "Файл готов"
    break
  fi
  sleep 2
done

Скачивание архива

Экспортированный файл лежит в Storage по указанному пути. Скачиваем обычным curl:

curl -u "login:API_KEY" -o lab-manual.zip \
  "https://PORTALNAME.documenterra.net/resources/Storage/exports/lab-manual.zip"

Шаг 3. Собираем автоматизацию в скрипт

Объединяем все вызовы в один bash-скрипт:

#!/usr/bin/env bash

set -Eeuo pipefail

HOST="https://PORTALNAME.documenterra.net"
USER="USERNAME"
PASS="APIKEY"

CONTAINER="docker-lab-v"
PORT="80"

# args: <target_dir> <project_id>
if [ "${2:-}" = "" ]; then
  echo "Usage: $0 <target_dir: docker-lab|microk8s-lab|gitlab-lab> <project_id>"
  exit 2
fi

SITE_DIR="$1"

case "$SITE_DIR" in
  docker-lab|microk8s-lab|gitlab-lab) ;;
  *)
    echo "Invalid directory: $SITE_DIR. Allowed: docker-lab, microk8s-lab, gitlab-lab"
    exit 2
    ;;
esac

PROJECT_ID="$2"
PUB_ID="${PROJECT_ID#project-}"

FORMAT="WebHelp"
PRESET="Default"

OUT_PATH="Storage/${PUB_ID}.zip"
RES_URL="${HOST}/resources/${OUT_PATH}"

TMP_DIR="$(mktemp -d -t "${PUB_ID}".XXXXXX)"
TMP_ZIP="${TMP_DIR}/${PUB_ID}.zip"

cleanup() {
  rm -rf "$TMP_DIR" || true
}

# trap cleanup EXIT

DEST_DIR="docker-lab"
MICRO_K8S_DIR="microk8s-lab"
GITLAB_DIR="gitlab-lab"

SITE_CONF="./nginx.conf"  # specify custom file if needed

need() {
  command -v "$1" >/dev/null || {
    echo "Is required '$1'"
    exit 3
  }
}

need curl
need unzip
need awk
need docker
need timeout

ensure_site_dirs() {
  echo ">> Checking publication catalogs..."

  for d in "$DEST_DIR" "$MICRO_K8S_DIR" "$GITLAB_DIR"; do
    if [ ! -d "$d" ]; then
      echo ">> Creating a catalog '$d'"
      mkdir -p "$d"
    fi
  done

  if [ ! -f "$SITE_CONF" ]; then
    echo "Nginx configuration file not found: $SITE_CONF"
    exit 5
  fi
}

check_project() {
  echo ">> Project verification '${PROJECT_ID}'..."

  if curl -u "${USER}:${PASS}" --location -g -f \
    "${HOST}/api/v1/projects/${PROJECT_ID}" > /dev/null; then
    echo ">> The project has been found."
  else
    echo "The project '${PROJECT_ID}' was not found." >&2
    exit 10
  fi
}

check_publication_exists() {
  echo ">> Verifying the existence of a publication '${PUB_ID}'..."

  if curl -u "${USER}:${PASS}" --location -g -f \
    "${HOST}/api/v1/projects/${PUB_ID}" > /dev/null; then
    echo ">> The publication exists."
    return 0
  else
    echo ">> The publication does not exist."
    return 1
  fi
}

create_publication() {
  echo ">> Creating a new publication '${PUB_ID}'..."

  curl -u "${USER}:${PASS}" --location -g -f \
    --request POST "${HOST}/api/v1/projects/${PROJECT_ID}?action=publish" \
    -H "Content-Type: application/json" \
    -d "{
      \"pubId\": \"${PUB_ID}\",
      \"pubName\": \"${PUB_ID}\",
      \"isPublishOnlyReadyTopics\": false,
      \"outputTags\": [\"OnlineDoc\"],
      \"pubVisibility\": \"Private\"
    }"
}

update_publication() {
  echo ">> Updating an existing publication '${PUB_ID}'..."

  curl -u "${USER}:${PASS}" --location -g -f \
    --request POST "${HOST}/api/v1/projects/${PROJECT_ID}?action=publish" \
    -H "Content-Type: application/json" \
    -d "{
      \"updatedPubId\": \"${PUB_ID}\",
      \"pubName\": \"${PUB_ID}\",
      \"updateMode\": \"FullReplace\",
      \"isReplacePubScripts\": true,
      \"isReplacePubStyles\": true,
      \"isPublishOnlyReadyTopics\": false,
      \"outputTags\": [\"OnlineDoc\"],
      \"pubVisibility\": \"Private\"
    }"
}

start_export() {
  echo ">> Exporting a publication '${PUB_ID}' to '${OUT_PATH}'..."

  curl -u "${USER}:${PASS}" --location -g -f \
    --request POST "${HOST}/api/v1/projects/${PUB_ID}?action=export" \
    -H "Content-Type: application/json" \
    -d "{
      \"format\": \"${FORMAT}\",
      \"outputFileName\": \"${OUT_PATH}\",
      \"exportPresetName\": \"${PRESET}\"
    }"
}

wait_file() {
  echo ">> Waiting for the file to be ready: ${RES_URL}"

  for i in {1..120}; do
    if curl -u "${USER}:${PASS}" --location -g -f -o /dev/null "${RES_URL}"; then
      echo ">> The file is available (attempt $i)."
      return 0
    fi

    if [ $((i % 10)) -eq 0 ]; then
      echo ">> Attempt $i/120: the file is not available yet"
    fi

    sleep 2
  done

  echo "The file did not appear on time." >&2
  return 1
}

download_zip() {
  echo ">> Downloading the archive to ${TMP_ZIP}..."
  curl -fSL --user "${USER}:${PASS}" -o "${TMP_ZIP}" "${RES_URL}"
}

unpack_zip() {
  if [ -z "${SITE_DIR}" ] || [ "${SITE_DIR}" = "/" ]; then
    echo "Incorrect unpacking directory"
    exit 4
  fi

  mkdir -p "${SITE_DIR}"

  echo ">> Clearing the catalog ${SITE_DIR}..."
  shopt -s dotglob nullglob
  rm -rf "${SITE_DIR}/"*
  shopt -u dotglob nullglob

  echo ">> Unpacking in ${SITE_DIR}..."
  unzip -o -q "${TMP_ZIP}" -d "${SITE_DIR}"

  echo ">> Unpacking is complete."
  echo ">> Contents of the catalog:"
  ls -la "${SITE_DIR}/"
}

run_nginx() {
  echo ">> Starting the container ${CONTAINER} on port ${PORT}"

  local LAB_DIR_ABS K8S_DIR_ABS GITLAB_DIR_ABS

  LAB_DIR_ABS="$(cd "$DEST_DIR" && pwd -P)"
  K8S_DIR_ABS="$(cd "$MICRO_K8S_DIR" && pwd -P)"
  GITLAB_DIR_ABS="$(cd "$GITLAB_DIR" && pwd -P)"

  if sudo docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then
    sudo docker stop "$CONTAINER" || true
    sudo docker rm -f "$CONTAINER" || true
  fi

  sudo docker run --restart=always -d \
    --name "$CONTAINER" \
    -p "${PORT}:80" \
    -v "${LAB_DIR_ABS}:/usr/share/nginx/html/docker-lab:ro" \
    -v "${K8S_DIR_ABS}:/usr/share/nginx/html/microk8s-lab:ro" \
    -v "${GITLAB_DIR_ABS}:/usr/share/nginx/html/gitlab-lab:ro" \
    nginx:alpine

  timeout 15s bash -c \
    "until sudo docker exec \"$CONTAINER\" test -d /etc/nginx/conf.d; do sleep 0.2; done"

  sudo docker cp "$SITE_CONF" "$CONTAINER":/etc/nginx/conf.d/sites.conf
  sudo docker exec "$CONTAINER" rm -f /etc/nginx/conf.d/default.conf
  sudo docker exec "$CONTAINER" nginx -t
  sudo docker exec "$CONTAINER" nginx -s reload

  echo "OK -> http://127.0.0.1:${PORT}/${SITE_DIR}/"
}

ensure_site_dirs
check_project

echo ">> Attempting to update publication (will create if not exists)..."

if check_publication_exists; then
  update_publication
else
  create_publication
fi

start_export
wait_file
download_zip
unpack_zip
run_nginx

Скрипт использует файл конфигурации nginx. Вот его содержимое — сохраните как nginx.conf в той же папке, где лежит скрипт:

server {
  listen 80 default_server;
  server_name _;
  charset utf-8;

  root /usr/share/nginx/html;
  index index.html;

  location /docker-lab/ {
    alias /usr/share/nginx/html/docker-lab/;
    try_files $uri $uri/ /docker-lab/index.html;
  }
  location /microk8s-lab/ {
    alias /usr/share/nginx/html/microk8s-lab/;
    try_files $uri $uri/ /microk8s-lab/index.html;
  }
  location /gitlab-lab/ {
    alias /usr/share/nginx/html/gitlab-lab/;
    try_files $uri $uri/ /gitlab-lab/index.html;
  }
}

Директива charset utf-8 важна для корректного отображения кириллицы.
Весь процесс — от запуска скрипта до появления обновлённой документации в браузерах студентов — занимает около 30 секунд. Но главное даже не скорость, а то, что процесс полностью автоматизирован: запустил скрипт и пошёл дальше, не нужно следить, копировать, проверять.

Шаг 4. Решаем проблему nginx с кириллическими путями

При экспорте одного из руководств столкнулся с неожиданной проблемой: все картинки пропали. Руководство было создано импортом из большого Word-документа с множеством изображений.

Погрузился в анализ HTML и нашёл причину. Все импортированные картинки попали в папку с названием «Импортировано». В HTML ссылки на них выглядели так:

<img src="/resources/Storage/project-test/%D0%98%D0%BC%D0%BF%D0%BE%D1%80%D1%82%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BE/img001.png">

Вероятно, проблема была связана с тем, как Nginx воспринимает URL-encoded кириллицу в пути. Вместо того, чтобы разбираться с конфигурацией Nginx, я выбрал более простое решение – переименовать папку «Импортировано» в «Import». И тут очень помогла функция глобальной замены в Документерре.

Глобальная замена — это мощный инструмент для работы с контентом по всему порталу. Она умеет:

  • Искать и заменять текст, ссылки на топики, ссылки на файлы
  • Работать со стилями, скриптами, ToDo-элементами, ключевыми словами для индекса
  • Использовать регулярные выражения для сложных паттернов поиска

Когда я переименовал папку в файловом менеджере, система автоматически предложила обновить все ссылки на файлы из этой папки. Один клик — и все пути в HTML обновились. После замены картинки появились.

Совет: Используйте латиницу для имен файлов и папок с самого начала проекта — это стандарт для статического хостинга.

Что в итоге получилось

Сейчас схема выглядит так:


Что это даёт на практике:

  • Никакой путаницы в папках. Процесс теперь всегда одинаковый: скрипт не забудет скопировать файл и не перепутает директории, как это часто бывает при ручном переносе.
  • Все правки — в одном месте. Вся работа идет только в редакторе Документерры. Не нужно следить за тем, чтобы «версия на сервере» совпадала с «версией в облаке».
  • Новые руководства добавляются за 5 минут. Сейчас по такой схеме работают уже три лабы. Если завтра понадобится четвертая — это вопрос нескольких минут, а не целого вечера настройки.
  • Студентам не нужно ничего скачивать. Чтобы увидеть обновленный текст, им достаточно просто нажать F5 в браузере. Больше никаких устаревших PDF-файлов.

Скрипт можно запускать вручную или автоматизировать через cron — зависит от того, как часто обновляется документация.

Что почитать

В процессе работы над этой автоматизацией я активно пользовался документацией. Привожу ссылки на материалы, которые оказались наиболее полезными — если захотите повторить что-то подобное, начните отсюда.
По API:

По работе с контентом:

Благодарность команде Документерры

Рад, что для ведения курса по технической документации удалось найти современную и активно развивающуюся платформу. Для меня важно, чтобы студенты осваивали инструменты, которые реально используются в индустрии — и Документерра отлично подходит для этого. Спасибо команде за продукт и за то, что поддержка быстро помогала с вопросами реализации моей задумки.
И.О. Ситников
к.т.н., доцент


* * *


Спасибо Игорю Олеговичу за интересный кейс! Оказывается, Документерра может работать как Static Site Generator — нужно лишь немного изобретательности и знание API.

Еще раз напомним: мы всегда открыты к сотрудничеству с учебными заведениями. Если вы хотите, чтобы ваши студенты осваивали документирование не «в теории», а на реальных промышленных инструментах и могли пробовать такие же интересные технические подходы на практике — напишите нам на success@documenterra.ru. Мы поможем во всём разобраться и предоставим вашей кафедре бесплатную лицензию.

Желаем всем студентам учиться на современных решениях, которые станут их реальным преимуществом в будущей карьере!

Нажимая кнопку, вы соглашаетесь с условиями обработки cookie-файлов и ваших данных о поведении на сайте, необходимых для аналитики. Запретить обработку cookie-файлов вы можете через настройки браузера.