Mosaica


Описание приложения

Тестовые новости

Карусель изображений

Mosaica (0.3.3)

Небольшое приложение на Node.js + Express для просмотра и редактирования AsciiDoc-документов, хранящихся в файлах.

Боевой контент, медиа и mosaica.site.json теперь живут только в отдельном site-repo. Сам движок больше не разворачивается как самостоятельный сайт: в корне этого репозитория нет папки content/, а пример полного site-repo вынесен в example_website/.

Что умеет

  • открывает AsciiDoc-документы из папки content/

  • показывает дерево файлов

  • создает, удаляет и перемещает файлы/папки прямо из компонента дерева

  • редактирует AsciiDoc в браузере

  • сохраняет изменения обратно в файлы

  • поддерживает авторизацию пользователей без базы данных

  • скрывает и показывает части документов по правам доступа пользователя

  • защищает редактор и файловый API ключами доступа

  • позволяет управлять пользователями, группами и ключами доступа через встроенную админ-страницу

  • хранит пользователей в SECURITY.JSON, а серверные сессии в SESSIONS.JSON

  • поддерживает внутренние компоненты через синтаксис webcomp::component-name[…​]

  • поддерживает файловое меню сайта и меню разделов через _menu.json

  • читает метаданные документов :title:, :author:, :published:, :tags: и :summary:

  • индексирует новостные разделы через news.config.json и news-tags.json

  • отдает JSON API новостей с фильтрацией по иерархическим тегам и сортировкой по дате

  • рендерит управляемую ленту новостей через webcomp::news-feed[…​][] с пагинацией или бесконечной подгрузкой

  • позволяет администратору подключать и обслуживать git-репозиторий сайта из интерфейса

  • позволяет редактировать и переключать темы оформления сайта из интерфейса

Структура проекта

  • example_website/ — пример отдельного site-repo с content/, content/media/, chart/, mosaica.site.json и workflow развертывания

  • docs/text-style-system.adoc — спецификация текстовых стилей, их наследования и соответствия AsciiDoc

  • SECURITY.JSON — пользователи, группы и ключи доступа

  • SESSIONS.JSON — файловое хранилище активных серверных сессий

  • THEMES.JSON — наборы тем оформления сайта и активная тема

  • src/ — сервер и клиентские исходники

  • public/ — собранные браузерные бандлы

  • dist/ — собранный серверный код

  • test/ — автоматические smoke/regression тесты

Как это работает

  • в развернутом site-repo сайт стартует с content/index.adoc

  • служебные страницы движка больше не лежат в content/; используются внутренние URL /editor, /access, /site-repository, /themes

  • дерево файлов показывает содержимое content/

  • все пути в ссылках и редакторе указываются относительно content/

  • префикс content/ в ссылках писать не нужно

  • content/section/index.adoc открывается по красивому адресу /section

  • отдельные AsciiDoc-страницы вроде content/page.adoc открываются как /page.adoc

  • вложенные AsciiDoc-страницы вроде content/news/release-0-3-0.adoc открываются как /news/release-0-3-0

  • html/css/js/svg и другие не-.adoc файлы внутри content/ отдаются напрямую по URL, например content/vibeCoding/chess.html будет доступен как /vibeCoding/chess.html

  • медиа сайта рекомендуется хранить в content/media/ и использовать по пути /media/…​

  • директивы с путями к файлам (например include::) должны быть относительными к текущему документу: include::./temp.adoc[]

  • заголовок вкладки браузера можно задать атрибутом документа :title:

  • документы могут задавать автора, дату публикации, теги и summary через :author:, :published:, :tags: и :summary:

  • если у новости не задан :published:, движок берет время изменения файла из файловой системы

  • новостной раздел описывается через news.config.json, а таксономия тегов хранится в news-tags.json

  • публикации внутри новостного раздела можно хранить как файлы news/*/.adoc любой глубины, включая каталоги с точками в имени, например news/1.1/release.adoc

  • также сохраняется совместимость с прежней схемой news/**/index.adoc

  • компонент webcomp::news-feed[…​][] рендерит ленту новостей сервером и добавляет клиентские фильтры по тегам, сортировку, постраничную навигацию или infinite scroll

  • шаблон карточки новости в ленте и обертка всей ленты задаются отдельными .adoc-шаблонами

  • пользователи, группы и права доступа хранятся в SECURITY.JSON

  • каталог ключей доступа также хранится в SECURITY.JSON

  • после входа сервер сохраняет сессию в SESSIONS.JSON и выдает cookie

  • блоки доступа рендерятся только для пользователей с нужным ключом доступа

  • внутри webcomp::access[…​] директивы include::…​[] обрабатываются как отдельный блок (включая случай inline-записи)

  • редактор, дерево и файловый API доступны только авторизованным пользователям с одним из ключей: edit, content.edit, editor, admin

  • управление доступом доступно авторизованным пользователям с одним из ключей: admin.users, admin.groups, admin.keys, admin

  • управление git-репозиторием сайта доступно авторизованным пользователям с одним из ключей: admin.git, admin

  • управление темами сайта доступно авторизованным пользователям с одним из ключей: admin.themes, admin

  • при открытии документа сервер ищет шаблон с именем из .env переменной template_name

  • шаблон ищется в папке документа и далее вверх по дереву до корня content/

  • content:: внутри шаблона заменяется содержимым открываемого документа до финального рендера AsciiDoc

  • ближайший шаблон новости может использовать слоты news-title::, news-author::, news-published::, news-tags::, news-summary:: и news-link::[Текст]

  • карусель изображений задаётся блоком carousel:: …​ [thumbs=true, captions=true] и рендерится в mosaica-carousel

  • кнопки редактирования (иконка карандаша) добавляются на этапе серверного рендера отдельно для шаблона и отдельно для текущей страницы

  • меню:

  • глобальное меню берется из content/_menu.json

  • меню раздела ищется как ближайший _menu.json вверх от текущего документа до корня content/

  • пункты меню можно ограничивать по ключу доступа через accessKey

  • сам движок ожидает, что content/ и mosaica.site.json будут наложены поверх workspace на этапе site-deploy; в этом репозитории они сохранены только внутри example_website/ как образец

