Skip to content

Cross-references and cards

The [[…]] marker

Three written forms are accepted:

[[7|Febrile seizures]]        article 7, shown as "Febrile seizures"
[[7#workup|the workup]]       article 7, jumping to the section slugged "workup"
[[febrile-seizures]]          legacy slug form, label taken from the slug

The id form is preferred because an id survives a rename. The slug form is rewritten to the id form on save, never in a bulk migration — rewriting prose nobody asked to change is how you lose an author's trust.

A section anchor only decides where the reader lands. A renamed or deleted section degrades to the top of the article rather than becoming a broken link.

Automatic linking

link_cross_references() runs on save of a published article and on publish — never on a draft — and is idempotent: it seeds its "already seen" set from the markers already in the text.

What it will not touch:

  • anything past the first mention of a term in a section
  • more than 6 links in one section
  • a partial word — matches are at word boundaries
  • code spans, existing links, existing markers, headings, image alt text, and table rows (a pipe inside a cell breaks the table)
  • anything on the stop list
  • titles under 4 characters

The manual counterpart is a two-step moderator tool: GET /articles/{id}/crosslinks returns proposals and writes nothing; POST /articles/{id}/crosslinks applies an approved subset, re-matching each proposal's offset at write time so nothing lands in text that has since moved. It is willing to match a short abbreviation — "CF", "TB" — where autolink is not, on a case-sensitive match.

The editorial rule

An article may link its own subject. This was tried the other way and reverted. One link per target per section; never inside a table row. Drug names have no target library yet. The full note is Linking and cards.

Hover previews

GET /api/v1/articles/preview/{slug}

Deliberately small: id, slug, title, a plain-text excerpt of up to 260 characters with markup, markers and images stripped, the section count and the status. It 404s for a non-moderator on an unpublished article.

The front end fetches it after a 350 ms hover and caches it per slug for the life of the page; on a touch device the card opens on tap instead of following the link.

GET /articles/link-targets?q= is a different endpoint for a different job — the editor's picker when writing a new marker. It needs sections and a title-first ranked match, not a definition excerpt.

The glossary

A shared definition for a term, written inline in any article, stem, explanation or option:

{{hyperoxia test}}               the shared definition for this term
{{ductal-dependent|…}}           a one-off inline explanation, not shared
{{|…|caution}}                   a trailing chip tied to no particular word

A GlossaryTerm has a lookup key, a display term, a definition of at most 400 characters, a kind of define, fact, why or caution, an optional article_id for "read more", and a source.

400 characters is about four sentences — where an explanation stops being an aside and starts being an article nobody has written yet.

A term with no entry renders as plain prose rather than an error, so the entry and the prose that references it can land in either order.

Endpoint Gate
GET /glossary?q= , GET /glossary/{key} any signed-in user
POST /glossary (upsert by key) moderator
POST /glossary/bulk (up to 200, all-or-nothing) moderator
DELETE /glossary/{key} moderator
docker compose exec backend python -m app.cli check-links

Exits non-zero if it finds anything. It reports:

Bucket Catches
markers A [[…]] pointing at a missing article, at an unpublished article from a published one, or at a redirect or old slug rather than the live article
key_points A question key point citing an article that is gone
section_links A question ↔ article link naming a section that no longer exists
one per pointer table Rows in question_article_links, study_plan_block_articles, flashcard_article_links, user_collection_articles and article_exam_links whose article is not among the living

It is stricter than the reader on purpose: a redirect still counts as broken, because library policy is to point at what is there.