[data-reveal]{opacity:1!important;transform:none!important}
CMS Interfaces

CMS HTML Render Documentation

Overview

The CMS HTML Render interface turns a named template plus a props object into an HTML string. You declare a template once with RegisterHtmlTemplate, which records the name in a process-wide registry and hands back a typed reference; passing that reference to GenerateHtml produces the rendered HTML.

Rendering itself does not happen in the backend process. GenerateHtml performs an HTTP POST to the render endpoint configured on the DMS (htmlRender.renderEndpoint), authenticated with a signed service token, and returns the HTML the endpoint sends back. The backend therefore owns the template names and the props types; the markup lives with the renderer behind that endpoint.

The interface produces HTML and nothing else. Delivering that HTML — as a transactional email, as a file, as a response body — is your code's job.

Everything below is exported from a single subpath:

import {
  GenerateHtml,
  GetAllHtmlTemplates,
  GetHtmlTemplate,
  RegisterHtmlTemplate,
} from "@antelopejs-private/cms/interfaces/cms-html-render";
import type {
  HtmlRenderConfig,
  HtmlTemplateInfo,
  HtmlTemplateRef,
} from "@antelopejs-private/cms/interfaces/cms-html-render";

Key Features

  • Named template registryRegisterHtmlTemplate records a template name at import time, so the whole process shares one catalogue of renderable templates.
  • Documented props — the type parameter on RegisterHtmlTemplate travels with the returned HtmlTemplateRef, recording on the reference itself which props the template expects.
  • Remote rendering over HTTPGenerateHtml calls the configured render endpoint with a signed service token and resolves to the returned HTML string.
  • Registry inspectionGetHtmlTemplate and GetAllHtmlTemplates read back what has been registered, one name at a time or in bulk.
  • Configurable endpoint and tokenHtmlRenderConfig types the htmlRender block of the DMS module configuration: endpoint URL, signing secret, token lifetime.

Dependencies

This interface relies on the following AntelopeJS packages:

Quick Start

Register a template next to the code that uses it, then render it with props.

import {
  GenerateHtml,
  RegisterHtmlTemplate,
} from "@antelopejs-private/cms/interfaces/cms-html-render";

interface WelcomeProps {
  userName: string;
  activationLink: string;
}

// Registration happens at import time; keep the ref in module scope.
const WelcomeTemplate = RegisterHtmlTemplate<WelcomeProps>("EmailWelcome");

export async function buildWelcomeHtml(
  userName: string,
  activationLink: string,
): Promise<string> {
  return GenerateHtml(WelcomeTemplate, { userName, activationLink });
}

GenerateHtml needs the render endpoint to be reachable, so it belongs in request or job code — do not call it at import time.

RegisterHtmlTemplate

Registers a template name and returns the reference you render with.

function RegisterHtmlTemplate<TProps = unknown>(
  name: string,
): HtmlTemplateRef<TProps>;
ParameterDescription
nameThe template identifier. It is the key the registry stores the template under, and the templateName sent to the render endpoint.

The type parameter TProps declares the props the template expects. It is carried by the returned HtmlTemplateRef as a record of the template's shape; GenerateHtml takes its props argument as unknown, so the compiler does not check the two against each other at the call site. Omitting TProps leaves the reference's props type as unknown.

import { RegisterHtmlTemplate } from "@antelopejs-private/cms/interfaces/cms-html-render";

interface InvoiceProps {
  invoiceNumber: string;
  amount: string;
  dueDate: string;
}

export const InvoiceTemplate =
  RegisterHtmlTemplate<InvoiceProps>("EmailInvoice");

The registry is keyed by name, so registering an existing name replaces the earlier entry. The DMS registers EmailResetPassword, EmailUserValidation, EmailAdminInvite and EmailTwoFactor for its own authentication emails — pick different names for your templates.

Call RegisterHtmlTemplate at module scope, in a file your module imports during startup. The returned reference is a plain object, so exporting it from that module is the usual way to share it.

The subpath also exports an internal namespace holding the registering proxy this function writes through. It is implementation detail — register through RegisterHtmlTemplate.

GenerateHtml

Renders a registered template and resolves to the resulting HTML.

const GenerateHtml: (
  template: HtmlTemplateRef<unknown>,
  props: unknown,
) => Promise<string>;
ParameterDescription
templateThe reference returned by RegisterHtmlTemplate. Pass the reference, not the name string.
propsThe data the template renders with, serialised into the request body. The render endpoint expects a JSON object and rejects anything else with a 400, so keep the props type an object shape. Typed unknown at this boundary, so the props type recorded on the reference is not enforced here — match it yourself.
import { GenerateHtml } from "@antelopejs-private/cms/interfaces/cms-html-render";
import { InvoiceTemplate } from "./templates";

export async function renderInvoice(invoiceNumber: string): Promise<string> {
  return GenerateHtml(InvoiceTemplate, {
    invoiceNumber,
    amount: "240.00 EUR",
    dueDate: "2026-08-15",
  });
}

The call POSTs { templateName, props } as JSON to htmlRender.renderEndpoint, with a service token signed from htmlRender.serviceSecret in the x-cms-service-token header, and reads the html field out of the JSON response.

GenerateHtml does not consult the registry before calling out: it sends whatever name the reference carries. A name the render endpoint does not know surfaces as a rejected promise, not as a missing-template check on the backend.

On a non-2xx response the promise rejects with an Error whose message starts with Failed to render HTML template:, followed by the response body's error string, its statusMessage, or HTTP <status>. When the endpoint is unreachable the underlying fetch failure propagates instead, without that prefix. Wrap the call when a failed render must not abort the surrounding operation:

