Creating Your Own Template
This guide walks you through creating a brand new Ceres template from scratch. We will use the basic-invoice-example template as a reference throughout.
By the end, you will have a working template that takes invoice data from an API and renders a styled document.
What you need
- Node.js 18 or higher
- The Ceres repo cloned locally
npm installdone in the repo root
Step 1: Create the folder
Every template lives in src/templates/. Create a new folder for yours:
mkdir src/templates/my-cool-invoice
You need to create five files inside this folder. We will go through each one.
Step 2: Create index.ts
This file wires everything together. It imports your template, your styles, and any widgets you want to use. Then it makes the template available to the main renderer.
Create src/templates/my-cool-invoice/index.ts:
// This comment tells TypeScript to ignore the .hbs import (it's handled by webpack)
// @ts-ignore - compiled via handlebars-loader
import template from "./template.hbs";
import "./styles.css";
// Import the widgets you want to use in your template
import "../../widgets/invoice-status";
import "../../widgets/demo-badge";
import "../../widgets/date-time";
import "../../widgets/markdown-viewer";
// This line is critical. The main renderer looks for window.CeresTemplate
// to get the compiled template function. Without this, nothing renders.
window.CeresTemplate = template;
Why each line matters:
import template from "./template.hbs"loads your Handlebars file and compiles it into a JavaScript function. Webpack does this at build time usinghandlebars-loader.import "./styles.css"pulls your CSS into the build. It gets extracted into a separatebundle.cssfile.- The widget imports register Handlebars partials. Without them,
{{> InvoiceStatus}}in your template would fail silently and show nothing. window.CeresTemplate = templateis the contract between your template and the main renderer. The renderer calls this function with the API data to get HTML.
Step 3: Create template.hbs
This is the heart of your template. It is a Handlebars file that contains HTML with placeholders for data.
Create src/templates/my-cool-invoice/template.hbs:
<div class="my-invoice">
<div class="header">
<h1>{{ invoiceTitle }}</h1>
<p>Invoice #{{ invoiceNumber }}</p>
</div>
<div class="details">
<div class="billed-by">
<h3>From</h3>
<p><strong>{{ billedBy.name }}</strong></p>
<p>{{ billedBy.street }}, {{ billedBy.city }}</p>
<p>{{ billedBy.state }} {{ billedBy.pincode }}</p>
</div>
<div class="billed-to">
<h3>To</h3>
<p><strong>{{ billedTo.name }}</strong></p>
<p>{{ billedTo.street }}, {{ billedTo.city }}</p>
<p>{{ billedTo.state }} {{ billedTo.pincode }}</p>
</div>
</div>
{{!-- This is a Handlebars comment. It won't appear in the output. --}}
{{!-- Status tag using the InvoiceStatus widget --}}
<p>Status: {{> InvoiceStatus }}</p>
{{!-- Loop through line items --}}
<table class="items-table">
<thead>
<tr>
<th>Item</th>
<th>Qty</th>
<th>Rate</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{ name }}</td>
<td>{{ quantity }}</td>
<td>{{ rate }}</td>
<td>{{ amount }}</td>
</tr>
{{/each}}
</tbody>
</table>
<div class="totals">
<p>Subtotal: {{ totals.subTotal }}</p>
<p><strong>Total: {{ totals.total }}</strong></p>
</div>
{{!-- Render notes as markdown --}}
{{#if notes}}
<div class="notes">
<h3>Notes</h3>
{{> MarkdownViewer (prepareMarkdownViewerData notes) }}
</div>
{{/if}}
</div>
Handlebars basics you need to know
| Syntax | What it does | Example |
|---|---|---|
{{ fieldName }} | Prints a value | {{ invoiceNumber }} prints INV-001 |
{{ object.field }} | Prints a nested value | {{ billedBy.name }} prints Acme Corp |
{{#if condition}}...{{/if}} | Only shows content if the value exists | {{#if notes}}...{{/if}} |
{{#each array}}...{{/each}} | Loops over an array | {{#each items}}...{{/each}} |
{{> PartialName}} | Inserts a widget (partial) | {{> InvoiceStatus}} |
{{!-- comment --}} | A comment that does not appear in output | |
{{helperName arg1 arg2}} | Calls a helper function | {{formateShortDateWithOffset invoiceDate ownerOffset}} |
Available helpers
These helpers are registered by the widgets. You can use them in your template:
| Helper | What it does | Example |
|---|---|---|
formateShortDateWithOffset | Formats a date with timezone offset | {{formateShortDateWithOffset invoiceDate ownerOffset}} |
formateDateWithOffset | Formats a date (longer format) | {{formateDateWithOffset date ownerOffset}} |
formatDateInTimeZone | Formats a date in a specific timezone | {{formatDateInTimeZone irn.AckDt "UTC" "SECONDARY_SHORT_DATE_FORMAT"}} |
formatDateAddDays | Formats a date with added days | {{formatDateAddDays dueDate 30 ownerOffset}} |
computeInvoiceStatus | Computes status tag data | Used internally by InvoiceStatus widget |
prepareMarkdownViewerData | Prepares text for the MarkdownViewer | {{> MarkdownViewer (prepareMarkdownViewerData notes)}} |
Step 4: Create styles.css
Style your template with plain CSS. The CSS will be extracted into a separate bundle.css file.
Create src/templates/my-cool-invoice/styles.css:
.my-invoice {
font-family: var(--ceres-font-family, 'Helvetica Neue', Arial, sans-serif);
max-width: 800px;
margin: 0 auto;
padding: 40px;
color: #333;
}
.header h1 {
color: var(--ceres-primary-color, #2e8555);
margin-bottom: 4px;
}
.details {
display: flex;
gap: 40px;
margin: 24px 0;
}
.items-table {
width: 100%;
border-collapse: collapse;
margin: 24px 0;
}
.items-table th,
.items-table td {
padding: 8px 12px;
border-bottom: 1px solid #eee;
text-align: left;
}
.items-table th {
background: var(--ceres-primary-background, #f8f9fa);
font-weight: 600;
}
.totals {
text-align: right;
margin-top: 16px;
}
/* Print-specific styles */
@media print {
.my-invoice {
padding: 0;
max-width: none;
}
}
CSS custom properties
When Lydia pushes style updates (like a color change), Ceres sets these CSS custom properties on the document root. Use them in your CSS to make your template respond to user customization:
| Property | What it controls |
|---|---|
--ceres-primary-color | Main accent color |
--ceres-secondary-color | Secondary color |
--ceres-primary-background | Main background color |
--ceres-secondary-background | Secondary background color |
--ceres-font-family | Font family |
Always provide a fallback value: var(--ceres-primary-color, #2e8555).
Step 5: Create version.json
This file is auto-managed by the build system. Just create it with a starting version:
{
"version": "1.0.0"
}
You never need to edit this file by hand. The build system will update it when your source changes. See Versioning for details.
Step 6: Create samples.json
This file holds test API URLs so you can preview your template locally. The URLs are base64-encoded.
Create src/templates/my-cool-invoice/samples.json:
{
"example": "aHR0cHM6Ly9hcGkucmVmcmVucy5jb20vaW52b2ljZXMvNjg4OWU1MmU3MGI1MjQwMDE5NzRkZDJkP19hdD0zcXdtVlp4OEZVV3hnMzdCNG0mY29weSZwb3B1bGF0ZUJ1c2luZXNzPXRydWU="
}
Each key is a name for the sample, and the value is a base64-encoded API URL. You can have multiple samples:
{
"simple-invoice": "aHR0cHM6Ly8...",
"invoice-with-gst": "aHR0cHM6Ly8...",
"invoice-with-payments": "aHR0cHM6Ly8..."
}
To base64-encode a URL, run this in your browser console:
btoa("https://api.refrens.com/invoices/YOUR_ID?_at=YOUR_TOKEN&populateBusiness=true")
Step 7: Build and test
Build just your template:
npm run build:template --template=my-cool-invoice
Then open dist/index.html in your browser with the right URL parameters:
file:///path/to/ceres/dist/index.html?template=my-cool-invoice&apiUrl=YOUR_BASE64_API_URL
Or start a local server:
npx http-server dist -p 1337
# Open: http://localhost:1337/index.html?template=my-cool-invoice&apiUrl=YOUR_BASE64_API_URL
Checklist
Before you submit your template, make sure:
-
index.tsimports all the widgets you use in your template -
window.CeresTemplate = templateis set - All Handlebars partials (
{{> WidgetName}}) have matching widget imports - CSS uses
var(--ceres-*)custom properties with fallbacks for user-customizable values - Print styles are included (
@media print) -
samples.jsonhas at least one working test URL -
npm run build:template --template=my-cool-invoicebuilds without errors - The template renders correctly in the browser