Guides

Graph: the Edges around a Block

The graph route that walks the Edges of the Block index in both directions, the walk's limits, the client call, and the @skaile/forge-editor-graph add-on that draws the answer with sigma in Vue and React.

The Edges of the Block index say which Block points at which. The graph is their neighbourhood in one answer: the Blocks within a few hops of a start, and the Edges between them. The library answers it (GET /api/editor/graph); the add-on @skaile/forge-editor-graph draws it.

The route

RouteGET /api/editor/graph?documentId=&blockId=&depth= (API.graph, port index)
StartdocumentId alone: every Block of the Document that has an Edge. With blockId: that one Block (kept even without an Edge).
depthHops from the start Blocks, 1 to 3, default 2
AnswerGraphData: { nodes: { address, title, type }[], edges: BlockEdge[], truncated: boolean }
ClientEditorBackend.index.graph({ documentId, blockId?, depth?, signal? }), optional: a backend without it leaves it out

The walk follows Edges in both directions (what a Block points at and what points at it). It never follows tag Edges (a tag is no Block) or Edges to a Block that does not exist, and it never leaves the Documents the user may read: a Block in another Document is a node only when the user may read that Document. Every Edge in edges has both its Blocks in nodes.

The answer holds at most 500 Blocks. When the walk meets more, it keeps the first ones in the order it found them (start Blocks first) and sets truncated. Edges are answered once each, in the order they were met.

StatusWhen
400no documentId, or a depth outside 1 to 3
401no user
403the user may not read the Document
404blockId names no Block of the Document
501the host has no index port

The walk itself is walkGraph(seeds, source, { depth, maxNodes?, keepIsolated? }) in @skaile/forge-editor/core, over a GraphSource (edges(address, direction) and node(address)). The route reads a GraphSource from the index port; the memory backend (createMemoryBackend) reads one from its projected rows (graphSourceOfRows), so a test or a demo draws the same graph.

The panel

import { GraphPanel } from '@skaile/forge-editor-graph/vue' // or '@skaile/forge-editor-graph/react'
<GraphPanel :document-id="documentId" :block-id="blockId" :open="openBlock" />
Prop
documentIdthe Document the graph is about
blockIdoptional: the graph around this Block; it is drawn larger and in the middle
open(address)what a click on a node does; resolve false when it did not happen
depthhops to start with (1 to 3, default 2); the panel has a select for it

Opening a Block is the host's job, as for a search hit: in the open Document editor.commands.revealBlock(blockId), else navigate to the Document with ?block=<id>. The workspace host of this repository does both in openHit; the same function serves the graph ({ documentId, blockId } is what a BlockSearchHit also carries).

The panel draws:

Nodeone per Block, labelled with its title (else its type); colour by Block type, the focus Block in rose
Edge colourthe kind: reference, derivation, embed, and one colour per authored kind (supports, contradicts, ...), the same one every time
Edge dashedthe Edge was written by hand (origin: 'meta', in the inspector's „Kanten“ section); content Edges are solid
Arrowat the Block the Edge points to
Legendthe kinds present, as they are drawn
Listevery node again as a button, for keyboards and screen readers (the canvas is WebGL and has only a label)
Noticetruncated: more than 500 Blocks, the rest left out

The panel renders nothing when the backend has no index.graph.

Framework-free parts

@skaile/forge-editor-graph (no sigma, no DOM):

buildGraph(data, { focus?, iterations? })the graphology MultiDirectedGraph: node attributes label x y size color documentId blockId blockType focus, Edge attributes color size dash gap kind origin; positions are deterministic (circle, then ForceAtlas2)
edgeStyleOf({ kind, origin }){ color, dashed }: the style table of the panel
legendOf(data)the legend entries
createGraphPanelModel({ load, open, depth? })the model both components render: state (target, depth, status, last answer), setTarget, setDepth, reload, open, dispose
graphLoaderOf(backend)load for createGraphPanelModel, over backend.index.graph
createGraphTranslator(locale, overrides?)the add-on's own German and English strings

sigma (WebGL) loads when a panel mounts, so importing the package costs nothing and server-side rendering never meets it. The dashed Edges use sigma's layerDashed with the Edge attributes dash and gap: an authored Edge has 8 px dashes and 5 px gaps, every other Edge one dash longer than the canvas and no gap.

Wiring a host

The add-on needs no feature and no port of its own. The host only mounts the panel where it wants it (a sidebar tab, the Block's details) and passes open:

<script setup lang="ts">
import { GraphPanel } from '@skaile/forge-editor-graph/vue'
</script>

<template>
  <GraphPanel :document-id="documentId" :block-id="blockId" :open="openHit" />
</template>

The repository's document pages (the file-drawer layout, app/layouts/default.vue) show it behind NUXT_PUBLIC_ADDONS_GRAPH=1; the kitchen sink's standalone pages have no workspace index and show no graph.

Copyright © 2026