Skip to main content
doc0blade47/fumadocsdocumentation home
Sign in

Structured Text Format

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

The Structured Text Format (stf) package provides a specialized data engine and reactive subscription model designed for interactive schema binding and state management within Fumadocs. It anchors client-side form controls and schema editors by maintaining nested hierarchical values, tracking field-level modifications, and publishing reactive update notifications to subscribed components.

Sources: packages/stf/src/index.ts:1-4, packages/stf/src/lib/stf.tsx:38-42, packages/stf/src/lib/stf.tsx:190-237

Data Engine Architecture and Reactive State

Call-Chain Execution Walkthrough

The core data structures and listener subscription model in DataEngine manage state propagation and reactivity across hierarchical fields. React hooks bridge component rendering cycles with the underlying data instance by subscribing to field-level mutations.

Sources: packages/stf/src/lib/stf.tsx:38-42, packages/stf/src/lib/stf.tsx:101-188, packages/stf/src/lib/stf.tsx:190-237

The traversal from a high-level component hook down to the core data instance flows through a deterministic sequence of function calls:

  1. useObject initiates structured property inspection for a given field key.
  2. useFieldValue wraps the field access, setting up computation and change detection.
  3. useListener establishes the lifecycle bindings and registers event handlers.
  4. useDataEngine resolves the active DataEngine instance from the provided argument or React context.

Sources: packages/stf/src/lib/stf.tsx:38-42, packages/stf/src/lib/stf.tsx:101-128, packages/stf/src/lib/stf.tsx:190-237, packages/stf/src/lib/stf.tsx:239-258

Sources: packages/stf/src/lib/stf.tsx:38-42, packages/stf/src/lib/stf.tsx:101-128, packages/stf/src/lib/stf.tsx:190-237, packages/stf/src/lib/stf.tsx:239-258

Note

When instance is passed directly to useDataEngine, it bypasses the React context lookup entirely, supporting standalone or multi-engine configurations.

Sources: packages/stf/src/lib/stf.tsx:38-42

Cross-Boundary Payload Serialization and Deserialization

Payload Serialization and Recursive JSON Handling

To safely transmit complex data structures across boundaries, Fumadocs utilizes specialized serialization routines. The @ungap/structured-clone/json package provides deep parsing and stringification capabilities capable of handling recursive and JSON-compatible data models.

export function serialize(value: unknown): string {
  return stringify(value);
}
 
export function deserialize(value: string): unknown {
  return parse(value);
}

Sources: packages/story/src/utils/serialization.ts:4-10

Note

The serialize and deserialize utility functions operate directly on unknown types, enabling transparent round-tripping of complex payloads without losing recursive references.

Sources: packages/story/src/utils/serialization.ts:1-10

Page Tree Deserialization and Client Hydration

Server-side loaders in Fumadocs produce serialized representations containing page tree configurations tagged with specific marker identifiers. The client inspects these payloads via isSerializedPageTree, verifying the engine identifier before running structural hydration.

function isSerializedPageTree(v: unknown): v is SerializedPageTree {
  return (
    typeof v === 'object' &&
    v !== null &&
    '$fumadocs_loader' in v &&
    v.$fumadocs_loader === 'page-tree'
  );
}

Sources: packages/core/src/source/client/index.tsx:62-69

During deserialization, deserializePageTree processes the root node by visiting each item recursively to hydrate raw HTML strings for item names and icons into React elements using dangerouslySetInnerHTML.

export function deserializePageTree(serialized: SerializedPageTree): PageTree.Root {
  const root = serialized.data as PageTree.Root;
  visit(root, (item) => {
    if ('icon' in item && typeof item.icon === 'string') {
      item.icon = (
        <span
          dangerouslySetInnerHTML={{
            __html: item.icon,
          }}
        />
      );
    }
    if (typeof item.name === 'string') {
      item.name = (
        <span
          className="fd-page-tree-item-name"
          dangerouslySetInnerHTML={{
            __html: item.name,
          }}
        />
      );
    }
  });
 
  return root;
}

