Worklog for task "Redesign the website structure, refine it, and rebuild the content"

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

I won't quote the entire chat with ChatGPT, but I've completely rethought the essence of concepts and facts, and it all boils down to the fact that there's no point in making facts separate entities in our implementation. Our concepts themselves can also be facts.

Here is a brief summary of how I see it:


We don't have a banking system to build detailed relationships. Our information lifecycle works like this:

1. A concept that didn't exist before is created. For example, Some Homestay.

2. Everything you want to convey is written inside it in plain text. This is all the stuff the user will read later. In our project, this is essentially a fact in itself, because we create all the content ourselves. And we immediately specify qualitative attributes for what is what. Over time, when this information turns out to be of poor quality, we will simply lower its quality attribute. Conceptually, we only need a single float metric from 0 to 1 right now. The higher the quality, the higher the visibility. For example, my website is full of old articles that are no longer needed and should be thrown away, but I can't throw them away because 404s aren't cool. I just need to push them further down.

3. Linking with other concepts. That means I just need cross-linking. Just like on classic websites, I can simply add links to other concepts. This is so the user can read and navigate to related cards. But this is an external link. I also need the reverse link. Accordingly, I don't just need anchor links here, but also accompanying technical link information (why I'm linking to this and, once again, some quality attribute).

4. When saving such a concept, all its relationships are recalculated. Those that were removed from the content are also removed from the relationship list, and added ones appear.

This is precisely where we need a flat many-to-many concept relationship table. Here, we can then look at the overall semantics of the entire knowledge base.

And here is what I am thinking, if we develop this idea further: we can create complete semantics at the HTML tag level. We simply insert links into the text. At the same time, they have innerText, title, rel, data-attributes, etc. All of this can be mirrored with our index relationship table. Meanwhile, parent and root remain as hard links.


Here is what ChatGPT summarized:

Yes, this is already looking very cohesive.

Then HTML/Markdown content becomes the primary semantic graph, and the relationship table acts as its materialized index for fast queries and back-linking.

At the same time:

  • parentId β€” hard direct hierarchy;
  • rootId β€” belonging to a single tree or section;
  • HTML links β€” soft semantic many-to-many relationships;
  • relationship table β€” a mirrored representation of links from the content.

Link as a full-fledged semantic relation

For example, in the editor:

<a
  href="/places/som-homestay"
  data-concept-id="som-homestay-id"
  data-relation="overnight"
  data-context="Suitable overnight stay before an early visit to the cave"
  data-quality="0.9"
  title="Som Homestay β€” overnight stay near the cave"
>
  stay overnight at Som Homestay
</a>

This already contains almost all the necessary semantics:

  • href β€” public address;
  • data-concept-id β€” stable technical identity;
  • innerText β€” natural anchor;
  • title β€” additional explanation for the user;
  • data-relation β€” type of semantic relationship;
  • data-context β€” why the link appeared;
  • quality β€” strength or usefulness of the link.

An index entry is created from it:

sourceId: cave-id
targetId: som-homestay-id
relation: overnight
anchor: stay overnight at Som Homestay
title: Som Homestay β€” overnight stay near the cave
context: Suitable overnight stay before an early visit to the cave
quality: 0.9

What to store in rel

I would be careful about using the standard HTML rel attribute.

It already has browser and SEO semantics:

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

It is safer to store your domain-specific semantics separately:

data-relation="overnight-before"

And use standard rel for its intended purpose:

rel="ugc nofollow"

Otherwise, you might later run into a conflict between your own model and the HTML standard.

Minimal link format

For manual work in the editor, too many attributes will get in the way. Therefore, I would make only the following mandatory:

<a
  data-concept-id="..."
  data-relation="..."
>
  link text
</a>

The rest can be determined automatically:

  • href β€” retrieve via SiteRoute;
  • anchor β€” take from innerText;
  • title β€” generate from the linked concept;
  • quality β€” use the default value;
  • context β€” extract from the surrounding sentence;
  • position β€” calculate via parser.

Advanced mode is only needed where the author wants to explicitly override the semantics:

<a
  data-concept-id="..."
  data-relation="overnight"
  data-quality="0.95"
  data-context="Best place to stay before an early departure"
>
  Som Homestay
</a>

Surrounding text also carries semantics

Suppose it is written:

Before an early visit to the cave, it's better to
<a data-concept-id="som-id">stay overnight at Som Homestay</a>,
since the drive in the morning takes only about 30 minutes.

Even without an explicit data-relation, an agent or parser can infer:

