Задача: Перенести сайт на haih-agent

Перенести сайт на haih-agent

Сейчас сам сайт крутится на старом next-js, а ИИ-агент в app. При этом старый next-js сервис выжирает гиг оперативы.

Надо все перенести в haih-agent. По сути, выполнить программирования сайта с нуля и перетащить все старые данные.

Текущая структура сайта

Ворклоги

Перенос старого сайта на новый движок и унификация модели данных

Основная работа на этом этапе — не просто перенос отдельных страниц или ресурсов, а фактически пересборка старого сайта на новом движке haih-agent с окончательным отказом от старой структуры данных, которая исторически сложилась ещё во времена MODX.

Проблема здесь по сути такая же, как и при обновлении других старых сайтов: со временем вокруг первоначальной CMS накопилась собственная структура данных, множество шаблонов, TV-полей, специальных типов ресурсов и прикладной логики, завязанной на особенности старого движка. Поэтому обычного «обновления» приложения недостаточно — новый сайт нужно переносить архитектурно, сохраняя существующий контент, но избавляясь от старых технических ограничений.

Что было в старой версии

Изначально сайт работал на MODX, и значительная часть структуры базы данных до сих пор наследовала эту модель. Разные смысловые сущности существовали отдельно друг от друга и зачастую имели собственные шаблоны и наборы TV-полей.

В частности, отдельно существовали:

  • города;
  • компании;
  • бани и сауны;
  • отзывы;
  • статьи и публикации;
  • обычные ресурсы/страницы;
  • блоги и другие типы контента.

То есть различие между объектами было зафиксировано не только на уровне бизнес-смысла, но и на уровне самой структуры хранения: разные сущности, разные шаблоны, разные поля и отдельная логика работы с ними. Для MODX это было естественным способом организации сайта, но при развитии нового приложения такая схема только усложняет поддержку и заставляет переносить старые ограничения в новую архитектуру.

Переход на KbConcept

Сейчас сайт окончательно уходит от старой базы и старой модели сущностей. Для всех основных типов данных уже написаны и работают импортёры, которые переносят записи из старой базы в новую.

Ключевое архитектурное изменение состоит в том, что практически весь контент теперь приводится к одной базовой сущности — KbConcept. Семантическое различие между объектами определяется не отдельной таблицей или отдельным классом модели, а полем type.

Например:

  • city:default — город;
  • company:default — компания;
  • resource:default — обычная веб-страница;
  • review:company — отзыв о компании;
  • blog:default — публичный блог;
  • blog:personal — персональный блог;
  • topic:default — публикация.

Таким образом, вместо большого набора исторически разросшихся сущностей получается единая модель данных с понятной системой типов. Это заметно упрощает GraphQL-схему, фронтенд, повторное использование компонентов, выборки, импорт данных и дальнейшее развитие сайта.

При этом унификация не означает потери типизации. Наоборот, TypeScript позволяет поверх общего KbConcept достаточно строго описать конкретные подтипы и безопасно работать с ними в прикладном коде.

Типизация KbConcept через template literal types

Особенно удачно здесь пригодилась возможность TypeScript использовать шаблонные строковые литералы в типах (template literal types). Сейчас код типов выглядит так:

import { EnumValueConfigMap, SchemaTypes } from '@pothos/core'
import { KbConceptFragment } from 'src/gql/generated'

export const CustomKbConceptType = {
  City: {
    value: 'city:default',
    description: 'Город',
  },
  Company: {
    value: 'company:default',
    description: 'Компания',
  },
  ResourceDefault: {
    value: 'resource:default',
    description: 'Веб-страница',
  },
  ReviewCompany: {
    value: 'review:company',
    description: 'Отзыв о компании',
  },
  BlogDefault: {
    value: 'blog:default',
    description: 'Публичный блог',
  },
  BlogPersonal: {
    value: 'blog:personal',
    description: 'Персональный блог',
  },
  TopicDefault: {
    value: 'topic:default',
    description: 'Публикация',
  },
} as const satisfies EnumValueConfigMap<SchemaTypes>

export type MapItemCompany = KbConceptFragment & {
  type: `company:${string}`
  lat: number
  lng: number
}

export function isMapItemCompany(
  concept: KbConceptFragment,
): concept is MapItemCompany {
  return concept.type?.startsWith('company:') && concept.lat && concept.lng
    ? true
    : false
}

export type Company = KbConceptFragment & {
  type: `company:${string}`
}

export function isCompany(concept: KbConceptFragment): concept is Company {
  return concept.type?.startsWith('company:') ? true : false
}

export type City = KbConceptFragment & {
  type: `city:${string}`
}

export function isCity(concept: KbConceptFragment): concept is City {
  return concept.type?.startsWith('city:') ? true : false
}

