An article¶
The three views¶
Every section of an article carries a variant, one of:
| Variant | Is |
|---|---|
short |
The summary — what you read when you want the shape of it. |
long |
The full article. The default, and what a section written before variants existed becomes. |
clinical |
The bedside view: management, doses, thresholds. |
The toggle only ever offers the views that are actually present, so an article written only at length does not show two empty tabs.
There is no ?variant= parameter. GET /articles/{id} returns every
section the reader is allowed to see and the page filters by variant in the
browser. What the server filters is different: which views the learner's
active exam allows. An editor additionally gets all_variants, unfiltered, so
the editor can see every view at once.
Sections may nest one level. A sub-section must sit in the same variant as its parent, and the server enforces it.
What an article is made of¶
| Part | Shape |
|---|---|
title, slug |
The slug is unique and is what a cross-reference addresses. |
summary |
One paragraph. |
content |
The markdown introduction, before the first section. |
sections |
A JSON list of {id, slug, title, content, parent_id, variant}. The id is a stable 32-hex string so a link survives a rename. |
references_json |
A list of structured citations — title, author, pages, journal, year, pmid, doi, url, and a rendered text line. A legacy flat string is normalised on read, with a trailing PMID: nnnn pulled out of it. |
category_id |
Where it is filed. This is what a grant reaches, and what a generated deck inherits. |
status |
draft, in_review or published. |
Entering in_review stamps submitted_at; entering published stamps
reviewed_at and reviewed_by. first_published_at is set once, on first
publish, and never cleared — it is what decides whether deleting the article
bins it or removes it outright.
Key points are not an article field. They belong to questions, and a question's key point may cite an article by id.
Aliases¶
Another name for an article, used by the cross-reference matcher so a term written in one article links to the article that explains it even when that article is titled something else.
Moderator-only, and a wholesale replace: the table is emptied and rebuilt
from the payload on every call. Article ids that no longer exist come back in
an ignored list rather than failing the batch.
In the matcher's ranking, an alias sits below an article's own title and above a section-title match.
Topic claims¶
A claim is a standing statement that this article covers this category. It is not read at request time — it is materialised into ordinary question ↔ article links, so a question filed into that category later gets the reading too.
| Option | Default | Effect |
|---|---|---|
include_subtopics |
true |
Reaches the category's descendants as well. |
fill_only |
false |
Links only questions that currently have no reading at all, so bulk-claiming does not drown questions that already have some. |
A claim is applied at three moments: when a question's filing changes, when the
claim itself is staked or edited, and by the apply_topic_claims sweep every
30 minutes, which catches bulk SQL changes, imports and restores.
A single claim may create at most 300 links. Staking one that would create more is refused rather than truncated.
| Endpoint | Does |
|---|---|
GET /articles/{id}/links/from-category |
Preview — how many questions would be linked |
POST /articles/{id}/links/from-category |
Stake or update a claim |
GET /articles/{id}/claims |
List the claims, with counts |
DELETE /articles/{id}/claims/{claim_id}?keep_links= |
Drop a claim. The links it already made stay by default. |
Section notes¶
A reader's own notes, one per section, private to the writer.
| Endpoint | Does |
|---|---|
GET /articles/{article_id}/notes |
This reader's notes. A note on a section that no longer exists comes back orphaned: true, carrying the heading as it was last seen, sorted last. |
PUT /articles/{article_id}/notes/{section_id} |
Write one. Maximum 8,000 characters. Empty content deletes the note. Writing on a section that no longer exists, with no note already there, is a 404. |
DELETE /articles/{article_id}/notes/{section_id} |
Delete, orphaned or not. |
These are private. Feedback addressed to whoever maintains the article is a different thing and goes elsewhere.
Reading links¶
A question ↔ article link may name a section or not. section_id null means
the whole article.
PUT /articles/{id}/linkswrites them, and checks permission on both ends — the caller must be able to edit the article and the question.DELETE /articles/{id}/links/{question_id}?section_id=unlinks. Moderator only, and there is no undo.GET /articles/{id}/questionsreads them back, filtered by what the asker may see.
On save, a link whose section no longer exists is pruned. A whole-article link survives everything.
Slugs, redirects and dead links¶
- Every slug an article has ever had is remembered, so an old address still resolves rather than 404ing.
- An
ArticleRedirectmaps a retired article to a live one. It is followed exactly once and never chased through a second redirect. - A redirect whose target has since been binned does not resolve.
- The link checker is stricter than the reader: a pointer that lands on a redirect or an old slug counts as broken, even though the page would have followed it. Library policy is to point at what is there. See check-links.
What "read" records¶
Two different things, and they are kept apart.
| Table | Written when | Means |
|---|---|---|
ArticleView |
Every time the article is opened | One row per reader per article, timestamp updated. Best-effort — a failure to record it never breaks the page. |
ArticleRead |
POST /articles/{id}/read?read=true |
"I have read this." Reader-reported, one row per reader per article. |
Marking read is idempotent, and read=false removes it. It is deliberately
reversible: progress nobody can correct stops being trusted.