Worklog for task "Redesign the website structure, refine it, and rebuild the content"
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 viaSiteRoute;anchorβ take frominnerText;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:
parentIdalways 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:
- Explicit
data-relation. - Context of the parent block.
- 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.