Компоненты

Авторизация

webcomp::auth[]

Только вход, без регистрации:

webcomp::auth[mode=login-only]

Дерево файлов

webcomp::file-tree[]

AsciiDoc-редактор

webcomp::asciidoc-editor[]

Меню

Вертикальное (по умолчанию):

webcomp::menu[type=site][]

Горизонтальное:

webcomp::menu[type=site,layout=horizontal][]

Меню раздела:

webcomp::menu[type=section][]

Меню раздела (горизонтальное):

webcomp::menu[type=section,layout=horizontal][]

Блок доступа

webcomp::access[key=demo][
Содержимое увидят только пользователи с ключом `demo`
]

Лента новостей

webcomp::news-feed[
  path=news,
  item-template=./news-item.adoc,
  template=./news-feed.adoc,
  sort=desc,
  mode=paged,
  page-size=10
][]

Поддерживаемые параметры:

  • path — путь к новостному разделу с news.config.json

  • item-template — шаблон одной новости в ленте, путь относительно текущей страницы

  • template — шаблон всей ленты, путь относительно текущей страницы

  • tags — стартовый фильтр тегов через запятую

  • matchany или all

  • sortdesc или asc

  • modepaged или infinite

  • page-size — размер страницы или пачки подгрузки

  • controls — видимые элементы управления через |, например sort|tags|page-size

Особенности отображения:

  • поле режима показа в интерфейсе не выводится

  • в mode=infinite настройка page-size не показывается, даже если указана в controls

Шаблон элемента ленты может использовать слоты:

  • news-title::

  • news-author::

  • news-published::

  • news-tags::

  • news-summary::

  • news-link::[Текст ссылки]

Шаблон всей ленты может использовать слоты:

  • news-items::

  • news-total::

  • news-count::

  • news-offset::

  • news-limit::

  • news-active-tags::

  • news-sort::

  • news-match::

  • news-path::

Управление доступом

Служебная страница движка: /access

На ней доступны компоненты:

webcomp::security-admin[]

и

webcomp::site-repo-admin[]

Репозиторий сайта

webcomp::site-repo-admin[]

Компонент показывает:

  • настройки репозитория сайта

  • trackedPaths для sparse checkout сайта

  • статус локального git-рабочего дерева

  • синхронизацию контента из git в рабочую копию сайта

  • создание commit и push в origin

Управление репозиторием ограничено путями из trackedPaths, то есть используется только для контента и настроек сайта. Медиа хранится внутри content/media/. Для push по HTTPS в окружении сайта должен быть настроен SITE_REPO_GIT_TOKEN. При необходимости можно также задать SITE_REPO_GIT_USERNAME. Дополнительно движок держит локальный untracked-кэш .mosaica-site-repo.local.json, чтобы sync не терял repoUrl и служебные поля, если tracked mosaica.site.json в git содержит только deploy-настройки.