export type ReviewCompany = KbConceptFragment & {
  type: typeof CustomKbConceptType.ReviewCompany.value
}

export function isReviewCompany(
  concept: KbConceptFragment,
): concept is ReviewCompany {
  return concept.type === CustomKbConceptType.ReviewCompany.value
}

Здесь есть несколько особенно полезных моментов.

as const satisfies ...

Конструкция:

} as const satisfies EnumValueConfigMap<SchemaTypes>

решает сразу две задачи.

as const не даёт TypeScript расширить значения вроде 'city:default' до общего типа string. В результате конкретные строки сохраняются как литеральные типы. Например, CustomKbConceptType.ReviewCompany.value имеет тип именно 'review:company', а не просто string.

При этом satisfies EnumValueConfigMap<SchemaTypes> проверяет, что весь объект соответствует контракту, ожидаемому Pothos, но не уничтожает точную информацию о литеральных значениях внутри объекта. Получается удобное сочетание строгой проверки структуры и максимально точного вывода типов.

Шаблонные литералы в типах

Самая интересная часть:

type: `company:${string}`

и аналогично:

type: `city:${string}`

Это позволяет выразить на уровне системы типов сам принцип устройства KbConcept.type: объект считается компанией не только при одном конкретном значении company:default, а при любом типе из пространства company:*.

Например, если в дальнейшем появятся company:premium, company:branch или другие специализированные варианты, тип Company уже сможет описывать их без создания отдельного union вручную.

То есть соглашение об именовании типов вида:

<группа>:<подтип>

становится не просто строковым соглашением в базе, а частью статической типизации приложения.

Type guards

Функции вида:

export function isCompany(concept: KbConceptFragment): concept is Company

являются пользовательскими type guard'ами. После проверки isCompany(concept) TypeScript уже знает, что внутри соответствующей ветки concept.type имеет форму company:${string}.

То же самое используется для городов и отзывов.

Отдельно полезен isMapItemCompany: он не только проверяет префикс company:, но и сужает объект до типа, в котором гарантированно доступны координаты lat и lng как числа. Благодаря этому код карты дальше работает не с «возможно компанией с возможно координатами», а уже с нормальным типизированным объектом карты.

Для точных специализированных вариантов можно использовать ещё более строгую проверку:

type: typeof CustomKbConceptType.ReviewCompany.value

Здесь тип ReviewCompany привязан непосредственно к значению из центрального объекта CustomKbConceptType. Если строковое значение типа будет изменено там, тип не придётся дублировать вручную в нескольких местах.

В итоге общая сущность KbConcept не превращает приложение в набор нетипизированных объектов. Наоборот, за счёт соглашения о type, template literal types и type guards получается сохранить удобство единой модели данных и одновременно получить строгую типизацию конкретных сценариев на фронтенде.

Импорт старых данных

На текущий момент импортёры старых сущностей уже написаны и работают. Импортированы основные данные, в том числе ресурсы, компании и связанные типы контента.

Это важный этап именно в контексте полного переезда: новая версия сайта уже не должна продолжать читать старую MODX-базу как основной источник данных. Старые сущности преобразуются в новую унифицированную модель и дальше приложение работает уже с новой базой и KbConcept.

Таким образом, задача постепенно перестаёт быть «новым интерфейсом поверх старого сайта» и становится полноценной миграцией на новую платформу.

Карта

Также перенесена карта с отображением компаний. Функциональность кластеризации маркеров сохранена: при большом количестве объектов близко расположенные точки объединяются в кластеры, а при изменении масштаба раскрываются в отдельные элементы.

Для карты как раз используется специализированный тип MapItemCompany, чтобы после фильтрации на уровне TypeScript были гарантированы и принадлежность концепта к company:*, и наличие координат.

Текущий результат

На данный момент:

  • новая архитектура сайта уже строится вокруг KbConcept;
  • основные старые сущности больше не требуют отдельных моделей в новом приложении;
  • написаны и работают импортёры данных из старой базы;
  • ресурсы, компании и другие основные сущности импортированы;
  • типы контента приведены к единой схеме <группа>:<подтип>;
  • на фронтенде добавлены type guards и строгая типизация конкретных разновидностей KbConcept;
  • перенесена карта;
  • восстановлена кластеризация объектов на карте.

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

Полностью переписан портал «Городские бани» на haih-agent

В рамках задачи сайт был не просто перенесён на другой движок, а по сути полностью переписан с нуля на архитектуре haih-agent / haih CMS.

Репозиторий движка: https://github.com/haih-net/agent/

Старая версия портала: https://old.gorodskie-bani.ru/

Новая версия: https://gorodskie-bani.ru/

