Ворклог по задаче "Переосмыслить структуру сайта, доработать ее и пересобрать контент"

13 июл. 2026 г., 22:44:12

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

Вот краткая выжимка как я это вижу:


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

1. Создается концепт, которого ранее еще не было. К примеру, Some Homestay.

2. В нем прописывается в обычном текстовом виде все, что хочется передать. Это все то, что потом прочитает пользовать. В нашем проекте по сути это само по себе факт, потому что весь контент создаем мы. И мы сразу указываем качественные признаки что есть что. Со временем, когда эта информация окажется некачественная, мы просто понизим ее качественный признак. Условно, нам достаточно сейчас только одной метрики от 0 до 1 флоэт. Чем выше качество, тем выше видимость. К примеру, у меня на сайте полно старых статей, которые уже не нужны, их надо выкинуть, но я не могу их выкинуть, ибо 404 не круто. Мне надо просто их дальше засунуть

3. Связка с другими концептами. То есть мне нужна просто перелинковка. Как и в классических сайтах, я просто могу добавить ссылки на другие концепты. Это чтобы пользователь мог читать и переходить в попутные карточки. Но это связь внешняя. А мне нужна и обратная. Соответственно, у меня не просто связи ссылки анкор тут нужны, а попутная техническая информация связи (почему я на это ссылаюсь и опять-таки, какой-то качественный признак).

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

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

И тут я вот что думаю, если эту идею развивать. Можно сделать полную сементику на уровне HTML-тегов. Мы в тексте просто вставляем ссылки. При этом у них есть innerText, title, rel, data-атрибуты и т.п. Все это можно зеркалить с нашей индекс-таблицей связей. При этом parent и root остаются как жесткие связки


Вот что чатгпт подытожила:

Да, это уже выглядит очень цельно.

Тогда HTML/Markdown-контент становится первичным семантическим графом, а таблица связей — его материализованным индексом для быстрых запросов и обратной перелинковки.

При этом:

  • parentId — жёсткая непосредственная иерархия;
  • rootId — принадлежность к одному дереву или разделу;
  • HTML-ссылки — мягкие смысловые связи многие-ко-многим;
  • таблица отношений — зеркальное представление ссылок из контента.

Ссылка как полноценная семантическая связь

Например, в редакторе:

<a
  href="/places/som-homestay"
  data-concept-id="som-homestay-id"
  data-relation="overnight"
  data-context="Подходящий ночлег перед ранним посещением пещеры"
  data-quality="0.9"
  title="Som Homestay — ночлег рядом с пещерой"
>
  переночевать в Som Homestay
</a>

Здесь уже содержится почти вся нужная семантика:

  • href — публичный адрес;
  • data-concept-id — стабильная техническая идентичность;
  • innerText — естественный анкор;
  • title — дополнительное пояснение пользователю;
  • data-relation — тип смысловой связи;
  • data-context — почему ссылка появилась;
  • data-quality — сила или полезность связи.

Из неё создаётся индексная запись:

sourceId: cave-id
targetId: som-homestay-id
relation: overnight
anchor: переночевать в Som Homestay
title: Som Homestay — ночлег рядом с пещерой
context: Подходящий ночлег перед ранним посещением пещеры
quality: 0.9

Что хранить в rel

Я бы осторожно использовал стандартный HTML-атрибут rel.

Он уже имеет браузерную и SEO-семантику:

  • nofollow;
  • ugc;
  • sponsored;
  • noopener;
  • noreferrer;
  • alternate;
  • author;
  • tag.

Свою предметную семантику безопаснее хранить отдельно:

data-relation="overnight-before"

А стандартный rel использовать по назначению:

rel="ugc nofollow"

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

Минимальный формат ссылки

Для ручной работы в редакторе слишком много атрибутов будет мешать. Поэтому обязательными я бы сделал только:

<a
  data-concept-id="..."
  data-relation="..."
>
  текст ссылки
</a>

Остальное можно определить автоматически:

  • href — получить через SiteRoute;
  • anchor — взять из innerText;
  • title — сгенерировать из связанного концепта;
  • quality — использовать значение по умолчанию;
  • context — извлечь из окружающего предложения;
  • position — вычислить парсером.

Расширенный режим нужен только там, где автор хочет явно переопределить семантику:

<a
  data-concept-id="..."
  data-relation="overnight"
  data-quality="0.95"
  data-context="Лучший ночлег перед ранним выездом"
>
  Som Homestay
</a>

Окружающий текст тоже несёт семантику

Допустим, написано:

Перед ранним посещением пещеры лучше
<a data-concept-id="som-id">переночевать в Som Homestay</a>,
поскольку дорога утром занимает всего около 30 минут.

Даже без явного data-relation агент или парсер может вывести:

relation: overnight_before
context: ночлег перед ранним посещением пещеры

Но я бы различал:

  • явную семантику автора;
  • автоматически определённую семантику.

Например:

origin: explicit

или:

origin: inferred
confidence: 0.72

Не обязательно показывать это редактору. Это техническая информация индексной таблицы.

Возможная схема таблицы

enum KBConceptRelationOrigin {
  explicit
  inferred
  generated
}

