Paragon

Authoring a report

The path from an empty project to a template another team can run. This is the designer's side; Quick start for a consuming application is what happens after you hand the folder over.


The shape of it

A project owns assets. A report belongs to one project. A report has one published template.

Assets — fonts, chart libraries, shared CSS — are deployed once per project and shared by every report in it. That is why a report belongs to exactly one project: it makes asset resolution unambiguous, and it is why two reports needing different versions of a chart library belong in different projects.

1. Create the project

A name and a code. Together they name the project's folder — {name}-{code} — so both are worth choosing deliberately: reports inside get codes like R-acme-erp-003, and that code is what a calling application sends.

The code alone may repeat. The same product set up for two clients is the same code twice; it is the name beside it that tells them apart, and the pair has to be unique.

2. Upload the assets

Everything the templates will reference, in the tree they will reference it by:

assets/js/charts/index.js
assets/css/site.css
assets/fonts/Cairo-Regular.woff2

Two rules that save an afternoon later:

  • Fonts must be here. The renderer blocks every external request, so a font that is not in the project simply does not exist as far as the report is concerned. Arabic without its font renders as disconnected letters or boxes.
  • Assets are served byte-for-byte and never bound. A .js file can contain {{ freely and a minified library is never mangled. Anything report-specific belongs in the template, not here.

Assets are versioned as a set. Exporting them bumps the project's AssetsVersion, and a template records the version it was built against.

3. Add the report

Name, direction, and what it produces — PDF, Word or PowerPoint. The caller never chooses the format; the report does, so changing it later needs no change in the consuming application.

Page setup — size and orientation — is on the report too. Word and PowerPoint are built from the same rendered pages as the PDF, so one page setup covers all three. Two things are deliberately not on the report: margins, which are always zero so that whitespace is written in the template where you can see it, and rasterization sharpness, which is fixed at 300 DPI for every report the engine produces. The running header and footer are not here either — they are markup, so they are authored in the template as inert islands. Both are in Pagination and right-to-left.

Turn on Template draws asynchronously if any chart code lives in an asset. See Waiting for several asynchronous things — this is the single most common way a chart-heavy report comes out blank.

4. Write the template

An ordinary HTML file. Style it however you like: the engine adds a @page rule from the page setup, turns off animations, and injects the readiness hook — nothing else. It never imposes a look.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="assets/css/site.css">
<style>
  thead { display: table-header-group; }
  tr { break-inside: avoid; }
</style>
</head>
<body>
  <h1>Invoice {{invoice.number}}</h1>
  <p>{{customer.name}}{{#if customer.vatNumber}} — VAT {{customer.vatNumber}}{{/if}}</p>

  <table>
    <thead><tr><th>Description</th><th>Amount</th></tr></thead>
    <tbody>
    {{#each invoice.lines}}
      <tr><td>{{description}}</td><td>{{money amount "en-US"}}</td></tr>
    {{/each}}
    </tbody>
  </table>
</body>
</html>

Template helper reference is the full reference. Two things worth knowing before you start:

  • There is no schema. An unknown path renders empty rather than failing, so the sample data is what documents the shape the template expects — it is also what a consuming team reads to know what to send. Make it realistic.
  • Author against real quantities. A table of three rows tells you nothing about a report; paginate it with sixty. See Pagination and right-to-left.

5. Preview, version, publish

Save a draft as often as you like; drafts are diffed against the published version so you can see exactly what changed. Publishing takes a version number, and published versions are kept — a report issued last quarter can be reproduced from the template that produced it.

6. Export

Export downloads a zip. One button, one archive: {code}.zip, holding a {code}/ folder with the template, one standalone file per page, the report's assets tree and its .report-assets.json stamp. The folder name is the report code, which is what the engine resolves against — so a consuming team unzips it into their ParagonTemplates/ folder and that is the deployment.

Nothing is kept on the designer's server. It never renders a report, so a copy left there would be a folder nobody runs from and nobody can tell is current.

The export writes the manifest into the template: schema version, project code, assets version, direction, output format, readiness flag, required assets with their hashes, per-format page setup, sample data, and an integrity hash of the whole file. After that the file travels alone.

Read the pre-export warnings. They catch what is invisible until production: a referenced asset the project does not have, an external URL that will be blocked, a chart with no readiness signal, and raw {{{ }}} output. Each is a question to answer rather than noise to clear.


Checklist before publishing

  • Rendered at the report's real page size, with realistic quantities of data
  • Table headers repeat, and rows that must not be halved say so
  • Page backgrounds are on body, or on a position: fixed element — see below
  • Every asset referenced is in the project — no external URLs
  • Arabic reports carry their font
  • Charts have animation off and signal readiness
  • Sample data reflects what the consuming team will actually send
  • Pre-export warnings read and answered
  • Every {{{ }}} is deliberate — see Security

Pages, and the one rule about backgrounds

A report is a list of pages. You add them, drag them into order, and delete them; they are numbered by position, so there is nothing to name. Each page is rendered as its own print job, which is what lets one page be landscape while the next is portrait — set that per page under Page setup.

A page is a unit of authoring, not a unit of output. If a page's content runs long, the browser paginates it and that one page becomes two or three in the finished document. That is fine and expected — but it is where the one real trap lives:

A background paints where its box reaches, and stops there. An element given the height of a page paints one page; whatever follows it lands on a bare sheet — and it will have looked perfect in preview, because preview only overflows when the data does.

The simplest fix is to put the background on body, where it is not an element background at all: CSS propagates it to the canvas, and the canvas is every sheet. No height needed.

body { background: url('assets/img/cover.jpg') center / cover; }

When it has to be an element — several backgrounds, or one behind only part of the page — make it fixed, which Chromium repeats on every printed sheet:

.slide-background {
    position: fixed;
    inset: 0;
    z-index: -1;
    background: url('assets/img/cover.jpg') center / cover;
}

The same applies to anything else meant to appear on every sheet of a page — a watermark, a border, a logo in the corner.

Two smaller consequences of pages being separate print jobs:

  • Nothing flows between pages. A table cannot start on page 2 and continue on page 3; each page is its own document. Within a page it paginates normally.
  • Page numbers are the engine's, not the browser's. Chromium counts within one print job, so it would number every page "1 of 1". The engine numbers the finished document instead.