Встроенная служебная страница движка: /site-repository

Темы сайта

webcomp::theme-admin[]

Компонент показывает:

  • список тем

  • выбор активной темы

  • редактирование стилевых свойств по сущностям верстки

  • сохранение текущей темы или новой копии

Темы хранятся отдельно в THEMES.JSON. Активная тема применяется ко всем серверно рендеримым страницам сайта.

Модель сущностей верстки и JSON-схема темы описаны в docs/theme-style-system.adoc.

Встроенная служебная страница движка: /themes

Карусель изображений

carousel::
./media/carousel/slide-1.svg|Подпись 1
./media/carousel/slide-2.svg|Подпись 2
./media/carousel/slide-3.svg|Подпись 3
[thumbs=true, captions=true, fit=contain, autoplay=true, interval=5000, lightbox=true]

Поддерживаемые параметры:

  • thumbs — показывать миниатюры

  • captions — показывать подпись активного кадра

  • fitcontain или cover

  • autoplay — автопрокрутка кадров

  • interval — интервал автопрокрутки в миллисекундах

  • lightbox — открывать увеличенный просмотр по клику на изображение

Пример mosaica.site.json:

{
  "repoUrl": "https://github.com/TulaArchery/mosaica_content.git",
  "branch": "main",
  "trackedPaths": ["content", "mosaica.site.json"],
  "engineRepoUrl": "https://github.com/TulaArchery/mosaica.git",
  "engineRef": "dockerfile-helm",
  "releaseName": "mosaica",
  "imageName": "mosaica",
  "domain": "mosaica.silinmo.ru",
  "namespace": "mosaica",
  "templateName": "template.adoc",
  "ingressClassName": "nginx",
  "clusterIssuer": "letsencrypt-prod",
  "tlsSecretName": "mosaica-tls",
  "contentSyncMode": "overlay",
  "commitAuthorName": "Mosaica Admin",
  "commitAuthorEmail": "admin@example.com"
}

Шаблонный слот

Шапка

content::

Подвал

Новости и метаданные

Базовые атрибуты документа:

= Заголовок новости
:title: Заголовок вкладки
:author: Имя автора
:published: 2026-06-14 10:30
:tags: 1.1, 2.a
:summary: Короткий анонс новости

Если :published: не задан, для новости используется время изменения файла.

Новостной раздел можно описать так:

{
  "tagsFile": "./news-tags.json",
  "defaultSort": "desc",
  "defaultPageSize": 10
}

Таксономия тегов хранится в news-tags.json. Теги можно объединять в дерево: если фильтровать по 1, в выборку попадут материалы с 1, 1.1, 1.2 и любыми другими потомками.

Пример шаблона страницы новости:

[.news-page-title]
news-title::

[.news-page-meta]
Автор: news-author::

[.news-page-meta]
Дата публикации: news-published::

[.news-page-meta]
Теги: news-tags::

content::

Пример шаблона элемента ленты:

[.news-feed-item-title]
news-title::

[.news-feed-item-meta]
Автор: news-author::

[.news-feed-item-meta]
Дата: news-published::

news-summary::

news-link::[Читать]

Пример обертки всей ленты:

[.news-feed-heading]
Лента новостей

[.news-feed-meta]
Всего публикаций: news-total::

news-items::

JSON API новостей:

  • GET /api/news?path=news

  • GET /api/news?path=news&tags=1

  • GET /api/news?path=news&tags=1,2&match=all

  • GET /api/news?path=news&sort=asc&limit=5&offset=10

  • GET /api/news/render?doc=news/index.adoc&path=news&template=./news-feed.adoc&itemTemplate=./news-item.adoc

Титул вкладки

Чтобы управлять названием вкладки браузера, задайте атрибут :title: в документе:

= Заголовок документа
:title: Мое название вкладки

Формат _menu.json

Файл может быть массивом пунктов или объектом вида { "items": […​] }.

Поддерживаемые поля пункта:

  • title — заголовок пункта

  • path — путь к документу (если не указан, пункт будет без ссылки)

  • accessKey — ключ доступа для ограничения видимости пункта

  • children — массив дочерних пунктов

Правила для path:

  • ./…​ и ../…​ считаются относительно папки, где лежит текущий _menu.json

  • остальные пути считаются относительно корня content/

Требования

  • Node.js >= 22

Установка

npm install