Главная цель переработки — отказаться от тяжёлой и фрагментированной архитектуры старого Next.js-проекта и перевести портал на значительно более простую, унифицированную и AI-first модель, где контент, структура страниц и дальнейшее развитие сайта максимально удобны для работы через ИИ.

Что было сделано

Фактически выполнены четыре крупных блока работ:

  • полностью собрана новая публичная версия сайта на haih-agent;
  • переработана архитектура хранения контента;
  • вся историческая база перенесена из MySQL в PostgreSQL;
  • сайт переведён на модель, в которой значительная часть управления и развития может выполняться ИИ-агентом через API и унифицированные контентные сущности.

Поэтому корректнее рассматривать этот этап не как обычный «редизайн» или «смену CMS», а как полную архитектурную переработку продукта.

Миграция MySQL → PostgreSQL

Старая версия сайта хранила данные в MySQL и использовала большое количество отдельных сущностей и таблиц под разные типы контента.

Для перехода на новую систему были написаны отдельные импортёры, которые:

  • читали данные из старой MySQL-базы;
  • преобразовывали старые структуры в новую унифицированную модель;
  • переносили данные в PostgreSQL;
  • сохраняли связи, исторический контент и существующие URL там, где это было необходимо;
  • адаптировали старый контент под новую систему файлов, изображений и страниц.

Миграция исторической базы была одной из самых существенных частей работы, потому что новый сайт должен был не просто стартовать с чистого листа, а сохранить накопленный за годы контент.

Вместо множества сущностей — один Concept

Одно из ключевых архитектурных изменений — отказ от большого количества специализированных сущностей.

В старом проекте отдельно существовали, например:

  • города;
  • бани и сауны;
  • отзывы;
  • комментарии;
  • статьи;
  • другие типы контента.

Для каждого такого типа обычно требовались собственные модели, запросы, шаблоны, логика отображения, API и административные сценарии.

В новой архитектуре большая часть контента приведена к одной универсальной сущности — Concept.

Concept содержит всего несколько базовых полей:

  • Название;
  • SEO-описание;
  • Интро для списков;
  • Контент;
  • Картинка;
  • Список файлов для галереи;
  • Координаты для карты.

При этом смысл сущности определяется не отдельной жёсткой таблицей в базе, а контекстом, связями, маршрутом и содержимым.

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

Почему унификация сильно упрощает кодовую базу

Такой подход заметно сокращает объём прикладного кода.

Раньше каждый новый тип контента потенциально требовал:

  • новой модели данных;
  • отдельных CRUD-операций;
  • отдельных API-контрактов;
  • отдельной административной формы;
  • отдельных шаблонов или React-компонентов;
  • дополнительной логики выборки и отображения.

После перехода на Concept большая часть этой инфраструктуры становится общей.

Это уменьшает:

  • количество специализированных моделей;
  • число повторяющихся запросов;
  • объём шаблонной логики;
  • количество мест, где нужно поддерживать одинаковое поведение;
  • стоимость последующего развития сайта.

Чем меньше специальных сущностей, тем меньше связанного кода необходимо держать в памяти приложения и выполнять при рендеринге страниц.

В результате архитектура становится проще как для сервера, так и для разработчика.

Меньше нагрузки на шаблонизацию

Унификация контента влияет не только на структуру базы, но и на рендеринг.

Вместо большого количества отдельных шаблонов для разных типов страниц используется общий набор компонентов и правил отображения.

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

Это позволяет одной системе рендеринга обслуживать большое количество разных страниц без необходимости создавать отдельную React-страницу или набор специальных шаблонов под каждую сущность.

Иными словами, сложность переносится из множества разрозненных программных моделей в унифицированный контент + переиспользуемые компоненты.

Главное изменение — сайт стал AI-first

Самое важное в этой переработке — не новый дизайн и даже не сама смена стека.

Новая архитектура изначально построена так, чтобы сайт можно было полноценно развивать и обслуживать через ИИ.

AI-агенту значительно проще работать с системой, где вместо десятков разных контрактов существует одна понятная модель Concept и несколько стандартных операций.

Для агента больше не требуется отдельно понимать:

  • как устроена таблица города;
  • чем API статьи отличается от API бани;
  • где лежат поля отзыва;
  • каким отдельным методом обновляется конкретный тип материала;
  • какая административная форма отвечает за тот или иной объект.

Большая часть контента обрабатывается одинаково.

Это означает, что агент может использовать одни и те же операции для:

  • создания материалов;
  • обновления текстов;
  • изменения SEO-описаний;
  • добавления изображений;
  • редактирования интро;
  • работы с координатами;
  • подготовки галерей;
  • перестройки страниц;
  • массового обновления контента;
  • поиска и анализа информации внутри сайта.

Полное управление контентом через ИИ

В новой системе классическая административная панель перестаёт быть центральным способом управления сайтом.

