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.