Render BlockNote Blocks in Next.js

Convert BlockNote blocks from the Article API into HTML using @blocknote/server-util.

Back to Developer
1) Fetch Blocks From The API

Request your article with format=block so the API returns raw BlockNote blocks in data.article.content.

curl -s \
  -H "x-api-key: <YOUR_API_KEY>" \
  "https://www.agent-engineering.dev/api/v1/articles/testing?format=block"

The content field is JSON:

{
  "success": true,
  "message": "OK",
  "data": {
    "article": {
      "id": "…",
      "title": "…",
      "content": [
        {
          "id": "…",
          "type": "paragraph",
          "props": {},
          "content": [{ "type": "text", "text": "Hello BlockNote" }],
          "children": []
        }
      ]
    }
  }
}
2) Configure Next.js serverExternalPackages

Add BlockNote packages to serverExternalPackages so Next.js treats them as server externals.

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  serverExternalPackages: ["@blocknote/server-util", "@blocknote/react"],
};

export default nextConfig;

This project already includes it in next.config.ts.

3) Convert Blocks To HTML (Server Component)

This is the same pattern used by the site’s article renderer in article pages.

import { Block } from "@blocknote/core";
import "@blocknote/core/fonts/inter.css";
import { ServerBlockNoteEditor } from "@blocknote/server-util";
import "@blocknote/shadcn/style.css";

type Props = {
  blocks: Block[];
};

export async function BlockNoteHtml({ blocks }: Props) {
  const editor = ServerBlockNoteEditor.create();
  const html = await editor.blocksToFullHTML(blocks);

  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

If your blocks come from JSON (like Prisma JSON or the API response), cast them to Block[] after you validate the caller is trusted.

4) Use It In A Page
import { Block } from "@blocknote/core";
import { BlockNoteHtml } from "./block-note-html";

type ArticleApiResponse = {
  success: boolean;
  message: string;
  data?: {
    article: {
      title: string;
      content: unknown;
    };
  };
};

export default async function Page() {
  const res = await fetch("https://www.agent-engineering.dev/api/v1/articles/testing?format=block", {
    headers: {
      "x-api-key": process.env.AGENT_ENGINEERING_API_KEY!,
    },
    cache: "no-store",
  });

  if (!res.ok) throw new Error(await res.text());
  const body = (await res.json()) as ArticleApiResponse;
  if (!body.success || !body.data) throw new Error(body.message);

  const blocks = body.data.article.content as Block[];

  return (
    <main>
      <h1>{body.data.article.title}</h1>
      <BlockNoteHtml blocks={blocks} />
    </main>
  );
}

For client-side rendering, prefer converting to HTML on the server and sending HTML down, unless you explicitly need a client editor.

5) Code Highlighting

If your BlockNote content includes code blocks, the site uses Highlight.js to apply syntax highlighting.

<link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/default.min.css"
/>
<Script
  src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js"
  strategy="afterInteractive"
/>
<Script
  id="highlight-init"
  strategy="afterInteractive"
  dangerouslySetInnerHTML={{
    __html: `(function () {
  function tryHighlight(attempt) {
    if (window.hljs && typeof window.hljs.highlightAll === "function") {
      window.hljs.highlightAll();
      return;
    }

    if (attempt >= 60) return;
    setTimeout(function () {
      tryHighlight(attempt + 1);
    }, 50);
  }

  if (document.readyState === "complete") {
    tryHighlight(0);
  } else {
    window.addEventListener("load", function () {
      tryHighlight(0);
    });
  }
})();`,
  }}
/>