---
title: "One Master Template, a Thousand Customer Variants"
description: "Stop copying templates per customer. Render a 50-product swag catalog from one locked master, with per-customer brand overrides, batch mockups and DPI-sized print plates."
url: "https://img.ly/blog/one-master-template-many-variants/"
type: "blog"
date: "2026-09-28"
author: "Rodrigo"
tags: ["CE.SDK","How-To","Print","Creative Automation"]
---

> This is the markdown version of [One Master Template, a Thousand Customer Variants](https://img.ly/blog/one-master-template-many-variants/). For all pages in one file, see [llms-full.txt](https://img.ly/llms-full.txt). For an index of all available pages, see [llms.txt](https://img.ly/llms.txt).

---

## What You Will Build: A Swag Catalog Rendered From One Master Template

After completing this tutorial, you will end with a working swag catalog: one locked master, three customer brands, fifty SKUs, and a batch job that writes a mockup plus a DPI-sized print plate for every print area. Pick a customer, render the catalog, change the master compliance line, and watch every mockup pick up the new line on the next run. This tutorial is meant for engineers and product managers at merchandise, franchise, dealer, or agency platforms that still copy one template per customer.

![Select a customer in the demo UI](https://img.ly/_astro/select-customer.Be4mLzqB.webp)

The reference app is the [cesdk-template-inheritance repository](https://github.com/imgly/cesdk-template-inheritance). Clone it, set a CE.SDK license in `.env`, and run `npm run dev` as described in the README. Alternatively, the live demo is at [labs.imgly.dev/p/2aqtwedr_template-inheritance](https://labs.imgly.dev/p/2aqtwedr_template-inheritance).

## Why Per-Customer Template Copies Stop Scaling

If you run a merchandise or direct mail platform, the bottleneck is rarely demand, but prepress. Most incoming customer logos and art files aren't ready for print: wrong resolution, no safe area, colors that clash with a locked brand palette. Consequently, graphic designers spend their week fixing that artwork instead of building your next campaign. Customers feel the same problem from the other side: they can self-serve an email in Canva, then wait on a graphics team to turn a logo into a postcard, a tee plate, or a dealer kit that actually clears DPI, safe area, and brand locks.

Duplicating one template per customer doesn't fix any of this. Rather, it spreads manual work: Every new brand is another copy to prep by hand; every master change (a legal line, a lock, a layout tweak) becomes a separate edit on every copy in the set, instead of one change that re-renders everywhere. At a few hundred SKUs, that manual editing is what actually slows down the next batch of print-ready files.

## Template Scaling Approaches Compared: Copies, Parameterized HTML, Headless Template API, Inheritance

Representatives are used to fill the middle two columns: **Handlebars + Puppeteer** for parameterized HTML/SVG, and **Abyssale** for a headless template API. They stand in for those approaches, not the only tools in each class.

|                                     | Copy templates per customer        | Parameterized HTML or SVG templates                                                                                     | Headless template API                                                                                       | Template inheritance in a scene format (this article)    |
| ----------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| Master change propagates            | No (edit every copy)               | Yes, if one shared template feeds every render                                                                          | Yes, if generations reuse the same template/design id                                                       | Yes (re-resolve and re-render)                           |
| Per-customer overrides              | Yes (whole file)                   | Yes (slots / CSS variables)                                                                                             | Yes (payload / element fields)                                                                              | Yes (customer JSON + artwork)                            |
| Per-product artwork locations       | Manual per copy                    | Manual: CSS/`@page` or separate templates per SKU                                                                       | Yes when the API supports per-format layout (e.g. Abyssale `template_format_names`)                         | Yes (`artworkLocation` + `pageSize` in catalog)          |
| Non-designer can edit a variant     | Rarely (design tool required)      | Yes if forms map into template variables; editing the HTML/SVG needs a developer                                        | Yes when the vendor ships a form or payload editor (e.g. Abyssale Quick Generation)                         | Yes for brand fields and adopter text scopes             |
| Print-ready output                  | Depends on the design tool         | Paper size/margins via headless PDF export (e.g. Puppeteer `page.pdf`); typically no CMYK/bleed/printer DPI in that API | Yes when the API documents print PDFs (e.g. Abyssale: mm/in, DPI, bleed/safe, CMYK; `printer` import Alpha) | Yes: `print.png` at `pageSize × DPI`                     |
| Preview at design time              | Yes in the design tool             | Browser preview of the HTML/SVG                                                                                         | Yes when the vendor provides an editor or live preview                                                      | Demo UI + mockup PNGs                                    |
| Batch render                        | Scripted exports if you build them | Scripted (e.g. headless Chromium)                                                                                       | Native batch/generate APIs                                                                                  | Headless CE.SDK Node batch                               |
| Storage per customer                | Full template per customer         | Template + parameter rows                                                                                               | Template + parameter rows                                                                                   | Master once + small customer JSON                        |
| What you have to build              | Process and file hygiene           | Template language + mapper                                                                                              | API integration + data model                                                                                | Master builder, overrides, catalog, resolver, batch      |
| What breaks when the master changes | Silent drift across copies         | Shared template refresh; renamed slots/keys stop resolving                                                              | Next generate uses the current template; integrations break if layer/field names change                     | Customer brand data stays; layout and compliance refresh |

Sources (checked 2026-09-23): [Handlebars](https://handlebarsjs.com/guide/), [Puppeteer PDFOptions](https://pptr.dev/api/puppeteer.pdfoptions), [Abyssale print import](https://developers.abyssale.com/rest-api/designs/import/print), [Abyssale print PDF generation](https://developers.abyssale.com/rest-api/generation/asynchronous-generation/generate-multi-format-pdfs-for-printing). Spot-check: Abyssale documents `bleed_size` / `safe_size`, `dpi`, and CMYK tokens on `printer` designs; multi-format print PDF generation supports crop marks and color profiles.

Copying or a simple HTML pipeline is enough when you have few brands and no print plate to ship. However, inheritance is preferable when one locked master must drive both marketing mockups and physical print plates across many SKUs without editing a file per customer.

## Step 1: Build the Master Template With Brand-Level Locks

The master is generated from TypeScript so a layout change is a readable diff, not a hand-edited scene blob. Run `npm run build:master`. No license is required for this step. The script only writes `templates/master.json`; it does not render images.

`scripts/build-master.ts` declares the variables customers may override, the blocks adopters may edit, and the blocks that stay locked:

```typescript
// scripts/build-master.ts
const OVERRIDE_VARIABLES = [
  'brandName',
  'logoUri',
  'primaryColor',
  'secondaryColor',
  'contactName',
  'contactPhone',
  'contactEmail',
  'contactAddress',
  'legalDisclaimer',
] as const;

const ADOPTER_EDITABLE_TEXT = ['Headline', 'Body', 'CTA'] as const;

const LOCKED_BLOCKS = [
  'BrandLogo',
  'BrandName',
  'BrandPrimarySwatch',
  'BrandSecondarySwatch',
  'BrandPaletteLabel',
  'ContactBlock',
  'LegalLine',
  'MasterCompliance',
] as const;

const PLACEHOLDER_BLOCKS = ['HeroImage'] as const;
```

The generated `templates/master.json` stores `masterOnly.complianceText`, the override allow list, and the scene string. For your own case, change the page size constants, locked block list, or compliance string in the builder, regenerate, and re-render.

## Step 2: Add Per-Customer Overrides (Logo, Palette, Contact, Brand Artwork)

Each file under `templates/customers/` extends the master and carries only brand data. In this repo, that includes the printed artwork: change it once and every product for that brand uses the new art. Campaign copy (`headline`, `body`, `cta`) and master compliance stay forbidden on the customer file.

```json
// templates/customers/bean-there-bean-good.json
{
  "id": "bean-there-bean-good",
  "name": "Bean there Bean good",
  "extends": "../master.json",
  "artwork": {
    "uri": "/images/minimalist-bean.png",
    "width": 1280,
    "height": 720
  },
  "variables": {
    "brandName": "Bean there Bean good",
    "logoUri": "/images/logo-bean.png",
    "primaryColor": "#050087",
    "secondaryColor": "#F1E1C7",
    "contactName": "Bean there Bean good",
    "contactPhone": "+1 (415) 555-0142",
    "contactEmail": "hello@beanthere.example",
    "contactAddress": "214 Folger Ave, San Francisco, CA",
    "legalDisclaimer": "© Bean there Bean good. All rights reserved."
  }
}
```

Customers are discovered at runtime. In the browser, `src/app/customer-catalog.ts` loads every JSON file with `import.meta.glob`. On Node, `scripts/lib/customers-node.ts` reads the same folder. Drop a new file in, restart the demo, and the picker lists it.

`src/customer-override.ts` writes the allowed variables, places brand artwork on the `HeroImage` placeholder, and sets logo and palette swatches:

```typescript
// src/customer-override.ts
const artwork = resolveCustomerArtwork(customer);
replaceImageByName(
  engine,
  'HeroImage',
  {
    uri: resolve(artwork.uri),
    width: artwork.width,
    height: artwork.height,
  },
  { required: true }
);
```

For your own case, keep compliance and campaign copy out of this layer. Put brand print art on the customer and SKU geometry on the product.

## Step 3: Define Artwork Locations for Every Product in the Catalog

`catalog/products.json` holds fifty SKUs. Each area has a physical `pageSize` (inches in this demo) and an `artworkLocation` in mockup image pixels. Products supply campaign copy. They do not supply brand artwork. Three examples from the catalog:

Apparel (`tshirt-01`, front; the product also defines a back area):

```json
// catalog/products.json (tshirt-01 / front)
{
  "id": "front",
  "label": "Front",
  "pageSize": { "width": 12, "height": 12 },
  "artworkLocation": {
    "x": 227,
    "y": 194,
    "width": 360,
    "height": 360,
    "unit": "px",
    "coordinateSpace": "mockup"
  }
}
```

Drinkware (`mug-01`, wrap):

```json
// catalog/products.json (mug-01 / wrap)
{
  "id": "wrap",
  "label": "Wrap",
  "pageSize": { "width": 3.5, "height": 4.5 },
  "artworkLocation": {
    "x": 240,
    "y": 180,
    "width": 300,
    "height": 386,
    "unit": "px",
    "coordinateSpace": "mockup"
  }
}
```

Flat print (`poster-01`, face):

```json
// catalog/products.json (poster-01 / face)
{
  "id": "face",
  "label": "Face",
  "pageSize": { "width": 18, "height": 24 },
  "artworkLocation": {
    "x": 45,
    "y": 60,
    "width": 810,
    "height": 1080,
    "unit": "px",
    "coordinateSpace": "mockup"
  }
}
```

Campaign fields on the same products look like this (tee example):

```json
// catalog/products.json (tshirt-01 campaign fields)
{
  "headline": "Classic Tee 1 — made for your brand",
  "body": "Soft hand-feel, durable print, ready for everyday wear.",
  "cta": "Shop the fit"
}
```

Measure `artworkLocation` against your mockup PNG dimensions so the plate lands on the garment, not in the margin.

## Step 4: Resolve Master, Customer and Product Into One Scene

`resolveScene` in `src/resolve.ts` loads the master named by `extends`, applies master-only compliance, applies the customer brand (including artwork), re-applies compliance so a stray customer key cannot win, applies product copy, then lays out print or mockup mode.

```typescript
// src/resolve.ts
export async function resolveScene(
  engine: CreativeEngine,
  options: ResolveOptions
): Promise<ResolvedScene> {
  const customer = resolveCustomerArg(options.customer);
  const product = resolveProduct(options.product);
  const area = resolveArea(product, options.areaId);
  const color = resolveColor(product, options.colorId);
  const resolveAssetPath = options.resolveAssetPath ?? defaultResolveAssetPath;
  const mode: ResolveMode = options.mode ?? 'mockup';
  const dpi = options.dpi ?? DEFAULT_DPI;

  const master = resolveMasterTemplate(customer);
  await engine.scene.loadFromString(master.sceneString!);
  applyDocumentTypeface(engine, {
    spec: master.typeface ?? DEFAULT_TYPEFACE,
    resolveAssetPath,
  });
  applyMasterOnlyFields(engine, master);

  applyCustomerOverride(engine, customer, {
    resolveAssetPath,
    allowedVariables: master.overrideVariables,
  });
  applyMasterOnlyFields(engine, master);

  applyProductLayer(engine, product);

  const pixelSize =
    mode === 'print'
      ? (() => {
          const size = printPixelSize(product, area, dpi);
          applyPrintLayout(engine, master, size, dpi);
          return size;
        })()
      : applyMockupComposition(engine, area, color, { resolveAssetPath });

  const sceneString = await engine.scene.saveToString();
  return {
    sceneString,
    customer,
    product,
    area,
    color,
    artworkLocation: area.artworkLocation,
    mode,
    pixelSize,
    ...(mode === 'print' ? { dpi } : {}),
  };
}
```

Inheritance chain as text:

```text
master (locks + compliance)
  -> customer (logo, palette, contact, legal, printed artwork)
    -> product (headline/body/cta, pageSize, artworkLocation)
      -> export print @ pageSize x DPI
      -> export mockup @ mockup photo + caption band
```

## Step 5: Batch Render Mockups and Print Files for the Whole Catalog

The CLI entry is `scripts/render-catalog.ts`. It calls the same core path as the Vite `POST /api/render-catalog` handler.

```bash
npm run render:catalog -- --customer bean-there-bean-good
```

Testing runs marked **70,097 ms**, **69,188 ms**, and **69,025 ms** (median **69,188 ms**) at 150 DPI, each writing **50 products**, **65 print areas**, and **130 PNGs** (`print.png` + `mockup.png` per area).

Nonetheless, for day-to-day checks in which you don't need to run the full catalog, the CLI accepts `--limit <n>` (first _n_ products only) and `--areas first|all` (one print area per product, or every area). Likewise, the demo UI accepts the same limit as a query string, for example `/?limit=2`. A smoke run with that URL reported **2 products · 4 areas · 8 images · 5.2 s** in the results' header. Publish throughput numbers only from full runs you actually timed; use the limit flags to iterate on master or customer changes without waiting for all 130 PNGs.

```bash
npm run render:catalog -- --customer bean-there-bean-good --limit 3 --areas first
```

Output layout:

```text
output/catalog/<customer-id>/<product-id>/<area-id>/
  print.png
  mockup.png
  resolved.json
output/catalog/<customer-id>/manifest.json
```

Print plates use physical size times DPI (for example a 12×12 in front at 150 DPI exports at 1800×1800). Mockups place that artwork inside `artworkLocation` on the product photo and keep brand chrome in a caption band under the photo.

![Print plate for Classic Tee 1 front](https://img.ly/_astro/print-plate-tshirt-front.ro15UoxU.webp)

![Mockup with brand caption band](https://img.ly/_astro/mockup-tshirt-front.BNgqMP9r.webp)

## Step 6: Change the Master and Re-Render

Edit `masterOnly.complianceText` in `templates/master.json` (or change it in `scripts/build-master.ts` and regenerate). Customer logos and artwork will stay put. Compliance is applied from the master after the customer overlay:

```typescript
// src/resolve.ts
export function applyMasterOnlyFields(
  engine: CreativeEngine,
  master: MasterTemplateDoc
): void {
  const complianceText =
    master.masterOnly?.complianceText ??
    'Master compliance: Template terms apply. Not for unauthorized redistribution.';

  engine.variable.setString('masterCompliance', complianceText);
  engine.block.replaceText(
    requireBlock(engine, 'MasterCompliance'),
    complianceText
  );
}
```

Before (default master line on the mockup caption):

![Mockup before compliance edit](https://img.ly/_astro/mockup-tshirt-front.BNgqMP9r.webp)

After changing the master line to the 2026 franchise wording at the bottom and re-rendering one product:

![Mockup after compliance edit](https://img.ly/_astro/mockup-after-compliance-edit.GSwxcIFf.webp)

## How to Verify: 50 Mockups From One Logo Upload

1. Set `CESDK_LICENSE` or `VITE_CESDK_LICENSE` in `.env` (never commit the key).
2. Run `npm run dev` and open the demo in the browser.
3. Select a customer and click **Render catalog**.
4. Confirm the results header counts products, areas, images, and elapsed time. For a short check use `/?limit=2` (this demo reported **2 products · 4 areas · 8 images · 5.2s**). For a publishable full-catalog number, use the timed runs in Step 5.
5. Open `output/catalog/<customer-id>/` and confirm 50 product folders and two PNGs per area after a full run.
6. Spot-check `print.png` against `pageSize × DPI` (Classic Tee front: 1800×1800 at 150 DPI).
7. Repeat the Step 6 compliance edit and confirm mockup captions update without editing customer files.

## When Copying Templates Is Faster Than Inheritance

A single dealer needs one tee for a trade show next week, and they will never touch the rest of your catalog. Duplicate the scene, drop their logo, ship the plate. Wiring a master, overrides, and a resolver for that job costs more than the print.

Your output is web banners only, and the team already owns a Handlebars or similar HTML pipeline that feeds headless Chromium. Keep it. A scene master and print geometry buy you nothing if nothing hits a press.

Two customers need different block trees, not different fills: one wants a full-bleed photo with a footer bar, the other wants a split layout and a QR block the first design never had. A shared master will fight every render. Give them separate masters, or keep copying until the layouts actually converge.

## FAQ: Template Inheritance and Batch Rendering

### What happens to a customer's edits when the master changes?

Brand fields in the customer JSON stay. Layout, locks, and `masterOnly.complianceText` come from the master on the next resolve. Re-render to refresh outputs. Customer artwork does not reset unless you change the customer file.

### Can a customer override more than logo and colors?

Yes: logo, palette, contact, legal, and printed `artwork`. No: `headline`, `body`, `cta`, and compliance fields. `src/customer-override.ts` throws if those forbidden keys appear under `variables`.

### How do I add a product to the catalog?

Append an object to `catalog/products.json` with `pageSize`, `artworkLocation`, mockup image paths, and campaign copy. Re-run the batch. Keep brand artwork on the customer file.

### Can the customer preview before batch rendering?

The Vite demo renders the same headless path as the CLI and shows mockup and print tiles. For an in-editor preview, load the resolved scene in CE.SDK UI with adopter scopes; this reference app focuses on batch output.

### How long does a 300-product render take?

This demo measures **50 products / 65 areas / 130 images in about 69.2 s** at 150 DPI (median of three consecutive runs on the machine used for this article). A linear estimate for 300 products at the same area density is roughly six times longer. Treat that as an estimate, not a benchmark. Measure on your hardware before you publish a number.

### Can I run this on a schedule or on an event?

Yes. Call `npm run render:catalog -- --customer <id>` from cron, CI, or a worker that receives a webhook when a logo is uploaded. Pass a customer object into `resolveScene` when the brand is created at runtime and is not yet a file on disk.

## Glossary: Template Terms

**Master template.** The locked base scene and metadata that every customer extends. Built in `scripts/build-master.ts`, stored as `templates/master.json`.

**Override.** A customer JSON that supplies brand variables and artwork while pointing at a master through `extends`.

**Artwork location.** The rectangle on the mockup photo where the print sits, in mockup pixels (`artworkLocation`).

**Page size.** The physical print size for a plate (`pageSize` in the product's `designUnit`, inches here).

**Placeholder.** A block adopters may replace or crop. In this master, `HeroImage` holds the brand artwork that prints.

**Variable.** A named string in the scene (`{{brandName}}`, `{{headline}}`, and so on) filled at resolve time.

**Creator role.** The role that configures locks, placeholders, and layout on the master.

**Adopter role.** The role that may edit allowed text and the artwork placeholder without touching locked brand chrome.

**Batch render.** Headless export of many scenes in one job (`scripts/render-catalog.ts` via `@cesdk/node`).

## Next Steps

This repo proves the inheritance merge and a batch path to mockups plus DPI-sized print PNGs. It is not a production merchandise or direct mail stack yet. Brands still enter as JSON under `templates/customers/` (or an ad-hoc object you build in code), not through a customer-facing artwork intake with resolution or safe-area checks. Plates are raster PNGs at `pageSize × DPI`; the demo does not run bleed, CMYK, or PDF/X preflight. The UI is an internal catalog picker, not a storefront upload flow your non-technical customers would use.

Take the pattern into your product when those gaps are on your roadmap. Start from the [cesdk-template-inheritance repository](https://github.com/imgly/cesdk-template-inheritance) or the [live demo](https://labs.imgly.dev/p/2aqtwedr_template-inheritance), the [CE.SDK templates overview](https://img.ly/docs/cesdk/js/create-templates/overview-4ebe30.md), and a trial key via the [CE.SDK licensing docs](https://img.ly/docs/cesdk/js/licensing-8aa063.md) and [IMG.LY pricing](https://img.ly/pricing.md).

---

## More Resources

- **[IMG.LY Website](https://img.ly/index.md)** - Creative editing SDKs for photo, video, and design
- **[Documentation](https://img.ly/docs/cesdk/)** - CE.SDK developer documentation
- **[Contact Sales](https://img.ly/forms/contact-sales.md)** - Get a custom quote. A public JSON API accepts the request directly, no account or key needed. Ask your user for consent and their details first.
