Перейти к основному содержимому

Архитектура конструктора веб-сайтов

Каждый церковный веб-сайт, обслуживаемый B1App, рендерится из дерева контента — страницы, разделы, элементы — хранимого в ContentApi и редактируемого визуально в B1Admin. Одна общая библиотека компонентов рендерит и предпросмотр редактора, и живой сайт, один каталог типов элементов определяет, что может появиться на странице, а отдельный сервис ИИ может генерировать или переписывать это дерево. На этой странице описан весь стек: контракт элементов в @churchapps/helpers, конвейер рендеринга, элементы церковных данных, общесайтовые виджеты, слой блога, страницы с ограниченным доступом, SEO, генерация ИИ и разговорные формы.

Обзор

┌──────────────────────────────┐             ┌─────────────────────────────────────────┐
│ B1Admin — editor │ │ Api — /content module (ContentApi) │
│ ContentEditor · SectionEdit │ POST /… │ │
│ ElementEdit · PageLinkEdit │ ──────────▶ │ pages ─ sections ─ elements blocks │
│ SiteWidgetsEdit · Blog │ │ posts redirects settings styles │
└──────────┬───────────────────┘ └───────────────┬─────────────────────────┘
│ │ GET /content/pages/:churchId/tree?url=…
│ shared render pipeline ▼ (anon, JWT honored)
│ ┌───────────────────────────────┐ ┌─────────────────────────────────┐
└──▶│ @churchapps/helpers │◀──│ B1App — public site (Next.js) │
│ ElementTypes.ts (catalog) │ │ Zone → Section → Element │
│ @churchapps/apphelper │ │ + widgets, JSON-LD, sitemap, │
│ ElementRegistry, renderers │ │ redirects, branded 404 │
│ SectionDivider, widgets │ └───────────────┬─────────────────┘
└───────────────────────────────┘ │ church-data elements
┌──────────────────────────────┐ ▼
│ AskApi — /website/* (AI) │ ┌─────────────────────────────────────────┐
│ generateSite · rewriteSection│ │ /giving/funds/public/…/total │
│ generateAltText · metaDesc │ │ /membership/groupmembers/public/… │
│ returns JSON; B1Admin saves │ │ /attendance/servicetimes/public/… │
└──────────────────────────────┘ └─────────────────────────────────────────┘

Три правила действуют во всём стеке:

  1. Одно дерево, два рендерера. Страница — это дерево pages → sections → elements, где каждый узел несёт свои настройки как JSON-блок answers. Одни и те же компоненты apphelper рендерят и редактор drag-and-drop в B1Admin, и серверно-рендерённый публичный сайт в B1App — отдельного «формата публикации» не существует.
  2. Контракт живёт в @churchapps/helpers. ElementTypes.ts — единственный каталог типов элементов; рендереры разрешаются через реестр в apphelper; формы редактора живут в B1Admin. Добавление типа элемента означает изменения во всех трёх местах, именно в этом порядке.
  3. Публичный сайт читает анонимные конечные точки. Всё, что нужно B1App — дерево страниц, настройки, посты блога, редиректы и конечные точки церковных данных в других модулях — публично. Аутентификация опциональна: JWT на анонимной конечной точке дерева открывает страницы только для участников, ничего больше не меняется.

Дерево контента

Модуль content (Api/src/modules/content) владеет данными конструктора:

ТаблицаРоль
pagesОдна страница на URL: url, title, layout, плюс visibility/groupIds (ограничение доступа) и metaDescription (SEO)
sectionsГоризонтальные полосы на странице (или в блоке): фон, цвет текста и answersJSON, несущий стилизацию плюс конфигурации разделителей формы dividerTop/dividerBottom
elementsЭлементы контента внутри раздела: elementType + answersJSON, вложены для типов компоновки (строка/колонка, карусель)
blocksПереиспользуемые группы разделов/элементов (блоки подвала, блоки элементов), общие для страниц
postsОтдельные посты блога (см. Блог)
redirectsПары fromPath → toPath на церковь, ограничены 200 записями (см. SEO)
settingsНастройки церкви в формате «ключ-значение»; строки, помеченные public, обслуживаются анонимно и несут конфигурацию виджетов/аналитики

Всё дерево для одного URL приходит одним анонимным вызовом — GET /content/pages/:churchId/tree?url=/about — именно из него B1App выполняет серверный рендеринг. Запросы редактора получают данные по id и сохраняют внутренние id.

Контракт элементов

Каталог (@churchapps/helpers)

Packages/helpers/src/ElementTypes.ts определяет каждый тип элемента как ElementTypeDefinition: elementType, label, category, schemaVersion, defaults и JSON-schema-подобную answersSchema для его настроек. validateElementAnswers() намеренно снисходительна — неизвестные типы и лишние ключи проходят, поэтому старый контент никогда не ломается при обновлении каталога. Сегодня поставляется 35 типов:

КатегорияТипы элементов
компоновка (6)row, column, box, carousel, whiteSpace, block
контент (11)text, textWithPhoto, card, faq, iconFeature, testimonial, socialIcons, countdown, stats, table, buttonLink
медиа (4)image, gallery, video, map
церковные (12)logo, sermons, stream, donation, donateLink, form, calendar, groupList, groups, campaignProgress, staffGrid, serviceTimes
расширенные (2)rawHTML, iframe

Элемент sermons — самый настраиваемый из церковных типов: настройка layout выбирает browse (устаревший полный браузер), grid, list или featuredLatest, а playlistId, itemCount, showTitles и showDates уточняют не-browse раскладки.

Рендереры (@churchapps/apphelper)

Рендереры живут в Packages/apphelper/src/website/components/elementTypes/, по одному компоненту на тип, разрешаемые через ElementRegistry.ts — двухуровневую карту, где Element.tsx регистрирует рендерер по умолчанию для всех 35 типов (registerDefaultElementRenderer), а принимающее приложение может переопределить любой из них во время выполнения (registerElementRenderer) без форка пакета.

Формы редактора (B1Admin)

Формы настроек редактора для каждого типа живут в B1Admin/src/site/admin/elements/ElementEdit.tsx направляет к выделенному компоненту (GalleryEdit, TestimonialEdit, StatsEdit, …) либо к встроенному конструктору полей для каждого типа. Отражение этого каталога, ориентированное на ИИ, — инструмент MCP API describe_page_builder (см. MCP-сервер).

Разделители формы разделов

Разделы могут нести декоративные разделители формы на любом краю. Конфигурация живёт в answersJSON раздела как объекты dividerTop / dividerBottom{ shape, color, height, flip }, где shape — один из wave, waves, slant, curve, triangle, peaks. Apphelper поставляет компонент SectionDivider и вспомогательную функцию parseDividerConfig(); рендереры разделов обоих приложений (B1App/src/components/Section.tsx, B1Admin/src/site/admin/Section.tsx) разбирают настройки и монтируют разделитель, а SectionEdit.tsx в B1Admin предоставляет интерфейс выбора. Пакеты поставляют только строительный блок — привязка на уровне раздела — работа потребляющих приложений.

Элементы церковных данных

Три типа элементов рендерят живые церковные данные, а не авторский контент. Изоляция модулей по-прежнему применяется — каждый вызывает публичную конечную точку своего собственного владеющего модуля из браузера:

ЭлементКонечная точкаПримечания
campaignProgressGET /giving/funds/public/:churchId/:fundId/totalВозвращает { fundId, totalAmount, donationCount }, опциональное окно ?startDate=&endDate=; элемент сравнивает это со своей настройкой goalAmount
staffGridGET /membership/groupmembers/public/:churchId/:groupIdТолько по подписке (opt-in): у группы должен быть установлен publicRoster (по умолчанию выключен). Проекция намеренно минимальна — personId, displayName, leader, фото — никаких контактных или демографических полей
serviceTimesGET /attendance/servicetimes/public/:churchIdВозвращает дерево кампус → служение → время; рендерер apphelper по возможности генерирует из него JSON-LD Event в формате schema.org (API возвращает обычные данные)
Предупреждение

publicRoster — это шлюз приватности для staffGrid. Никогда не расширяйте публичную проекцию членов группы и не обходите этот флаг — конечная точка списка участников анонимна по замыслу, и минимальный список полей — это её свойство безопасности.

Общесайтовые виджеты

Два виджета рендерятся на каждой публичной странице, а не внутри дерева: AnnouncementBanner (закрываемая полоса вверху страницы) и Launcher (плавающий центр действий для ссылок в стиле «пожертвовать/посетить/смотреть»). Оба компонента и их вспомогательные функции parse*Config() поставляются в apphelper. Конфигурация — это две публичные строки настроек — ключи announcementBanner и launcher — записываемые SiteWidgetsEdit B1Admin (на странице Appearance) и читаемые публичным макетом B1App через GET /content/settings/public/:churchId. API рассматривает их как непрозрачные пары «ключ-значение»; имена ключей — это соглашение между двумя приложениями.

Блог

Блог — это отдельный тип контента, а не слой поверх страниц конструктора. Строка posts хранит весь пост: title, slug, excerpt, content (тело в markdown), authorId, photoUrl, publishDate, category, tags. Публичная поверхность (всё анонимно, PostController):

МаршрутНазначение
GET /content/posts/public/:churchIdОпубликованные посты, фильтруемые по ?category=&tag=, с пагинацией
GET /content/posts/public/:churchId/categoriesОтдельные категории среди опубликованных постов
GET /content/posts/public/:churchId/slug/:slugОдин опубликованный пост
GET /content/posts/rss/:churchId?siteUrl=RSS 2.0-фид, озаглавленный именем церкви, с категорией и описанием (выдержкой или контентом) для каждого элемента

Пост считается «опубликованным», как только установлена и прошла дата publishDate; будущая publishDate означает запланированный пост (скрыт публично, показывается с чипом «Запланировано» в администрировании). Конечные точки чтения обогащают каждый пост полем authorName, разрешаемым из authorId через шлюз модуля членства. Отсутствующие выдержки заменяются урезанным markdown-контентом (~160 символов) в карточках списка, метаописаниях и RSS. B1App обслуживает /{sdSlug}/blog — редакционный список (центрированный заголовок, становящийся именем активной категории/тега при фильтрации, строка фильтра по категориям-чипам, строки постов с миниатюрой слева, подписями авторов и выдержками) с RSS-фидом, объявленным как альтернативная ссылка — и /{sdSlug}/blog/[postSlug], отдельный маршрут (не конвейер Zone/Section) с центрированным заголовком (категория-плашка, заголовок, подпись автора, акцентная линия основного цвета), героем 16:9 по ширине контейнера, телом markdown в колонке чтения ~720px, чипами тегов в подвале статьи, полосой связанных постов «Больше в {category}» и JSON-LD BlogPosting, включающим автора. Обе страницы полностью стилизуются из токенов темы, поэтому наследуют палитру каждой церкви. URL блога включены в карту сайта для каждой церкви. Интерфейс написания B1Admin (Site → Blog) редактирует посты в диалоге: markdown-редактор с переключателем предпросмотра, выбор изображения галереи с обрезкой 16:9, выбор автора-человека (по умолчанию редактирующий пользователь), автодополнение категории, заполненное существующими категориями, проверка на дублирующийся slug и переключатель публикации; опубликованные строки ссылаются на живой пост, а страница подталкивает администраторов добавить навигационную ссылку /blog.

Страницы только для участников

pages.visibility переиспользует перечисление навигационных ссылок — everyone (по умолчанию), visitors, members, staff, team, groupsgroupIds) — но как жёсткий шлюз доступа, а не фильтр навигации (PageVisibilityHelper.canViewPage). Поток:

  1. Анонимная конечная точка дерева проверяет видимость при запросах по URL. Анонимные вызывающие стороны для ограниченной страницы получают { restricted: true, visibility } вместо контента — дерево никогда не утекает.
  2. Конечная точка по-прежнему учитывает JWT: CustomAuthProvider проверяет заголовок Authorization при каждом запросе, включая анонимные маршруты, так что запрос авторизованного участника к тому же URL разрешается нормально.
  3. B1App рендерит RestrictedPage при ответе restricted: он восстанавливает сессию из сохранённых учётных данных, заново запрашивает дерево с JWT и рендерит его — либо показывает шлюз входа с returnUrl, когда сессии нет.
Информация

Детализация шлюза различается по уровням: groups проверяет groupIds токена против списка страницы, а staff проверяет membershipStatus, но members и team в текущей реализации пропускают любого авторизованного пользователя церкви. Считайте groups строгим вариантом.

SEO и обнаруживаемость

Всё это — рендеринг на стороне B1App поверх данных ContentApi — API хранит, приложение выдаёт:

АспектКак это работает
Метаописанияpages.metaDescription (≤300 символов) проходит через MetaHelper.getMetaData() в Metadata Next.js (описание + Open Graph) на каждом маршруте, рендерящемся конструктором. Настройки страницы B1Admin включают кнопку ИИ «Generate» (см. ниже)
РедиректыСтроки redirects для каждой церкви, управляемые на /content/redirects (content.edit, лимит 200 строк, нормализованные пути). При потенциальной 404 маршрут страницы B1App разрешает путь против GET /content/redirects/public/:churchId и выдаёт HTTP 308 через permanentRedirect из Next; несовпавшие пути проваливаются к notFound()
Брендированная 404not-found.tsx рендерит BrandedNotFound с логотипом церкви, именем и темой вместо обобщённой ошибки
Структурированные данныеJSON-LD BlogPosting на постах блога; VideoObject на страницах отдельных проповедей (/{sdSlug}/sermons/[sermonId]) и на страницах, содержащих элемент sermons; Event из элементов календаря/мероприятия на страницах конструктора; schema.org Event из элемента serviceTimes
Страницы проповедейКаждая публичная проповедь получает индексируемую страницу на /sermons/[sermonId] с полными метаданными — проповеди больше не заперты внутри клиентского элемента-браузера
АналитикаПубличный ключ настроек ga4MeasurementId (управляется рядом с редиректами в B1Admin) внедряет тег GA4 gtag для каждой церкви через next/script
Карта сайта и фидыМаршрут sitemap.xml для каждой церкви включает страницы конструктора и URL блога; список блога объявляет RSS-фид
ДоступностьПубличное окружение рендерит ссылку пропуска, ведущую к ориентиру <main id="main-content"> в каждой обёртке макета

Генерация ИИ (AskApi)

Генерация страниц и сайтов выполняется в AskApi, отдельном сервисе, под контроллером /website. Он аутентифицируется тем же JWT CustomAuthProvider, что и всё остальное, и не хранит состояние контента: каждая конечная точка возвращает JSON, а вызывающая сторона (B1Admin) сохраняет результат через ContentApi (POST /content/pages/temp/ai сохраняет сгенерированный пакет страница-разделы-элементы одним вызовом).

Информация

По состоянию на 2026-07-03 точки входа B1Admin в этот конвейер — шаблон сайта «AI» в AddPageModal, кнопка перезаписи SectionToolbar и кнопка «Generate Site» в списке страниц — закомментированы на стороне клиента, пока функция перерабатывается. Конечные точки AskApi ниже не затронуты и по-прежнему отвечают; скрыт только интерфейс B1Admin.

Конечная точкаНазначение
POST /website/generatePageOutlinegenerateSectionИсходный двухшаговый поток страницы: сначала план, затем один вызов на раздел. Шаблон страницы «AI» в AddPageModal B1Admin управляет этим — план, затем параллельная генерация разделов, затем предпросмотр
POST /website/generateSiteГенерация всего сайта. Намеренно двухфазная: вызов с planOnly: true возвращает только многостраничный план (один быстрый вызов модели), затем клиент запрашивает полный контент — удерживая каждый запрос в пределах тайм-аута Lambda/API Gateway
POST /website/rewriteSectionПерезапись с сохранением структуры: модель может менять только текстонесущие ответы. Рекурсивная сигнатура структуры (id + типы + порядок) сравнивается до и после; любое несовпадение возвращает исходный раздел с fallback: true вместо повреждённой структуры
POST /website/generateAltTextВызов зрения над до 20 URL изображений; возвращает лаконичный alt-текст (≤125 символов, префиксы «photo of» удаляются)
POST /website/generateMetaDescriptionОдно SEO-метаописание (≤155 символов) из текстового содержимого страницы — подключено к кнопке Generate на настройках страницы B1Admin

Подсказки — это markdown-файлы в AskApi/config/instructions/, включая каталог элементов, из которого генерирует модель. Два дизайн-решения удерживают каталог честным: клиент передаёт availableElementTypes при каждом запросе (подсказка может использовать только типы из этого списка — сервер никогда не зашивает полный набор в код), а инструмент MCP API describe_page_builder несёт тот же справочник для ИИ-агентов, работающих через MCP. Модели — это Anthropic Claude через OpenRouter — 3.5 Haiku для контента разделов (задержка), 3.5 Sonnet для планов, макетов сайта и зрения — с резервом на OpenAI, когда ключ OpenRouter не настроен.

Разговорные формы

Формы (модуль membership) получили разговорный режим, ориентированный на страницы в стиле карточки контактов. Четыре столбца в forms управляют этим: displayMode (standard | conversational), autoCreatePerson, followUpSubject, followUpBody.

  • РендерингFormSubmissionEdit apphelper переключается на компонент ConversationalForm (по одному вопросу за раз), когда displayMode равен conversational; страница формы B1App передаёт режим дальше. Полезная нагрузка отправки одна и та же в обоих случаях.
  • Автосоздание человека — при отправке с установленным autoCreatePerson ConversationalFormHelper.findOrCreatePerson дедуплицирует по email (без учёта регистра) и иначе создаёт домохозяйство + человека со membershipStatus: "Guest", затем связывает отправку с этим человеком.
  • Письмо продолжения — когда заданы тема и тело, отправитель получает шаблонное письмо (с токенами {firstName} / {churchName}) через существующий транзакционный путь (TransactionalEmailHelper), никогда не через дверь дайджеста уведомлений. Оба побочных эффекта не критичны: сбой никогда не приводит к потере отправки.

Эти четыре поля сегодня устанавливаются через API; редактор форм B1Admin пока их не раскрывает.

Связанные страницы