Skip to main content

Rendering Pipeline

This page walks through exactly what happens when Ceres renders a document. Every step, in order.

Step 1: The page loads

The browser opens index.html. This file is simple. It has:

  • A <div id="documentOutput"> where the rendered document will go
  • A tiny script that fetches main-manifest.json
<div id="documentOutput" class="loading-message">
Trying to load document...
</div>
<script>
fetch("./main-manifest.json")
.then(function(r) { return r.json(); })
.then(function(manifest) {
var script = document.createElement("script");
script.src = "./" + manifest.js;
document.head.appendChild(script);
});
</script>

Step 2: The main renderer loads

The main-manifest.json file tells the browser where to find the renderer JavaScript. It looks like this:

{
"js": "main-renderer/renderer.a1b2c3d4.js"
}

Once that script loads, it runs renderDocument() automatically.

Step 3: Reading the URL parameters

The renderer reads two things from the URL:

  • template tells it which template to use (like basic-invoice-example)
  • apiUrl tells it where to get the data (base64-encoded)

For example:

https://lstatic.refrens.com/ceres/index.html?template=basic-invoice-example&apiUrl=aHR0cHM6Ly9hcGkucmVmcmVucy5jb20vaW52b2ljZXMvMTIzND9fYXQ9YWJjJnBvcHVsYXRlQnVzaW5lc3M9dHJ1ZQ==

The apiUrl value is base64-encoded. The renderer decodes it to get the real URL:

https://api.refrens.com/invoices/1234?_at=abc&populateBusiness=true

Step 4: Loading the template

The renderer figures out the template's manifest URL and fetches it. Each template has its own manifest.json:

{
"version": "1.0.101",
"manifest": "./1.0.101/manifest.json",
"assets": {
"js": "./1.0.101/bundle.js",
"css": "./1.0.101/bundle.css",
"thumbnail": "./1.0.101/thumbnail.png"
}
}

Then it loads the template's JS and CSS in parallel:

await Promise.all([
loadScript(jsUrl),
cssUrl ? loadCSS(cssUrl) : Promise.resolve()
]);

Step 5: The template registers itself

When the template's JS bundle loads, it runs and does one important thing: it sets window.CeresTemplate to a function.

Here is what basic-invoice-example/index.ts looks like:

import template from "./template.hbs";
import "./styles.css";
import "../../widgets/invoice-status";
import "../../widgets/demo-badge";
import "../../widgets/date-time";
import "../../widgets/markdown-viewer";

window.CeresTemplate = template;

The template variable is a compiled Handlebars function. When you call it with data, it returns an HTML string.

Step 6: Fetching the API data

The renderer fetches the decoded API URL:

const response = await fetch(apiEndpoint);
const data = await response.json();

This returns a JSON object with all the invoice/document data (line items, addresses, totals, dates, etc.).

Step 7: Rendering

The renderer calls the template function with the data and puts the result in the page:

const html = window.CeresTemplate(data);
outputDiv.innerHTML = html;

Step 8: Waiting for everything to settle

Before telling Lydia that the content is ready, Ceres waits for two things:

  1. Fonts to finish loading (so text does not reflow later)
  2. Images to finish loading (so the height does not jump)
await Promise.all([document.fonts.ready, waitForImages(outputDiv)]);

Step 9: Reporting height

If Ceres is running inside Lydia's iframe (when isLydiaMode=1 is in the URL), it measures the content height and sends it to Lydia:

lydiaBridge.reportContentHeight("render-complete");

This fires a postMessage to the parent window so Lydia can resize the iframe to fit the content perfectly.

Style options

The API data can include template style options (colors, fonts, logo URLs). If present, the renderer applies them before rendering:

const templateStyleOptions = extractTemplateStyleOptions(payload);
if (templateStyleOptions) {
applyPreviewStyles(templateStyleOptions);
}

These get applied as CSS custom properties (like --ceres-primary-color) on the document root. Your template CSS can use them.