TypeScript-powered DatoCMS schema builder with fluent API for rapid content model development
Quickly create Models, Blocks, and content structures in DatoCMS from your TypeScript source code with concurrent builds, interactive generation, comprehensive validation, and dynamic field reference resolution.
- Installation
- Configuration
- CLI Commands
- Usage
- Comprehensive Field & Validator Reference
- Text Fields
- Numeric Fields
- Boolean Fields
- Date & Time Fields
- Media Fields
- Link Fields
- Structural Fields
- Aliases
- Validators Appendix
date_rangedate_time_rangeenumextensionfile_sizeformatslug_formatimage_dimensionsimage_aspect_ratioitem_item_typeitems_item_typelengthnumber_rangerequiredrequired_alt_titlerequired_seo_fieldstitle_lengthdescription_lengthrich_text_blockssingle_block_blockssanitized_htmlstructured_text_blocksstructured_text_inline_blocksstructured_text_links
- Type Compatibility Matrix
npm install --save-dev dato-builderCreate a dato-builder.config.js in your project root:
/** @type {import("dato-builder").DatoBuilderConfig} */
const config = {
apiToken: process.env.DATO_CMA_TOKEN,
overwriteExistingFields: false,
blockApiKeySuffix: "block",
environment: undefined, // Uses default environment
};
export default config;| Option | Type | Description |
|---|---|---|
apiToken |
string |
Required. Your DatoCMS Content Management API token. Find this in your DatoCMS project settings under "API tokens". |
| Option | Type | Default | Description |
|---|---|---|---|
overwriteExistingFields |
boolean |
false |
Controls field update behavior (see Field Update Behavior) |
modelApiKeySuffix |
string | null |
"model" |
Suffix for model API keys (e.g., "page" → "page_model") |
blockApiKeySuffix |
string | null |
"block" |
Suffix for block API keys (e.g., "hero" → "hero_block") |
modelsPath |
string |
"./datocms/models" |
Path where CLI searches for model definitions |
blocksPath |
string |
"./datocms/blocks" |
Path where CLI searches for block definitions |
logLevel |
LogLevel |
LogLevel.INFO |
Minimum logging level (see Logging Configuration) |
environment |
string |
undefined |
Target DatoCMS environment (see Environment Configuration) |
The overwriteExistingFields option controls how dato-builder handles existing fields in DatoCMS:
/** @type {import("dato-builder").DatoBuilderConfig} */
const config = {
overwriteExistingFields: false, // Safe: preserve manual changes
};
export default config;- ✅ New fields: Created as defined in code
- ✅ Removed fields: Deleted from DatoCMS
- 🔒 Existing fields: Left untouched (preserves manual dashboard changes)
- Use case: Development workflows where content editors make manual adjustments
/** @type {import("dato-builder").DatoBuilderConfig} */
const config = {
overwriteExistingFields: true, // Sync: code is source of truth
};
export default config;- ✅ New fields: Created as defined in code
- ✅ Removed fields: Deleted from DatoCMS
⚠️ Existing fields: Updated to match code (overwrites manual changes)- Use case: Production deployments where code definitions are authoritative
/** @type {import("dato-builder").DatoBuilderConfig} */
const config = {
apiToken: process.env.DATO_CMA_TOKEN,
overwriteExistingFields: false,
// ... other options
};
export default config;// dato-builder.config.ts
import type { DatoBuilderConfig } from "dato-builder";
const config: DatoBuilderConfig = {
apiToken: process.env.DATO_CMA_TOKEN!,
overwriteExistingFields: false,
environment: "production", // Target specific environment
// ... other options
};
export default config;Target specific DatoCMS environments by setting the environment option:
/** @type {import("dato-builder").DatoBuilderConfig} */
const config = {
apiToken: process.env.DATO_CMA_TOKEN,
environment: "staging", // Target staging environment
// ... other options
};
export default config;Configure logging verbosity with the logLevel option:
import { LogLevel } from "dato-builder";
/** @type {import("dato-builder").DatoBuilderConfig} */
const config = {
logLevel: LogLevel.DEBUG, // or use numeric values
};
export default config;| Level | Value | Description | When to Use |
|---|---|---|---|
LogLevel.NONE |
-1 |
No logging | Production (quiet) |
LogLevel.ERROR |
0 |
Errors only | Production |
LogLevel.WARN |
1 |
Warnings and errors | Production |
LogLevel.INFO |
2 |
General information | Default |
LogLevel.DEBUG |
3 |
Detailed debugging | Development |
LogLevel.TRACE |
4 |
Maximum verbosity | Troubleshooting |
npx dato-builder build
Build all DatoCMS types and blocks with enhanced options:--skip-deletion- Skip deletion detection and removal of orphaned items--skip-deletion-confirmation- Skip confirmation prompts for deletions--concurrent- Enable concurrent builds (default concurrency: 3)--concurrency <number>- Set specific concurrency level (implies --concurrent)--auto-concurrency- Automatically determine concurrency based on CPU cores
-
npx dato-builder generate
Interactive generator to create new blocks or models with guided prompts. -
npx dato-builder generate:block
Generate a new DatoCMS block with interactive prompts for name and configuration. -
npx dato-builder generate:model
Generate a new DatoCMS model with options for singleton, sortable, and tree structure.
npx dato-builder clear-cache
Clear all caches to force fresh builds.
All commands support these global options:
-d, --debug- Output detailed debugging information-v, --verbose- Display fine-grained trace logs-q, --quiet- Only display errors-n, --no-cache- Disable cache usage
| Command | Purpose | Key Options |
|---|---|---|
build |
Build all blocks/models | --auto-concurrency, --skip-deletion |
generate |
Interactive file creation | None |
generate:block |
Create new block | None |
generate:model |
Create new model | None |
clear-cache |
Reset all caches | None |
Common Issues & Solutions:
Build Errors
# Problem: Build fails with permission errors
# Solution: Check DatoCMS API token permissions
npx dato-builder build --debug
# Problem: Slow builds
# Solution: Enable concurrent processing
npx dato-builder build --auto-concurrencyCache Issues
# Problem: Stale data or unexpected results
# Solution: Clear cache and rebuild
npx dato-builder clear-cache
npx dato-builder build --no-cacheEnvironment Issues
# Problem: Changes appear in wrong environment
# Solution: Verify config file environment setting
# Check: dato-builder.config.js environment property
# Problem: "Environment not found" error
# Solution: Ensure environment exists in DatoCMS project
# Verify: DatoCMS dashboard > Settings > EnvironmentsGeneration Errors
# Problem: Generator validation fails
# Solution: Ensure proper naming (PascalCase)
# ✅ Good: MyNewBlock, BlogPost
# ❌ Bad: my-block, blog_post// datocms/blocks/TestBlock.ts
import { BlockBuilder, type BuilderContext } from "dato-builder";
export default async function buildTestBlock({ config }: BuilderContext) {
return new BlockBuilder({
name: "Test Block",
config,
options: {
// Dynamic field reference - finds title field automatically
presentation_title_field: (fields) => {
const titleField = fields.find(f => f.api_key === "title");
return titleField?.id || null;
},
},
})
.addHeading({
label: "Title",
body: { api_key: "title" },
})
.addTextarea({ label: "Description" })
.addImage({
label: "Image",
body: { validators: { required: true } },
});
}Run it:
npx dato-builder buildCreate new blocks and models interactively:
# Interactive generator
npx dato-builder generate
# Or generate specific types directly
npx dato-builder generate:block
npx dato-builder generate:model// datocms/models/TestModel.ts
import { ModelBuilder, type BuilderContext } from "dato-builder";
export default async function buildTestModel({ config, getBlock }: BuilderContext) {
return new ModelBuilder({
name: "Test Model",
config,
})
.addHeading({ label: "Title" })
.addTextarea({ label: "Description" })
.addModularContent({
label: "Content",
body: {
validators: { rich_text_blocks: { item_types: [await getBlock("TestBlock")] } },
},
});
}Run it:
npx dato-builder build🔗 Advanced Usage: See Field Reference for all available field types and validators.
# Build with performance optimizations
npx dato-builder build --auto-concurrency
# Build without deletion detection
npx dato-builder build --skip-deletionThis reference covers all available fields, grouped by category, along with their configuration options, supported validators, usage examples, and available aliases.
builder.addSingleLineString({
label: "Username",
body: { validators: { required: true, length: { min: 3, max: 20 } } },
options: { placeholder: "Enter username" },
});builder.addMultiLineText({
label: "Description",
body: {
validators: { sanitized_html: { sanitize_before_validation: true } },
},
});builder.addMarkdown({
label: "Body",
toolbar: ["bold", "italic", "link"],
});builder.addWysiwyg({
label: "Content",
toolbar: ["format", "bold", "italic", "link", "image"],
});builder.addTextarea({
label: "Notes",
placeholder: "Type notes here...",
});builder.addInteger({
label: "Quantity",
body: { validators: { number_range: { min: 1 } } },
});builder.addFloat({
label: "Price",
body: { validators: { required: true } },
});builder.addBoolean({
label: "Published",
});builder.addBooleanRadioGroup({
label: "Active?",
positive_radio: { label: "Yes" },
negative_radio: { label: "No" },
});builder.addDate({
label: "Publish Date",
body: { validators: { required: true } },
});builder.addDateTime({
label: "Event Time",
});builder.addSingleAsset({
label: "Avatar",
body: {
validators: { required: true, extension: { predefined_list: "image" } },
},
});builder.addAssetGallery({
label: "Gallery",
body: { validators: { size: { min: 1 } } },
});builder.addExternalVideo({
label: "Promo Video",
});builder.addLink({
label: "Author",
body: { validators: { item_item_type: { item_types: ["author_item_type_id"] } } },
});builder.addLinks({
label: "Related Articles",
body: {
validators: {
items_item_type: { item_types: ["article_item_type_id"] },
size: { min: 1, max: 5 },
},
},
});builder.addSlug({
label: "URL slug",
url_prefix: "/blog/",
placeholder: "my-post",
body: { validators: { slug_format: { predefined_pattern: "webpage_slug" } } },
});builder.addLocation({
label: "Venue",
body: { validators: { required: true } },
});Option A: Use
getBlock()viaBuilderContextThe
BuilderContextnow includes agetBlock()& agetModel()helper to automatically resolve block API keys (IDs) / Model API Keys (IDs) at runtime.
// datocms/models/TestModel.ts
import { ModelBuilder, type BuilderContext } from "dato-builder";
export default async function buildTestModel({ config, getBlock, getModel }: BuilderContext) {
return new ModelBuilder({
name: "Test Model",
config
})
.addHeading({ label: "Title" })
.addTextarea({ label: "Description" })
.addModularContent({
label: "Content Sections",
body: {
validators: {
rich_text_blocks: {
item_types: [
await getBlock("SectionBlock"), // dynamically resolve SectionBlock ID
await getModel("HighlightModel"), // dynamically resolve HighlightModel ID
],
},
size: { min: 1 },
},
},
});
}Option B: Hard-coded IDs
If you already know the API keys (e.g. from a previous run), you can skip the async calls and just list them directly:
// datocms/models/TestModel.ts
import { ModelBuilder, type BuilderContext } from "dato-builder";
export default function buildTestModel({config}: BuilderContext) {
return new ModelBuilder({
name: "Test Model",
config
})
.addHeading({ label: "Title" })
.addTextarea({ label: "Description" })
.addModularContent({
label: "Content Sections",
body: {
validators: {
rich_text_blocks: {
// replace these with the real API keys you got from DatoCMS
item_types: ["section_block_item_type_id", "highlight_model_item_type_id"],
},
size: { min: 1 },
},
},
});
}Option A: Use
getBlock()viaBuilderContextThe
BuilderContextnow includes agetBlock()& agetModel()helper to automatically resolve block API keys (IDs) / Model API Keys (IDs) at runtime.
// datocms/models/PageModel.ts
import { ModelBuilder, type BuilderContext } from "dato-builder";
export default async function buildPageModel({ config, getBlock }: BuilderContext) {
return new ModelBuilder({
name: "Page Model",
config,
})
.addSingleBlock({
label: "Hero",
type: "framed_single_block",
start_collapsed: true,
body: {
validators: {
single_block_blocks: {
item_types: [await getBlock("HeroBlock")],
},
},
},
});
}Option B: Hard-coded IDs
If the block ID is known and stable:
builder.addSingleBlock({
label: "Hero",
type: "framed_single_block",
start_collapsed: true,
body: {
validators: {
single_block_blocks: {
item_types: ["hero_block_item_type_id"],
},
},
},
});Option A: Use
getBlock()viaBuilderContextThe
BuilderContextnow includes agetBlock()& agetModel()helper to automatically resolve block API keys (IDs) / Model API Keys (IDs) at runtime.
// datocms/models/ArticleModel.ts
import { ModelBuilder, type BuilderContext } from "dato-builder";
export default async function buildArticleModel({ config, getBlock }: BuilderContext) {
return new ModelBuilder({
name: "Article Model",
config,
})
.addStructuredText({
label: "Content",
nodes: ["heading", "paragraph", "link"],
marks: ["strong", "emphasis"],
body: {
validators: {
structured_text_blocks: {
item_types: [await getBlock("QuoteBlock")],
},
},
},
});
}Option B: Hard-coded IDs
List known block IDs directly:
builder.addStructuredText({
label: "Content",
nodes: ["heading", "paragraph", "link"],
marks: ["strong", "emphasis"],
body: {
validators: {
structured_text_blocks: {
item_types: ["quote_block_item_type_id"],
},
},
},
});builder.addSeo({
label: "SEO",
fields: ["title", "description", "image"],
previews: ["google", "twitter"],
body: {
validators: { required_seo_fields: { title: true, description: true } },
},
});Some methods are provided as convenient aliases:
| Alias Method | Primary Method |
|---|---|
addImage |
addSingleAsset |
Example
builder.addImage({ label: "Logo" }); // same as addSingleAssetBelow is a detailed breakdown of each supported validator, including parameter definitions, requirements, and usage examples.
Description:
Accepts dates only inside a specified range.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| min | Date |
No | Earliest allowed date (must be a JavaScript Date object). |
| max | Date |
No | Latest allowed date (must be a JavaScript Date object). |
⚠️ At least one ofminormaxmust be specified.
Example Usage:
builder.addDate({
label: "Start Date",
body: {
validators: {
date_range: {
min: new Date("2025-01-01"),
// max: new Date("2025-12-31"), // you can omit one side if you only care about an open-ended range
},
},
},
});Description: Accept date/time values only inside a specified range.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| min | Date, string |
No | Minimum datetime |
| max | Date, string |
No | Maximum datetime |
At least one of
minormaxmust be specified.
Example:
builder.addDateTime({
label: "Deadline",
body: { validators: { date_time_range: { max: "2025-12-31T23:59:59Z" } } },
});Description: Only accept a specific set of string values.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| values | Array<string> |
Yes | Allowed values |
Example:
builder.addSingleLineString({
label: "Status",
body: {
validators: { enum: { values: ["draft", "published", "archived"] } },
},
});Description: Only accept assets with specified file extensions or types.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| extensions | Array<string> |
No | Allowed file extensions |
| predefined_list | image, transformable_image, video, document |
No | Predefined asset category |
Only one of
extensionsorpredefined_listmust be specified.
Example:
builder.addSingleAsset({
label: "Upload",
body: { validators: { extension: { predefined_list: "image" } } },
});Description: Accept assets only within a specified file size range.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| min_value | number |
No | Minimum file size value |
| min_unit | B, KB, MB |
No | Unit for minimum size |
| max_value | number |
No | Maximum file size value |
| max_unit | B,KB,MB |
No | Unit for maximum size |
At least one value/unit pair must be specified.
Example:
builder.addSingleAsset({
label: "Image",
body: { validators: { file_size: { max_value: 5, max_unit: "MB" } } },
});Description: Accept only strings matching a custom or predefined format.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| custom_pattern | RegExp |
No | Custom regex pattern |
| predefined_pattern | email ,url |
No | Predefined format type |
| description | string |
No | Hint shown on validation failure (only with custom) |
Only one of
custom_patternorpredefined_patternmust be specified.
Example:
builder.addSingleLineString({
label: "Email",
body: { validators: { format: { predefined_pattern: "email" } } },
});Description: Only accept slug values matching a custom or predefined slug pattern.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| custom_pattern | RegExp |
No | Custom regex for slug |
| predefined_pattern | webpage_slug |
No | Predefined slug format |
Only one of
custom_patternorpredefined_patternmust be specified.
Example:
builder.addSlug({
label: "Slug",
body: { validators: { slug_format: { predefined_pattern: "webpage_slug" } } },
});Description: Accept assets only within specified width/height bounds.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| width_min_value | number |
No | Minimum width |
| width_max_value | number |
No | Maximum width |
| height_min_value | number |
No | Minimum height |
| height_max_value | number |
No | Maximum height |
At least one width/height pair must be specified.
Example:
builder.addSingleAsset({
label: "Thumbnail",
body: {
validators: {
image_dimensions: { width_min_value: 300, height_min_value: 200 },
},
},
});Description: Accept assets only within a specified aspect ratio.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| min_ar_numerator | number |
No | Minimum aspect ratio numerator |
| min_ar_denominator | number |
No | Minimum aspect ratio denominator |
| eq_ar_numerator | number |
No | Exact aspect ratio numerator |
| eq_ar_denominator | number |
No | Exact aspect ratio denominator |
| max_ar_numerator | number |
No | Maximum aspect ratio numerator |
| max_ar_denominator | number |
No | Maximum aspect ratio denominator |
At least one ratio pair must be specified.
Example:
builder.addSingleAsset({
label: "Banner",
body: {
validators: {
image_aspect_ratio: { eq_ar_numerator: 16, eq_ar_denominator: 9 },
},
},
});Description: Accept references only to specified model records.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| item_types | Array<string> |
✅ | IDs of allowed model types |
| on_publish_with_unpublished_references_strategy | fail, publish_references |
No | Strategy when publishing with unpublished references |
| on_reference_unpublish_strategy | fail,unpublish,delete_references |
No | Strategy when referenced record is unpublished |
| on_reference_delete_strategy | fail, delete_references |
No | Strategy when referenced record is deleted |
Example:
builder.addLink({
label: "Author",
body: {
validators: {
item_item_type: {
item_types: ["author_item_type_id"],
on_publish_with_unpublished_references_strategy: "fail",
},
},
},
});Description: Accept multiple references only to specified model records.
(Same parameters and strategies as item_item_type)
Example:
builder.addLinks({
label: "Related Posts",
body: { validators: { items_item_type: { item_types: ["post_item_type_id"] } } },
});Description: Accept strings only with a specified character count.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| min | number |
No | Minimum length |
| eq | number |
No | Exact length |
| max | number |
No | Maximum length |
At least one of
min,eq, ormaxmust be specified.
Example:
builder.addSingleLineString({
label: "Code",
body: { validators: { length: { eq: 6 } } },
});Description: Accept numbers only inside a specified range.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| min | number |
No | Minimum value |
| max | number |
No | Maximum value |
At least one of
minormaxmust be specified.
Example:
builder.addFloat({
label: "Rating",
body: { validators: { number_range: { min: 0, max: 5 } } },
});Description: Value must be specified or validation fails.
Example:
builder.addSingleLineString({
label: "Title",
body: { validators: { required: true } },
});Description: Assets must include custom title or alt text.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| title | boolean |
No | Require a custom title |
| alt | boolean |
No | Require alternate text |
At least one of
titleoraltmust be true.
Example:
builder.addSingleAsset({
label: "Image",
body: { validators: { required_alt_title: { title: true, alt: true } } },
});Description: SEO inputs must include one or more specified fields.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| title | boolean |
No | Require meta title |
| description | boolean |
No | Require meta description |
| image | boolean |
No | Require social sharing image |
| twitter_card | boolean |
No | Require Twitter card type |
At least one field must be true.
Example:
builder.addSeo({
label: "Meta",
body: {
validators: { required_seo_fields: { title: true, description: true } },
},
});Description: Limits the SEO title length.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| min | number |
No | Minimum length |
| max | number |
No | Maximum length |
At least one of
minormaxmust be specified.
Example:
builder.addSeo({
label: "Meta",
body: { validators: { title_length: { max: 60 } } },
});Description: Limits the SEO description length.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| min | number |
No | Minimum length |
| max | number |
No | Maximum length |
At least one of
minormaxmust be specified.
Example:
builder.addSeo({
label: "Meta",
body: { validators: { description_length: { max: 155 } } },
});Description: Only accept specified block models in rich text block nodes.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| item_types | Array<string> |
✅ | IDs of allowed block models |
Example:
builder.addModularContent({
label: "Sections",
body: { validators: { rich_text_blocks: { item_types: ["section_item_type_id"] } } },
});Description: Only accept specified block models in single-block fields.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| item_types | Array<string> |
✅ | IDs of allowed block models |
Example:
builder.addSingleBlock({
label: "Hero",
body: { validators: { single_block_blocks: { item_types: ["hero_block_item_type_id"] } } },
});Description: Checks for malicious code in HTML input fields.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| sanitize_before_validation | boolean |
✅ | Sanitize content before validation |
Example:
builder.addMarkdown({
label: "Notes",
toolbar: [],
body: {
validators: { sanitized_html: { sanitize_before_validation: true } },
},
});Description: Only accept specified block models in structured text block nodes.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| item_types | Array<string> |
✅ | IDs of allowed block models |
Example:
builder.addStructuredText({
label: "Content",
body: { validators: { structured_text_blocks: { item_types: ["quote_item_type_id"] } } },
});Description: Only accept specified block models in inline block nodes of structured text.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| item_types | Array<string> |
✅ | IDs of allowed block models |
Example:
builder.addStructuredText({
label: "Content",
body: {
validators: { structured_text_inline_blocks: { item_types: ["link_item_type_id"] } },
},
});Description: Only accept itemLink and inlineItem nodes for specified models in structured text.
Parameters:
| Name | Type | Required | Description |
|---|---|---|---|
| item_types | Array<string> |
✅ | IDs of allowed models |
| on_publish_with_unpublished_references_strategy | fail, publish_references |
No | Strategy when publishing with unpublished references |
| on_reference_unpublish_strategy | fail , unpublish , delete_references |
No | Strategy when referenced record is unpublished |
| on_reference_delete_strategy | fail , delete_references |
No | Strategy when referenced record is deleted |
Example:
builder.addStructuredText({
label: "Content",
body: { validators: { structured_text_links: { item_types: ["author_item_type_id"] } } },
});| Field Class | Validators Supported |
|---|---|
| SingleLineString | required, unique, length, format, enum |
| MultiLineText, Markdown, Wysiwyg, Textarea | required, length, format, sanitized_html |
| Integer, Float | required, number_range |
| Boolean, BooleanRadioGroup | required |
| DateField | required, date_range |
| DateTime | required, date_time_range |
| SingleAsset | required, extension, file_size, image_dimensions, image_aspect_ratio, required_alt_title |
| AssetGallery | size, extension, file_size, image_dimensions, image_aspect_ratio, required_alt_title |
| ExternalVideo | required |
| Link | item_item_type, required, unique |
| Links | items_item_type, size |
| Slug | required, length, slug_format, slug_title_field |
| Location | required |
| ModularContent | rich_text_blocks, size |
| SingleBlock | single_block_blocks, required |
| StructuredText | length, structured_text_blocks, structured_text_inline_blocks, structured_text_links |
| Seo | required_seo_fields, file_size, image_dimensions, image_aspect_ratio, title_length, description_length |
MIT License - see LICENSE file for details.