@uniweb/press
v0.4.6
Published
Press your React components into downloadable files — Word, Excel, PDF. Registration-based pattern with lazy-loaded format adapters.
Readme
@uniweb/press
Downloadable files from Uniweb content. A Uniweb site is, in a useful sense, a URL that represents a document — titles, paragraphs, tables, lists, tagged data blocks, all already parsed and available to section components. Press lets those same section components turn that content into downloadable files: Word documents, spreadsheets, typeset PDFs, and whatever else a foundation wants to support. One source, many outputs.
npm install @uniweb/pressWhat Press is
Press is the output layer of the Uniweb ecosystem. It's a small registration layer (what section components call into to opt in to a format) plus a set of format adapters (the things that know how to produce .docx, .typ, .xlsx files). The registration layer is format-agnostic; the adapters are format-specific and dynamic-loaded, so a foundation only pays for the formats it actually offers.
The core insight that makes Press tractable is that Uniweb has already done the semantic work. Press doesn't parse markdown, resolve Loom expressions, or fetch data — by the time a section component renders, content.paragraphs, content.title, and everything else are already concrete values. Press just walks what's there and emits.
Three properties follow from that design:
- Same source, many outputs. The JSX a section renders for the browser can be the same JSX Press compiles into a Word doc or a PDF. No duplication, no drift between preview and file.
- Frontend-first. Most formats compile entirely in the browser. When a format needs a backend (Typst PDFs, future LaTeX), it's an escape hatch — not the architecture.
- Additive formats. Adding a new format is a new adapter module plus an entry in a map. Nothing else in Press needs to change.
Hello world
A section component that renders as a preview and registers itself for docx compilation, using the same JSX for both:
import { useDocumentOutput } from '@uniweb/press'
import { H1, H2, Paragraphs } from '@uniweb/press/docx'
function Cover({ content, block }) {
const body = (
<>
<H1 data={content.title} />
<H2 data={content.subtitle} />
<Paragraphs data={content.paragraphs} />
</>
)
useDocumentOutput(block, 'docx', body)
return <div className='max-w-4xl mx-auto'>{body}</div>
}A few things worth noticing:
- The
bodyvariable is used twice — registered for docx compilation and rendered into the visible layout. No duplication to drift between them. useDocumentOutputis a no-op outside aDocumentProvider, so sections that don't opt into docx simply don't contribute. Mixing opt-in and non-opt-in sections on the same page is fine.- The
/docxbuilders (H1,H2,Paragraphs, etc.) emit ordinary HTML withdata-*attributes. The same tree serves as the browser preview and as the source the compile pipeline walks.
Somewhere higher in the tree — a layout, a header, a dedicated download section — wrap everything in a DocumentProvider and wire up a button:
import {
DocumentProvider,
useDocumentCompile,
triggerDownload,
} from '@uniweb/press'
function ReportLayout({ children }) {
return (
<DocumentProvider>
{children}
<DownloadButton />
</DocumentProvider>
)
}
function DownloadButton() {
const { compile, isCompiling } = useDocumentCompile()
const handleDownload = async () => {
const blob = await compile('docx', { title: 'Annual Report' })
triggerDownload(blob, 'annual-report.docx')
}
return (
<button onClick={handleDownload} disabled={isCompiling}>
{isCompiling ? 'Generating…' : 'Download'}
</button>
)
}Every section between the provider and the button that called useDocumentOutput('docx', ...) contributes to the compiled document. The docx library itself is loaded dynamically on the first compile('docx') call — it's never in the main bundle.
For a fuller walkthrough, see the quick start.
What's shipped
Press ships three format adapters today:
- docx — Word documents via the
docxlibrary. Paragraphs, headings, text runs, tables, lists, images, hyperlinks, headers/footers, page numbering. The flagship format. - typst — Typeset PDFs via the Typst compiler. Same-source preview from
/typstbuilders; whole-book compile viacompileSubtree;sourcesmode (pure frontend, user compiles locally) andservermode (via a companion endpoint — see deployment.md). - xlsx — Spreadsheets via
exceljs. Plain{ title, headers, data }objects, multi-sheet, preview and compiled output independent (because spreadsheets aren't typography).
For what's next (Paged.js for browser-paginated PDFs, EPUB, slides, and a handful of speculative formats) see the format roadmap. For writing a custom adapter against an unsupported format, see the custom adapter guide.
Three ways to use Press
Press doesn't prescribe what the site is for. Three shapes are all first-class:
Interactive report with downloads. A live web document — navigable, themed, possibly with dynamic data — that also offers one or more Download buttons. Readers browse on screen; those who need a file click Download. The flagship case: annual reports, CVs, research outputs.
Multi-format exports. The same site registers fragments for several formats. A report section might contribute to docx (for the narrative) and xlsx (for the underlying data); a cover might contribute only to docx. Compilation is per-format and lazy — only the adapter you click loads.
Headless export. Sections register fragments and return null. The site has no visible output and exists purely as a compile target for an automated pipeline, admin tool, or generator UI. Press works here because registration is decoupled from rendering.
For how these shapes interact with preview strategies (same-source JSX vs compiled-blob), and the fuller mental model, see concepts.
Books and whole-site compilation
The patterns above cover one-page documents. For books — where the download's scope is every chapter, not just the current page — Press provides compileSubtree:
await compileSubtree(<ChildBlocks blocks={website.allBlocks} />, 'typst')This renders the whole content graph off-screen through its own provider, gathers every registration, and compiles. No page routing, no DOM mounts. See the book publishing guide for the end-to-end flow.
Subpath exports
| Entry point | What's in it |
|---|---|
| @uniweb/press | Core: DocumentProvider, useDocumentOutput, useDocumentCompile, triggerDownload, createStore, compileRegistrations, compileSubtree |
| @uniweb/press/docx | React builders for docx (Paragraph, TextRun, H1–H4, Image, Link, List, …) |
| @uniweb/press/typst | React builders for Typst (ChapterOpener, Heading, Paragraph, CodeBlock, Table, BlockQuote, Image, Asterism, Raw, Sequence, …) |
| @uniweb/press/sections | Section and StandardSection — register-and-render helpers with their own <section> wrapper (for non-Uniweb React contexts) |
| @uniweb/press/ir | IR utilities (htmlToIR, attributeMap, compileOutputs) for custom adapter authors |
| @uniweb/press/vite-plugin-typst | Dev-server Vite plugin answering Typst server compile mode locally |
Format adapters themselves (compileDocx, compileTypst, compileXlsx, and the libraries they depend on) are deliberately not in the exports field. They're reached only through the dynamic import() inside useDocumentCompile, which keeps multi-megabyte libraries out of the main bundle.
Documentation
Start here:
- Concepts — the mental model: why Press exists, the registration pattern, preview strategies, how to think about fragments. Read this if you're deciding how Press fits into a foundation.
- Quick start — ten minutes from
npm installto a working Download button, with a live preview.
Guides:
- Publishing a book — whole-site aggregation via
compileSubtree, the Typst pipeline, the two web modalities. - The preview pattern — render a compiled
.docxback into a sandboxed iframe viadocx-previewfor high-fidelity cross-check views. - Multi-block reports — how
DocumentProvideraggregates output across many section components into one document. - Writing a custom adapter — build a non-shipped format adapter using
@uniweb/press/ir. - Citations — bibliographies in preview and docx via the
citestyle+ Press pattern. - Style pack — copy-paste
paragraphStylesandnumberingdefinitions for common bibliography patterns.
Architecture and design:
- Principles — the architectural commitments Press is built on. Read before proposing any change that touches the public surface or introduces a new format.
- Overview — how Press is put together under the hood, for contributors orienting to the codebase.
- Adding a format — worked examples (LaTeX, Paged.js) and the general checklist.
- Deployment — wire protocol, reference implementations, and operational concerns for formats that need a backend.
- Format roadmap — what's shipped, what's next, what's speculative.
Runnable example:
examples/preview-iframe/— a minimal standalone Vite app demonstrating compile + preview + download end to end. Not a Uniweb foundation; a plain React app demonstrating that Press works outside Uniweb as well.
See also
@uniweb/loom— expression language for weaving data into text. A typical Uniweb report foundation runs Loom'sinstantiateContentover the parsed content tree before Press sees it, which means dynamic reports look identical to static ones from Press's point of view.@uniweb/kit— React components and hooks for Uniweb foundations. Preview-side typography often uses Kit components alongside Press builders for compiled output.
Status
Pre-1.0, published on npm. The registration architecture, builder components, section templates, IR layer, and the docx, typst, and xlsx adapters are stable; the public surface is expected to hold through 1.0.
Releases go through the workspace publish pipeline. Breaking changes are acceptable when justified, but each release is a published artifact — the exports field and documented subpaths stay coherent across versions.
Development
Press ships raw source with no build step. Edits in src/ are immediately effective in any linked workspace package.
pnpm test # vitest run — full suite
pnpm test:watch # watch mode
pnpm test tests/docx/ # one directory
pnpm test -t 'inline marks' # by test nameTo run the preview-iframe demo:
cd examples/preview-iframe
pnpm install
pnpm devFor contributor conventions — adding a builder, format adapter, or section helper; the docx image-emission invariants; testing patterns — see CLAUDE.md.
License
Apache-2.0 — see LICENSE.
