headless lms

Custom editor

The contract a lesson content editor implements, and what the app hosting it does.

Lesson content is edited and rendered by a swappable editor. @headless-lms/editor defines the contract, and Plate is the editor that ships. The backend stores a lesson's content without reading it, so everything that depends on the format goes through the contract.

The editor module

An editor default-exports an EditorModule:

MemberWhat it is
EditorClient component that edits the content.
RendererServer-safe component that displays it: { config, assetUrls }.
assetIds(config)Required. The ids of the host assets the content uses, once each, in document order.
validate(config)Optional structural check, run before a save.
slotsThe host slots the editor fills: toolbar, blocks.
meta{ type, version } of the content format. Content in any other format is refused, not shown.

A config stores the asset id of each media node, never a URL. The URL comes from assetUrls, the map the API returns on each activity, keyed by asset id.

assetIds is how the API knows which files a lesson may serve. A lesson's assetUrls holds only the files its last save listed, and the file URL answers 404 for anything else, so an editor whose assetIds misses a media node publishes a lesson with broken media.

Editor props

PropWhat it is
initialConfigThe stored content. null or invalid starts empty.
assetUrlsURLs for the assets the initial content uses.
onSave(config)Persists the content. The config carries asset ids, not URLs.
onChange(config)Called on every edit, for host-owned save buttons and dirty state.
uploadFile(file, { onProgress })Host upload. Resolves { id, name, size, type, url }.
pickAsset({ kind })Host media library picker. Resolves the same shape, or null when cancelled.
slotsDOM elements for the toolbar and blocks slots.

Store the id returned by uploadFile and pickAsset on the media node. It is what assetIds returns and what assetUrls is keyed by. The returned url shows the file during the editing session.

Media playback

EditorMediaModule.MediaProvider passes MediaTracking to the editor's media players: onEvent reports playback and startPosition(assetId) gives the resume point.

What the host does

Saving

Send the content and assetIds(config) together:

import { Content } from "@headless-lms/sdk";
import editor from "./editor.config";

await Content.updateActivity({
  courseId,
  moduleId,
  activityId,
  settings: {
    ...settings,
    content: { type: editor.meta.type, version: editor.meta.version, config },
  },
  assetIds: editor.assetIds(config),
});

A save without assetIds keeps the lesson's current list, so send it whenever the content changes. Every id must be a ready file in the organization's media library; anything else is refused with a 409 naming the ids.

Rendering

Pass the activity's assetUrls to the Renderer, for students and for staff previews alike:

<Renderer config={activity.settings.content.config} assetUrls={activity.assetUrls} />

Each URL is stable and checks access on every request. See Building a frontend for the rules and the cookie setup.

On this page