Render: export a Block or a Document to a file
A render target turns one Typed block, or a whole Document, into a file: Markdown, a PDF, a slide deck, a spoken recording. The editor ships one target, markdown. A host or an add-on contributes the others. The render capability connects the pieces:
| Piece | What it does | Port (capability) |
|---|---|---|
Renderer | lists the targets a user may use and renders one input to one target | render |
GET /api/editor/render/targets | the targets with the Block types each accepts and its MIME type | render |
POST /api/editor/render | one render, streamed as server-sent events; the result is stored as an asset of the Document | render and assets |
EditorBackend['render'] | targets() and render(target, input, { onProgress }) | render |
Block action render and command render-document | menus of the targets that take the Block type, or the whole Document | render |
A render lives as long as its request. There is no job store: the server streams progress while the target runs, stores the file, and ends the stream. A client that disconnects aborts the target through the AbortSignal that Renderer.render receives.
Add a target
A target (RenderTarget, @skaile/forge-editor/adapters/render) is a RenderTargetInfo (id, label, accepts, mime) plus run. accepts lists Block type ids, and 'document' for a whole Document. run receives the input (documentId, blockId?, content, options?), a progress reporter and an AbortSignal, and returns { data, mime, fileName }. createRenderer(targets) makes the Renderer of the host's ports.render. A target whose id repeats an earlier one is dropped, so a host can list its own before the defaults.
import { createRenderer, markdownTarget, type RenderTarget } from '@skaile/forge-editor/adapters/render'
import type { EditorPorts } from '@skaile/forge-editor/server'
// A render target names the Block types (or 'document') it accepts and the MIME type it produces. `run` reports its
// steps through `progress` and returns the file; a thrown error reaches the client as the `error` event.
const plainText: RenderTarget = {
id: 'plain-text',
label: 'Text (.txt)',
accepts: ['document'],
mime: 'text/plain',
async run({ content }, progress) {
progress({ phase: 'collect', percent: 50 })
const text = JSON.stringify(content)
return { data: new TextEncoder().encode(text), mime: 'text/plain', fileName: 'document.txt' }
},
}
// The renderer is one port: the host passes its targets, the built-in `markdown` among them.
export const render: NonNullable<EditorPorts['render']> = createRenderer([markdownTarget(), plainText])
A host that needs its own target logic, for example a per-user list, implements the Renderer interface directly instead of calling createRenderer. See the Renderer port reference.
The built-in markdown target
markdownTarget(options?) renders a Typed block or a whole Document as Markdown, the same text the block action "Als Markdown exportieren" downloads: every Typed block becomes a heading with its description, tags and body beneath it. It accepts 'document' and every Block type, takes schema and blockTypes options for a host with its own Block types, and needs no editor, only the schema.
The route
POST /api/editor/render takes { target, input }. The server checks the request before the stream starts:
| Status | Cause |
|---|---|
400 | malformed body or a Document ID that is not valid |
401 | no user |
403 | the user may not read the Document |
404 | unknown target |
422 | the target does not accept this Block type, or a whole Document |
501 | the host has no render or no assets port |
After that the answer is a text/event-stream with the events progress ({ phase, percent? }), then done ({ asset, fileName, mime }) or error ({ message }). The result is stored through ports.assets.storeAsset as an asset of input.documentId. asset is the asset name: read it with the asset route (GET /api/editor/assets/:documentId/:asset), like any upload.
The client
backend.render is present when the server serves the render capability. backend.render.render(target, input, { onProgress, signal }) resolves with { asset, fileName, mime, url } and rejects with the server's message when the render fails. The loop over the stream is postSse (core/sse.ts), the same helper the AI streams use (postAiSse calls it), so there is one parser for server-sent events in the client.
In the editor
The composites feature adds both entries when the backend serves render, and hides them otherwise:
- the block action „Erzeugen als …“ (
render) on a Typed block, a menu of the targets whoseacceptsnames the Block type; - the command „Dokument erzeugen als …“ (
render-document, ⌘K palette), a menu of the targets that accept'document'.
A notice reports the phase and percent of the running render, then the end. The finished file downloads at once.
Explore: backlinks, related Blocks and the trail
What is connected to a Block: the Blocks that point at it, the Blocks related to it, and the chain of Derivations behind it. The explore Feature, the panel, the related route and derivationTrailOf.
Importers: what a dropped file becomes
The importer registry of the feature contract: Importer and ImportPlan, planImport, the Markdown, subtitle and BibTeX importers of the media feature, and how composite-media wraps the plan.