Sources: packages/core/src/source/client/index.tsx:14-39

Warning

Icon and name strings found within page tree items are transformed into raw markup wrapper spans via dangerouslySetInnerHTML; ensure server-provided strings are sanitized before serialization.

Sources: packages/core/src/source/client/index.tsx:16-35

The useFumadocsLoader hook memoizes the deserialization process across component lifecycles, scanning input properties and automatically decoding recognized loader formats while preserving all unrelated properties in the output object.

export function useFumadocsLoader<V>(serialized: V): Serialized<V> {
  return useMemo(() => {
    const out: Record<string, unknown> = {};
    for (const k in serialized) {
      const v = serialized[k];
      if (isSerializedPageTree(v)) {
        out[k] = deserializePageTree(v);
      } else {
        out[k] = v;
      }
    }
    return out as Serialized<V>;
  }, [serialized]);
}

Sources: packages/core/src/source/client/index.tsx:47-60

Unified Stringification and Structural Transformations

Stringification Options and AST Customization

Fumadocs utilizes a unified stringifier configuration built on top of mdast-util-to-markdown and mdast-util-mdx to serialize Abstract Syntax Tree nodes into plain text or formatted markdown. The stringification engine accepts configuration options controlling element inclusion, attribute filtering, and custom node rendering.

OptionTypeDefaultPurpose
filterElement(node: Nodes) => boolean | 'children-only'File, TypeTable, Callout, Card return true; other MDX elements return 'children-only'; all other nodes return trueDetermines whether to include an element, drop it, or preserve only its children.
filterMdxAttributes(node, attribute) => booleanundefinedFilters individual attributes on MDX flow and text elements during stringification.
stringify(node, parent, state, info, ctx) => string | undefinedundefinedOverrides the default stringification logic for specific nodes.

Sources: packages/core/src/mdx-plugins/stringifier.ts:34-83, packages/core/src/mdx-plugins/stringifier.ts:94-114

Note

Nodes can carry extra metadata via node.data._stringify, which supports three values: 'children-only', { node } to replace the node entirely during serialization, or { text } to return a pre-computed string form directly.

Sources: packages/core/src/mdx-plugins/stringifier.ts:18-28, packages/core/src/mdx-plugins/stringifier.ts:126-135

Placeholder Rendering and Docgen Transformers

The renderPlaceholder function evaluates control character placeholders delimited by null bytes (\0) within markdown or text strings, asynchronously resolving structured JSON payloads and rendering child nodes recursively.

