Most Forkshop installs need only the built-ins: gallery and tree layouts, plus the three Node kinds (inline-react, iframe-route, iframe-component). For everything else, the engine exposes three extension points.

Custom NodeType

A NodeType tells the engine how to render a kind of Node. To add one, write an object that satisfies the NodeType<T> interface and register it in forkshop.config.tsx.

import type { NodeType, AnyNode, RenderProps } from "@forkshop/engine"

type StorybookStoryNode = AnyNode & {
  kind: "storybook-story"
  storyId: string
}

export const storybookStoryNodeType: NodeType<StorybookStoryNode> = {
  id: "storybook-story",
  match: (node): node is StorybookStoryNode => node.kind === "storybook-story",
  render: ({ node }: RenderProps<StorybookStoryNode>) => (
    <iframe
      src={`http://localhost:6006/?path=/story/${node.storyId}`}
      style={{ width: "100%", height: "100%", border: 0 }}
    />
  ),
}

Register it in forkshop.config.tsx:

import { defineConfig, BUILTIN_NODE_TYPES } from "@forkshop/engine"
import { storybookStoryNodeType } from "./node-types/storybook-story-node-type"

export const forkshopConfig = defineConfig({
  mount: "app/forkshop",
  nodeTypes: [...BUILTIN_NODE_TYPES, storybookStoryNodeType],
  // ...
})

<BoardRegistry> picks it up automatically and dispatches any Node with kind: "storybook-story" to your custom render.

Custom Layout

A Layout arranges Nodes on the canvas. Two ship by id (gallery, tree); add your own with defineLayout().

import { defineLayout, forkshopIcons, type Layout } from "@forkshop/engine"

export const radialLayout: Layout<{ radius: number }> = defineLayout({
  id: "radial",
  icon: forkshopIcons.flows,
  defaultOptions: { radius: 300 },
  stageSize: () => ({ width: 800, height: 800 }),
  render: ({ entries, options, nodePositions, onPositionChange }) => {
    // Compute positions around a circle of radius `options.radius`
    // Render each entry at its computed (x, y) — use NodeView or
    // your own React tree
    return /* ... */
  },
})

Register and reference in a Board:

import { defineConfig, BUILTIN_LAYOUTS } from "@forkshop/engine"
import { radialLayout } from "./layouts/radial"

export const forkshopConfig = defineConfig({
  // ...
  layouts: [...BUILTIN_LAYOUTS, radialLayout],
})
// In a Board:
import { defineBoard } from "@forkshop/engine"
import { radialLayout } from "./layouts/radial"

export default defineBoard({
  id: "radial-demo",
  match: (s) => s.kind === "section" && s.sectionId === "radial",
  layout: radialLayout,
  layoutOptions: { radius: 400 },
  useEntries: () => /* ... */,
})

Escape hatch — withBoardMeta

When the useEntries + Layout model doesn't fit (e.g., a Board that manages its own canvas via direct <ForkshopCanvas>), wrap a raw React component:

import { withBoardMeta } from "@forkshop/engine"

const CustomBoard = withBoardMeta(
  function MyBoard() {
    return /* whatever JSX renders directly on the canvas */
  },
  {
    id: "custom",
    label: "Custom",
    match: (s) => s.kind === "section" && s.sectionId === "custom",
  },
)

<BoardRegistry> treats this like any other Board for sidebar rendering and selection matching, but skips the Layout dispatch and just renders your component.

Use sparingly — the canonical pattern is defineBoard() with a Layout.