Website, editor and Node starters
Download a complete project, install its pinned dependencies, and run it locally. No provider account or hosted API.
Use Node 24. Each ZIP includes its package manifest, a real deck, source files and a README. Run the commands inside the extracted directory. Commit the generated package-lock.json to keep later installs reproducible.
Since OPF 0.16 the renderer's PNG and PDF converters (@resvg/resvg-js, sharp, pdf-lib) and its font packages are optional peers. Each starter's package.json lists the ones it uses, pinned: the PNG converter, sharp for pictures, and the Roboto font packages (website and editor: the Office substitutes too). Add pdf-lib only for raster PDFs. A missing one fails with converter-missing or names the package and the install command.
A website with presentation downloads
Download the website starter. It embeds two slides with thumbnails and presentation mode, and offers PowerPoint, PDF and original OPF downloads. The export step generates those files locally before the site runs.
npm install
npm run fonts
npm run export
npm run devOpen the localhost URL printed by Vite. The status becomes Ready after the deck and its fonts load. Vite processes npm imports in this HTML; opening the file directly or serving it without a bundler cannot resolve the package import. Replace public/deck.opf.json with your deck, then rerun the export step.
Website source
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>OPF website starter</title><link rel="stylesheet" href="/style.css"></head>
<body>
<h1>Quarterly Review</h1>
<p id="status" role="status">Loading presentation…</p>
<opf-deck src="/deck.opf.json" fonts="/opf-fonts/" thumbnails present></opf-deck>
<p><a href="/downloads/deck.pptx" download>Download PowerPoint</a><a href="/downloads/deck.pdf" download>Download PDF</a><a href="/deck.opf.json" download>Download OPF source</a></p>
<script type="module" src="/main.js"></script>
</body>
</html>The component exposes ready, slidechange, presentstart, presentend and error. Its JSON source and font files are served from the same origin. Keep the generated font directory and its license notices in your static build.
An editor embedded in your app
Download the editor starter. It mounts a canvas, shows committed OPF source, supports slide navigation and Undo/Redo, saves OPF, and exports PowerPoint or PDF. It shares one fonts handle between preview and export.
npm install
npm run fonts
npm run devAfter Ready, click slide text to edit. Cmd/Ctrl+Enter commits and Escape cancels. Download actions commit the active draft first. The host owns persistence, authentication and collaboration. Optional exporters load on the download click, and cleanup disposes the canvas and its owned fonts.
Editor source
import { createCanvasEditor } from '@openpresentation/opf-editor/canvas';
import { loadPreviewFonts } from '@openpresentation/opf-render/preview-fonts';
const status = document.querySelector('#status');
const source = document.querySelector('#source');
let canvas, fonts, slideIndex = 0;
const sync = message => { source.value = JSON.stringify(canvas.editor.presentation, null, 2); status.textContent = message; };
function save(bytes, type, name) {
const url = URL.createObjectURL(new Blob([bytes], { type }));
const link = document.createElement('a');
link.href = url; link.download = name; link.click();
setTimeout(() => URL.revokeObjectURL(url), 10000);
}
function action(id, run) {
const button = document.querySelector('#' + id);
button.disabled = false;
button.addEventListener('click', async () => {
try { await run(); } catch (error) { status.textContent = 'Error: ' + error.message; }
});
}
try {
const response = await fetch('/deck.opf.json');
if (!response.ok) throw new Error('Deck request failed: ' + response.status);
const presentation = await response.json();
fonts = await loadPreviewFonts('/opf-fonts/');
canvas = createCanvasEditor(document.querySelector('#slide'), { presentation, fonts,
onCommit: () => sync('Committed'), onError: error => { status.textContent = 'Error: ' + error.message; } });
await canvas.ready;
sync('Ready');
action('undo', () => { canvas.editor.undo(); sync('Undo'); });
action('redo', () => { canvas.editor.redo(); sync('Redo'); });
for (const [id, delta] of [['previous', -1], ['next', 1]]) action(id, () => {
const count = canvas.editor.presentation.slides.length;
const next = (slideIndex + delta + count) % count;
if (canvas.setSlide(next) === false) throw new Error('Commit or cancel the active draft first.');
slideIndex = next; sync('Slide ' + (slideIndex + 1));
});
action('save', () => {
if (!canvas.commit()) throw new Error('The draft cannot be committed.');
save(JSON.stringify(canvas.editor.presentation, null, 2), 'application/json', 'deck.opf.json'); sync('OPF saved');
});
action('pptx', async () => {
if (!canvas.commit()) throw new Error('The draft cannot be committed.');
const deck = canvas.editor.presentation;
await fonts.ensure(deck);
const { toPptx } = await import('@openpresentation/opf-pptx');
const diagnostics = [];
const bytes = await toPptx(deck, { fonts, onDiagnostic: item => diagnostics.push(item) });
save(bytes, 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'deck.pptx');
sync('PowerPoint exported' + (diagnostics.length ? ': ' + diagnostics.map(item => item.code + ': ' + item.message).join('; ') : ''));
});
action('pdf', async () => {
if (!canvas.commit()) throw new Error('The draft cannot be committed.');
const { convert } = await import('@openpresentation/opf-editor/export');
const { files: [download] } = await convert(canvas.editor.presentation, { format: 'pdf', fonts });
save(download.bytes, download.type, download.name); sync('PDF exported');
});
window.addEventListener('pagehide', () => { canvas.destroy(); fonts.dispose(); }, { once: true });
} catch (error) {
canvas?.destroy(); fonts?.dispose();
status.textContent = 'Startup error: ' + error.message;
}For React and Svelte controls and structural editing, continue to the editor guide.
A Node export pipeline
Download the Node starter. It validates the source, paginates with the same fonts handle, and writes OPF, SVG, PNG, vector PDF and editable PowerPoint under output/.
npm install
npm run export
# Or choose input and output paths:
node export.mjs deck.opf.json outputNode source
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { assertValid } from "@openpresentation/opf";
import { paginate } from "@openpresentation/opf/pagination";
import { loadFonts } from "@openpresentation/opf-render/fonts-node";
import { toPdf, toPng, toSvg } from "@openpresentation/opf-render";
import { toPptx } from "@openpresentation/opf-pptx";
const input = process.argv[2] ?? "deck.opf.json";
const output = process.argv[3] ?? "output";
const deck = JSON.parse(await readFile(input, "utf8"));
assertValid(deck);
// This starter chooses Roboto, covered by base. Load office when your deck requires it.
const fonts = await loadFonts({ pack: "base", presentation: deck, scripts: "auto", embedScriptFonts: true });
const { presentation } = paginate(deck, { fonts });
const svgs = toSvg(presentation, { fonts }); // one SVG per slide
await mkdir(output, { recursive: true });
await writeFile(path.join(output, "deck.opf.json"), JSON.stringify(presentation, null, 2));
await writeFile(path.join(output, "slide-1.svg"), svgs[0]);
await writeFile(path.join(output, "slide-1.png"), await toPng(svgs[0], { fonts }));
await writeFile(path.join(output, "deck.pdf"), await toPdf(svgs, { fonts }));
await writeFile(path.join(output, "deck.pptx"), await toPptx(presentation, { fonts, onDiagnostic: item => console.warn(item.code + ": " + item.message) }));
console.log("Exported " + presentation.slides.length + " slides to " + output);The starter chooses Roboto, which the base pack covers. Use the Office pack for decks that require its substitutes, and install the renderer’s optional script-font packages for the languages you use before measuring. Standalone SVG exports enableembedScriptFonts: true so their drawn script faces travel with the file. PPTX conversion diagnostics are printed to the terminal.
PPTX names chosen fonts but does not embed their binaries; the viewer needs those fonts. Full provenance restores supported authored semantics when native evidence is unchanged. Mechanical import can normalize fields and unsupported Office features; keep the original OPF as canonical source.
Delivery and hosting
For the browser starters run npm run build, then npm run preview to inspect the production bundle. Serve the generated dist/ from your static host. No upload or deployment is part of these commands.
Measure compressed JavaScript separately from font requests. The interactive renderer includes font measurement and shaping code; loading font faces on demand does not remove that JavaScript cost. Dynamic export imports defer conversion code until requested.
For a content site, generate SVG, PDF and PPTX ahead of time and cache those immutable outputs by deck revision, package versions, render options and font-file hashes. A standalone SVG carries embedded faces and may be large. Multi-slide self-contained HTML can repeat them: render the first slide initially, or supply the exact matching fonts through host-owned CSS for shared delivery.
For on-demand exports, run the Node pipeline in a worker or background job with bounded input size and concurrency, cancellation and a timeout. Reuse a fonts handle per worker. Keep CPU-heavy conversion off the request handler; cache completed results. A container needs the locked packages and pinned fonts, and does not need desktop Office. Choose memory and worker limits from measurements of your own deck sizes, scripts and image assets.