Skip to content

Direct course editing

The direct editor lets the authenticated owner change an allowlisted, Blob-backed course on its normal reading page. Readers and compiled courses never mount the draft or publication machinery.

Availability and modes

The shell probes GET /api/course-editor/session once per document. It offers Edit only when the session belongs to the configured owner and the current course came from the Blob repository. Read, Edit, and Draw are mutually exclusive; Escape returns a working mode to Read unless focus is inside a text field.

Edit mode exposes course, topic, and collection metadata, topic slugs, container ordering, and an in-page toolbar for moving, duplicating, deleting, inserting, and supported block conversions. Field editors currently exist for core.theory, core.key-points, and core.callout. Other registered blocks remain renderable and can still be moved, duplicated, or deleted. The insertion menu lists only block types with parser-validated starter content; adding one never depends on the course already containing an example of that type.

Draft and publication contract

  • One authored draft per course is stored in IndexedDB database tue-study-hub-drafts, store drafts. Changes autosave after 800 ms of idle time and flush on field blur.
  • Render-resolved Blob asset URLs never enter authored drafts or publish requests. The session keeps source paths separately, and repairs drafts written by editor versions that mixed those layers.
  • Leaving Edit publishes only when content changed. Publish now is the explicit retry/recovery action. A request id stays stable across retries of identical content and changes with the content.
  • POST /api/course-editor/publish is owner-only, mutation-header protected, byte-capped, and restricted to the intersection of the publication and Blob-course allowlists. It validates the complete course before the existing immutable manifest-last publisher moves the live pointer.
  • Transient failures use bounded exponential backoff. Rate limits honor a capped Retry-After. Exhaustion keeps the draft locally and offers both retry and JSON download.
  • A stale base fetches the current authored revision and opens a three-way base/live/draft conflict surface. It never silently overwrites the live course.
  • A successful publish pins the returned revision in the owner session before refresh. Slug moves remap drawings through the drawing replacement barrier, while aliases and the published identity index reconnect progress, annotations, and review state.
  • Successful editor publication immediately expires the cached mutable pointer and the course route tree. Blob-backed course pages render at request time, and each visible open page checks the small public revision identity every three seconds and refreshes only when its sequence changes.
  • A successful publish retains its local draft as a receipt until a server render confirms that same content at the published or newer sequence. A stale or failed refresh therefore cannot make a successfully published edit appear lost; the matching receipt is discarded after confirmation.

Timed publication is intentionally absent: edits only write the local draft, while leaving Edit or pressing Publish now crosses the paid publication boundary. A floating status surface pairs each state with a plain-language scope such as Private to this browser or Published for everyone — open pages sync automatically, so local persistence and public visibility are never conflated.

Conflict comparison operates at manifest, topic, or collection granularity. For each unit it compares the served base with the current live revision and local draft. Conflicts open in a focused bottom sheet that identifies which side changed and asks whether to use the published version or local draft before publishing against the new live base.

Identity rules

Edit Identity result
Reorder Preserves ids; no renamedIds entry.
Duplicate Mints a new block id; review-item identity is scoped by the new block.
Theory/key-points conversion Preserves the compatible block id.
Delete Emits no rename; the publication identity index retains a tombstone.
Topic slug move Preserves content ids, writes the old-path alias, and remaps the drawing page key.
Container-id rename in the pure operation Emits the container plus every descendant block and review-item mapping. Stable ids are not exposed as ordinary editor fields.

To add a field editor, implement the BlockEditorProps contract and attach it as the optional editor member of that block's registered BlockDefinition. Keep parsing and semantic validation in the definition; the editor only emits replacement JSON props. Add a focused editor test and verify that Read mode still uses the ordinary renderer.

Drafts are deliberately not part of workspace backup. Recover one by returning to the same course in the same browser, or use Download draft in local-draft-only mode. Published content and personal state keep their existing backup contracts.

Configuration

The editor uses the Goal 3 publication/authentication configuration, plus:

  • PUBLICATION_EDITOR_COURSE_IDS: comma-separated course ids the direct editor may publish.
  • NEXT_PUBLIC_BLOB_COURSE_IDS: Blob-backed course ids. A course must occur in both lists.
  • The existing owner identity, allowed Blob origins/base URL, and publication write credentials.

Code authority

  • Session and UI: src/course-os/ui/editor, src/course-os/ui/CourseModeProvider.client.tsx
  • Pure edits and drafts: src/course-os/editor
  • Owner endpoints: src/app/api/course-editor
  • Publication boundary: src/course-os/publication
  • Identity follow-up: src/course-os/editor/localFollowUp.ts, src/course-os/services/personalState/localData.ts

Verification

For an editor change, run the closest Vitest file first, then npm run check, npm run build, and npm run test:integration. Before declaring the exit gate complete, use an authenticated preview to edit and publish one allowlisted Blob course, reload it, exercise a two-tab conflict, and verify in a signed-out window that only the atomic published revision is visible.