Task: Merge next-js and app services

Merge next-js and app services

Right now the website itself runs on the old next-js, while the AI agent is in the app. At the same time, the old next-js service eats up a gig of RAM.

We need to migrate everything to the app.

Π’ΠΎΡ€ΠΊΠ»ΠΎΠ³ΠΈ

Migration of the old website to a new engine and data model unification

The main work at this stage is not just migrating individual pages or resources, but essentially rebuilding the old website on the new haih-agent engine while finally abandoning the old data structure that historically evolved back in the MODX era.

The problem here is essentially the same as when updating other legacy websites: over time, a custom data structure, numerous templates, TV fields, special resource types, and application logic tied to the quirks of the old engine have accumulated around the original CMS. Therefore, a standard application "update" is not enough β€” the new website needs an architectural migration that preserves existing content while getting rid of old technical limitations.

What the old version had

Originally, the website ran on MODX, and a significant portion of the database structure still inherited that model. Different semantic entities existed separately from one another and often had their own templates and sets of TV fields.

Specifically, the following existed separately:

  • cities;
  • companies;
  • bathhouses and saunas;
  • reviews;
  • articles and publications;
  • regular resources/pages;
  • blogs and other content types.

In other words, the distinction between objects was fixed not only at the business-logic level, but also at the storage-structure level: different entities, different templates, different fields, and separate handling logic. For MODX, this was a natural way to organize a website, but as the new application develops, such a scheme only complicates maintenance and forces old limitations into the new architecture.

Transition to KbConcept

Now the website is finally moving away from the legacy database and the old entity model. Importers that transfer records from the old database to the new one have already been written and are running for all main data types.

The key architectural change is that practically all content is now reduced to a single base entity β€” KbConcept. The semantic distinction between objects is determined not by a separate table or model class, but by the type field.

For example:

  • city:default β€” city;
  • company:default β€” company;
  • resource:default β€” regular web page;
  • review:company β€” company review;
  • blog:default β€” public blog;
  • blog:personal β€” personal blog;
  • topic:default β€” publication.

Thus, instead of a large set of historically grown entities, we get a unified data model with a clear type system. This significantly simplifies the GraphQL schema, the frontend, component reuse, queries, data imports, and further website development.

At the same time, unification does not mean a loss of typing. On the contrary, TypeScript allows describing specific subtypes quite strictly on top of the general KbConcept and working with them safely in application code.

Typing KbConcept via template literal types

TypeScript's ability to use template literal types came in particularly handy here. Currently, the type code looks like this:

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

export const CustomKbConceptType = {
  City: {
    value: 'city:default',
    description: 'City',
  },
  Company: {
    value: 'company:default',
    description: 'Company',
  },
  ResourceDefault: {
    value: 'resource:default',
    description: 'Web page',
  },
  ReviewCompany: {
    value: 'review:company',
    description: 'Company review',
  },
  BlogDefault: {
    value: 'blog:default',
    description: 'Public blog',
  },
  BlogPersonal: {
    value: 'blog:personal',
    description: 'Personal blog',
  },
  TopicDefault: {
    value: 'topic:default',
    description: 'Publication',
  },
} 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
}

There are several particularly useful points here.

as const satisfies ...

The construction:

} as const satisfies EnumValueConfigMap<SchemaTypes>

solves two tasks at once.

as const prevents TypeScript from widening values like 'city:default' to a general string type. As a result, specific strings are preserved as literal types. For example, CustomKbConceptType.ReviewCompany.value has the exact type 'review:company' rather than just string.

Meanwhile, satisfies EnumValueConfigMap<SchemaTypes> checks that the entire object conforms to the contract expected by Pothos, without destroying the exact information about literal values inside the object. This provides a convenient combination of strict structure checking and highly precise type inference.

Template literal types

The most interesting part:

type: `company:${string}`

and similarly:

type: `city:${string}`

This makes it possible to express the design principle of KbConcept.type at the type system level: an object is considered a company not just for one specific value of company:default, but for any type within the company:* namespace.

For example, if company:premium, company:branch, or other specialized variants appear in the future, the Company type will already be able to describe them without manually creating a separate union.

Thus, the type naming convention of the form:

<group>:<subtype>

becomes not just a string convention in the database, but part of the application's static typing.

Type guards

Functions of the form:

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

are custom type guards. After checking isCompany(concept), TypeScript already knows that inside the corresponding branch, concept.type has the form company:${string}.

The same approach is used for cities and reviews.

isMapItemCompany is particularly useful: it not only checks for the company: prefix, but also narrows the object down to a type where the lat and lng coordinates are guaranteed to be available as numbers. Thanks to this, subsequent map code no longer works with a "potentially a company with potentially coordinates", but with a properly typed map object.

For precise specialized variants, an even stricter check can be used:

type: typeof CustomKbConceptType.ReviewCompany.value

Here, the ReviewCompany type is tied directly to the value from the central CustomKbConceptType object. If the string value of the type is changed there, the type does not need to be manually duplicated in multiple places.

