BookStackApp/BookStack · error · Error

To use $generateHtmlFromNodes in headless mode please initia

Error message

To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom before calling this function.

What it means

$generateHtmlFromNodes serializes editor nodes to HTML using document.createElement, which requires a browser-like DOM. In headless mode (Node.js without a DOM implementation) `document`/`window` are undefined, so the function throws this descriptive error telling you to set up JSDom or similar first.

Source

Thrown at resources/js/wysiwyg/lexical/html/index.ts:74

        lexicalNodes = lexicalNodes.concat(lexicalNode);
      }
    }
  }

  $unwrapArtificalNodes(allArtificialNodes);

  return lexicalNodes;
}

export function $generateHtmlFromNodes(
  editor: LexicalEditor,
  selection?: BaseSelection | null,
): string {
  if (
    typeof document === 'undefined' ||
    (typeof window === 'undefined' && typeof global.window === 'undefined')
  ) {
    throw new Error(
      'To use $generateHtmlFromNodes in headless mode please initialize a headless browser implementation such as JSDom before calling this function.',
    );
  }

  const container = document.createElement('div');
  const root = $getRoot();
  const topLevelChildren = root.getChildren();

  for (let i = 0; i < topLevelChildren.length; i++) {
    const topLevelNode = topLevelChildren[i];
    $appendNodesToHTML(editor, topLevelNode, container, selection);
  }

  const nodeCode = [];
  for (const node of container.childNodes) {
    if ("outerHTML" in node) {
      nodeCode.push(node.outerHTML)
    } else {

View on GitHub (pinned to 18f8469a1c)

Solutions

  1. Install jsdom (npm i jsdom) and assign global.window and global.document from a new JSDom instance before calling the function.
  2. Run tests with environment: 'jsdom' (Jest testEnvironment / vitest environment jsdom).
  3. Perform HTML generation inside a DOM-capable environment (browser or happy-dom) rather than pure Node.
  4. If only state inspection is needed, serialize with $convertToEditorStateJSON or walk nodes manually instead of generating HTML.

Example fix

// before
import { $generateHtmlFromNodes } from './html';
const html = $generateHtmlFromNodes(editor, null); // throws in node

// after
import { JSDOM } from 'jsdom';
const dom = new JSDOM('<!DOCTYPE html>');
global.window = dom.window;
global.document = dom.window.document;
const html = $generateHtmlFromNodes(editor, null);
Defensive patterns

Strategy: validation

Validate before calling

function hasBrowserDom(): boolean {
  return typeof document !== 'undefined' &&
    (typeof window !== 'undefined' || typeof (globalThis as any).window !== 'undefined');
}
if (!hasBrowserDom()) throw new Error('Set up JSDom before HTML generation');

Try / catch

try {
  html = $generateHtmlFromNodes(editor, null);
} catch (e) {
  if (e instanceof Error && e.message.includes('headless mode')) {
    setupJSDomGlobals();
    html = $generateHtmlFromNodes(editor, null);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling $generateHtmlFromNodes (directly or via $getHtmlContent) from an environment where `typeof document === 'undefined'` and no global window is installed — e.g. a Node test harness or server-side rendering without a DOM polyfill.

Common situations: Jest/Vitest tests running in the default node environment instead of jsdom; SSR/server code exporting editor content to HTML; tools generating static email or preview HTML from editor state.

Related errors


AI-assisted analysis of BookStackApp/BookStack@18f8469a1c (2026-09-02). Data as JSON: /api/errors/2f7befea322dd7bd. Report an issue: GitHub.