Контент доступен через API, а ИИ-агент может выполнять изменения по задаче человека.

Фактически рабочий процесс теперь выглядит так:

человек формулирует задачу → ИИ анализирует сайт и данные → выполняет изменения через API → человек проверяет результат.

Это принципиально отличается от традиционной CMS, где человек вынужден вручную проходить по административным формам и редактировать каждую сущность отдельно.

В результате ИИ становится не дополнительным «чатиком» поверх сайта, а полноценным интерфейсом управления и развития проекта.

ИИ-помощник для конечных пользователей

Та же унификация контента полезна и для агента, который общается с посетителями портала.

Поскольку города, заведения, публикации и другие материалы представлены в согласованном формате, агенту проще:

  • искать нужную информацию;
  • понимать связи между объектами;
  • находить заведения в конкретном городе;
  • использовать координаты и географические данные;
  • работать с описаниями и публикациями;
  • формировать ответы на основе существующего контента.

То есть новая архитектура одновременно улучшает две стороны работы с ИИ:

  1. ИИ как инструмент разработчика и владельца сайта — создаёт и редактирует контент, меняет структуру страниц и помогает развивать проект.
  2. ИИ как интерфейс для посетителя — ориентируется в каталоге и отвечает на вопросы пользователей.

Обновление интерфейса и публичной части

Параллельно с архитектурной миграцией полностью обновлена публичная часть портала.

В новой версии реализованы:

  • обновлённая главная страница;
  • новая навигация и типографика;
  • каталог заведений;
  • индивидуальные страницы бань и саун;
  • фотогалереи;
  • пагинация;
  • справочник городов;
  • алфавитная навигация и поиск;
  • городские страницы с подборками заведений;
  • карта с маркерами и группировкой объектов;
  • публикации по историческим адресам;
  • интерфейс обращения к ИИ-помощнику.

Таким образом, новая архитектура не ограничилась внутренним рефакторингом — на ней полностью собрана рабочая публичная версия портала с реальными историческими данными.

SEO и сохранение исторического контента

При миграции важно было не потерять поисковую ценность существующего сайта.

Поэтому при переносе учитывались:

  • исторические URL;
  • старые публикации;
  • страницы заведений;
  • страницы городов;
  • внутренняя перелинковка;
  • SEO-описания;
  • существующий контент.

Сохранение старых маршрутов там, где это возможно, уменьшает риск массового выпадения проиндексированных страниц после смены движка.

При этом сам переход на haih-agent не означает автоматического роста поисковых позиций. SEO-эффект новой архитектуры заключается прежде всего в том, что сайт становится проще поддерживать, быстрее изменять, легче масштабировать по контенту и удобнее системно улучшать с помощью ИИ.

Теперь массовое обновление метаданных, текстов, структуры страниц и контента может выполняться значительно быстрее, чем в прежней архитектуре.

Почему это важно для дальнейшего развития

Главный результат миграции — не только уменьшение технического долга.

Портал теперь находится в архитектуре, которая позволяет гораздо быстрее выполнять будущие задачи.

Новый рабочий цикл выглядит так:

развернуть движок → настроить тему и компоненты → собрать страницы → загрузить или создать контент → дальше развивать сайт через ИИ и API.

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

Это особенно важно для крупных контентных проектов, где стоимость поддержки часто определяется не сложностью одной страницы, а количеством различных сущностей, форм и шаблонов.

Срок выполнения

Полная переработка портала, включая новый интерфейс, перенос исторических данных и миграцию на новую архитектуру, была выполнена примерно за три дня.

При этом значительная часть времени ушла именно на совместимость со старым сайтом и импорт существующей базы.

Новый проект без необходимости переносить legacy-структуру на уже подготовленном движке мог бы быть собран существенно быстрее.

Итог

В рамках задачи сайт «Городские бани» был фактически создан заново на haih-agent.

Старая фрагментированная архитектура с большим количеством специализированных сущностей заменена на унифицированную модель Concept, историческая база перенесена из MySQL в PostgreSQL с помощью специально написанных импортёров, а публичная часть портала полностью пересобрана на новом движке.

Главное достижение этой работы — переход к AI-driven архитектуре сайта.

Теперь ИИ может не только отвечать посетителям, но и полноценно участвовать в развитии самого проекта: работать с контентом, изменять страницы, обновлять SEO-данные, управлять файлами, структурой материалов и выполнять массовые операции через API.

В результате сайт стал проще по архитектуре, дешевле в дальнейшем развитии и значительно лучше приспособлен к разработке, наполнению и сопровождению с помощью искусственного интеллекта.

haih CMS / haih-agent в этом проекте используется не просто как новый движок, а как основа для сайта, где ИИ является полноценным участником всего жизненного цикла — от разработки до управления контентом и общения с конечным пользователем.