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.

Everything below happens inside the designer, which is mounted in your own application rather than run on its own: Paragon:Enabled in development, a connection string for its database, then browse to /paragon. That is step 6 of the quick start, and it has to be done before section 1 here means anything.

This guide is the HTML route. A report can also be a real .docx or .pptx, written in Word or PowerPoint with the same {{ }} bindings — see Reports authored in Word or PowerPoint. Sections 1 and 2 below apply to both; the rest of this guide is about writing a template as a page.

A page is not what a workbook is made of. An Excel report is written as a page like any other, but only its tables reach the sheets, and its columns have to say what they hold — see Excel reports.


The shape of it

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

Assets — fonts, chart libraries, shared CSS — go in the project's shared-assets folder, and every report in the project can reference them. One copy of a typeface for seven reports, changed in one place. A report that needs its own version of a file keeps it in its own assets folder, which is searched first; the shared copy is what it falls back to.

A report belongs to exactly one project, which is what makes asset resolution unambiguous.

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/Roboto-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, PowerPoint or Excel. 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, and a single page can override it. Word and PowerPoint are built from the same rendered pages as the PDF, so one page setup covers all three. Excel has no page setup at all, and the designer hides the section: a workbook has no sheet of paper to be laid out on — see Excel reports. Margins are not a setting: they are always zero, so whitespace is written in the template where you can see it — see Pagination and right-to-left. The running header and footer are not here either; they are markup, authored in the template as inert islands — see Report template format — schema 1.0.

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

Preview renders the report the engine would load, built in memory from what the designer is holding right now rather than from whatever the last export left on disk. It runs the same flattening, stamps the same manifest an export does, and resolves page setup back out of that manifest with the same code the engine uses — the consumer's code path over the consumer's bytes.

The sample data on the report is the preview payload, which is the other reason to make it realistic: preview only overflows when the data does.

6. Export

Export downloads a zip. One button, one archive: {code}.zip, holding a {code}/ folder with the template, each page as it was authored, an 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.

Pages ship unflattened. The engine merges the template into each page as it loads, so doing it here as well would fold a whole document into the template's body. The template carries the only manifest there is, and it is the thing that declares the pages.

Shared assets travel as copies. The assets tree is the project's shared-assets with the report's own folder laid over the top — the same resolution order a render uses, resolved once, here. The receiving team has no shared folder to put anything in, so a shared typeface arrives as an asset of this report like any other.

Nothing is kept on the designer's server; the archive is the deliverable.

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 exporting

  • 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
  • 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
  • For Excel: the checks in Excel reports, which the preview cannot show you

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.

Page setup is the report's, and a page may override it. Size and orientation are set once, in step 3, and every page is laid out on that sheet unless it carries its own — which is what lets one page be landscape in a portrait report. Each page renders as its own print job, which is where the rest of this section comes from.

A page is a unit of authoring, not a unit of output. If a page's content runs long, the Paragon engine 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/images/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 the Paragon engine repeats on every printed sheet:

.slide-background {
    position: fixed;
    inset: 0;
    z-index: -1;
    background: url('assets/images/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 belong to the finished document, not to a page. A count taken inside one print job would read "1 of 1" on every sheet, so the Paragon engine numbers the assembled document instead.