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:
templatetells it which template to use (likebasic-invoice-example)apiUrltells 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:
- Fonts to finish loading (so text does not reflow later)
- 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.