---
title: "Render: export a Block or a Document to a file"
description: "The Renderer port with its render targets, the SSE route that streams progress and stores the result as an asset, the client call, and the block action and command that offer it."
canonical_url: "https://skaile-ai.github.io/forge-editor/guides/render"
---
# Render: export a Block or a Document to a file

> The Renderer port with its render targets, the SSE route that streams progress and stores the result as an asset, the client call, and the block action and command that offer it.

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:

<table>
<thead>
  <tr>
    <th>
      Piece
    </th>
    
    <th>
      What it does
    </th>
    
    <th>
      Port (capability)
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Renderer
      </code>
    </td>
    
    <td>
      lists the targets a user may use and renders one input to one target
    </td>
    
    <td>
      <code>
        render
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GET /api/editor/render/targets
      </code>
    </td>
    
    <td>
      the targets with the Block types each accepts and its MIME type
    </td>
    
    <td>
      <code>
        render
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        POST /api/editor/render
      </code>
    </td>
    
    <td>
      one render, streamed as server-sent events; the result is stored as an asset of the Document
    </td>
    
    <td>
      <code>
        render
      </code>
      
       and <code>
        assets
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        EditorBackend['render']
      </code>
    </td>
    
    <td>
      <code>
        targets()
      </code>
      
       and <code>
        render(target, input, { onProgress })
      </code>
    </td>
    
    <td>
      <code>
        render
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Block action <code>
        render
      </code>
      
       and command <code>
        render-document
      </code>
    </td>
    
    <td>
      menus of the targets that take the Block type, or the whole Document
    </td>
    
    <td>
      <code>
        render
      </code>
    </td>
  </tr>
</tbody>
</table>

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.

```ts [docs/snippets/render-target.ts]
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](https://skaile-ai.github.io/forge-editor/reference/renderer-port).

## 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:

<table>
<thead>
  <tr>
    <th>
      Status
    </th>
    
    <th>
      Cause
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        400
      </code>
    </td>
    
    <td>
      malformed body or a Document ID that is not valid
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        401
      </code>
    </td>
    
    <td>
      no user
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        403
      </code>
    </td>
    
    <td>
      the user may not read the Document
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        404
      </code>
    </td>
    
    <td>
      unknown target
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        422
      </code>
    </td>
    
    <td>
      the target does not accept this Block type, or a whole Document
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        501
      </code>
    </td>
    
    <td>
      the host has no <code>
        render
      </code>
      
       or no <code>
        assets
      </code>
      
       port
    </td>
  </tr>
</tbody>
</table>

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 whose `accepts` names 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.


## Sitemap

See the full [sitemap](https://skaile-ai.github.io/forge-editor/sitemap.md) for all pages.
