Embed custom fonts in DOCX export

Available in Start planBetav0.20.0

Word only renders a font that's installed on the reader's machine — unless the font is embedded into the document itself. @tiptap-pro/extension-export-docx can embed the fonts your content uses so the exported .docx looks the same everywhere, even where those fonts aren't installed.

There are two ways to do it:

  • Manual — you supply the font bytes via the fonts option. Full control, no network dependency, works on the server.
  • Automatic — set embedFonts: true and the export detects, fetches, and embeds the fonts your document uses for you. Browser only.

Only the regular variant is embedded

DOCX font embedding carries the regular (upright, 400-weight) face of each family. Word synthesizes bold and italic from it — you don't need to (and can't, through this API) embed separate bold/italic font files.

Manual embedding

Pass a fonts array — one entry per font family, each with its raw TTF or OTF bytes. The font is obfuscated into the document package (word/fonts/font<N>.odttf, per the OOXML spec) and bound to every run that uses the matching family name.

import { ExportDocx } from '@tiptap-pro/extension-export-docx'

// In the browser: fetch the font and hand its ArrayBuffer straight to the export.
const data = await fetch('/fonts/PlayfairDisplay-Regular.ttf').then((response) =>
  response.arrayBuffer(),
)

editor
  .chain()
  .exportDocx({
    fonts: [{ name: 'Playfair Display', data }],
  })
  .run()

On the server, read the file into a Buffer instead:

import { readFile } from 'node:fs/promises'
import { exportDocx } from '@tiptap-pro/extension-export-docx'

const data = await readFile('./fonts/PlayfairDisplay-Regular.ttf')

await exportDocx({
  document: editorJSON,
  exportType: 'buffer',
  customNodes: [],
  styleOverrides: {},
  fonts: [{ name: 'Playfair Display', data }],
})

DocxFontDefinition

interface DocxFontDefinition {
  name: string
  data: Buffer | Uint8Array | ArrayBuffer
  characterSet?: DocxFontCharacterSet
}
FieldDescription
nameThe font family name. Must exactly match the font name used by your content — the first family of a textStyle mark's fontFamily (quotes stripped, so '"Playfair Display", serif' resolves to Playfair Display) or the font set via styleOverrides / textRunOverrides. A mismatch silently falls back to a substitute font.
dataRaw TTF or OTF bytes. Accepts a Node Buffer, a Uint8Array, or an ArrayBuffer (so await fetch(url).then((r) => r.arrayBuffer()) works directly in the browser). WOFF/WOFF2 data is not accepted here — see automatic embedding for converting WOFF2.
characterSetOptional Word charset code, written as <w:charset> in the font table. Leave unset for ordinary Latin fonts. Use the re-exported CharacterSet constant for the value (e.g. CharacterSet.GREEK).

Automatic embedding

Set embedFonts: true and the export does the work: it collects every font family the document references, locates each font's file, converts it to an embeddable format if needed, and embeds it — no per-export font plumbing.

ExportDocx.configure({
  embedFonts: true,
  // appId + token are only used when a font needs WOFF2 → TTF conversion
  appId: 'your-app-id',
  token: 'your-jwt',
  endpoint: 'https://api.tiptap.dev/v2/convert', // optional, this is the default
  onCompleteExport: (blob) => {
    /* download blob */
  },
})

How it works

  1. Detect — collects the families used across textStyle fontFamily marks, CSS styles, and run-level style overrides. Generic keywords (serif, monospace, …) and families you already passed via fonts are skipped.
  2. Locate — finds each font's file from the page's readable @font-face rules, picking the regular face. When a family is split into per-language subsets — as Google Fonts, and many self-hosted setups, are — the subset that covers the characters your document actually uses is chosen: a Latin document embeds the Latin glyphs, a Cyrillic document the Cyrillic glyphs, and so on. A family with no readable rule — the usual case for a Google Fonts <link>, whose cross-origin stylesheet can't be read from JavaScript — falls back to a Google Fonts lookup.
  3. ConvertTTF/OTF files are embedded as-is. WOFF2 files are converted to TTF through the Convert Service's POST /fonts/convert endpoint (this is the step that needs appId / token).
  4. Embed — the resulting fonts are embedded exactly as if you'd passed them through the manual fonts option. Manually supplied fonts always win over auto-resolved ones with the same name.

Requirements and credentials

  • Browser only. Automatic embedding reads document.styleSheets, so it does nothing in a server environment. Use the manual fonts option server-side.
  • appId / token are needed only for WOFF2 conversion. Self-hosted TTF/OTF fonts embed with no Convert Service call at all. WOFF2 fonts (including everything resolved from Google Fonts) require the credentials so the conversion endpoint can run.
  • Convert Service ≥ v2.25.0 must back your endpoint for the WOFF2 → TTF conversion to be available.

Embedding never breaks an export

Every font is resolved independently. If one can't be found, fetched, or converted — or credentials are missing for a WOFF2 font — the export logs a single console.warn for that family and continues. The affected text falls back to Word's font substitution, exactly as it would without embedding; the export itself always succeeds.

One font used for multiple scripts

A DOCX embeds one file per font family. If the same family is used for text in more than one script (for example Latin and Cyrillic) within a single document, the subset covering the most of that text is embedded and text in the other scripts falls back to Word's substitution. To embed full coverage for such a family, supply one complete (non-subset) font file — self-host it as a single @font-face, or pass it through the manual fonts option. Families used for only one script each are unaffected.

Options

ParameterDescriptionDefault
embedFontsEnable automatic detection and embedding. Browser only.false
appIdTiptap Convert app ID, sent as the X-App-Id header. Required only when a WOFF2 font needs conversion.undefined
tokenTiptap Convert JWT, sent as the Authorization bearer token. Required only when a WOFF2 font needs conversion.undefined
endpointTiptap Convert REST endpoint base used for WOFF2 → TTF conversion.https://api.tiptap.dev/v2/convert

Make the editor match the export

For the editor preview to match the embedded output, load the same font in the page with an @font-face rule (this is also what automatic embedding reads to locate self-hosted fonts):

@font-face {
  font-family: 'Playfair Display';
  src: url('/fonts/PlayfairDisplay-Regular.ttf') format('truetype');
  font-weight: 400 900;
  font-style: normal;
  font-display: swap;
}

Then apply the family to content with the fontFamily mark — its sanitized name is what both the editor and the export bind to:

editor.chain().focus().setFontFamily('Playfair Display').run()

Convert Service endpoint

Automatic embedding's WOFF2 conversion is backed by a REST endpoint you can also call directly:

POST {endpoint}/fonts/convert

  • Body: multipart/form-data with a file field (the font binary) and an optional fontFamily field.
  • Response: the embeddable font as font/ttf — TTF/OTF uploads are returned unchanged, WOFF2 uploads are converted to TTF.
  • Auth: the same Authorization: Bearer <token> and X-App-Id headers as the other conversion endpoints.

Available on Tiptap Cloud and on-premises from Convert Service v2.25.0.

See also

  • Editor extension overview — base ExportDocx configuration and the full options table.
  • CSS to DOCX — map your editor CSS (including font-family) into DOCX styles.
  • StylesstyleOverrides and textRunOverrides, where a font is also picked up for embedding.
  • REST API — server-side conversion endpoint.