Опционально можно создать .env из .env.example.

Важно: этот репозиторий больше не содержит deployable content/ в корне. Для локального запуска нужно передать TREE_BASE_DIR, SECURITY_FILE и SESSION_FILE, указывающие на отдельный site-repo или на копию example_website/, развернутую вне корня движка.

Запуск

Режим разработки

npm run dev

Сборка

npm run build

Запуск production-сборки

npm start

Тесты

npm test

Развертывание

Схема развертывания была разработана Игорем Васильевым aka igorjum.

Docker

Репозиторий движка сам по себе больше не собирается в deployable image. Dockerfile ожидает, что поверх workspace уже наложены content/ и mosaica.site.json из site-repo. Это делает workflow site-repo перед docker build.

Приложение хранит контент, пользователей и сессии в файлах. Для runtime-контейнера используются внешние пути:

  • TREE_BASE_DIR — директория с content/

  • SECURITY_FILE — путь к SECURITY.JSON

  • SESSION_FILE — путь к SESSIONS.JSON

Kubernetes и Helm

Helm chart теперь живет в site-repo. В этом репозитории он сохранен только как часть примера example_website/chart/mosaica.

Chart сайта:

  • поднимает 1 реплику

  • создает PersistentVolumeClaim

  • хранит content, SECURITY.JSON и SESSIONS.JSON в одном persistent volume

  • при старте берет content/ и mosaica.site.json из образа, уже собранного site-repo workflow

  • публикует ingress для домена, заданного в mosaica.site.json

Если у кластера есть свой ingress class или cert-manager, это настраивается через chart/mosaica/values.yaml внутри site-repo, например:

ingress:
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
  tls:
    - hosts:
        - mosaica.silinmo.ru
      secretName: mosaica-tls

GitHub Actions deploy

У движка больше нет собственного deploy workflow. Развертывание выполняется только из site-repo, пример которого лежит в example_website/.github/workflows/deploy.yml.

Site-repo workflow делает следующее:

  • читает mosaica.site.json

  • забирает движок из engineRepoUrl

  • накладывает поверх него content/ и mosaica.site.json

  • собирает Docker-образ

  • использует локальный chart/mosaica из site-repo

  • пушит его в registry

  • выполняет helm upgrade --install

Нужные GitHub Secrets в site-repo:

  • REGISTRY_URL

  • REGISTRY_USERNAME

  • REGISTRY_PASSWORD

  • ENGINE_REPO_TOKEN

  • SITE_REPO_GIT_TOKEN — чтобы кнопка Push внутри сайта могла отправлять изменения обратно в GitHub

  • SITE_REPO_GIT_USERNAME — опционально, если для HTTPS-аутентификации нужен явный username

ENGINE_REPO_TOKEN должен иметь как минимум Contents: Read-only к приватному репозиторию движка. Доступ к Kubernetes workflow берет с self-hosted runner через локальный /home/captgreen/.kube/config. При наличии SITE_REPO_GIT_TOKEN workflow создает в namespace secret site-repo-git-auth, а chart автоматически прокидывает его в контейнер как SITE_REPO_GIT_TOKEN и SITE_REPO_GIT_USERNAME.

Отдельный repo сайта

Поддерживается схема из двух репозиториев:

  • mosaica.git — движок

  • mosaica_content.git — контент, content/media/, chart/, mosaica.site.json и workflow развертывания

В example_website/ лежит готовый пример такого site-repo, который можно скопировать в новый репозиторий почти без изменений.

В отдельном site-repo workflow:

  1. читает mosaica.site.json

  2. клонирует движок из engineRepoUrl

  3. накладывает поверх него content/ и mosaica.site.json

  4. собирает образ сайта

  5. использует локальный chart/mosaica из site-repo

  6. публикует образ и делает helm upgrade --install

Для site-repo рекомендуется режим contentSyncMode = overlay, чтобы файлы из git обновлялись на сайте, а новые локальные файлы, которых нет в репозитории, не удалялись.

Пользовательский сценарий

Что делает владелец сайта от создания repo до появления домена:

  1. Создает новый репозиторий сайта на основе example_website/.

  2. Заполняет content/ своими страницами, шаблонами и файлами в content/media/.

  3. Настраивает mosaica.site.json: repoUrl, domain, namespace, при необходимости engineRef.

  4. Добавляет в Secrets нового repo:

    • REGISTRY_URL

    • REGISTRY_USERNAME

    • REGISTRY_PASSWORD

    • ENGINE_REPO_TOKEN

  5. Подключает self-hosted runner с доступом к Docker, kubectl, helm и локальному /home/captgreen/.kube/config.

  6. Делает push в main.

  7. Workflow подтягивает движок mosaica, накладывает контент сайта, собирает образ и выполняет helm upgrade --install через локальный chart/mosaica этого site-repo.

  8. После завершения workflow сайт открывается по домену из domain.

