Skip to content

Programmatic Templates

Most users will design templates visually in the drag-and-drop editor. But sometimes you need to create templates in code -- seeding default content, building templates from API data, or generating emails programmatically on the server.

Templatical provides factory functions for every block type. Use them to build templates in code, then pass them to init() or render them directly with the renderer.

Blank template

ts
import { createDefaultTemplateContent } from '@templatical/types';

const content = createDefaultTemplateContent();
// { blocks: [], settings: { width: 600, backgroundColor: '#ffffff', fontFamily: 'Arial' } }

createDefaultTemplateContent() accepts an optional font family string:

ts
const content = createDefaultTemplateContent('Georgia, serif');

Building a template

Every block type has a corresponding create*Block() function. Each accepts an optional partial object to override defaults. All factory functions auto-generate a unique id for each block.

ts
import {
  createDefaultTemplateContent,
  createTitleBlock,
  createParagraphBlock,
  createImageBlock,
  createButtonBlock,
  createDividerBlock,
} from '@templatical/types';

const content = createDefaultTemplateContent();

content.blocks = [
  createTitleBlock({
    content: '<h1 style="text-align: center;">Welcome aboard</h1>',
    level: 1,
  }),
  createImageBlock({
    src: 'https://example.com/hero.jpg',
    alt: 'Welcome hero image',
    width: 'full',
  }),
  createDividerBlock(),
  createParagraphBlock({
    content: '<p>Thanks for signing up. Here is what happens next.</p>',
  }),
  createButtonBlock({
    text: 'Get Started',
    url: 'https://example.com/dashboard',
    backgroundColor: '#1a73e8',
    textColor: '#ffffff',
    borderRadius: 6,
  }),
];

Block factory reference

Title

ts
createTitleBlock({
  content: '<h1>Welcome, {{name}}!</h1>',
  level: 1,
  textAlign: 'center',
})

Paragraph

ts
createParagraphBlock({
  content: '<p>Thanks for signing up. Here is what happens next.</p>',
})

Image

ts
createImageBlock({
  src: 'https://cdn.example.com/hero.png',
  alt: 'Hero banner',
  width: 560,
  linkUrl: 'https://example.com',
})

Button

ts
createButtonBlock({
  text: 'Get Started',
  url: 'https://example.com/signup',
  backgroundColor: '#6366f1',
  borderRadius: 8,
})

Divider

ts
createDividerBlock({
  lineStyle: 'dashed',
  color: '#e5e7eb',
  thickness: 2,
})

Spacer

ts
createSpacerBlock({ height: 40 })

HTML

ts
createHtmlBlock({
  content: '<div style="text-align:center;">Custom markup</div>',
})

Social Icons

ts
createSocialIconsBlock({
  iconStyle: 'circle',
  iconSize: 'large',
  icons: [
    { id: crypto.randomUUID(), platform: 'twitter', url: 'https://x.com/acme' },
    { id: crypto.randomUUID(), platform: 'github', url: 'https://github.com/acme' },
  ],
})
ts
createMenuBlock({
  items: [
    { id: crypto.randomUUID(), text: 'Home', url: 'https://example.com', openInNewTab: false, bold: false, underline: false },
    { id: crypto.randomUUID(), text: 'Blog', url: 'https://example.com/blog', openInNewTab: false, bold: false, underline: false },
    { id: crypto.randomUUID(), text: 'Docs', url: 'https://docs.example.com', openInNewTab: true, bold: false, underline: false },
  ],
  separator: '-',
})

Table

ts
createTableBlock({
  hasHeaderRow: true,
  rows: [
    { id: crypto.randomUUID(), cells: [{ id: crypto.randomUUID(), content: 'Plan' }, { id: crypto.randomUUID(), content: 'Price' }] },
    { id: crypto.randomUUID(), cells: [{ id: crypto.randomUUID(), content: 'Starter' }, { id: crypto.randomUUID(), content: '$9/mo' }] },
    { id: crypto.randomUUID(), cells: [{ id: crypto.randomUUID(), content: 'Pro' }, { id: crypto.randomUUID(), content: '$29/mo' }] },
  ],
})

Video

ts
createVideoBlock({
  url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
  thumbnailUrl: 'https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
  alt: 'Product demo video',
})

Section

ts
createSectionBlock({
  columns: '2',
  children: [
    [createParagraphBlock({ content: '<p>Left column</p>' })],
    [createImageBlock({ src: 'https://cdn.example.com/photo.jpg' })],
  ],
})

The columns property accepts: '1' (single), '2' (two equal), '3' (three equal), '2-1' (two-thirds / one-third), '1-2' (one-third / two-thirds). See Sections and Columns for full details.

Custom

createCustomBlock takes a CustomBlockDefinition (not a partial block). It generates field values from the definition's field defaults. See Custom Blocks for defining custom block types.

Utilities

Generic factory

Create any block by type string:

ts
import { createBlock } from '@templatical/types';

const block = createBlock('title'); // TitleBlock with defaults

Cloning

Deep-clone a block with a new ID:

ts
import { cloneBlock } from '@templatical/types';

const copy = cloneBlock(existingBlock);
// copy.id !== existingBlock.id

Type guards

Narrow a Block union to a specific type:

ts
import { isTitle, isParagraph, isImage, isButton, isSection } from '@templatical/types';

if (isTitle(block)) {
  console.log(block.level); // TypeScript knows this is TitleBlock
}

if (isParagraph(block)) {
  console.log(block.content); // TypeScript knows this is ParagraphBlock
}

if (isImage(block)) {
  console.log(block.src);
}

Every block type has a corresponding guard: isTitle(), isParagraph(), isImage(), isButton(), isDivider(), isSpacer(), isHtml(), isSocialIcons(), isMenu(), isTable(), isVideo(), isSection(), isCustomBlock().

Template settings

Template settings control the global properties of the email:

ts
const content = createDefaultTemplateContent();

content.settings.width = 640;
content.settings.backgroundColor = '#f5f5f5';
content.settings.fontFamily = 'Helvetica, Arial, sans-serif';
content.settings.preheaderText = 'Your weekly digest is here';
SettingTypeDescription
widthnumberEmail width in pixels
backgroundColorstringOuter background color
fontFamilystringDefault font stack
preheaderTextstringPreview text shown in inbox list

For default values and how to customize them, see Block & Template Defaults.

Loading saved content

Pass previously saved JSON back to the editor:

ts
const saved = await fetch('/api/templates/123').then(r => r.json());

const editor = await init({
  container: '#editor',
  content: saved,
});

You can also update content after initialization:

ts
editor.setContent(newContent);

Next steps