export async function renderPlaceholder(
  text: string,
  renderers: Record<string, (data: PlaceholderData) => Awaitable<string>>,
): Promise<string> {
  let out = '';
  let idx = 0;
 
  for (const match of text.matchAll(regex)) {
    out += text.slice(idx, match.index);
    const inner = match[1];
    try {
      const data = JSON.parse(inner) as PlaceholderData;
      const renderer = data.name && renderers[data.name];
      if (data.children.trim()) {
        data.children = await renderPlaceholder(data.children, renderers);
      }
 
      if (renderer) {
        out += await renderer(data);
      } else {
        out += data.children;
      }
    } catch {
      out += match[0];
    }
 
    idx = match.index + match[0].length;
  }
  out += text.slice(idx);
 
  return out;

Sources: packages/core/src/mdx-plugins/remark-llms.runtime.ts:12-42

Caution

If JSON parsing fails on the inner content of a \0...\0 placeholder, the parser catches the error and silently appends the raw match string back into the output without modification.

Sources: packages/core/src/mdx-plugins/remark-llms.runtime.ts:22-36

Additionally, remarkDocGen transforms code blocks matching specific docgen generators by visiting code nodes where the language is json and the meta string matches the doc-gen:<name> pattern.

export function remarkDocGen({ generators = [] }: RemarkDocGenOptions): Transformer<Root, Root> {
  return async (tree, file) => {
    generators.forEach((gen) => gen.onFile?.(tree, file));
    const queue: Promise<void>[] = [];
 
    visit(tree, 'code', (code, _, parent) => {
      if (code.lang !== 'json' || !code.meta || !parent) return;
 
      const matches = metaRegex.exec(code.meta);
      if (!matches) return;
 
      const name = matches[1];
      const gen = generators.find((g) => g.name === name);
 
      const run = async (): Promise<void> => {
        const result = await gen?.run(JSON.parse(code.value), {
          cwd: file.cwd,
          path: file.path,
          node: code,
        });
        const index = parent.children.findIndex((c) => c === code);
 
        if (result && index !== -1) {
          const items = Array.isArray(result)
            ? (result as BlockContent[])
            : [result as BlockContent];
 
          parent.children.splice(index, 1, ...items);
        }
      };
 
      queue.push(run());
    });
 
    await Promise.all(queue);
  };

Sources: packages/doc-gen/src/remark-docgen.ts:31-66

Client Field Hooks and Form Bindings

React Context and Engine Integration

The structured text format framework supplies React bindings through StfProvider, useStf, and useDataEngine to expose data engines across component subtrees. StfProvider accepts an Stf instance containing a DataEngine and wraps child elements using React's context provider. useStf accepts configuration options with defaultValues and instantiates a memoized DataEngine.

const Context = createContext<Stf | null>(null);
 
export interface Stf {
  dataEngine: DataEngine;
}
 
export function StfProvider({ value, children }: { value: Stf; children: ReactNode }) {
  return <Context value={value}>{children}</Context>;
}
 
export function useStf(options: {
  /**
   * Note: the passed object will be modified in place, use `structuredClone()` to keep the original object unchanged.
   */
  defaultValues?: DefaultValue<Record<string, unknown>>;
}): Stf {
  const optionsRef = useRef(options);
  optionsRef.current = options;
 
  return useMemo(
    () => ({
      dataEngine: new DataEngine(optionsRef.current.defaultValues),
    }),
    [],
  );
}
 
export function useDataEngine(instance?: Stf | DataEngine) {
  if (instance instanceof DataEngine) return instance;
  if (instance) return instance.dataEngine;
  return use(Context)!.dataEngine;
}

Sources: packages/stf/src/lib/stf.tsx:11-42

Note

When useStf is called, the passed defaultValues object is modified in place during engine initialization; callers requiring original object preservation should pass a clone via structuredClone().

Sources: packages/stf/src/lib/stf.tsx:21-25

useDataEngine resolves an active DataEngine instance by inspecting an explicitly passed instance or falling back to the React context.

export function useDataEngine(instance?: Stf | DataEngine) {
  if (instance instanceof DataEngine) return instance;
  if (instance) return instance.dataEngine;
  return use(Context)!.dataEngine;
}

Sources: packages/stf/src/lib/stf.tsx:38-42

Field and Listener Hooks

Field bindings rely on useFieldValue and useListener to subscribe components to specific data engine paths, re-rendering when values initialize, update, or delete. useFieldValue accepts a field key, an optional stf reference, a defaultValue, a compute transformation function, and an isChanged comparator.

export function useFieldValue<V = unknown>(
  key: FieldKey,
  options: {
    stf?: Stf | DataEngine;
    defaultValue?: DefaultValue;
 
    /**
     * compute value from the actual field value.
     *
     * to re-compute on in-place updates (may happen on objects, arrays), you should clone the object here.
     */
    compute?: (currentValue: unknown) => V;
    /** determine whether the value/computed value is changed */
    isChanged?: (prev: V, next: V) => boolean;
  } = {},
) {
  const { stf, compute = (v) => v as V, defaultValue, isChanged = (a, b) => a !== b } = options;
  const engine = useDataEngine(stf);
  const [value, setValue] = useState<V>(() =>
    compute(defaultValue === undefined ? engine.get(key) : engine.init(key, defaultValue)),
  );
  const prevEngineRef = useRef(engine);
 
  if (prevEngineRef.current !== engine) {
    setValue(
      compute(defaultValue === undefined ? engine.get(key) : engine.init(key, defaultValue)),
    );
    prevEngineRef.current = engine;
  }
 
  function onUpdate() {
    const computed = compute(engine.get(key));
    setValue((prev) => (isChanged(prev, computed) ? computed : prev));
  }
 
  useListener({
    field: key,
    stf,
    onInit: onUpdate,
    onUpdate: onUpdate,
    onDelete() {
      const computed = compute(undefined);
      setValue((prev) => (isChanged(prev, computed) ? computed : prev));
    },
  });
 
  return [value, (newValue: unknown) => engine.update(key, newValue)] as const;
}

Sources: packages/stf/src/lib/stf.tsx:190-237

Hook / UtilityParametersReturn ValuePurpose
useStfoptions: { defaultValues?: DefaultValue<Record<string, unknown>> }StfCreates a memoized Stf context object managing a new DataEngine.
useDataEngineinstance?: Stf | DataEngineDataEngineResolves the data engine instance from arguments or context.
useFieldValuekey: FieldKey, options?: { stf, defaultValue, compute, isChanged }[value, (newValue) => void]Subscribes to a field path, computing values and returning a getter tuple with an updater.
useListenerlistener: DataEngineListener & { stf?: Stf | DataEngine }voidSubscribes raw lifecycle listeners (onInit, onUpdate, onDelete) to a field key.

Sources: packages/stf/src/lib/stf.tsx:21-36, packages/stf/src/lib/stf.tsx:38-42, packages/stf/src/lib/stf.tsx:190-237, packages/stf/src/lib/stf.tsx:239-258

Array and Object Collection Hooks

Collection controls are provided via useArray and useObject, mapping structured schema properties and array indices to interactive form bindings. useArray computes array item information objects and exposes methods to insert or remove items at specific indices.

export function useArray(
  field: FieldKey,
  options: { defaultValue?: DefaultValue<unknown[]> } = {},
) {
  const engine = useDataEngine();
  const { defaultValue } = options;
  const [items] = useFieldValue(field, {
    defaultValue,
    compute(value) {
      const items: ArrayItemInfo[] = [];
      if (!Array.isArray(value)) return items;
 
      for (let i = 0; i < value.length; i++) {
        items.push({
          field: [...field, i],
          index: i,
        });
      }
      return items;
    },
    isChanged(prev, next) {
      return prev.length !== next.length;
    },
  });
 
  return {
    items,
    insertItem(itemValue?: unknown) {
      const value = engine.get(field);
      const idx = Array.isArray(value) ? value.length : 0;
      engine.init([...field, idx], itemValue);
    },
    removeItem(index: number) {
      engine.delete([...field, index]);
    },
  };
}

Sources: packages/stf/src/lib/stf.tsx:49-85

useObject inspects plain objects using defined properties, pattern properties, fallbacks, and lazy evaluation flags, returning an array of property item descriptors alongside append and delete handlers.

export function useObject<T>(
  field: FieldKey,
  options: {
    defaultValue?: DefaultValue<object>;
    /** ignore fixed properties unless defined  */
    lazy?: boolean;
    properties: Record<string, T>;
    patternProperties?: Record<string, T>;
    fallback?: T;
  },
) {
  const {
    properties: definedProps,
    patternProperties: definedPatternProps = {},
    defaultValue,
    fallback,
    lazy,
  } = options;
  const engine = useDataEngine();
  const [objectKeys] = useFieldValue(field, {
    defaultValue,
    compute(currentValue) {
      return isPlainObject(currentValue) ? Object.keys(currentValue) : [];
    },
    isChanged(prev, next) {
      return !deepEqual(prev, next);
    },
  });
 
  const properties = useMemo(() => {
    const properties: PropertyItemInfo<T>[] = [];
    const unknownKeys = new Set(objectKeys);
    for (const [key, prop] of Object.entries(definedProps)) {
      if (lazy && !unknownKeys.has(key)) continue;
      unknownKeys.delete(key);
 
      properties.push({
        kind: 'fixed',
        field: [...field, key],
        key,
        info: prop,
      });
    }
 
    for (const [pattern, prop] of Object.entries(definedPatternProps)) {
      const regex = RegExp(pattern);
 
      for (const key of unknownKeys) {
        if (!key.match(regex)) continue;
        unknownKeys.delete(key);
        properties.push({
          kind: 'pattern',
          info: prop,
          key,
          pattern,
          field: [...field, key],
        });
      }
    }
 
    if (fallback) {
      for (const key of unknownKeys) {
        properties.push({
          kind: 'fallback',
          field: [...field, key],
          key,
          info: fallback,
        });
      }
    }
 
    return properties;
  }, [definedPatternProps, definedProps, fallback, field, lazy, objectKeys]);
 
  return {
    properties,
    _objectKeys: objectKeys,
    onAppend(name: string, value?: unknown) {
      name = name.trim();
      if (name.length === 0) return;

Sources: packages/stf/src/lib/stf.tsx:101-188

Warning

When using useObject with lazy: true, fixed properties defined in properties are ignored unless they are already present as keys in the underlying object data.

Sources: packages/stf/src/lib/stf.tsx:101-135

Markdown Ingestion and Dynamic Parsing

Block and Inline Transforms

Fumadocs processes raw markdown and inline snippets through a unified pipeline combining remark, remark-gfm, remark-rehype, and hast-util-to-jsx-runtime. The createMarkdownRenderer function configures a processor that parses content into an mdast tree, runs transforms, and evaluates it into React elements via JSX runtimes.

Sources: packages/core/src/content/md.ts:30-48

const processor = remark()
  .use(remarkPlugins)
  .use(remarkRehype, remarkRehypeOptions)
  .use(rehypePlugins);

Sources: packages/core/src/content/md.ts:35-38

Specialized markdown processors adapt block structures for interactive contexts. For instance, live previews apply a custom rehypeWrapWords visitor that splits text and element nodes into individual word spans decorated with animation classes, while skipping pre block nodes.

Sources: packages/preview/src/components/markdown.tsx:24-54

Warning

The rehypeWrapWords transformer explicitly skips pre elements (node.tagName === 'pre') to prevent code block contents from being fractured into individual word spans.

Sources: packages/preview/src/components/markdown.tsx:27

Code blocks embedded within markdown are intercepted by preprocessors like MarkdownPre or Pre, which extract language names from class attributes (converting language-mdx to md) and forward stripped code content to specialized rendering components.

Sources: packages/preview/src/components/markdown.tsx:80-95, packages/asyncapi/src/ui/index.tsx:222-235

Twoslash Code Intelligence Integration

Code intelligence and type popup annotations are injected via transformerTwoslash, which wraps Shiki highlighters with a lazy-loaded Twoslasher instance configured with moduleResolution: 100 (ModuleResolutionKind.Bundler).

Sources: packages/twoslash/src/index.ts:31-52

  return (cachedInstance ??= createTwoslasher({
    ...twoslashOptions,
    compilerOptions: {
      moduleResolution: 100 satisfies ModuleResolutionKind.Bundler,
      baseUrl: undefined,
      ...twoslashOptions.compilerOptions,
    },
  }));
}

Sources: packages/twoslash/src/index.ts:38-46

The renderer configures Hast node mappings for hover triggers, popup contents, and documentation boxes, translating JSDoc links ({@link ...}) during nested markdown parsing.

Sources: packages/twoslash/src/index.ts:60-107, packages/twoslash/src/index.ts:133-137

Hast Element MappingTarget Tag NameProperties / ClassesPurpose
hoverTokenPopup—Wraps inspected tokens requiring hover inspection.
hoverPopupPopupContentclass: 'nd-copy-ignore'Container for type and documentation popups.
popupDocsdivclass: 'prose twoslash-popup-docs'Styles JSDoc documentation sections.
popupTypesdivclass: 'twoslash shiki fd-codeblock prose-no-margin'Renders type signature code blocks inside popups.
nodesHighlightspanclass: 'highlighted-word twoslash-highlighted'Highlights code tokens associated with type info.

Sources: packages/twoslash/src/index.ts:61-105

Was this page helpful?