model KBConceptRelation {
  id        String   @id @default(cuid()) @db.VarChar(36)
  createdAt DateTime @default(now()) @db.Timestamp(3)
  updatedAt DateTime @default(now()) @updatedAt @db.Timestamp(3)

  sourceId String
  Source   KBConcept @relation(
    "ConceptRelationSource",
    fields: [sourceId],
    references: [id],
    onDelete: Cascade
  )

  targetId String
  Target   KBConcept @relation(
    "ConceptRelationTarget",
    fields: [targetId],
    references: [id],
    onDelete: Cascade
  )

  relation String? @db.VarChar(100)

  anchor  String? @db.Text
  title   String? @db.Text
  context String? @db.Text

  quality   Float @default(0.5)
  confidence Float?

  origin KBConceptRelationOrigin @default(explicit)

  // Стабильный идентификатор конкретной ссылки в редакторе
  nodeId String? @db.VarChar(100)

  // Положение в содержимом
  position Int?

  // Исходные или дополнительные HTML-атрибуты
  data Json?

  @@unique([sourceId, nodeId])
  @@index([sourceId])
  @@index([targetId])
  @@index([sourceId, relation])
  @@index([targetId, relation])
  @@index([quality])
}

quality и confidence здесь имеют разный смысл:

  • quality — насколько связь полезна для пользователя;
  • confidence — насколько уверенно определена её семантика.

Для явно заданной ссылки:

origin = explicit
confidence = 1

Для автоматически распознанной:

origin = inferred
confidence = 0.74

На первом этапе confidence можно вообще не добавлять.

Нужен стабильный идентификатор HTML-узла

Если просто использовать sourceId + targetId, возникнет проблема: один текст может ссылаться на один концепт несколько раз по разным причинам.

Например:

<a
  data-node-id="link-a17"
  data-concept-id="som-id"
  data-relation="overnight-before"
>
  ночлег перед посещением
</a>

<a
  data-node-id="link-b24"
  data-concept-id="som-id"
  data-relation="cold-weather-warning"
>
  зимой здесь может быть холодно
</a>

Это две самостоятельные связи с одной и той же целью.

data-node-id должен создаваться редактором автоматически и сохраняться при перемещении ссылки внутри документа.

Тогда синхронизация при сохранении проста:

  • ссылка с тем же nodeId — обновляется;
  • новый nodeId — добавляется;
  • отсутствующий nodeId — удаляется.

HTML и индекс должны иметь владельца истины

Здесь желательно избежать двустороннего редактирования.

Я бы зафиксировал:

Контент является источником истины для исходящих редакционных связей.

Таблица — индекс.

Значит:

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

Иначе появится рассинхронизация: таблица говорит одно, HTML — другое.

Автоматические связи можно хранить отдельно от контентных и не зеркалить в HTML до явного принятия.

Обратная связь

Если в карточке пещеры есть:

<a
  data-concept-id="som-id"
  data-relation="overnight-after"
>
  Som Homestay
</a>

То на странице Som Homestay можно автоматически вывести:

Подходит для ночлега после посещения

Пещера X — автор рекомендует этот хоумстей как ночлег после посещения.

Или встроить в текст динамический блок:

<concept-backlinks
  data-relation="overnight-after"
  data-limit="5"
></concept-backlinks>

Причём обратное представление не обязано повторять исходный анкор. Оно строится из:

  • типа отношения;
  • контекста;
  • исходного концепта;
  • качества связи.

parent и root действительно стоит оставить

Они решают другую задачу.

parentId

Жёсткая принадлежность или основной контейнер:

Тхай Нгуен
└── Som Homestay

либо структура контента:

Жильё
└── Хоумстеи
    └── Som Homestay

Нужно только выбрать один смысл parentId и не смешивать несколько иерархий.

Например:

parentId всегда означает редакционную принадлежность к основному разделу.

rootId

Ускоряет:

  • получение всего дерева;
  • поиск внутри раздела;
  • уникальность кода;
  • управление большой веткой;
  • удаление или перенос раздела.

Мягкие ссылки

Всё остальное:

  • рекомендуемый ночлег;
  • часть комплекса;
  • альтернатива;
  • следующая точка маршрута;
  • связано с погодным риском;
  • упоминается в маршруте;

идёт через индекс ссылок.

Полная семантика не обязательно требует сложных тегов

Можно начать с обычной ссылки:

<a data-concept-id="som-id">Som Homestay</a>

А тип отношения определять по трём уровням:

  1. Явный data-relation.
  2. Контекст родительского блока.
  3. Анализ окружающего текста агентом.

Например:

<section data-semantic-context="recommended-overnight">
  <p>
    Перед поездкой лучше остановиться в
    <a data-concept-id="som-id">Som Homestay</a>.
  </p>
</section>

Здесь каждая ссылка внутри секции наследует:

relation = recommended_overnight

Это может сильно упростить ручную разметку.

То же самое в Markdown-компонентах:

:::semantic{relation="overnight-before"}
Перед ранним посещением лучше остановиться в
[Som Homestay](concept:som-id).
:::

Семантика может жить на разных уровнях DOM

На ссылке

Для точной индивидуальной связи:

<a data-relation="alternative">

На абзаце

Для общего контекста:

<p data-semantic-context="winter-warning">

На секции

Для группы ссылок:

<section data-semantic-context="where-to-stay">

На кастомном компоненте

Для динамической выборки:

<concept-list
  data-relation="overnight-before"
  data-source="current"
></concept-list>

Парсер может наследовать семантику сверху вниз:

link.relation
  ?? paragraph.semanticContext
  ?? section.semanticContext
  ?? inferredFromText
  ?? "reference"

Что получится в итоге

У тебя появляется четыре согласованных уровня:

Жёсткое дерево:
parentId / rootId

Авторский смысл:
HTML-контент и ссылки

Быстрый граф:
KBConceptRelation

Динамическое представление:
обратные ссылки, списки и агентские блоки

Это очень практичная архитектура. Она не требует отдельно описывать весь мир в таблицах: семантика возникает прямо в процессе написания нормального человеческого текста, а база лишь материализует её для поиска, анализа и обратного использования.