Create and customize font combinations (text components) programmatically in a headless server environment for batch processing and automation.
Font combinations (also known as text components) are pre-designed text layouts stored as serialized blocks in the asset library. This guide shows how to create custom text components using the headless engine for server-side generation and batch processing.
This guide covers creating styled text components, serializing them for storage, generating thumbnails, and saving files for deployment.
Setting Up the Engine#
We initialize the headless engine for server-side text component generation. The engine runs without a UI, making it ideal for batch processing.
const engine = await CreativeEngine.init({ // license: process.env.CESDK_LICENSE,});Always dispose of the engine when finished to free system resources.
Creating a Scene#
We create a scene with a page to hold our text components. The page dimensions define the canvas where we build components.
// Create a scene with a page to hold our text componentsconst scene = engine.scene.create();const page = engine.block.create('page');engine.block.appendChild(scene, page);engine.block.setWidth(page, 800);engine.block.setHeight(page, 600);The page provides a container for positioning and styling text components.
Creating a Text Component#
We create a text block and configure its styling, dimensions, and position. The component uses fixed frame dimensions with clipping enabled to ensure consistent display. We set an explicit font using external CDN URLs with the setFont() API.
// Create a styled text block that will become our custom componentconst textComponent = engine.block.create('text');engine.block.appendChild(page, textComponent);
// Set text content and stylingengine.block.replaceText(textComponent, 'Custom Title');engine.block.setFloat(textComponent, 'text/fontSize', 72);
// Set text color to a brand colorengine.block.setTextColor(textComponent, { r: 0.2, g: 0.4, b: 0.8, a: 1.0});
// Configure dimensions - use fixed frame with clippingengine.block.setWidthMode(textComponent, 'Absolute');engine.block.setHeightMode(textComponent, 'Absolute');engine.block.setWidth(textComponent, 400);engine.block.setHeight(textComponent, 100);engine.block.setBool(textComponent, 'clipped', true);
// Position the component on the pageengine.block.setPositionX(textComponent, 50);engine.block.setPositionY(textComponent, 50);
// Define a custom typeface// With saveToArchive(), fonts are automatically bundled in the archive// You can use any font - CDN URLs, bundle:// URIs, or custom fontsconst caveatTypeface = { name: 'Caveat', fonts: [ { uri: 'https://cdn.img.ly/assets/v3/ly.img.typeface/fonts/Caveat/Caveat-Regular.ttf', subFamily: 'Regular' }, { uri: 'https://cdn.img.ly/assets/v3/ly.img.typeface/fonts/Caveat/Caveat-Bold.ttf', subFamily: 'Bold' } ]};
// Set the font - saveToArchive() will include the font files in the archiveengine.block.setFont( textComponent, caveatTypeface.fonts[0].uri, caveatTypeface);The text block now has custom styling including font size, color, and fixed dimensions. The clipped property ensures text that exceeds the frame boundaries is hidden. With saveToArchive(), fonts are automatically bundled in the archive, so you can use any font source.
Configuring Constraints#
We configure the text component with automatic font sizing and constraints. This ensures the component adapts gracefully when users resize it while maintaining readability.
// Configure constraints for flexible resizing// Enable automatic font sizing within constraintsengine.block.setBool(textComponent, 'text/automaticFontSizeEnabled', true);engine.block.setFloat(textComponent, 'text/minAutomaticFontSize', 24);engine.block.setFloat(textComponent, 'text/maxAutomaticFontSize', 120);The automatic font sizing scales text between 24pt and 120pt as the frame is resized.
Serializing the Component#
We serialize the text component using saveToArchive(). This creates a zip archive containing the blocks.blocks file and all referenced resources (fonts, images). The archive is self-contained and portable.
// Save the component to a blocks archive file// saveToArchive() returns a Blob containing a zip with blocks.blocks and resourcesconst archiveBlob = await engine.block.saveToArchive([textComponent]);console.log('Archive size:', archiveBlob.size, 'bytes');The archive blob can be saved as a .zip file. Extract the archive before deployment to create the directory structure needed for hosting.
Legacy: Using saveToString()#
For backward compatibility, you can use saveToString() instead of saveToArchive(). This approach serializes the block as a string but requires external font URLs to remain accessible.
// Legacy approach - requires external font URLsconst serializedComponent = await engine.block.saveToString( [textComponent], ['bundle', 'buffer', 'http', 'https'] // Allowed resource schemes);
// Save as .blocks filewriteFileSync(`${outputDir}/customTitle.blocks`, serializedComponent);
// Load with loadFromString() or loadFromURL()const loadedBlocks = await engine.block.loadFromString(serializedComponent);Limitations of saveToString():
- Font URLs must remain accessible at load time
- No automatic resource bundling
- Requires
allowedResourceSchemesconfiguration
We recommend saveToArchive() for new implementations as it bundles all resources automatically.
Generating a Thumbnail#
We generate a 400x320px thumbnail for the asset library using block.export(). Thumbnails help users preview components before inserting them.
// Generate a thumbnail for the text component// Thumbnails should be 400x320px for the asset libraryconst thumbnailBlob = await engine.block.export(textComponent, { mimeType: 'image/png', targetWidth: 400, targetHeight: 320});console.log('Thumbnail size:', thumbnailBlob.size, 'bytes');The thumbnail blob can be converted to a buffer and saved as a PNG file.
Creating the Content.json Structure#
We create the content.json structure that defines the asset source. This file lists all components with their metadata, including paths to the serialized blocks and thumbnails.
// Create the content.json structure for the custom components// The uri points to the blocks.blocks file within the extracted archive directoryconst contentJson = { version: '3.0.0', id: 'my.custom.textComponents', assets: [ { id: '//my.custom.textComponents/customTitle', label: { en: 'Custom Title' }, meta: { // URI points to blocks.blocks within the extracted archive directory // The {{base_url}} placeholder is replaced with your configured base URL uri: '{{base_url}}/my.custom.textComponents/data/customTitle/blocks.blocks', thumbUri: '{{base_url}}/my.custom.textComponents/thumbnails/customTitle.png', mimeType: 'application/ubq-blocks-string' } }, { id: '//my.custom.textComponents/promo', label: { en: 'Promo' }, meta: { uri: '{{base_url}}/my.custom.textComponents/data/promo/blocks.blocks', thumbUri: '{{base_url}}/my.custom.textComponents/thumbnails/promo.png', mimeType: 'application/ubq-blocks-string' } } ], blocks: []};console.log('Content.json structure:');console.log(JSON.stringify(contentJson, null, 2));In production, the uri and thumbUri fields point to your hosted files. The mimeType must be "application/ubq-blocks-string" for text components.
Saving Files#
We save the archive, thumbnail, and content.json to the filesystem. The archive is saved as a .zip file that must be extracted before deployment.
// Save the archive, thumbnail, and content.json to files// Create output directory structureconst outputDir = './output';const dataDir = `${outputDir}/data`;const thumbnailsDir = `${outputDir}/thumbnails`;
if (!existsSync(outputDir)) { mkdirSync(outputDir, { recursive: true });}if (!existsSync(dataDir)) { mkdirSync(dataDir, { recursive: true });}if (!existsSync(thumbnailsDir)) { mkdirSync(thumbnailsDir, { recursive: true });}
// Save the archive as a zip file// Extract this archive to get the blocks.blocks file and any resources (fonts, images)// After extraction, the directory structure should be:// data/customTitle/blocks.blocks// data/customTitle/fonts/... (if any custom fonts)// data/customTitle/images/... (if any images)const archivePath = `${dataDir}/customTitle.zip`;const archiveBuffer = Buffer.from(await archiveBlob.arrayBuffer());writeFileSync(archivePath, archiveBuffer);console.log(`Saved archive to: ${archivePath}`);console.log('Extract this archive to create the data/customTitle/ directory');
// Save the thumbnailconst thumbnailPath = `${thumbnailsDir}/customTitle.png`;const thumbnailBuffer = Buffer.from(await thumbnailBlob.arrayBuffer());writeFileSync(thumbnailPath, thumbnailBuffer);console.log(`Saved thumbnail to: ${thumbnailPath}`);
// Save the content.jsonconst contentJsonPath = `${outputDir}/content.json`;writeFileSync(contentJsonPath, JSON.stringify(contentJson, null, 2));console.log(`Saved content.json to: ${contentJsonPath}`);The output directory contains all files needed to host your custom text components. Extract each .zip archive to create the expected directory structure with blocks.blocks and any bundled resources.
Registering the Asset Source#
In production, host the extracted files and register the asset source from a URL using addLocalAssetSourceFromJSON(). The SDK will use loadFromURL() to load components from the URIs specified in content.json.
// In production, host the files and register the asset source from a URL://// await engine.asset.addLocalAssetSourceFromJSON(// new URL('https://your-server.com/my.custom.textComponents/content.json')// );//// The SDK will then use block.loadFromURL() to load components from// the URIs specified in content.json (e.g., data/customTitle/blocks.blocks)console.log('\nTo use these components in production:');console.log('1. Extract the archives:');console.log(' unzip data/customTitle.zip -d data/customTitle/');console.log(' unzip data/promo.zip -d data/promo/');console.log('2. Host the output directory on your server');console.log( '3. Register with: engine.asset.addLocalAssetSourceFromJSON(new URL(contentJsonUrl))');The console output shows the steps needed for production deployment: extract archives, host the files, and register the asset source with the hosted content.json URL.
Creating Additional Components#
We create a second component with different styling to demonstrate building a library of text components.
const promoComponent = engine.block.create('text');engine.block.appendChild(page, promoComponent);
engine.block.replaceText(promoComponent, 'SALE');engine.block.setFloat(promoComponent, 'text/fontSize', 96);
// Use a bold red color for the promo textengine.block.setTextColor(promoComponent, { r: 0.9, g: 0.2, b: 0.2, a: 1.0});
// Set a bold font for the promo componentconst robotoTypeface = { name: 'Roboto', fonts: [ { uri: 'https://cdn.img.ly/assets/v3/ly.img.typeface/fonts/Roboto/Roboto-Bold.ttf', subFamily: 'Bold' } ]};engine.block.setFont( promoComponent, robotoTypeface.fonts[0].uri, robotoTypeface);
engine.block.setWidthMode(promoComponent, 'Absolute');engine.block.setHeightMode(promoComponent, 'Absolute');engine.block.setWidth(promoComponent, 300);engine.block.setHeight(promoComponent, 120);engine.block.setBool(promoComponent, 'clipped', true);
engine.block.setPositionX(promoComponent, 50);engine.block.setPositionY(promoComponent, 200);
// Save the promo component as an archiveconst promoArchiveBlob = await engine.block.saveToArchive([promoComponent]);const promoArchiveBuffer = Buffer.from(await promoArchiveBlob.arrayBuffer());writeFileSync(`${dataDir}/promo.zip`, promoArchiveBuffer);console.log(`Saved promo archive to: ${dataDir}/promo.zip`);
// Export promo thumbnailconst promoThumbnail = await engine.block.export(promoComponent, { mimeType: 'image/png', targetWidth: 400, targetHeight: 320});const promoThumbnailBuffer = Buffer.from(await promoThumbnail.arrayBuffer());writeFileSync(`${thumbnailsDir}/promo.png`, promoThumbnailBuffer);console.log(`Saved promo thumbnail to: ${thumbnailsDir}/promo.png`);Each component can have unique styling, dimensions, and behavior. Build a library by creating multiple components and adding them to your content.json.
Cleaning Up#
We dispose of the engine to free system resources. Always clean up the engine when finished with processing.
// Always dispose of the engine to free resourcesengine.dispose();Troubleshooting#
Archive creation fails: Ensure the block has valid dimensions and is part of the scene hierarchy before calling saveToArchive().
Engine initialization fails: Verify Node.js version compatibility and ensure @cesdk/node is properly installed.
Thumbnail export fails: Ensure the block has valid dimensions and is part of the scene hierarchy.
File write errors: Verify the output directory exists or use mkdirSync with recursive: true.
Resources missing after extraction: Ensure you extract the complete archive including all subdirectories (fonts/, images/).
Memory issues with batch processing: Dispose of the engine between batches or use a single engine instance with scene cleanup.
API Reference#
| Method | Purpose |
|---|---|
CreativeEngine.init() | Initialize the headless engine |
engine.scene.create() | Create a new scene with options |
engine.block.create() | Create a new block of specified type |
engine.block.replaceText() | Set text content on a text block |
engine.block.setFloat() | Set numeric properties like font size |
engine.block.setFont() | Set font with typeface definition and URI |
engine.block.setTextColor() | Set text color with RGBA values |
engine.block.setWidthMode() | Set width mode: ‘Absolute’, ‘Percent’, ‘Auto’ |
engine.block.setHeightMode() | Set height mode: ‘Absolute’, ‘Percent’, ‘Auto’ |
engine.block.setBool() | Enable/disable boolean properties |
engine.block.setColor() | Set color properties like background |
engine.block.saveToArchive() | Save blocks to zip archive with all resources |
engine.block.saveToString() | Legacy: Save blocks as string (requires external resources) |
engine.block.loadFromString() | Legacy: Load blocks from string |
engine.block.export() | Export block as image blob |
engine.asset.addLocalAssetSourceFromJSON() | Register custom asset source from URL |
engine.dispose() | Clean up engine resources |