import { Logging } from "@antelopejs/interface-core/logging";
import { GenerateHtml } from "@antelopejs-private/cms/interfaces/cms-html-render";
import { InvoiceTemplate } from "./templates";

export async function renderInvoiceOrNull(
  invoiceNumber: string,
): Promise<string | null> {
  try {
    return await GenerateHtml(InvoiceTemplate, {
      invoiceNumber,
      amount: "240.00 EUR",
      dueDate: "2026-08-15",
    });
  } catch (error) {
    Logging.Error(`Invoice render failed: ${(error as Error).message}`);
    return null;
  }
}

GetHtmlTemplate

Looks a single template up in the registry.

const GetHtmlTemplate: (key: string) => Promise<HtmlTemplateInfo | undefined>;
ParameterDescription
keyThe name the template was registered under.

Resolves to the HtmlTemplateInfo stored for that name, or to undefined when nothing is registered under it.

import { GetHtmlTemplate } from "@antelopejs-private/cms/interfaces/cms-html-render";

export async function isTemplateRegistered(name: string): Promise<boolean> {
  return (await GetHtmlTemplate(name)) !== undefined;
}

GetAllHtmlTemplates

Resolves to every registered template, keyed by name.

const GetAllHtmlTemplates: () => Promise<Record<string, HtmlTemplateInfo>>;
import { GetAllHtmlTemplates } from "@antelopejs-private/cms/interfaces/cms-html-render";

export async function listTemplateNames(): Promise<string[]> {
  return Object.keys(await GetAllHtmlTemplates());
}

The result is a fresh object built from the registry, so mutating it does not change what is registered. Because registration happens as modules are imported, call this after startup rather than at import time if you want the complete set.

HtmlTemplateInfo

What the registry stores for each template, and what GetHtmlTemplate and GetAllHtmlTemplates resolve to.

FieldTypeDescription
namestringThe registered template name.
import type { HtmlTemplateInfo } from "@antelopejs-private/cms/interfaces/cms-html-render";
import { GetAllHtmlTemplates } from "@antelopejs-private/cms/interfaces/cms-html-render";

export async function describeTemplates(): Promise<string> {
  const templates: Record<string, HtmlTemplateInfo> =
    await GetAllHtmlTemplates();

  return Object.values(templates)
    .map((info) => info.name)
    .join(", ");
}

HtmlTemplateRef

The handle RegisterHtmlTemplate returns and GenerateHtml accepts.

FieldTypeDescription
namestringThe template name sent to the render endpoint.
_propsTypeTProps | undefinedType-only carrier for the props type. It is never assigned a value; do not read it.

Annotate the reference explicitly when you collect handles in a shared module and want the props type visible in that module's declarations:

import type { HtmlTemplateRef } from "@antelopejs-private/cms/interfaces/cms-html-render";
import { RegisterHtmlTemplate } from "@antelopejs-private/cms/interfaces/cms-html-render";

interface AlertProps {
  title: string;
  body: string;
}

export const AlertTemplate: HtmlTemplateRef<AlertProps> =
  RegisterHtmlTemplate<AlertProps>("EmailAlert");

HtmlRenderConfig

Types the htmlRender block of the DMS module configuration in antelope.config.ts.

OptionTypeDescription
renderEndpointstringAbsolute URL GenerateHtml posts to. Default http://localhost:3001/api/html/render.
serviceSecretstringSecret used to sign the service token that authenticates the render call. Default "dev" — set a strong value in production.
serviceTokenLifetimenumberLifetime of that service token, passed to the token signer as expiresIn, which reads a plain number as a count of seconds. Default 300000.

The DMS merges the block you supply over those defaults, so a configuration only needs the keys it changes:

import { defineConfig } from "@antelopejs/interface-core/config";

const renderSecret = process.env.CMS_HTML_RENDER_SECRET;
if (!renderSecret) {
  throw new Error("CMS_HTML_RENDER_SECRET is not set");
}

export default defineConfig({
  name: "acme-admin",
  modules: {
    cms: {
      source: {
        type: "package",
        package: "@antelopejs-private/cms",
        version: "^0.2.5",
      },
      config: {
        htmlRender: {
          renderEndpoint: "https://admin.example.com/api/html/render",
          serviceSecret: renderSecret,
        },
      },
    },
  },
});

The Configuration chapter of the DMS guide covers where this block sits alongside the other DMS options.

Sending Rendered HTML as Email

Rendering and sending are separate steps. Render with GenerateHtml, then hand the string to the standard AntelopeJS email interface, which needs an implementing module (such as @antelopejs/nodemailer) loaded in your project configuration.

import { Send } from "@antelopejs/interface-email";
import {
  GenerateHtml,
  RegisterHtmlTemplate,
} from "@antelopejs-private/cms/interfaces/cms-html-render";

interface ReceiptProps {
  userName: string;
  orderId: string;
}

const ReceiptTemplate = RegisterHtmlTemplate<ReceiptProps>("EmailReceipt");

export async function sendReceipt(
  email: string,
  userName: string,
  orderId: string,
): Promise<void> {
  const html = await GenerateHtml(ReceiptTemplate, { userName, orderId });

  const result = await Send({
    to: email,
    subject: `Your receipt for order ${orderId}`,
    html,
  });

  if (!result.success) {
    throw new Error(`Failed to send receipt: ${result.error?.message}`);
  }
}

This is the shape the DMS uses for its own account emails, and it keeps the two failure modes apart: a rejected GenerateHtml means the HTML was never produced, while a success: false response from Send means it was produced but not delivered.