Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions docs/content/docs/features/server-processing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,29 @@ While you can use the `BlockNoteEditor` on the client side, you can also use `Se
For example, use the following code to convert a BlockNote document to HTML on the server:

```tsx
import { ServerBlockNoteEditor } from "@blocknote/server-util";

const editor = ServerBlockNoteEditor.create();
import { ServerBlockNoteEditor, DomShim } from "@blocknote/server-util";
import { JSDOM } from "jsdom";

// Create a DOM shim (you can use jsdom, happydom, or any other DOM implementation)
const jsdomShim: DomShim = {
acquire() {
const dom = new JSDOM();
return {
window: dom.window as any,
document: dom.window.document as any,
};
},
};

const editor = ServerBlockNoteEditor.create({}, jsdomShim);
const html = await editor.blocksToFullHTML(blocks);
```

`ServerBlockNoteEditor.create` takes the same BlockNoteEditorOptions as `useCreateBlockNote` and `BlockNoteEditor.create` ([see docs](/docs/getting-started)),
so you can pass the same configuration (for example, your custom schema) to your server-side BlockNote editor as on the client.

**Note:** Methods that require DOM (like `blocksToFullHTML`, `blocksToHTMLLossy`, `blocksToMarkdownLossy`, etc.) require a `DomShim` to be provided. You can use any DOM implementation (jsdom, happydom, etc.) by implementing the `DomShim` interface.

## Functions for converting blocks

`ServerBlockNoteEditor` exposes the same functions for converting blocks as the client side editor ([HTML](/docs/features/import/html), [Markdown](/docs/features/import/markdown)):
Expand Down
15 changes: 13 additions & 2 deletions examples/02-backend/04-rendering-static-documents/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,20 @@ import "@blocknote/mantine/style.css";
/**
On Server Side, you can use the ServerBlockNoteEditor to render BlockNote documents to HTML. e.g.:

import { ServerBlockNoteEditor } from "@blocknote/server-util";
import { ServerBlockNoteEditor, DomShim } from "@blocknote/server-util";
import { JSDOM } from "jsdom";

const editor = ServerBlockNoteEditor.create();
const jsdomShim: DomShim = {
acquire() {
const dom = new JSDOM();
return {
window: dom.window as any,
document: dom.window.document as any,
};
},
};

const editor = ServerBlockNoteEditor.create({}, jsdomShim);
const html = await editor.blocksToFullHTML(document);

You can then use render this HTML as a static page or send it to the client. Make sure to include the editor stylesheets:
Expand Down
2 changes: 1 addition & 1 deletion packages/server-util/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
"@blocknote/react": "0.42.0",
"@tiptap/core": "^3.10.2",
"@tiptap/pm": "^3.10.2",
"jsdom": "^25.0.1",
"y-prosemirror": "^1.3.7",
"y-protocols": "^1.0.6",
"yjs": "^13.6.27"
Expand All @@ -70,6 +69,7 @@
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"eslint": "^8.57.1",
"jsdom": "^25.0.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"rollup-plugin-webpack-stats": "^0.2.6",
Expand Down
158 changes: 156 additions & 2 deletions packages/server-util/src/context/ServerBlockNoteEditor.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
import { Block } from "@blocknote/core";
import { describe, expect, it } from "vitest";
import { ServerBlockNoteEditor } from "./ServerBlockNoteEditor.js";
import { JSDOM } from "jsdom";
import { DomShim, ServerBlockNoteEditor } from "./ServerBlockNoteEditor.js";

const jsdomShim: DomShim = {
acquire() {
const dom = new JSDOM();
return {
window: dom.window as any,
document: dom.window.document as any,
};
},
};

describe("Test ServerBlockNoteEditor", () => {
const editor = ServerBlockNoteEditor.create();
const editor = ServerBlockNoteEditor.create({}, jsdomShim);

const blocks: Block[] = [
{
Expand Down Expand Up @@ -124,3 +135,146 @@ describe("Test ServerBlockNoteEditor", () => {
expect(blockOutput).toMatchSnapshot();
});
});

describe("ServerBlockNoteEditor with domShim", () => {
it("uses provided domShim correctly", async () => {
let acquireCalled = false;
let releaseCalled = false;
let releasedGlobals: any = null;

const testShim: DomShim = {
acquire() {
acquireCalled = true;
const dom = new JSDOM();
return {
window: dom.window as any,
document: dom.window.document as any,
};
},
release(globals) {
releaseCalled = true;
releasedGlobals = globals;
},
};

const editor = ServerBlockNoteEditor.create({}, testShim);
const blocks: Block[] = [
{
id: "1",
type: "paragraph",
props: {
backgroundColor: "default",
textColor: "default",
textAlignment: "left",
},
content: [
{
type: "text",
text: "Test",
styles: {},
},
],
children: [],
},
];

const html = await editor.blocksToFullHTML(blocks);

expect(acquireCalled).toBe(true);
expect(releaseCalled).toBe(true);
expect(releasedGlobals).toBeTruthy();
expect(releasedGlobals.document).toBeTruthy();
expect(releasedGlobals.window).toBeTruthy();
expect(html).toBeTruthy();
});

it("throws error when no domShim is provided and globals don't exist", async () => {
// Save original globals
const originalWindow = globalThis.window;
const originalDocument = globalThis.document;

try {
// Remove globals to simulate server environment
delete (globalThis as any).window;
delete (globalThis as any).document;

const editor = ServerBlockNoteEditor.create();
const blocks: Block[] = [
{
id: "1",
type: "paragraph",
props: {
backgroundColor: "default",
textColor: "default",
textAlignment: "left",
},
content: [
{
type: "text",
text: "Test",
styles: {},
},
],
children: [],
},
];

await expect(editor.blocksToFullHTML(blocks)).rejects.toThrow(
"DOM globals (window/document) are required but not available",
);

await expect(editor.blocksToHTMLLossy(blocks)).rejects.toThrow(
"DOM globals (window/document) are required but not available",
);

await expect(editor.blocksToMarkdownLossy(blocks)).rejects.toThrow(
"DOM globals (window/document) are required but not available",
);
} finally {
// Restore original globals
globalThis.window = originalWindow;
globalThis.document = originalDocument;
}
});

it("works when globals already exist without domShim", async () => {
// This test verifies that if window/document already exist globally,
// methods work without a domShim
// Note: This test only works if the test environment has globals set up
// (e.g., via jsdom in vitest config)
if (
typeof globalThis.window !== "undefined" &&
typeof globalThis.document !== "undefined"
) {
const editor = ServerBlockNoteEditor.create();
const blocks: Block[] = [
{
id: "1",
type: "paragraph",
props: {
backgroundColor: "default",
textColor: "default",
textAlignment: "left",
},
content: [
{
type: "text",
text: "Test",
styles: {},
},
],
children: [],
},
];

// Should work if globals exist (like in a test environment with jsdom)
const html = await editor.blocksToFullHTML(blocks);
// eslint-disable-next-line jest/no-conditional-expect
expect(html).toBeTruthy();
} else {
// Skip test if globals don't exist in this environment
// eslint-disable-next-line jest/no-conditional-expect
expect(true).toBe(true);
}
});
});
Loading
Loading