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