As a result, the general KbConcept entity does not turn the application into a set of untyped objects. On the contrary, thanks to the type convention, template literal types, and type guards, it is possible to maintain the convenience of a unified data model while achieving strict typing for specific scenarios on the frontend.

Importing old data

At present, importers for old entities have already been written and are operational. Main data has been imported, including resources, companies, and related content types.

This is an important milestone in the context of a complete migration: the new version of the website should no longer continue reading the old MODX database as the primary data source. Old entities are converted into the new unified model, and from then on, the application works with the new database and KbConcept.

Thus, the task gradually ceases to be a "new interface on top of the old website" and becomes a full-fledged migration to a new platform.

Map

The map displaying companies has also been migrated. Marker clustering functionality is preserved: when there are many objects, closely positioned points are merged into clusters, and when zooming in, they expand into individual elements.

The map uses the specialized MapItemCompany type so that after filtering at the TypeScript level, both the concept's belonging to company:* and the presence of coordinates are guaranteed.

Current result

At the moment:

  • the new website architecture is already built around KbConcept;
  • the main old entities no longer require separate models in the new application;
  • data importers from the old database have been written and are working;
  • resources, companies, and other main entities have been imported;
  • content types have been brought to a uniform <group>:<subtype> scheme;
  • type guards and strict typing for specific KbConcept varieties have been added on the frontend;
  • the map has been migrated;
  • object clustering on the map has been restored.

The next stage is to finalize the design and polish the visual part of the new website. After that, publication of the new version and the final transition to it are planned.

The "Gorodskie Bani" portal has been completely rewritten on haih-agent

As part of the task, the website was not just migrated to another engine, but essentially completely rewritten from scratch on the haih-agent / haih CMS architecture.

Engine repository: https://github.com/haih-net/agent/

Old version of the portal: https://old.gorodskie-bani.ru/

New version: https://gorodskie-bani.ru/

The main goal of the redesign was to abandon the heavy and fragmented architecture of the old Next.js project and move the portal to a significantly simpler, unified, and AI-first model, where content, page structure, and further website development are as convenient as possible for working via AI.

What was done

Four major blocks of work were actually completed:

  • a completely new public version of the website was built on haih-agent;
  • the content storage architecture was redesigned;
  • the entire historical database was migrated from MySQL to PostgreSQL;
  • the website was transferred to a model where a significant part of management and development can be performed by an AI agent via API and unified content entities.

Therefore, it is more accurate to consider this stage not as a routine "redesign" or "CMS change", but as a complete architectural redesign of the product.

MySQL β†’ PostgreSQL Migration

The old version of the website stored data in MySQL and used a large number of separate entities and tables for different types of content.

To transition to the new system, custom importers were written that:

  • read data from the old MySQL database;
  • transformed old structures into the new unified model;
  • migrated data to PostgreSQL;
  • preserved links, historical content, and existing URLs where necessary;
  • adapted old content to the new file, image, and page system.

Migrating the historical database was one of the most substantial parts of the work, because the new website needed not just to start with a blank slate, but to preserve the content accumulated over the years.

Instead of Multiple Entities β€” One Concept

One of the key architectural changes is the abandonment of a large number of specialized entities.

In the old project, there existed separately, for example:

  • cities;
  • bathhouses and saunas;
  • reviews;
  • comments;
  • articles;
  • other types of content.

Each such type usually required its own models, queries, templates, display logic, API, and administrative scenarios.

In the new architecture, most content is reduced to a single universal entity β€” Concept.

Concept contains just a few basic fields:

  • Title;
  • SEO description;
  • Intro for lists;
  • Content;
  • Image;
  • List of files for the gallery;
  • Map coordinates.

At the same time, the meaning of the entity is determined not by a separate rigid table in the database, but by the context, relationships, route, and contents.

The same basic mechanism can be used for a city, an establishment, an article, or another type of material.

Why Unification Greatly Simplifies the Codebase

This approach significantly reduces the volume of application code.

Previously, each new content type potentially required:

  • a new data model;
  • separate CRUD operations;
  • separate API contracts;
  • a separate administrative form;
  • separate templates or React components;
  • additional fetching and rendering logic.

After switching to Concept, most of this infrastructure becomes shared.

This reduces:

  • the number of specialized models;
  • the number of repetitive queries;
  • the amount of boilerplate logic;
  • the number of places where identical behavior must be maintained;
  • the cost of subsequent website development.

The fewer special entities there are, the less related code needs to be kept in the application's memory and executed when rendering pages.

As a result, the architecture becomes simpler for both the server and the developer.

Less Load on Templating

Content unification affects not only the database structure, but also rendering.

Instead of a large number of separate templates for different page types, a common set of components and display rules is used.

Pages can be assembled from Markdown and special components: cards, covers, galleries, maps, columns, links, AI interaction blocks, and other elements.

This allows a single rendering system to serve a large number of different pages without the need to create a separate React page or a set of special templates for each entity.

In other words, complexity is shifted from many disparate software models to unified content + reusable components.

