PDF Export
It's possible to export BlockNote documents to PDF, completely client-side. The exporter is powered by the Typst typesetting engine (compiled to WebAssembly) and produces accessible, tagged documents; see PDF/UA conformance.
This feature is provided by the @blocknote/xl-pdf-exporter. xl- packages
are fully open source, but released under a copyleft license. A commercial
license for usage in closed source, proprietary products comes as part of the
Business subscription.
First, install the @blocknote/xl-pdf-exporter package:
npm install @blocknote/xl-pdf-exporterThen, create an instance of the PDFExporter class and export the document:
import {
PDFExporter,
typstDefaultSchemaMappings,
} from "@blocknote/xl-pdf-exporter";
// Create the exporter
const exporter = new PDFExporter(editor.schema, typstDefaultSchemaMappings);
// Export the document; the result carries the PDF as bytes and as a Blob
const result = await exporter.toPDF(editor.document, {
title: "My document",
lang: "en",
});
if (!result.error) {
const url = URL.createObjectURL(result.blob);
// e.g. open or download `url`
}This works out of the box, fully offline: exports match the editor's look, and everything needed (the default fonts and the compiler itself) ships inside the package. Nothing is fetched from a CDN.
See the full example with a live PDF preview below:
Customizing the PDF
toPDF takes per-export options: the document metadata and page setup.
const result = await exporter.toPDF(editor.document, {
// Document title, shown in the viewer's title bar
title: "My document",
// Document author, written to the PDF metadata
author: "John Doe",
// BCP-47 language tag of the document's natural language
lang: "en",
// Typst paper name, e.g. "a4" (default) or "us-letter"
paper: "a4",
// Page margin as a Typst length
margin: "48pt",
// Raw Typst markup for the running page header / footer, e.g. a
// page counter: "#context counter(page).display()"
header: "My document",
footer: "#context counter(page).display()",
});The remaining export options are
tryDeclarePdfUA, assets (extra files for
caller-supplied header/footer markup), and creationTimestamp (a fixed
Unix timestamp for byte-reproducible output).
A document that fails to compile (e.g. text in a script the loaded fonts
don't cover) is reported in the result as { error: "compile-failed" }
with the compiler's diagnostics, rather than thrown.
PDF/UA conformance
The produced PDF is always tagged: it carries a logical structure tree (headings, paragraphs, lists, tables, figures with alt text, links) that screen readers can navigate. On top of that, the exporter declares PDF/UA-1 conformance when, and only when, the document earns it: Typst validates conformance during the compile, and a nonconforming document is exported as tagged-but-unclaimed instead, with the violations reported in the result:
const result = await exporter.toPDF(editor.document, {
title: "My document",
lang: "en",
});
if (
!result.error &&
!result.pdfUA.declared &&
result.pdfUA.reason === "nonconforming"
) {
// e.g. "PDF/UA-1 error: the first heading must be of level 1"
console.info(result.pdfUA.violations.map((v) => v.message));
}What conformance requires of the document:
- Title and language: pass
titleandlangin the export options.langis required to attempt the claim at all: a wrong language declaration is an accessibility defect no validator can catch, so exporting without it throws. - Headings: the first heading must be level 1, and levels must be consecutive (no jumping from H1 to H3).
Images always get the alt text PDF/UA requires, derived from the caption with the file name as fallback. Captions make far better alt text, so encourage them.
Pass tryDeclarePdfUA: false in the export options to skip the validation
and claim entirely, e.g. for a live preview, where the validation compile
would be wasted work. For live previews, also create a fresh exporter per
export, since an instance accumulates the image assets it resolves.
Custom mappings / custom schemas
The PDFExporter constructor takes a schema and mappings parameter. A
mapping converts a BlockNote schema element into a Typst markup
string, and the mappings are shared with the standalone Typst export. See
custom mappings
there; everything on writing them applies to this exporter unchanged.
Math & diagram blocks
The math and diagram blocks ship their own Typst mappings: math exports as native Typst equations (real text, not images), diagrams as embedded vector graphics. See exporting math and exporting diagrams for the setup.
Fonts
By default, exports use a bundled font set matching the editor: Inter (body), Geist Mono (code), New Computer Modern Math (math), and Noto Color Emoji. To use your own fonts instead, pass the font bytes and the matching family names to the constructor:
const exporter = new PDFExporter(editor.schema, typstDefaultSchemaMappings, {
fontFamily: "My Font",
monoFontFamily: "My Mono Font",
// Uint8Array[], or a promise of them
fonts: [myFontBytes, myMonoFontBytes],
// An emoji-capable font; replaces the default independently of `fonts`
emojiFont: myEmojiFontBytes,
});Typst selects fonts by the family name embedded in the font file itself, so
the fontFamily options must match what your files declare. A mismatch
shows up as an unknown font family entry in the result's
compileWarnings.
To extend the defaults rather than replace them (e.g. adding a CJK font as a per-glyph fallback), spread the exported default loaders:
import {
DEFAULT_FONT_FAMILY,
loadDefaultBodyFonts,
PDFExporter,
typstDefaultSchemaMappings,
} from "@blocknote/xl-pdf-exporter";
const exporter = new PDFExporter(editor.schema, typstDefaultSchemaMappings, {
fontFamily: [DEFAULT_FONT_FAMILY, "Noto Sans SC"],
fonts: loadDefaultBodyFonts().then((fonts) => [...fonts, notoSansSCBytes]),
});Self-hosting the compiler
The Typst compiler is a ~25MB wasm file from @blocknote/xl-typst-compiler,
by default emitted as an asset by your bundler. To control where it is
served from (e.g. with caching headers), pass its URL or bytes:
import compilerWasmUrl from "@blocknote/xl-typst-compiler/wasm?url";
const exporter = new PDFExporter(editor.schema, typstDefaultSchemaMappings, {
wasm: compilerWasmUrl,
});Exporter options
The PDFExporter constructor takes an optional third options parameter:
const defaultOptions = {
// a function to resolve external resources (e.g. images) in order to avoid
// CORS issues; by default, this calls a BlockNote hosted server-side proxy
resolveFileUrl: corsProxyResolveFileUrl,
// the strings rendered into the exported document (file link texts, error
// placeholders); pass a locale from @blocknote/core/locales (or your
// editor's dictionary) to export in another language
dictionary: locales.en,
// the colors used for highlighting, background colors and font colors
colors: COLORS_DEFAULT, // defaults from @blocknote/core
// font families (DEFAULT_FONT_FAMILY / DEFAULT_MONO_FONT_FAMILY) and the
// font bytes they resolve against - see "Fonts" above
fontFamily: "Inter 18pt",
monoFontFamily: "Geist Mono",
fonts: loadDefaultBodyFonts(),
emojiFont: loadDefaultEmojiFont(),
// base font size in points
fontSize: 12,
// where the compiler wasm loads from - see "Self-hosting the compiler"
wasm: undefined, // the packaged wasm
};Exporting Typst markup
The underlying Typst source export is available standalone (e.g. to compile with your own Typst toolchain, including server-side); see Typst export.
Deprecated: the react-pdf exporter
Previous versions of @blocknote/xl-pdf-exporter exported PDFs with
react-pdf, producing untagged (not accessible)
documents. That exporter is deprecated and will be removed after a few
releases; until then it remains available unchanged from the
@blocknote/xl-pdf-exporter/react-pdf subpath:
import {
PDFExporter,
pdfDefaultSchemaMappings,
} from "@blocknote/xl-pdf-exporter/react-pdf";Note that its mappings are react-pdf mappings; when migrating to the new exporter, custom blocks need a Typst mapping instead.