Основные маршруты

  • / — открывает content/index.adoc

  • /section — открывает content/section/index.adoc

  • /page.adoc — открывает content/page.adoc

  • /news/release-0-3-0 — открывает content/news/release-0-3-0.adoc или content/news/release-0-3-0/index.adoc

  • /editor — открывает страницу редактора и требует права доступа

  • /health — health check, возвращает status, name, version и gitSha

  • /api/tree — дерево файлов, требует права доступа

  • /api/tree/create — создать файл/папку, требует права доступа

  • /api/tree/delete — удалить файл/папку, требует права доступа

  • /api/tree/move — переместить/переименовать файл/папку, требует права доступа

  • /api/file — чтение и сохранение файлов, требует права доступа

  • /api/asciidoc/render — рендер AsciiDoc в HTML

  • /api/news — индекс новостей в JSON (path, tags, match, sort, limit, offset)

  • /api/news/render — готовый HTML ленты новостей (doc, path, itemTemplate, template, tags, match, sort, limit, offset, mode)

  • /api/menu — отдает меню сайта/раздела (type=site|section)

  • /api/auth/session — текущая сессия пользователя

  • /api/auth/register — регистрация

  • /api/auth/login — вход

  • /api/auth/logout — выход

  • /api/auth/profile — обновление профиля

  • /api/security/store — текущее состояние пользователей, групп и ключей, требует admin-доступ

  • /api/security/users — создание пользователя, требует admin-доступ

  • /api/security/users/:email — изменение или удаление пользователя, требует admin-доступ

  • /api/security/groups — создание группы, требует admin-доступ

  • /api/security/groups/:name — изменение или удаление группы, требует admin-доступ

  • /api/security/keys — создание ключа доступа, требует admin-доступ

  • /api/security/keys/:name — изменение или удаление ключа доступа, требует admin-доступ

  • /api/site-repo — настройки и статус git-репозитория сайта, требует admin.git

  • /api/site-repo/settings — сохранить настройки mosaica.site.json, требует admin.git

  • /api/site-repo/connect — инициализировать локальное git-подключение, требует admin.git

  • /api/site-repo/sync — подтянуть контент из git в сайт, требует admin.git

  • /api/site-repo/commit — создать commit из изменений сайта, требует admin.git

  • /api/site-repo/push — отправить локальные коммиты в origin, требует admin.git

  • /api/themes — список тем и активная тема

  • /api/themes/css — CSS активной темы

  • /api/themes/save — сохранить тему и при необходимости сделать её активной, требует admin.themes

Ограничения

  • приложение работает только с файлами внутри content/

  • выход за пределы базовой директории запрещен

  • бинарные файлы не поддерживаются

  • большие файлы могут быть отклонены по лимиту размера

  • данные пользователей и прав хранятся только в SECURITY.JSON

  • активные сессии хранятся только в SESSIONS.JSON

  • при удалении группы или ключа сервер не даст сломать связанные записи: сначала нужно снять зависимости

Пример ссылки внутри контента

link:/editor[Открыть редактор]

или

link:index.adoc[На главную]

Пример содержимого SECURITY.JSON

{
  "users": [
    {
      "email": "user@example.com",
      "username": "user",
      "passwordHash": "scrypt:...",
      "groups": ["demo", "Editor"]
    }
  ],
  "groups": [
    {
      "name": "Editor",
      "keys": ["edit"]
    },
    {
      "name": "demo",
      "keys": ["demo"]
    },
    {
      "name": "Admin",
      "keys": ["admin.git", "admin.users", "admin.groups", "admin.keys", "edit"]
    }
  ],
  "keys": [
    {
      "name": "admin.git",
      "title": "Git-репозиторий сайта",
      "description": "Подключение, синхронизация и отправка изменений сайта в git"
    },
    {
      "name": "edit",
      "title": "Редактирование",
      "description": "Доступ к редактору и файловым операциям"
    },
    {
      "name": "admin.users",
      "title": "Пользователи",
      "description": "Управление пользователями"
    }
  ]
}

© Michail O Silin aka captGreen, 2026