The Main Change β€” The Website Has Become AI-First

The most important thing in this redesign is not the new design or even the change of stack itself.

The new architecture is initially built so that the website can be fully developed and maintained via AI.

It is much easier for an AI agent to work with a system where, instead of dozens of different contracts, there is one clear Concept model and a few standard operations.

The agent no longer needs to separately understand:

  • how the city table is structured;
  • how an article's API differs from a bathhouse's API;
  • where the review fields are located;
  • what specific method is used to update a particular type of material;
  • which administrative form is responsible for a particular object.

Most content is processed in the same way.

This means the agent can use the same operations for:

  • creating materials;
  • updating texts;
  • changing SEO descriptions;
  • adding images;
  • editing intros;
  • working with coordinates;
  • preparing galleries;
  • rebuilding pages;
  • mass content updates;
  • searching and analyzing information within the website.

Full Content Management via AI

In the new system, the classic administrative panel ceases to be the central way to manage the website.

Content is available via API, and the AI agent can perform changes based on a human task.

In fact, the workflow now looks like this:

a human formulates the task β†’ AI analyzes the website and data β†’ performs changes via API β†’ a human checks the result.

This is fundamentally different from a traditional CMS, where a person is forced to manually go through administrative forms and edit each entity separately.

As a result, AI becomes not an additional "chat" on top of the website, but a fully-fledged management and project development interface.

AI Assistant for End Users

The same content unification is also useful for the agent interacting with portal visitors.

Since cities, establishments, publications, and other materials are presented in a consistent format, it is easier for the agent to:

  • search for necessary information;
  • understand relationships between objects;
  • find establishments in a specific city;
  • use coordinates and geographical data;
  • work with descriptions and publications;
  • generate responses based on existing content.

Thus, the new architecture simultaneously improves two sides of working with AI:

  1. AI as a tool for the developer and website owner β€” creates and edits content, changes page structures, and helps develop the project.
  2. AI as an interface for the visitor β€” navigates the catalog and answers user questions.

Interface and Public Part Update

In parallel with the architectural migration, the public part of the portal was completely updated.

The new version implements:

  • an updated homepage;
  • new navigation and typography;
  • an establishment catalog;
  • individual pages for bathhouses and saunas;
  • photo galleries;
  • pagination;
  • a city directory;
  • alphabetical navigation and search;
  • city pages with establishment selections;
  • a map with markers and object clustering;
  • publications by historical addresses;
  • an interface for addressing the AI assistant.

Thus, the new architecture was not limited to internal refactoring β€” a working public version of the portal with real historical data was fully built on top of it.

SEO and Preservation of Historical Content

During migration, it was important not to lose the search value of the existing website.

Therefore, the transfer took into account:

  • historical URLs;
  • old publications;
  • establishment pages;
  • city pages;
  • internal linking;
  • SEO descriptions;
  • existing content.

Preserving old routes where possible reduces the risk of mass drop-off of indexed pages after changing the engine.

At the same time, the transition to haih-agent does not mean an automatic growth in search rankings. The SEO effect of the new architecture lies primarily in the fact that the website becomes easier to maintain, faster to modify, easier to scale in terms of content, and more convenient to systematically improve using AI.

Now, mass updates of metadata, texts, page structures, and content can be performed significantly faster than in the previous architecture.

Why This Is Important for Future Development

The main result of the migration is not only a reduction in technical debt.

The portal is now in an architecture that allows future tasks to be completed much faster.

The new workflow looks like this:

deploy the engine β†’ configure the theme and components β†’ build pages β†’ upload or create content β†’ continue developing the website via AI and API.

If necessary, complex domain-specific processes can still have separate software models, but standard content no longer requires creating separate infrastructure for each new page type.

This is especially important for large content projects where maintenance costs are often determined not by the complexity of a single page, but by the number of different entities, forms, and templates.

Completion Time

The complete redesign of the portal, including the new interface, transfer of historical data, and migration to the new architecture, was completed in approximately three days.

At the same time, a significant amount of time was spent specifically on compatibility with the old website and importing the existing database.

A new project without the need to transfer legacy structures on an already prepared engine could have been built significantly faster.

Summary

As part of the task, the "Gorodskie Bani" website was essentially recreated from scratch on haih-agent.

The old fragmented architecture with a large number of specialized entities was replaced with the unified Concept model, the historical database was migrated from MySQL to PostgreSQL using custom-written importers, and the public part of the portal was completely rebuilt on the new engine.

The main achievement of this work is the transition to an AI-driven website architecture.

Now AI can not only answer visitors, but also fully participate in the development of the project itself: work with content, modify pages, update SEO data, manage files, material structures, and perform mass operations via API.

As a result, the website has become simpler in architecture, cheaper in future development, and significantly better adapted for development, population, and maintenance with the help of artificial intelligence.

haih CMS / haih-agent in this project is used not just as a new engine, but as a foundation for a website where AI is a full participant in the entire lifecycle β€” from development to content management and communication with the end user.