relation: overnight_before
context: overnight stay before an early visit to the cave

But I would distinguish between:

  • explicit author semantics;
  • automatically determined semantics.

For example:

origin: explicit

or:

origin: inferred
confidence: 0.72

It's not necessary to show this to the editor. This is technical information for the index table.

Possible table schema

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)

  // Stable identifier of a specific link in the editor
  nodeId String? @db.VarChar(100)

  // Position within the content
  position Int?

  // Original or additional HTML attributes
  data Json?

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

quality and confidence have different meanings here:

  • quality β€” how useful the connection is for the user;
  • confidence β€” how confidently its semantics are determined.

For an explicitly specified link:

origin = explicit
confidence = 1

For an automatically recognized one:

origin = inferred
confidence = 0.74

At the initial stage, confidence can be omitted altogether.

A stable HTML node identifier is needed

If you simply use sourceId + targetId, a problem arises: a single text might link to the same concept multiple times for different reasons.

For example:

<a
  data-node-id="link-a17"
  data-concept-id="som-id"
  data-relation="overnight-before"
>
  overnight stay before the visit
</a>

<a
  data-node-id="link-b24"
  data-concept-id="som-id"
  data-relation="cold-weather-warning"
>
  it can get cold here in winter
</a>

These are two independent relationships with the exact same target.

data-node-id should be generated automatically by the editor and preserved when moving the link within the document.

Then synchronization upon saving is simple:

  • link with the same nodeId β€” updated;
  • new nodeId β€” added;
  • missing nodeId β€” deleted.

HTML and the index must have a single source of truth

It is advisable to avoid two-way editing here.

I would establish that:

Content is the source of truth for outgoing editorial links.

The table is an index.

This means:

  • the user edits the link in the text;
  • after saving, the index is recalculated;
  • direct editing of index table rows is prohibited or restricted;
  • backlink blocks are generated from the index table;
  • modifying an incoming relationship is done through the source concept.

Otherwise, desynchronization will occur: the table says one thing, HTML says another.

Automatic links can be stored separately from content links and not mirrored into HTML until explicitly accepted.

Backlinks

If the cave card contains:

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

Then on the Som Homestay page, you can automatically display:

Suitable for staying overnight after the visit

Cave X β€” the author recommends this homestay as a place to stay after the visit.

Or embed a dynamic block into the text:

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

Moreover, the backlink representation does not have to repeat the original anchor. It is built from:

  • relationship type;
  • context;
  • source concept;
  • link quality.

parent and root are indeed worth keeping

They solve a different problem.

parentId

Hard ownership or main container:

Thai Nguyen
└── Som Homestay

or content structure:

Accommodation
└── Homestays
    └── Som Homestay

You just need to choose a single meaning for parentId and not mix multiple hierarchies.

For example:

parentId always means editorial belonging to the main section.

rootId

Speeds up:

  • retrieving the entire tree;
  • searching within a section;
  • code uniqueness;
  • managing a large branch;
  • deleting or moving a section.

Soft links

Everything else:

  • recommended overnight stay;
  • part of a complex;
  • alternative;
  • next route point;
  • associated with weather risk;
  • mentioned in the itinerary;

goes through the link index.

Full semantics do not necessarily require complex tags

You can start with a regular link:

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

And determine the relationship type across three levels:

  1. Explicit data-relation.
  2. Context of the parent block.
  3. Analysis of the surrounding text by an agent.

For example:

<section data-semantic-context="recommended-overnight">
  <p>
    Before the trip, it's best to stop at
    <a data-concept-id="som-id">Som Homestay</a>.
  </p>
</section>

Here, every link within the section inherits:

relation = recommended_overnight

This can greatly simplify manual markup.

The same thing in Markdown components:

:::semantic{relation="overnight-before"}
Before an early visit, it's best to stay at
[Som Homestay](concept:som-id).
:::

Semantics can live at different DOM levels

On the link

For precise individual relationships:

<a data-relation="alternative">

On the paragraph

For general context:

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

On the section

For a group of links:

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

On a custom component

For dynamic fetching:

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

The parser can inherit semantics from top to bottom:

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

What you get in the end

You end up with four aligned levels:

Hard tree:
parentId / rootId

Author meaning:
HTML content and links

Fast graph:
KBConceptRelation

Dynamic representation:
backlinks, lists, and agent blocks

This is a very practical architecture. It doesn't require describing the entire world in tables separately: semantics emerge right in the process of writing normal human text, and the database merely materializes it for search, analysis, and reuse.

13.07.2026