Skip to content

Notes & Annotation System ​

The learning workspace at /subjects/:subject (SubjectLayout) pairs a YouTube lecture player with the subject's Docy notes, and layers a full annotation system on top. It's the most rewrite-scarred part of the codebase — this page documents both the current architecture and why it looks the way it does.

Content flow ​

Notes markdown lives in the private Docy repo, fetched at runtime through two Pages Functions — list-notes (directory listing for the subject's chapter list) and get-notes (file body) — which proxy GitHub's Contents API with a server-side GITHub_TOKEN. Editing a note in Docy updates the live site on the next fetch with no website redeploy. See Docy.

Rendering is the shared markdown pipeline (GFM + KaTeX + highlight). The workspace chrome — header and sidebar — auto-hides while reading/watching, reclaiming the space for an immersive layout; it reappears on hover/interaction.

Annotations: the architecture that survived ​

Users can highlight note text and attach notes to it, persisted per user in users/{uid}/annotations. The first implementation stored DOM ranges and mutated the rendered HTML directly. It failed in every way that approach always fails:

  • DOM mutation broke React reconciliation — highlights vanished or duplicated on re-render.
  • KaTeX offsets — math renders to markup whose text content doesn't match the source, so character offsets computed against the DOM drifted whenever a formula appeared upstream.
  • Multi-block selections corrupted ranges when a highlight spanned paragraph/list boundaries.
  • A macOS-specific black-screen crash on certain selections.

The rewrite inverted the model: annotations are data, anchored to the source markdown, not to the DOM.

  • Each annotation stores block-level anchors + text offsets computed against the source content, with the selected text stored alongside as a fuzzy fallback if content shifts.
  • Highlights are applied during render as spans emitted by the pipeline — React owns every node; nothing mutates the DOM after mount.
  • Multi-block selections decompose into per-block segments, each anchored independently, so one annotation can span structures safely.
  • KaTeX regions are treated as atomic — selections snap around them instead of measuring through them.

useAnnotations owns load/save/merge; the toolbar and popover are pure consumers.

The popover clipping fix ​

The annotation popover originally rendered inside the notes scroll container, whose overflow: hidden clipped it near edges. The fix is the standard one worth naming: the popover renders through createPortal to document.body and positions against the selection rect — the same pattern later reused by other floating UI in the app.

ShortNotes + rich formatting ​

A floating ShortNotes panel lets users keep running per-subject notes beside the lecture (persisted via useShortNotes), with a rich formatting toolbar (bold/italic/code/lists/headings) that edits the underlying markdown rather than HTML — keeping storage format identical to everything else.

Revision integration ​

Lecture notes and annotations feed the spaced-repetition system: marking a note section or annotation for revision creates an entry in users/{uid}/revisions that revision.js schedules through expanding intervals, surfacing due items on /revision and the dashboard. The learning workspace is therefore an input to retention, not a dead-end reader.

Failure modes to know ​

SymptomCause
Highlights slightly misplaced after a Docy editanchors drifted; fuzzy text fallback re-locates, exact offsets re-save on next interaction
Notes pane empty, console 500GITHUB_TOKEN missing/expired server-side (tokens are rotated; the original one was revoked after an exposure — see Docy)
Popover appears at wrong corner on zoomed displaysselection rect vs portal coordinate spaces — recompute on scroll/resize is already wired; check for a missing listener before re-deriving math

Ediky Workflow — internal engineering documentation.