Setup
A headless viewer needs two packages: the one for your framework, and an engine. This page installs them, creates the engine for your whole app, and covers server-side rendering, bundlers and fonts.
Install#
npm install @embedpdf/react@next @cloudpdf/engine@nextThe framework package holds every plugin, one import path each (@embedpdf/react/search,
@embedpdf/react/annotation). A plugin you don’t import isn’t in your bundle.
It works with React 18 and later.
Create the engine once#
Create one engine when your app starts, and share it with every viewer:
// pdf.ts
import { cloudEngine } from '@cloudpdf/engine';
export const engine = cloudEngine();It connects to CloudPDF SaaS. Your own server names itself:
cloudEngine({ baseUrl: 'https://pdf.example.com' }). Creating the engine makes no request.
<Viewer> can also own the engine. Pass a function, and the viewer creates the engine when it
mounts and destroys it when it unmounts:
import { Viewer } from '@embedpdf/react/runtime';
import { cloudEngine } from '@cloudpdf/engine';
import { plugins } from './pdf';
export function DocumentViewer() {
return (
<Viewer engine={() => cloudEngine({ baseUrl: 'https://pdf.example.com' })} plugins={plugins} />
);
}An engine you pass in yourself is never destroyed by the viewer, so one engine can serve every viewer in your app.
Register the plugins#
Plugins are a list you create once, next to the engine:
import { stagePlugin } from '@embedpdf/react/stage';
import { renderPlugin } from '@embedpdf/react/render';
import { interactionPlugin } from '@embedpdf/react/interaction';
import { selectionPlugin } from '@embedpdf/react/selection';
export const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), selectionPlugin()];The viewer reads engine and plugins once, when it mounts. Creating the list inside a component
would make a new list on every render; the viewer ignores it and warns you while you develop.
Order doesn’t matter, and a plugin that needs another says so when the app starts: the annotation plugin, for example, needs the interaction plugin.
Server-side rendering#
The headless packages render nothing on the server that needs the engine: the engine only starts in the browser. So a module-level engine is safe everywhere.
In the Next.js App Router, mark the component that renders <Viewer> as a client component:
'use client';
import { Viewer } from '@embedpdf/react/runtime';
import { engine, plugins } from './pdf';
export function DocumentViewer() {
return <Viewer engine={engine} plugins={plugins}>{/* … */}</Viewer>;
}Tokens#
Each document opens with a token your backend creates: it says who the user is and what they may do. Pass a function, so a long session gets fresh tokens:
import { Viewer } from '@embedpdf/react/runtime';
import { fetchDocumentToken } from './api';
import { engine, plugins } from './pdf';
export function DocumentViewer({ documentId }: { documentId: string }) {
return (
<Viewer
engine={engine}
plugins={plugins}
initialDocuments={[{ source: { kind: 'token', token: () => fetchDocumentToken(documentId) } }]}
/>
);
}Authentication covers the three ways to open a document and what a token carries. Fonts, limits and storage are your server’s configuration.
Good to know#
One viewer, many documents#
A viewer can hold several open documents at once, like tabs. See Several documents.
Changing engine or plugins#
Both are read once. To use another engine or another set of plugins, mount a new <Viewer>, for
example by giving it a new key.
The ready-made Viewer uses these same packages. If you start there, everything on these pages still applies when you move to headless.