Skip to main content

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 install done 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 using handlebars-loader.
  • import "./styles.css" pulls your CSS into the build. It gets extracted into a separate bundle.css file.
  • The widget imports register Handlebars partials. Without them, {{> InvoiceStatus}} in your template would fail silently and show nothing.
  • window.CeresTemplate = template is 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​

SyntaxWhat it doesExample
{{ 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:

HelperWhat it doesExample
formateShortDateWithOffsetFormats a date with timezone offset{{formateShortDateWithOffset invoiceDate ownerOffset}}
formateDateWithOffsetFormats a date (longer format){{formateDateWithOffset date ownerOffset}}
formatDateInTimeZoneFormats a date in a specific timezone{{formatDateInTimeZone irn.AckDt "UTC" "SECONDARY_SHORT_DATE_FORMAT"}}
formatDateAddDaysFormats a date with added days{{formatDateAddDays dueDate 30 ownerOffset}}
computeInvoiceStatusComputes status tag dataUsed internally by InvoiceStatus widget
prepareMarkdownViewerDataPrepares 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:

PropertyWhat it controls
--ceres-primary-colorMain accent color
--ceres-secondary-colorSecondary color
--ceres-primary-backgroundMain background color
--ceres-secondary-backgroundSecondary background color
--ceres-font-familyFont 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.ts imports all the widgets you use in your template
  • window.CeresTemplate = template is 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.json has at least one working test URL
  • npm run build:template --template=my-cool-invoice builds without errors
  • The template renders correctly in the browser