unipress for developers
The developer-track companion to the README. Read this if you're considering writing a foundation that compiles to documents — or to documents and websites from the same source.
What you actually get
A foundation is a Uniweb component system. unipress is the headless runtime for it: run unipress compile <dir> and the same foundation that drives a Uniweb website produces a downloadable file from a content directory.
That means everything the framework does for websites, you can do for documents. The visual editor goes away; the compile pipeline stays.
Section types are the vocabulary
A foundation declares section types — addressable React components that content authors reference by name from markdown frontmatter:
---
type: Chapter
title: "Chapter One"
---
Prose body here.The foundation decides what Chapter means: its layout, its typography, what data it expects, how it renders across each output format the foundation declares. Authors don't think in components — they think in kinds of section. The component is the foundation developer's responsibility; the content is the author's.
Mix prose with structured data
A document is rarely just prose. Foundations declare data inputs; authors fill them in. The data can come from any of these surfaces:
- Markdown content — the prose body of each
.mdfile, parsed into a structured tree (title, paragraphs, items, links, images, code blocks). - Frontmatter params — declared in each section's YAML frontmatter, typed by the foundation's
meta.js. - File-based queries — records under
records/<name>/(JSON, YAML, BibTeX, or markdown frontmatter), named by a query underqueries:indocument.yml. Compile to one/data/<name>.jsonper query. - External queries — a query with
url:names a public API. A website's visitors' browsers fetch it;unipress compilereads file-based queries only. - Computed values via Loom — Loom is an expression language for instantiating templates against hierarchical data. Pull a publications list from a query's records, format each entry with a Loom expression, and the result lands typeset in your output.
A section type declares the content.data keys it reads in its meta.js data: — data: { publications: {} } — and a section receives those keys and nothing else. Keys a foundation's content handlers read go in its main.js data:, and every section receives them.
All of these flow through the same foundation pipeline — same primitives whether the data came from a YAML file, a JSON dump, or (on a website) an API endpoint.
Academic writing: bibliographies and citations
For scholarly work specifically, citestyle handles the formatting layer Loom is too general for. CSL-based, lightweight, used directly from foundations: pick from APA, MLA, Chicago author-date, IEEE, Vancouver, Harvard at the section level (style: chicago-author-date); citestyle dynamically loads only the styles you reference and formats every entry to spec — the author ordering, journal abbreviations, punctuation, and conventions academic readers expect.
The combination is the powerful part. Loom interpolates ("In {section.title}, we describe…"); citestyle formats bibliographic entries (a complete back-matter Bibliography, or inline [Author Year] citation marks). A thesis, monograph, or annual research report uses both: Loom for dynamic prose and per-section metadata, citestyle for every bibliographic touch in the document.
Working example: the data-report template ships a PublicationsList section backed by YAML bib data in records/members/*.yml, with per-section style selection. Run unipress create my-report --template data-report to see it. Bibliography support in long-form prose templates (book, monograph, future thesis/paper) lands across upcoming releases.
Outputs are foundation-declared
// foundation/src/main.js
export default {
outputs: {
pdf: { extension: 'pdf', via: 'typst', getOptions: buildTypstOptions },
epub: { extension: 'epub', getOptions: buildEpubOptions },
docx: { getOptions: buildDocxOptions },
'tax-form-2024': { getOptions: buildTaxFormOptions }, // your own adapter
},
}Each entry says: which Press adapter handles the format (via:), which file extension to default to (extension:), and how to assemble adapter-specific options from the website's resolved content (getOptions(website, hostHints)).
Press ships adapters for Word, Excel, Typst (PDF), and HTML-shaped output. For a format Press doesn't ship — a CSV feed, a domain-specific XML, a one-of-a-kind regulatory submission — write a custom adapter using @uniweb/press/ir and declare it on the foundation. unipress dispatches by name; nothing in unipress itself needs to know about your format.
Same foundation, two runtimes
unipress compile is one consumer. The Uniweb website pipeline (uniweb dev, uniweb build) is the other. Same foundation, same content shape, same data declarations — different runtime.
That's the structural payoff: the work of defining section types, theming, and rendering isn't single-purpose. A "regulatory report" foundation can drive both an HTML site that stakeholders browse and a PDF/XLSX/DOCX bundle filed quarterly. A "research output" foundation can drive a public-facing journal site and a print-ready PDF for archival. Authoring once produces every modality.
A worked example
A research-group annual-report foundation (@yourorg/annual-report) might ship:
- Section types:
Cover,ExecutiveSummary,Members,PublicationsList,FundingTable,Charts,Appendix,Bibliography. - Data: a
publicationsquery over records with full bibliographic fields per paper; afundingquery over grant records; and, for the website, an externalmetricsquery pulling live citation counts — which the site's visitors fetch and a compiled document does not. - citestyle for bibliographic precision: pick a style at the section level (
style: chicago-author-date) and citestyle formats every entry to spec — author ordering, journal abbreviations, the punctuation that marks the difference between a draft and a polished publication. - Loom templates: dynamic prose elsewhere — running headers (
{book.title} — Chapter {n}), section subtitles, the table-of-contents introductions. Loom and citestyle cooperate: citestyle owns bibliographic formatting; Loom owns everything else. - Outputs:
pdf(typeset for the dean's office),xlsx(one sheet per data block, for finance),docx(for collaborators who don't open PDFs),pagedjs(HTML for the public site).
A content author writes:
---
type: PublicationsList
title: "Publications"
fetch:
query: publications
where: { year: 2024 }
---
The group published 47 papers in 2024, a 30% increase over 2023.unipress compile annual-2024 --format pdf walks the content through the foundation, resolves Loom expressions over the filtered records, runs the typst adapter, ships a typeset PDF. unipress compile annual-2024 --format xlsx produces a workbook with one sheet per data section. The same source directory; two file types, both honest.
Where to go from here
- Foundation contract — the
outputs:map,meta.jssection-type discovery, thegetOptions(website, hostHints)signature: see foundation configuration. - Data fetching — queries, the keys a section receives, predicates, where-objects: see the data fetching reference.
- Loom — expression language for instantiating templates against hierarchical data:
@uniweb/loom. - citestyle — CSL-based citation and bibliography formatter. Domain-specific complement to Loom: Loom interpolates, citestyle formats academic-style references:
citestyle. - Press (the output layer) —
@uniweb/pressdocs cover the registration pattern foundations consume, the IR layer for custom adapters, and per-adapter notes (docx invariants, typst conventions, the EPUB pipeline). - Building a foundation from scratch — Uniweb framework docs cover the full authoring story; everything written for sites applies to documents.
A few things worth knowing upfront
Documents are static at compile time. unipress produces bytes; once the file is on disk, it doesn't change. "Dynamic data" in a document means the data backing the document can vary between compiles (re-run unipress compile after its records change; get a new PDF). It does not mean the PDF live-updates while someone reads it. That's website territory.
Foundations stay environment-agnostic. unipress runs in Node; the Uniweb editor runs in the browser. Foundations should not branch on typeof window or do their own asset I/O. The compile pipeline supplies a loadAsset(src) helper that hosts implement appropriately for their runtime; foundations just call it. The book foundation in this repo is the reference example.
Compile failures are debuggable. unipress compile <dir> --verbose streams every step; unipress inspect <dir> dumps the parsed content tree as JSON before the foundation runs (useful when output looks wrong and you want to know whether the input was right). Press emits structured warnings when an adapter receives unexpected shapes.
On 1.x. The foundation outputs: map, the meta.js discovery model, the compileDocument API, the section-type vocabulary — all stable enough to build against. A change you may need to act on arrives as a minor version.