Skip to content

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 citationstitle, 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.

PUT /api/v1/articles/aliases
{ "aliases": { "42": ["Alport syndrome", "hereditary nephritis"] } }

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.

A question ↔ article link may name a section or not. section_id null means the whole article.

  • PUT /articles/{id}/links writes 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}/questions reads 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.

  • Every slug an article has ever had is remembered, so an old address still resolves rather than 404ing.
  • An ArticleRedirect maps 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.