Уже несколько лет мы сотрудничаем с Уральским федеральным университетом, предоставляя бесплатную лицензию для учебного процесса. Для нас это важная инвестиция в будущее индустрии: нам хочется, чтобы студенты ещё в вузе осваивали инструменты, которые реально используются в компаниях. Если вы тоже преподаёте документирование или смежные дисциплины и хотите использовать Документерру в обучении — мы с радостью поддержим вашу кафедру и предоставим лицензию, просто напишите нам на 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, а isSucceeded — true, архив готов.
Я использовал альтернативный подход — просто проверял доступность файла в определенной папке файлового хранилища портала. Для моей задачи этого было достаточно, так как мне не нужна была информация о прогрессе выполнения.
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:
По работе с контентом:
- Руководство пользователя Документерры — начало работы с платформой
- Экспорт в WebHelp
- Глобальный поиск и замена — тот самый инструмент, который спас меня с кириллическими путями
Благодарность команде Документерры
Рад, что для ведения курса по технической документации удалось найти современную и активно развивающуюся платформу. Для меня важно, чтобы студенты осваивали инструменты, которые реально используются в индустрии — и Документерра отлично подходит для этого. Спасибо команде за продукт и за то, что поддержка быстро помогала с вопросами реализации моей задумки.
* * *
Спасибо Игорю Олеговичу за интересный кейс! Оказывается, Документерра может работать как Static Site Generator — нужно лишь немного изобретательности и знание API.
Еще раз напомним: мы всегда открыты к сотрудничеству с учебными заведениями. Если вы хотите, чтобы ваши студенты осваивали документирование не «в теории», а на реальных промышленных инструментах и могли пробовать такие же интересные технические подходы на практике — напишите нам на success@documenterra.ru. Мы поможем во всём разобраться и предоставим вашей кафедре бесплатную лицензию.
Желаем всем студентам учиться на современных решениях, которые станут их реальным преимуществом в будущей карьере!



