Руководство по установке и администрированию
Для того, кто ставит сайт на сервер и ведёт его содержимое. Ниже — вся схема: установка, настройка, обновление и работа в админке.
Как устроен сайт
Сайт — это ASP.NET Core (Razor Pages) на .NET 10, без базы данных и без JavaScript на публичных страницах. Весь контент лежит файлами в git-репозитории: репозиторий одновременно и база данных, и бэкап, и механизм обновления.
- Контент — каталог
Content/: структура сайта, тексты, данные разделов-плагинов (Content/plugins/). Медиа разделов — вwwwroot/media/. - Плагины — каталог
Plugins/: содержимое разделов (статьи, программы, файлы, форма связи, поиск) рисуют плагины на Lua. Сам сайт — раздатчик: структура, меню, админка, формы, медиа, git. - Админка — нестандартный секретный адрес (задаётся при установке, здесь не публикуется), вход по одному паролю. Каждое сохранение пишет файлы контента и сразу коммитит их в git.
- Сервер — Kestrel за nginx. Код попадает на сервер
только через
git push— из админки это невозможно.
Разделы сайта модульные: набор, порядок, названия и адреса задаются в
Content/site.json через админку «Структура». Один тип раздела
можно включить несколько раз (например, два файловых архива). У раздела две
разные галки: «раздел работает» и «показывать в меню». Выключенный раздел не
открывается по адресу (404) и не находится поиском; а рабочий раздел можно
убрать из меню, оставив живым — так устроен поиск: форма стоит в шапке, а
пункта меню у него нет.
Установка на сервер
Требования: Ubuntu 26, от 1 ГБ RAM (обязательно swap 2 ГБ — иначе сборка
на сервере может падать по памяти), .NET SDK 10, nginx. Домен (и поддомен
www) должен указывать на сервер A-записями.
Самый простой путь — скрипт deploy/install.sh:
- Скопируйте репозиторий (или папку
deploy/) на сервер. - Заполните поля вверху скрипта: минимум
SSH_USER(ваш SSH-пользователь) иADMIN_PASSWORD(пароль входа в админку); остальные поля уже заполнены разумными значениями. - Запустите:
sudo bash deploy/install.sh
Скрипт делает всё: swap, ставит .NET SDK 10, создаёт системного пользователя
viruzober и bare-репозиторий /srv/git/viruzober.git,
ставит post-receive хук, systemd-сервис, nginx, создаёт файл секретов и
запускает certbot (HTTPS). Повторный запуск безопасен: уже сделанные шаги
пропускаются. Сервис стартует только после первого git push —
хук рестартует его сам.
Если ставите вручную — те же шаги расписаны по порядку в
deploy/README.md: swap, пользователь и каталоги, bare-репозиторий,
хук, разрешение на рестарт сервиса через sudo visudo, systemd,
nginx, certbot.
После установки — первый пуш с вашей машины:
git remote add origin ssh://ВАШ_ПОЛЬЗОВАТЕЛЬ@СЕРВЕР/srv/git/viruzober.git
git push -u origin main
Первый пуш запускает сборку на сервере (около минуты на одном ядре; в это время сайт может не отвечать), дальше — только перезапуск сервиса.
Настройка: пароль, почта, git
Секреты живут в одном файле на сервере —
/srv/www/viruzober/appsettings.Production.json. В git он не попадает
никогда (в .gitignore), права на файл — только у пользователя сервиса
(chmod 600). Обновления через push этот файл не трогают.
{
"Admin": { "Password": "ПРИДУМАЙТЕ_НАДЁЖНЫЙ_ПАРОЛЬ" },
"Mail": { "To": "адрес@куда-приходят-письма" },
"Git": {
"RepositoryDir": "/srv/git/viruzober.git",
"AuthorName": "Viruzober",
"AuthorEmail": "admin@viruzober.com"
}
}
- Admin:Password — пароль входа в админку. Если поле
пустое, при входе админка честно скажет «Пароль не задан: добавьте
Admin:Password в конфигурацию сервера» — задайте пароль и перезапустите
сервис (
sudo systemctl restart viruzober). - Mail:To — адрес, на который приходят письма формы «Обратная связь». Отправка идёт через postfix на самом сервере.
- Git:* — куда админка коммитит контент и от чьего имени.
Обновление сайта
Изменения кода публикуются только через git push: хук на сервере
делает checkout, собирает проект и перезапускает сервис. Контент при этом
читается прямо из рабочего дерева, так что правки админки видны сразу.
Админка на сервере коммитит в тот же bare-репозиторий, поэтому изменения не теряются при следующем пуше. Одно предостережение: checkout при пуше перетирает незакоммиченные изменения в дереве — админка коммитит сразу после каждого сохранения, окно маленькое, но не сохраняйте ничего в админке в момент пуша.
Вход в админку
Адрес — нестандартный (не /admin), задаётся при установке сайта
и хранится в коде сервера — здесь намеренно не публикуется, это защита от
автоматического перебора типовых адресов админки ботами. Пароль — из
Admin:Password. Ссылка «Админка» в навигации сайта
появляется после входа. На страницах админки меню сайта нет — сверху
«Viruzober — админка» и ссылка «На сайт», навигация инструментов внутри.
- Пять неудачных попыток подряд — блок на 15 минут (защита от подбора).
- Все формы админки защищены от подделки запросов.
- Выход — кнопка «Выйти» в админке.
Внутри админки пять экранов, и они не дублируют друг друга:
- Модули — список всего, что наполняется содержимым, с тремя действиями у каждой строки: Наполнить (содержимое), Настроить (название, адрес, описание, показ в меню) и Удалить. У ссылки, сервиса, группы и обратной связи «Наполнить» нет — наполнять там нечего, и рядом написано почему.
- Страницы — блоки страницы: то же самое, куда ведёт «Наполнить» у раздела типа «Страница». Если страниц несколько, вверху появляется выбор раздела.
- Структура — порядок и вложенность меню; те же три действия есть и здесь, у каждой строки дерева.
- Плагины — что установлено, что просит каждый плагин и что ему разрешено.
- Пустой сайт — заголовок и текст, которые видит посетитель на «/», пока главной страницы нет.
«Модули» — это и есть первый экран админки: после входа открывается он. Когда был последний коммит, написано в шапке админки на любом экране.
«Наполнить» всегда открывает именно тот раздел, на строке которого вы стоите — даже если разделов одного типа несколько (два раздела статей, например).
Структура разделов
Админка → «Структура» — дерево всех разделов сайта в том порядке, в каком они стоят в меню. Что здесь можно:
- Включить/выключить — выключенный раздел исчезает из меню и поиска, его адреса отдают 404, контент остаётся в git.
- Убрать из меню, не выключая — в «Настроить» две отдельные галки: «раздел работает» и «показывать в меню». Снимите вторую, если раздел нужен по адресу, но не пунктом меню; так стоит поиск — форма в шапке есть, пункта нет. В дереве такая строка помечена «не в меню», выключенная — «выключен».
- Перемещать — стрелками дерева меняется порядок в меню.
- Вкладывать — вложенный раздел показывается второй строкой меню на страницах раздела-родителя (меню показывает два уровня; глубже навигация идёт такими же шагами).
- Создать — сначала тип раздела одним списком радиокнопками (стрелками вверх-вниз, выбор — пробелом): Страница из блоков; Список вложенных разделов — подменю с описаниями; Переход по адресу; Сервис; и по строке на каждый установленный плагин — список берётся из того, что установлено на самом деле. Дальше название и описание. Если нажать «Далее», ничего не отметив, форма скажет об этом и останется на месте. Адрес (slug) и (у «Сервиса») его адрес выводятся сами — руками их набирать не нужно, изменить при желании можно сразу после создания.
- Настроить — название, описание, адрес (slug), а у раздела-плагина — ещё и настройки самого плагина.
- Наполнить — открыть содержимое этого раздела: экран плагина или блоки страницы. У ссылки, сервиса и группы такой кнопки нет.
- Удалить — страница подтверждения перечислит всё, что будет стёрто: раздел, подразделы, их данные и медиа. Восстановление — из git-истории. Удалить можно любой раздел, включая главную.
Типы разделов:
| Тип | Что это | Настройки |
|---|---|---|
| Раздел-плагин | Содержимое рисует плагин из Plugins/: статьи, программы, файлы, обратная связь, поиск — что установлено | выбор плагина и его настройки; каталог данных сайт выдаёт сам |
| Страница | Страница из блоков: текст, встроенный сервис, ссылка, виджет плагина — в любом порядке | без пути к файлу — блоки хранятся в структуре |
| Ссылка | Пункт меню — переход на любой адрес | URL: /… — свой сайт, https://… — внешний |
| Сервис | Отдельное приложение на этом же домене (конвертер, редактор — что угодно), показывается во фрейме поверх шапки и подвала; под фреймом — ссылка открыть сервис в отдельной вкладке (запасной путь для скринридера) | адрес выбирается из списка сервисов (манифесты Services/*/manifest.json в репозитории) — адрес /s/имя подставится сам; руками — только если сервиса в списке нет |
| Группа | Пункт меню без своего содержимого — только чтобы собрать несколько разделов (например, несколько «Сервисов») в подменю | без контента |
Отдельного типа «Главная» нет: главная — это роль, а не сущность. Главной становится тот раздел, у которого не задан адрес: он и открывается по «/». Ею может быть «Страница», «Группа» или «Раздел-плагин»; главная на сайте одна — если адрес пуст уже у другого раздела, форма об этом скажет и адрес попросит. Главную можно переназначить (убрать адрес у одного раздела и задать другому) и удалить, как любой раздел. Пока главной нет, на «/» показывается заглушка из админки → «Пустой сайт», а не пустой 404.
У «Ссылки» и «Сервиса» не бывает подразделов; у «Группы», наоборот, могут быть любые дети, включая «Ссылку» и «Сервис» — так собирается подменю. Описание раздела (необязательное, до 300 символов) показывается под его ссылкой в списке вложенных — так страница группы превращается в подменю с названиями и описаниями. Если экземпляров одного типа несколько, вверху страниц контента появляется выбор «Раздел» — переключение между ними.
Админка → «Модули» — обзор всех источников содержимого на одном экране: у каждого раздела видно его тип, в меню ли он (и у какого родителя), куда встроен блоками страниц, выключен ли, и его описание. Ниже — сервисы, у которых ещё нет раздела, со ссылкой «Создать раздел для этого сервиса» — форма откроется сразу с типом «Сервис» и выбранным сервисом.
Контент
Всё содержимое правится в админке; каждое сохранение коммитится в git.
Страницы
Админка → «Страницы» — раздел типа «Страница» (например, «О сайте» или главная) собирается из блоков — список с клавиатурой, как «Структура»: стрелками переставляются, «Настроить» открывает поля блока, «Удалить» — подтверждение. Блоки четырёх видов:
- Текст — заголовок (необязательно) + markdown.
- Сервис — встраивает раздел типа «Сервис» тем же фреймом, что и его собственная страница.
- Ссылка — кнопка-переход с подписью на любой адрес.
- Виджет плагина — кусок, который рисует плагин: например, «Последние заметки» из раздела заметок. Список виджетов собирается сам — по группе на раздел-плагин; если он пуст, значит ни один установленный плагин виджетов не даёт. Виджетов на странице показывается не больше восьми: каждый — отдельный запуск плагина, и страница не должна из-за них открываться долго. Что не поместилось, посетитель не увидит, а вы (пока вошли в админку) увидите на этом месте объяснение.
Так на одной странице можно собрать вступительный текст, сервис и ссылки — раньше страница могла быть только одним markdown-файлом. Встроить можно и выключенный раздел: в меню его нет и по адресу он не открывается, но на странице блоком работает (в списке выбора выключенные помечены «(выключен)»).
Разделы-плагины
Всё остальное содержимое ведут плагины: у каждого раздела в админке своя
кнопка «Наполнить», и что там за экраны — решает плагин. Данные раздела
лежат в Content/plugins/{id раздела}/, медиа — в
wwwroot/media/{id раздела}/; каталог сайт выдаёт сам, руками
путь нигде не вводится. Удаление раздела уносит и его данные (страница
подтверждения скажет, сколько файлов).
Статьи
Статьи в markdown, сгруппированные в разделы. Каждая статья — файл
{слаг}.md с шапкой в формате YAML:
---
title: Название статьи
date: 2026-08-22
description: Краткое описание (показывается в списке)
tags: [первый, второй]
---
Текст статьи в markdown.
Адрес статьи составляется из заголовка сам («Как я слушаю музыку» →
kak-ya-slushayu-muzyku), при совпадении добавляется
-2. У уже созданной статьи адрес можно поправить в поле «Слаг».
Заголовки внутри текста начинаются с третьего уровня: первый — название
раздела, второй — заголовок статьи.
В статьях бывают картинки, аудио и видео. Файлы загружаются полем
«Загрузить медиафайлы» — в том числе у новой, ещё не сохранённой статьи:
они уйдут вместе с первым сохранением, и вы останетесь в редакторе со
списком файлов. Чтобы вставить файл в текст, поставьте курсор в нужное место
и нажмите Ctrl+M (или кнопку «Вставить в текст» у файла) —
сайт сам подставит  и поставит курсор
на слово «описание». По расширению сайт сам покажет изображение, плеер или
ссылку на скачивание. Описание у картинок обязательно (его озвучивает
скринридер). Если JavaScript выключен, рядом с каждым файлом напечатан готовый
фрагмент — его можно скопировать в текст руками.
Программы
Каталог программ: категории, внутри — подкатегории (один уровень)
и программы. У программы: название, краткое описание, полное описание
(markdown) и таблица версий. Каждая версия — это версия, дата, имя файла,
размер и заметки об изменениях; сам файл загружается в медиа раздела.
На странице программы версии показываются таблицей со ссылками «Скачать».
Адрес программы или категории сайт составляет сам из названия («Читалка
книг» → chitalka-knig).
Файлы
Файловый архив: папки, загрузка нескольких файлов сразу, переименование
и удаление. На сайте раздел отдаёт листинг папок и скачивание файлов.
Имена файлов и папок: без / и \, не
. и .., не начинаются с точки (такие файлы скрыты
из листинга).
Обратная связь
Форма письма: имя, адрес, текст. Письмо уходит через postfix на сервере
на адрес из Mail:To. Одно письмо в минуту с адреса — потолок
держит сам сайт.
Место в шапке и в подвале сайта плагин просит сам, и виден такой кусок на каждой странице (так стоит форма поиска). А виджет — наоборот: где ему быть, решаете вы, блоком на нужной странице.
Поиск
Поиск — тоже раздел-плагин, и форму в шапке сайта рисует он сам: плагин просит место в шапке, сайт это место даёт. Если раздела поиска нет или он выключен, формы в шапке просто не будет; а вот «показывать в меню» на неё не влияет — форма остаётся, даже когда пункта меню нет. Ищет по страницам и по всем разделам-плагинам — только по включённым.
Горячие клавиши в поле текста
В любом поле markdown в админке (текстовый блок страницы, заглушка пустого сайта) работают сочетания. Выделите текст и нажмите — оформление применится, повторное нажатие снимет его. Результат каждого действия озвучивается, фокус остаётся в тексте; Ctrl+Z отменяет как обычно. Раскладка не важна.
- Ctrl+B — полужирный;
- Ctrl+I — курсив;
- Ctrl+K — ссылка (курсор встаёт на слово «адрес»);
- Ctrl+1, Ctrl+2, Ctrl+3 — заголовок 1, 2 или 3 уровня;
- Ctrl+L — пункт маркированного списка;
- Ctrl+M — там, где у раздела есть медиа (например, в статье): перейти к списку медиафайлов, Tab — следующий файл, Enter — вставить.
Список сочетаний есть и на самой странице — раскрывающийся блок «Сочетания клавиш в поле текста» под полем. Без JavaScript сочетаний нет: разметка набирается руками, как раньше.
Плагины
Плагин — папка в Plugins/ с кодом на Lua: он добавляет свой
тип модуля. Админка → «Плагины» показывает все папки, какая версия
установлена, сколько на плагине разделов, и — главное — что плагин
о себе заявляет: пишет ли файлы, шлёт ли письма, ходит ли
в интернет, ищет ли по сайту. Это заявление о намерениях, а не запрет:
плагины ставит владелец сайта и только свои. Установка и обновление —
кнопкой на этом же экране; установленный плагин появляется в списке типов
модуля при создании раздела.
Скрипт разметкой плагин вернуть не может — такую разметку сайт не пропускает вообще. Свой JavaScript у плагина всё же бывает (например, у плеера, который весь работает в браузере), но не спрятанным в разметке: файл лежит в папке плагина и назван в его манифесте, тег подключает сам сайт — и только на страницах этого плагина. В списке заявок на экране «Плагины» это видно строкой вида «подключает свой скрипт js/app.js — он работает в браузере посетителя». Страница обязана оставаться рабочей и без скрипта: JavaScript на сайте — добавка, а не условие. Редактор разметки в админке (см. ниже) подключает сайт — тоже по просьбе плагина.
Git и бэкапы
- Коммиты админки затрагивают только контент:
Content/, иwwwroot/media/. - Локально контент-коммиты идут в обычный репозиторий, на сервере —
в bare-репозиторий
/srv/git/viruzober.git. - История git — это и есть бэкап: удалённый в админке файл или раздел можно достать из прошлых коммитов.
- Код в git-истории от админки не меняется никогда.
Локальная разработка
На своей машине сайт поднимается одной командой из каталога репозитория:
dotnet run
→ http://localhost:5000. Адрес админки локально — тот же
секретный путь, что и на сервере (см. deploy/README.md
в репозитории), тестовый пароль —
dev-password (задан в appsettings.Development.json,
используется только для разработки; на сервере пароль свой — в
appsettings.Production.json). Контент читается из файлов при
каждом запросе: файл, положенный в Content/ вручную, виден
сразу, перезапуск не нужен.
Админка с NVDA
Все формы подписаны, сообщения об ошибках и подтверждениях озвучиваются автоматически (в NVDA для этого должна быть включена настройка «Сообщать динамические изменения содержимого» — она включена по умолчанию). Отдельно — дерево в «Структуре»:
- фокус встаёт на дерево один раз: Вверх/Вниз — по разделам; «свёрнуто» читается только у разделов, у которых есть подразделы, Вправо/Влево — развернуть/свернуть (озвучивается «Раздел … развёрнут/свёрнут»);
- Пробел берёт раздел для перемещения (озвучивается «Выбран для перемещения раздел …»), стрелки перемещают его — каждый шаг озвучивается: «Раздел … перемещён в позицию 3 вместо раздела …»; второй Пробел или Enter — отпустить;
- Вправо в режиме перемещения — вложить под раздел выше («Раздел … перемещён в раздел …: позиция 2, всего пунктов 3»), Влево — поднять на уровень выше с озвучкой позиции;
- Enter в обычном режиме — открыть настройки раздела; Tab со строки уводит на её действия: «Наполнить», «Настроить», «Удалить» (у остальных строк они из таб-порядка убраны, чтобы Tab не шёл по всему дереву);
- без JavaScript дерево работает кнопками: «↑», «↓», «→» — вложить, «←» — поднять.
Лучше всего сайт работает в Firefox с последней версией NVDA.