Skip to content

content-model

Terminal window
npx skills add https://github.com/crystallizeapi/ai --skill content-model

Crystallize Content Model Skill

Design and implement content structures in Crystallize using a flexible modeling system for products, documents, and taxonomies.

Consultation Approach

Before designing shapes, understand the business. Ask clarifying questions:

  1. What are you selling or publishing? Products, articles, courses, services?
  2. How will customers discover items? Browse categories, search, filter by attributes?
  3. What makes each item unique? Specs, features, editorial content, variants?
  4. What’s shared across items? Brands, materials, certifications, authors?
  5. Do you need multiple languages/markets? Which fields vary by language?
  6. What’s the scale? Number of products, categories, content pages?
  7. Do items have relationships? Bundles, kits, compatibility, recommendations?

Use the answers to choose shape types, decide on classification patterns (topics vs documents), and plan the component structure. A content model that reflects the business intent is more valuable than one that’s technically impressive.

Output Format

The output of this skill is a mass operations JSON file — a structured document that creates all shapes and pieces in the correct order.

After generating the mass operations JSON, always validate it using the build-mass-operation MCP tool. This tool validates against the official @crystallize/schema and returns either valid JSON or detailed error feedback. Fix any validation errors before presenting the result.

The mass operations file follows the 4-phase ordering described in the Mass Operations section below.

Quick Start Decision Tree

When designing shapes:

  1. What is the item? → Choose shape type (Product, Document, Folder)
  2. What data does it need? → Add components (see Component Decision Guide)
  3. How does it relate to others? → Use Item Relations with acceptedShapeIdentifiers
  4. Is it part of a classification? → Consider design patterns (see below)

Core Concepts

The Five Building Blocks

  1. Shapes - Blueprints defining item structure (product/document/folder)
  2. Components - Atomic fields added to shapes (text, numbers, media, relations)
  3. Pieces - Reusable field sets embedded across shapes
  4. Topic Maps - Hierarchical taxonomies for classification
  5. Grids - Curated editorial collections

Shape Types

TypePurposeHas VariantsCan Have ChildrenExample
ProductSellable items with pricingYesNoShoes, Subscription
DocumentEditorial content (leaf nodes)NoNoArticle, Brand, FAQ
FolderCatalogue hierarchy organizationNoYesCategory, Landing Page, Collection

Built-in vs Custom Fields

Every item has built-in fields: name, id, language, tree, topics, createdAt, updatedAt.

Product variants have additional built-ins: sku, images, videos, price, stock, attributes.

Components are the additional fields you define beyond these built-ins. For product shapes, use variantComponents in the shape definition to add custom components directly to product variants (e.g., custom certifications, variant-level specs).

IMPORTANT: Do NOT add components for price or stock - these are native properties managed through the product variant’s built-in fields. Only create custom price/stock components for specific edge cases (e.g., tiered pricing models, subscription pricing, price modifiers for configurators).

Product Identification: The built-in sku field on product variants is the primary product identifier — use it for GTIN, EAN, ISBN, or any single identifier. Do NOT add a separate gtin or ean single-line component on the variant. If a product needs multiple identifiers (e.g., both ISBN-10 and ISBN-13, or a GTIN plus a legacy system ID), add an identifiers chunk with named fields to the shape:

Product Shape: Book
└── identifiers (contentChunk)
├── isbn-10 (Single Line)
├── isbn-13 (Single Line)
└── legacy-id (Single Line)
Product Shape: Consumer Electronics
└── identifiers (contentChunk)
├── gtin (Single Line)
├── ean (Single Line)
├── upc (Single Line)
└── legacy-id (Single Line)

Each identifier gets its own named component — not a generic key/value pattern. This keeps the data typed, queryable, and self-documenting. Also useful during migrations to preserve old system identifiers alongside the new SKU.

See Shapes Reference and Content Modelling Guide for details.

Component Selection Guide

”I need to store text”

  • Single Line → Short, unformatted (subtitle, GTIN, URL)
  • Rich Text → Formatted content (descriptions, specifications)
  • Paragraph Collection → Editorial sections with mixed media

”I need to store numbers”

  • Numeric → Measurable values with selectable units (e.g. ["g","mg","kg"] for weight, ["cm","m","in"] for dimensions). Configure units in config.numeric.units.
  • Properties Table → Arbitrary key-value specs
  • Item Relation → Link to catalogue items (products, documents, folders)
    • Always use acceptedShapeIdentifiers to restrict which shapes can be linked
    • Set minItems/maxItems for cardinality constraints
  • Grid Relation → Link to curated grids

”I need controlled options”

  • Selection → Dropdown/checkboxes with predefined options
  • Switch → Boolean flag
  • Datetime → Date/timestamp (type: datetime)
  • Location → Geographic coordinates

”I need to group fields”

  • Chunk (contentChunk) → Group of related fields (can be repeatable or single)
    • Use chunk when: Fields are specific to one location and won’t be reused elsewhere
    • Repeatable chunk: For lists (ingredients, specifications, USP items, CTA buttons)
      • Examples: Width/Height/Depth (repeating for multiple dimensions), Street/City/Zip (repeating addresses)
    • Non-repeatable chunk: For one-time field groups (metadata, settings, single address)
      • Examples: SEO fields (title/description/keywords as one group), single shipping address, contact info block
    • Decision: If the group only occurs once in the entire content model → use non-repeatable chunk
    • Can be used inside pieces for nested grouping
    • NOT a direct child of componentMultipleChoice
  • Piece → Reusable component groups needed in multiple places
    • Use piece when: The exact same structure will be used in multiple shapes OR multiple pieces
    • Examples: Banner sections (used across landing pages, products, articles), Hero blocks (reused), Author bios (appears in articles and testimonials)
    • Think: “Will this exact structure be used in 2+ locations?”
    • Rule: If only used once → use chunk instead of piece
    • Perfect for componentMultipleChoice options (page builder pattern)
  • Choice → One of N mutually exclusive forms (polymorphic)
    • MUST have at least 2 choices - Single option is invalid
  • Component Choice (componentChoice) → Similar to choice but for component-level choices
    • MUST have at least 2 choices - Single option is invalid
    • EVERY choice item MUST have a type field - choices with no type or with a raw components array are invalid API structures
    • When each choice needs multiple fields → create a piece/upsert operation for each, then reference with { "type": "piece", "config": { "piece": { "identifier": "..." } } }
  • Multiple Choice (componentMultipleChoice) → Multiple coexisting forms (page builders)
    • MUST have at least 2 choices - Single option is invalid
    • Use when: Editors choose between different content types at the same location
    • Valid children: Piece references (type: "piece") or single regular components (images, singleLine, etc.)
    • NEVER as children: contentChunk, choice, componentChoice, or nested componentMultipleChoice
    • NEVER: anonymous objects with only a components array and no type field — these are not valid API structures
    • Common pattern: Page sections where each option is a piece (Banner, Hero, USP, Gallery)
    • Examples: Blocks (banner piece, hero piece, usp piece), Content areas (text piece, video piece)

Structural Component Nesting Rules

Structural components = contentChunk, componentChoice, componentMultipleChoice

Critical Rule: Structural components can only contain pieces or regular components (singleLine, richText, numeric, images, etc.) as direct children.

NEVER nest structural components directly within each other:

  • ❌ componentChoice cannot have componentChoice/componentMultipleChoice/contentChunk as direct children
  • ❌ componentMultipleChoice cannot have componentChoice/componentMultipleChoice/contentChunk as direct children
  • ❌ contentChunk cannot have componentChoice/componentMultipleChoice/contentChunk as direct children

Correct pattern for complex structures:

componentChoice/componentMultipleChoice
└─ Piece (reusable component group)
└─ contentChunk/componentChoice (now allowed inside piece)
└─ Regular components

Example - Product Type Selector (CORRECT):

{
"id": "type",
"name": "Product Type",
"type": "componentChoice",
"config": {
"componentChoice": {
"choices": [
{
"id": "fresh-flowers",
"name": "Fresh Flowers",
"type": "piece",
"config": { "piece": { "identifier": "fresh-flowers-details" } }
},
{
"id": "arrangement",
"name": "Flower Arrangement",
"type": "piece",
"config": { "piece": { "identifier": "arrangement-details" } }
}
]
}
}
}

For complete component reference with nesting rules, validation, and localization, see Components Reference.

Design Patterns for Relationships

When to use each pattern?

NeedPatternStructure
Rich classification (brand, allergen, cert)Semantic BridgeItem Relation → Document
Relationship with quantity (recipe, BOM, kit)Quantised BridgeChunk(Numeric + Item Relation)
Relationship with role (contributor, usage)Composite BridgeChunk(Item Relation + Selection)
Relationship with rules (configurator)Conditional BridgeChunk(Item Relation + Rule Type)
Single concept, multiple forms (instrument)Polymorphic ChoiceChoice/Multiple Choice → Pieces

Semantic Classification Bridge

Problem: Attributes need more than just labels (brands need logos, allergens need warnings).

Solution: Create classification document shapes, link via Item Relations.

{
"id": "brand",
"type": "itemRelations",
"config": {
"itemRelations": {
"acceptedShapeIdentifiers": ["brand"],
"minItems": 1,
"maxItems": 1
}
}
}

Key point: acceptedShapeIdentifiers enforces type safety - only “brand” documents can be linked.

Quantised Classification Bridge

Problem: Relationships have measurable quantities (200g flour, 4 screws).

Solution: Repeating contentChunk with numeric + item relation.

{
"id": "ingredients",
"type": "contentChunk",
"config": {
"contentChunk": {
"repeatable": true,
"components": [
{ "id": "quantity", "type": "numeric" },
{ "id": "unit", "type": "selection" },
{
"id": "ingredient",
"type": "itemRelations",
"config": {
"itemRelations": {
"acceptedShapeIdentifiers": ["ingredient"],
"minItems": 1,
"maxItems": 1
}
}
}
]
}
}
}

Critical: Always Use acceptedShapeIdentifiers

Without shape restrictions:

// ❌ Bad - editors can link ANY shape
{
"type": "itemRelations",
"config": { "itemRelations": {} }
}

With shape restrictions:

// ✅ Good - enforces semantic meaning
{
"type": "itemRelations",
"config": {
"itemRelations": {
"acceptedShapeIdentifiers": ["brand"],
"minItems": 1,
"maxItems": 1
}
}
}

This acts like foreign key constraints in relational databases, preventing data integrity issues.

Page Builder Pattern (componentMultipleChoice with Pieces)

Problem: Editors need flexible page layouts with different section types.

Solution: componentMultipleChoice with piece references, each piece containing repeating chunks for items.

// Folder Shape: Landing Page (folders can have children, documents cannot)
{
"components": [
{
"id": "blocks",
"name": "Page Sections",
"type": "componentMultipleChoice",
"config": {
"componentMultipleChoice": {
"allowDuplicates": true,
"choices": [
{ "type": "piece", "config": { "piece": { "identifier": "banner-section" } } },
{ "type": "piece", "config": { "piece": { "identifier": "usp-section" } } },
{ "type": "piece", "config": { "piece": { "identifier": "hero-section" } } }
]
}
}
}
]
}
// Piece: banner-section
{
"identifier": "banner-section",
"name": "Banner Section",
"components": [
{ "id": "title", "type": "singleLine" },
{ "id": "description", "type": "richText" },
{ "id": "background", "type": "images" },
{
"id": "ctas",
"name": "Call to Actions",
"type": "contentChunk",
"config": {
"contentChunk": {
"repeatable": true,
"components": [
{ "id": "text", "type": "singleLine" },
{ "id": "url", "type": "singleLine" }
]
}
}
}
]
}
// Piece: usp-section
{
"identifier": "usp-section",
"name": "USP Section",
"components": [
{ "id": "title", "type": "singleLine" },
{
"id": "items",
"name": "USP Items",
"type": "contentChunk",
"config": {
"contentChunk": {
"repeatable": true,
"components": [
{ "id": "icon", "type": "images" },
{ "id": "title", "type": "singleLine" },
{ "id": "description", "type": "richText" }
]
}
}
}
]
}

Key points:

  • componentMultipleChoice contains pieces (banner-section, usp-section, hero-section)
  • Each piece contains regular components and repeating chunks for items
  • Chunks (CTAs, USP items) are inside pieces, not direct children of componentMultipleChoice
  • This allows editors to build pages with flexible, reusable sections

For complete pattern details with examples, see Design Patterns Reference.

Shape Consolidation Strategy

Minimizing Product and Folder Shapes

Principle: Keep the number of product and folder shapes to a minimum by using polymorphic patterns instead of creating separate shapes for similar items.

When to Consolidate Shapes

Use the polymorphic product/folder pattern when:

  1. Similarity threshold: Two or more product/folder shapes share 50%+ of their components

    • Example: Plant and Vase both have description and SEO components
  2. Common base components: Shapes have the same foundational components but differ in specialized attributes

    • Common: description, SEO, images, pricing
    • Differ: plant-specific fields (care-level, light-requirement) vs vase-specific fields (material, dimensions)
  3. Semantic similarity: Items represent variations of the same concept rather than fundamentally different entities

    • Example: Guitar, Amplifier, and Pedal are all music equipment (consolidate into “Music Product”)
    • Example: Plant and Vase are both home decor items (consolidate into “Home Decor Product”)

When NOT to Consolidate

Keep shapes separate when:

  1. Fundamentally different purposes: Items serve completely different business functions

    • Product vs Folder (different shape types)
    • Apparel vs Digital Download (different fulfillment, no pricing overlap)
  2. Low component overlap: Less than 30% of components are shared

    • Consolidation would create a confusing, bloated shape
  3. Different business logic: Items require distinct workflows, permissions, or integrations

    • Separate checkout flows
    • Different inventory systems
    • Distinct pricing models
  4. Editor clarity: The consolidated shape would confuse content editors

    • Too many conditional fields make editing difficult

Consolidation Approach

Step 1: Identify common components

  • Extract all shared components (description, SEO, images, etc.)
  • These become the base components of the consolidated shape

Step 2: Create a type selector

  • Add a selection component to distinguish between variants
  • Example: product-type with options “plant”, “vase”, “bundle”

Step 3: Extract variant-specific attributes into pieces

  • Create separate pieces for each variant’s unique components
  • Example: plant-attributes piece, vase-attributes piece

Step 4: Use choice component for polymorphic structure

  • Add a choice component that conditionally shows the appropriate piece based on the type selection

Example: Consolidating Plant and Vase into Product

Before consolidation (2 shapes):

Shape: Plant (product)
├── description (richText)
├── care-level (itemRelations → care-level docs)
├── light-requirement (selection)
├── mature-size (selection)
└── seo (piece)
Shape: Vase (product)
├── description (richText)
├── material (itemRelations → material docs)
├── dimensions (contentChunk)
└── seo (piece)

After consolidation (1 shape + 2 pieces):

{
"intent": "shape/upsert",
"identifier": "product",
"name": "Product",
"type": "product",
"components": [
{
"id": "product-type",
"name": "Product Type",
"type": "selection",
"config": {
"selection": {
"options": [
{ "key": "plant", "value": "Plant" },
{ "key": "vase", "value": "Vase" }
]
}
}
},
{ "id": "description", "name": "Description", "type": "richText" },
{
"id": "variant-attributes",
"name": "Product Details",
"type": "choice",
"config": {
"choice": {
"choices": [
{
"id": "plant",
"name": "Plant Details",
"type": "piece",
"config": {
"piece": { "identifier": "plant-attributes" }
}
},
{
"id": "vase",
"name": "Vase Details",
"type": "piece",
"config": {
"piece": { "identifier": "vase-attributes" }
}
}
]
}
}
},
{
"id": "seo",
"name": "SEO",
"type": "piece",
"config": { "piece": { "identifier": "seo" } }
}
]
}

Supporting pieces:

{
"intent": "piece/upsert",
"identifier": "plant-attributes",
"name": "Plant Attributes",
"components": [
{
"id": "care-level",
"name": "Care Level",
"type": "itemRelations",
"config": {
"itemRelations": {
"acceptedShapeIdentifiers": ["care-level"],
"minItems": 1,
"maxItems": 1
}
}
},
{
"id": "light-requirement",
"name": "Light Requirement",
"type": "selection",
"config": {
"selection": {
"options": [
{ "key": "low", "value": "Low Light" },
{ "key": "medium", "value": "Medium Light" },
{ "key": "bright", "value": "Bright Light" },
{ "key": "direct", "value": "Direct Sunlight" }
]
}
}
},
{
"id": "mature-size",
"name": "Mature Size",
"type": "selection",
"config": {
"selection": {
"options": [
{ "key": "small", "value": "Small (under 12\")" },
{ "key": "medium", "value": "Medium (12-24\")" },
{ "key": "large", "value": "Large (24-48\")" },
{ "key": "xl", "value": "Extra Large (48\"+)" }
]
}
}
}
]
}
{
"intent": "piece/upsert",
"identifier": "vase-attributes",
"name": "Vase Attributes",
"components": [
{
"id": "material",
"name": "Material",
"type": "itemRelations",
"config": {
"itemRelations": {
"acceptedShapeIdentifiers": ["material"],
"minItems": 1,
"maxItems": 3
}
}
},
{
"id": "dimensions",
"name": "Dimensions",
"type": "contentChunk",
"config": {
"contentChunk": {
"components": [
{
"id": "height",
"name": "Height",
"type": "numeric",
"config": { "numeric": { "units": ["cm", "in"] } }
},
{
"id": "diameter",
"name": "Diameter",
"type": "numeric",
"config": { "numeric": { "units": ["cm", "in"] } }
}
]
}
}
}
]
}

Benefits:

  • Single product shape instead of multiple
  • Shared components (description, SEO) defined once
  • Type-specific attributes cleanly separated into pieces
  • Easier to maintain and extend
  • Clearer information architecture

Trade-offs:

  • Slightly more complex for editors (choice component requires selecting variant type)
  • Frontend may need to handle polymorphic structure
  • Only worthwhile when shapes share significant overlap

Mass Operations Pattern for Consolidated Shapes

When using the 4-phase ordering with consolidated shapes:

  1. Phase 1: Create variant-specific pieces (plant-attributes, vase-attributes)
  2. Phase 2: Create consolidated shape container
  3. Phase 3: Add components to variant pieces
  4. Phase 4: Add components (including choice with piece references) to consolidated shape

This ensures all piece references in the choice component resolve correctly.

Common Use Cases

Standard SEO Piece

A reusable piece for SEO metadata, commonly added to products, folders (landing pages), and documents (articles):

{
"identifier": "seo",
"name": "SEO",
"components": [
{ "id": "title", "name": "Title", "type": "singleLine" },
{ "id": "description", "name": "Description", "type": "richText" },
{ "id": "image", "name": "Image", "type": "images" }
]
}

Usage in a shape:

{
"id": "seo",
"name": "SEO",
"type": "piece",
"config": { "identifier": "seo" }
}

E-commerce Product with Variants

Product Shape: Clothing
├── Description (Rich Text, translatable)
├── Material (Item Relations → material documents, acceptedShapeIdentifiers: ["material"])
├── Brand (Item Relations → brand document, acceptedShapeIdentifiers: ["brand"], min:1, max:1)
└── Care Instructions (Rich Text, translatable)
Variant-level (built-in):
├── Size (attribute)
├── Color (attribute)
├── SKU (built-in)
├── Images (built-in)
└── Price (built-in)

Recipe with Ingredients

Document Shape: Recipe
├── Description (Rich Text)
├── Ingredients (contentChunk, repeating)
│ ├── Quantity (Numeric)
│ ├── Unit (Selection: g, ml, pcs)
│ └── Ingredient (Item Relations → ingredient products, acceptedShapeIdentifiers: ["ingredient"])
└── Allergens (Item Relations → allergen documents, acceptedShapeIdentifiers: ["allergen"])

Blog Post with Topics

Document Shape: Blog Post
├── Hero Image (Images)
├── Content (Paragraph Collection)
├── Author (Item Relations → author document, acceptedShapeIdentifiers: ["author"])
├── SEO (Piece → seo-metadata)
└── topics → Category topic map, Tags topic map

Product Configurator

Product Shape: Configurable Product
└── Options (contentChunk, repeating)
├── Option (Item Relations → option products, acceptedShapeIdentifiers: ["option"])
└── Quantity (Numeric)
Document Shape: Configuration Rule
├── If Option (Item Relations → option, acceptedShapeIdentifiers: ["option"])
├── Rule Type (Selection: requires, excludes, optional)
└── Then Option (Item Relations → option, acceptedShapeIdentifiers: ["option"])

Best Practices

Naming Conventions

  1. Use normalcase for identifiers - Shape and component identifiers should be lowercase with hyphens (e.g., product-page, hero-section)
  2. Be concise, use hierarchy - Let the structure provide context. Use title not product-page-hero-section-title
  3. Don’t repeat container names - Within a piece called seo, use title not seo-title. The piece name provides context.
  4. Use semantic names - Name components for what they represent, not how they’re displayed
  5. Avoid redundant prefixes - Bad: “Meta Title”, “Meta Description”. Good: “Title”, “Description” (when inside SEO piece)
  6. Keep names simple and direct - Bad: “Social Share Image”. Good: “Image” (context is clear from parent)

Example - SEO Piece Naming:

Piece: "SEO" (not "SEO Metadata")
├─ title (not "meta-title" or "Meta Title")
├─ description (not "meta-description")
└─ image (not "social-share-image" or "og-image")

Design Principles

  1. Start with intent, not structure - Understand what you’re modeling before creating shapes
  2. Use pieces for shared fields - SEO, environmental data, common metadata
  3. Always restrict item relations - Use acceptedShapeIdentifiers to enforce type safety
  4. Set min/max constraints - Guide editors with cardinality rules (minItems, maxItems)
  5. Keep shapes focused - One shape per logical content type
  6. Consolidate similar shapes - When classification documents share the same structure (e.g., “Coffee Origin” and “Tea Origin” both need name, description, image), combine them into a single shape (“Origin”) and differentiate via folder placement or topic tags
  7. Use chunks for grouping - Related fields that repeat together
  8. Choose topics vs documents wisely - Simple labels → topics, rich content → documents
  9. Plan localization early - Enable isTranslatable from the start
  10. Mark fields discoverable - Enable for filtering/search in Discovery API
  11. Apply design patterns - Use established patterns for scalable models

Anti-Patterns to Avoid

Item Relations without acceptedShapeIdentifiers - Allows linking any shape, breaks semantic meaning

Using Item Relations for simple labels - Use Topic Maps instead (better performance, simpler)

Flat component lists - Group related fields with Chunks for better structure

Mixing variant data with product data - Use variant attributes (built-in) for size/color/sku

Adding GTIN/EAN/ISBN as a variant component - The built-in sku field handles single product identifiers. For multiple identifiers, add a chunk with named single-line components (e.g., isbn-10, isbn-13, gtin, ean, upc, legacy-id) at the shape level, not on the variant.

Adding price or stock components to product shapes - Products have native price and stock fields on variants. Only add custom pricing/stock components for specific edge cases (tiered pricing, subscription models, configurator price modifiers, bulk discounts)

Selection for expandable values - Use documents + item relations for brands/categories that grow

Not planning for localization - Adding multilingual support later requires migration

Using contentChunk as direct child of componentMultipleChoice - Chunks should be inside pieces, not direct children

Using choice/componentChoice as direct children of componentMultipleChoice - Choices cannot be nested directly

Nesting structural components directly - componentChoice, componentMultipleChoice, and contentChunk can only have pieces or regular components as direct children, never other structural components. Use pieces as intermediaries.

Choice items without a type field - Every item in a choices / componentConfigs array MUST have a type field. Objects like { "id": "smartphone", "name": "Smartphone", "components": [...] } are INVALID — the components key does not exist on a choice item and type is missing. If a choice needs multiple fields, create a separate piece and reference it with { "type": "piece", "config": { "piece": { "identifier": "smartphone-spec" } } }.

Polymorphic Choice with inline component groups - When different choice variants (e.g. Smartphone, Laptop, Headphones) each need their own set of fields, do NOT embed a components array directly on the choice object. This is the most common componentChoice mistake. Always create a piece/upsert operation for each variant and reference the piece. Example: { "type": "piece", "config": { "piece": { "identifier": "smartphone-spec" } } } where smartphone-spec is a separate piece containing Screen Size, Storage, OS fields.

CTA/USP items as componentMultipleChoice choices - These should be repeating chunks inside a piece, not top-level choices

Single-choice configurations - Choice components (choice, componentChoice, componentMultipleChoice) MUST have at least 2 options. Single option is invalid. If you only have one option, use a direct piece reference instead of wrapping it in a componentChoice.

Wrapping single piece in componentChoice - If a component only needs one piece (e.g., CTA), add the piece directly. Don’t use componentChoice with one option.

Empty contentChunk (no components) - A contentChunk with zero child components is meaningless and invalid. Always define at least 1 component inside a chunk. If you find yourself creating an empty chunk, reconsider the design.

See Content Modelling Guide for detailed anti-patterns and migration strategies.

Mass Operations — 4-Phase Ordering

When creating shapes and pieces via mass operations (Bulk Task API), use a 4-phase ordering to handle cross-references and relations correctly.

Phase Order

  1. Create empty piecesintent: "piece/upsert" (identifier + name, NO components)
  2. Create empty shapesintent: "shape/upsert" (identifier + name + type, NO components/variantComponents)
  3. Add components to piecesintent: "piece/upsert" (identifier + name + components)
  4. Add components to shapesintent: "shape/upsert" (identifier + name + type + components/variantComponents)

REQUIRED FIELDS:

  • All upsert operations MUST include identifier and name
  • Shape upserts MUST include type

How upsert works: Multiple operations with the same identifier will update the same resource. Phase 1-2 create containers, phase 3-4 populate them with components.

Why This Order?

Components can reference pieces (via type: "piece") and shapes (via itemRelations with acceptedShapeIdentifiers). By creating containers first (phases 1-2), then populating them with components that may contain references (phases 3-4), all references resolve correctly.

Example Operations File

{
"version": "1.0.0",
"operations": [
// Phase 1: Create piece containers (no components)
{
"intent": "piece/upsert",
"identifier": "seo",
"name": "SEO"
},
{
"intent": "piece/upsert",
"identifier": "hero",
"name": "Hero Section"
},
// Phase 2: Create shape containers (no components)
{
"intent": "shape/upsert",
"identifier": "product",
"name": "Product",
"type": "product"
},
{
"intent": "shape/upsert",
"identifier": "brand",
"name": "Brand",
"type": "document"
},
// Phase 3: Update pieces with components (same identifier - upsert updates existing)
{
"intent": "piece/upsert",
"identifier": "seo",
"name": "SEO",
"components": [
{ "id": "title", "name": "SEO Title", "type": "singleLine" },
{ "id": "description", "name": "SEO Description", "type": "singleLine" }
]
},
{
"intent": "piece/upsert",
"identifier": "hero",
"name": "Hero Section",
"components": [
{ "id": "headline", "name": "Headline", "type": "singleLine" },
{ "id": "image", "name": "Hero Image", "type": "images" }
]
},
// Phase 4: Update shapes with components (can now safely reference pieces)
{
"intent": "shape/upsert",
"identifier": "product",
"name": "Product",
"type": "product",
"components": [
{ "id": "description", "name": "Description", "type": "richText" },
{
"id": "seo",
"name": "SEO",
"type": "piece",
"config": { "piece": { "identifier": "seo" } }
},
{
"id": "brand",
"name": "Brand",
"type": "itemRelations",
"config": {
"itemRelations": {
"maxItems": 1,
"acceptedShapeIdentifiers": ["brand"]
}
}
}
],
"variantComponents": [{ "id": "gtin", "name": "GTIN", "type": "singleLine" }]
},
{
"intent": "shape/upsert",
"identifier": "brand",
"name": "Brand",
"type": "document",
"components": [
{ "id": "description", "name": "Description", "type": "richText" },
{ "id": "logo", "name": "Logo", "type": "images" }
]
}
]
}

Note: Using piece/upsert or shape/upsert in a single operation (without the 4-phase split) will work ONLY if there are no cross-references. When pieces reference other pieces or shapes reference pieces/other shapes, the 4-phase order is required.

Validation Checklist

Before presenting the mass operations JSON, verify:

  1. Validate with build-mass-operation tool — Run the operations array through the MCP tool and fix any schema errors. This is the source of truth.
  2. acceptedShapeIdentifiers on every itemRelations component — no untyped relations
  3. No structural nesting violationscontentChunk, componentChoice, componentMultipleChoice cannot be direct children of each other
  4. Every contentChunk has at least 1 component — empty chunks are invalid
  5. Every componentChoice/componentMultipleChoice has at least 2 choices — single-choice is invalid
  6. No custom price/stock components on product shapes — these are built-in on variants
  7. paragraphCollection config always has multilingual array — this is required, not optional (use [] for no localization, ["title", "body", "images", "videos"] for full localization)
  8. datetime config uses only valid fieldsrequired, discoverable, multilingual only. There is no format field.
  9. files config: if acceptedContentTypes is provided, each entry needs contentType (required). If maxFileSize is provided, both size and unit are required.
  10. Naming conventions — all identifiers use lowercase-with-hyphens (no camelCase, no underscores)
  11. Reusable SEO piece — create an SEO piece and reference it from multiple shapes
  12. Localization strategy — translatable for marketing text, non-translatable for universal data (codes, dimensions)

References

Examples by Industry

Fashion: Product with variants (size/color), material classifications, brand documents, care instructions

Food/Recipe Domain: Complete model using Quantised Bridge pattern:

Product Shape: Ingredient
├── Description (Rich Text)
├── Nutrition (Piece → nutrition)
└── Allergens (Item Relations → allergen docs, acceptedShapeIdentifiers: ["allergen"])
Product Shape: Recipe
├── Description (Rich Text)
├── Prep Time (Numeric, unit: minutes)
├── Cook Time (Numeric, unit: minutes)
├── Servings (Numeric)
├── Ingredients (contentChunk, repeating) ← Quantised Bridge
│ ├── Quantity (Numeric)
│ ├── Unit (Selection: g, ml, pcs, tbsp, tsp, cup)
│ └── Ingredient (Item Relations → ingredient, acceptedShapeIdentifiers: ["ingredient"])
├── Instructions (Paragraph Collection)
└── Allergens (Item Relations → allergen docs, computed from ingredients)
Product Shape: Meal Kit
├── Description (Rich Text)
├── Recipes (contentChunk, repeating) ← Quantised Bridge
│ ├── Servings (Numeric)
│ └── Recipe (Item Relations → recipe, acceptedShapeIdentifiers: ["recipe"])
└── Dietary Tags (Item Relations → dietary docs)
Document Shape: Allergen
├── Icon (Images)
├── Warning Text (Rich Text)
└── Severity (Selection: mild, moderate, severe)
Piece: nutrition
├── Calories (Numeric, unit: kcal)
├── Protein (Numeric, unit: g)
├── Carbs (Numeric, unit: g)
└── Fat (Numeric, unit: g)

This pattern enables: ingredient → recipe → meal kit composition with quantity tracking at each level, automatic allergen rollup, and nutrition calculation.

Electronics: Product with configuration options, compatibility rules, technical specs, brand classifications

Music/Instruments: Guitar store example using Semantic Bridge pattern:

Product Shape: Guitar
├── Description (Rich Text)
├── Body Type (Item Relations → body-type docs, acceptedShapeIdentifiers: ["body-type"])
├── Pickup Configuration (Item Relations → pickup docs, acceptedShapeIdentifiers: ["pickup"])
├── Wood Type (Item Relations → wood docs, acceptedShapeIdentifiers: ["wood"])
├── Brand (Item Relations → brand doc, acceptedShapeIdentifiers: ["brand"], min:1, max:1)
└── SEO (Piece → seo)
Classification Documents:
├── body-type: Stratocaster, Les Paul, Telecaster, SG...
├── pickup: Single Coil, Humbucker, P90, Active...
├── wood: Alder, Mahogany, Maple, Rosewood...
└── brand: Fender, Gibson, PRS, Ibanez...

This pattern allows rich content for each classification (e.g., brand logo, wood tone characteristics) while maintaining type safety through acceptedShapeIdentifiers.

B2B: Service packages with tiers, case studies, industry classifications, expertise levels

Publishing: Books with contributors (composite bridge), series, publishers, multi-format variants

See Content Modelling Guide for complete industry-specific patterns.


Reference Details

Components Reference

Components are the dynamic fields you add to a Shape. They sit on top of built-in fields that every item already has (name, id, language, tree, topics, createdAt, updatedAt). Product variants also have built-in fields for sku, images, videos, price, stock, and attributes — these are NOT components.

Choosing the right component for each piece of additional data determines how editors interact with it, how it renders on the frontend, and whether it can be filtered/searched via the Discovery API.

Decision Guide: Which Component to Use

”I need to store text”

ScenarioComponent
Short, single-value text (subtitle, GTIN, EAN, MPN, ISBN, URL)Single Line
Formatted content with headings, links, lists, boldRich Text
Long-form editorial with mixed media sectionsParagraph Collection
  • Single Line → structured, unformatted, indexable. Ideal for labels, codes, and short metadata. Note: item name and variant sku are built-in fields, not Single Line components — but additional identifiers (GTIN, EAN, UPC, MPN) beyond the built-in SKU are valid Single Line components.
  • Rich Text → formatted text block. Produces structured JSON (not HTML). Supports headings, lists, links, bold, italic, inline code. Use for product descriptions, marketing copy, specification text.
  • Paragraph Collection → repeating sections, each with: title (single line) + body (rich text) + media (images/video). Use for blog posts, editorial storytelling, landing page content blocks, any content that alternates text and media.

”I need to store numbers”

ScenarioComponent
A measurable value with a unit (weight, length, power)Numeric
Arbitrary key-value specs (varied, not fixed)Properties Table
  • Numeric → stores a number with one or more selectable units (e.g. g, mg, kg for weight; cm, m, in for length). Editors pick which unit applies when entering a value. Preferred when the metric is known and fixed — enables Discovery API filtering and sorting. Create one Numeric component per metric (e.g., weight, length, power).
    • Units config: define the list of valid unit options in config.numeric.units as a string array — e.g. ["g", "mg", "kg"], ["cm", "m", "in"], ["W", "kW"]. Editors can then select from these units when entering values. Leave empty if the unit is fixed/implied.
    • Example: { "id": "weight", "name": "Weight", "type": "numeric", "config": { "numeric": { "units": ["g", "mg", "kg"] } } }
  • Properties Table → key-value pairs in table format. Use when keys are arbitrary or user-defined. Does NOT enable typed filtering in Discovery. Use for: technical specification sheets where keys vary per product.
    • Fixed keys (configured in shape): Keys are predefined in the shape definition. Editors fill in values for preset keys. Useful when you know the specification names upfront but prefer a table layout over individual Numeric components.
    • Fluid/Dynamic keys (defined at content entry): Editors define both keys AND values when adding content. Useful for flexible technical specifications where key names vary by product category or are unpredictable.

Rule of thumb: If you always know the key name upfront → use Numeric. If the keys vary per item → use Properties Table.

”I need to store media”

ScenarioComponent
Product photos, banners, galleriesImages
Product demos, tutorials, promo videosVideos
Downloadable files (PDFs, ZIPs, etc.)Files
  • Images → auto-transcoded to responsive sizes (Avif, WebP), served via Crystallize CDN. Supports multiple images per component (galleries, carousels). Each image gets url, variants (with width/height/size), altText, caption. Note: product variant images are built-in — use an Images component for product-level lifestyle photos, document hero images, or other non-variant media.
  • Videos → auto-transcoded for web/mobile streaming, CDN-delivered. Upload or embed.
  • Files → upload files for download. Use for manuals, datasheets, certificates, whitepapers.
ScenarioComponent
Link to another catalogue item (product, document, folder)Item Relation
Link to a curated grid of itemsGrid Relation
  • Item Relation → the backbone of all bridge design patterns. Links to one or more items. On product shapes, can link to specific variants (SKU-level). Use for: related products, brand references, ingredients, accessories, any semantic relationship.
    • Configuration options (like relational database constraints):
      • Accepted shape identifiers: restrict which shapes can be linked (e.g., only allow “Brand” documents, or only “Product” shapes)
      • Min relations: minimum number of items that must be linked (e.g., every product must have at least 1 brand)
      • Max relations: maximum number of items that can be linked (e.g., product can link to max 5 related products)
    • These constraints enforce data integrity and guide editors to create consistent relationships.
  • Grid Relation → links to a curated grid. Use for: seasonal landing pages, campaign highlights, curated collections.

When choosing between Item Relation and Topic Maps for classification:

  • Item Relation when the classification value needs its own rich content (description, images, metadata)
  • Topic Maps when classification is just labels for navigation/filtering

”I need a controlled set of options”

ScenarioComponent
Predefined dropdown or multi-select (color, size, material)Selection
Boolean flag (featured, in-stock, certified)Switch
A date or timestampDatetime
Geographic coordinatesLocation
  • Selection → dropdown, radio buttons, or checkboxes with predefined options. Each option has a key (API value) and value (display label). Configure min/max selections to control behavior:
    • Radio/Enum pattern: min=1, max=1, required → exactly one selection, renders as radio buttons or dropdown
    • Checkboxes pattern: min=0, max=N → optional multi-select, renders as checkboxes, can be null Use when the options are a fixed, small set that don’t need their own content. If options need enrichment (images, descriptions), use Item Relations instead (Semantic Bridge pattern).
    • API option structure: { key: "red", value: "Red", isPreselected?: Boolean }key is used in queries/filters, value is what editors see
  • Switch → true/false toggle. Use for: “Featured”, “On Sale”, “Available in store”, “Organic certified”.
  • Datetime → date with optional time (type: datetime). Use for: launch date, preorder availability, expiration, event scheduling.
  • Location → latitude/longitude. Use for: store locator, pickup points, product origin.

”I need to group or structure fields”

ScenarioComponent
Group related fields that repeat together (ingredient + quantity + unit)Chunk
One of N mutually exclusive field sets (image hero OR video hero)Choice
Multiple of N field sets that can coexist (physical attrs AND digital)Multiple Choice

These are structural components — they don’t store data directly but organize other components:

  • Chunk (contentChunk) → a group of components that belong together (repeatable OR single occurrence)
    • Can be repeatable: Enable repeatable: true to create lists (ingredients, specifications, addresses)
    • Can be single: Use repeatable: false (or omit) for one-time groups that only occur once in the model
    • When to use chunk: Fields are tightly coupled and specific to one location in your content model
    • Examples (repeatable): Ingredients list, multiple addresses, product specifications, USP items
    • Examples (non-repeatable): SEO metadata (title+description+keywords as one group), single shipping address, environment settings
    • Decision rule: If this exact group structure only appears once in the entire content model → use a non-repeatable chunk instead of creating a piece
    • Building block for all bridge patterns (semantic, quantised, conditional, composite)
    • A chunk must always have at least 1 child component — empty chunks are invalid.
  • Choice (choice, componentChoice) → select exactly ONE structure from a set. The editor picks which form applies, and only sees fields for that choice. Implements the Polymorphic Choice pattern. Use when a concept has multiple valid but mutually exclusive representations.
  • Multiple Choice (componentMultipleChoice) → select ONE OR MORE structures from a set. Similar to Choice but allows combining forms. Use when multiple structural forms can coexist on the same item.
    • Common pattern: Define each option as a Piece (reusable component group) for consistency across shapes
    • Validation: allowDuplicates (API field) — when false, each choice can only be selected once; when true, editors can add the same choice multiple times (e.g., two Banner sections on a page)
    • Use cases:
      • Page builders: Multiple Choice of section Pieces (Banner, Testimonials, Product Grid, Text Block) — editors add/remove/reorder sections
      • Polymorphic products: Products that are similar but details vary — add fragmented Pieces (Physical Attributes, Digital License, Subscription Terms) that editors select based on product type
      • Flexible content: Any scenario where an item can have multiple coexisting structural forms

Critical Nesting Rule for Structural Components

Structural components (contentChunk, componentChoice, componentMultipleChoice) can ONLY have pieces or regular components as direct children.

They CANNOT have other structural components as direct children:

  • ❌ componentChoice cannot contain componentChoice/componentMultipleChoice/contentChunk
  • ❌ componentMultipleChoice cannot contain componentChoice/componentMultipleChoice/contentChunk
  • ❌ contentChunk cannot contain componentChoice/componentMultipleChoice/contentChunk

Valid direct children of componentChoice / componentMultipleChoice:

  • ✅ Regular components: images, videos, singleLine, richText, numeric, files, boolean, selection, itemRelations, etc.
  • ✅ Piece references: { "type": "piece", "config": { "piece": { "identifier": "..." } } } — the piece MUST exist as a separate piece/create operation

Invalid direct children (NEVER allowed):

  • contentChunk — structural, cannot be a direct child
  • componentChoice — nested structural, cannot be a direct child
  • componentMultipleChoice — nested structural, cannot be a direct child

Concrete examples — componentChoice with media options:

// ✅ CORRECT — choices are regular component types (images, videos)
{
"id": "background-media",
"name": "Background Media",
"type": "componentChoice",
"config": {
"componentChoice": {
"componentConfigs": [
{ "id": "image", "name": "Image", "type": "images" },
{ "id": "video", "name": "Video", "type": "videos" }
]
}
}
}
// ✅ CORRECT — choices are piece references (pieces MUST be created separately)
{
"id": "product-type",
"name": "Product Type",
"type": "componentChoice",
"config": {
"componentChoice": {
"componentConfigs": [
{ "id": "smartphone", "type": "piece", "config": { "piece": { "identifier": "smartphone-spec" } } },
{ "id": "laptop", "type": "piece", "config": { "piece": { "identifier": "laptop-spec" } } }
]
}
}
}
// + separate piece/create operations for "smartphone-spec" and "laptop-spec" MUST also be in the operations array
// ❌ WRONG — contentChunk as a direct choice option
{
"id": "product-type",
"type": "componentChoice",
"config": {
"componentChoice": {
"componentConfigs": [
{ "id": "smartphone", "name": "Smartphone", "type": "contentChunk", "components": [...] }
]
}
}
}
// ❌ WRONG — anonymous inline component group (no `type` field, `components` array directly on choice)
// THIS IS THE MOST COMMON MISTAKE. The `components` key does not exist on a choice item.
// Every choice item MUST have a `type` field — there is no such thing as an "anonymous group" choice.
{
"id": "product-type",
"type": "componentChoice",
"config": {
"componentChoice": {
"choices": [
{
"id": "smartphone",
"name": "Smartphone",
"components": [
{ "id": "screen-size", "type": "numeric" },
{ "id": "storage", "type": "numeric" }
]
// ❌ Missing "type" field — this is INVALID. `components` is not a valid key on a choice item.
}
]
}
}
}

Rule: if each choice needs multiple fields grouped together, create a separate piece/create operation for each choice and reference it. NEVER use contentChunk as a choice option. NEVER put a components array directly on a choice item.

Polymorphic Choice with per-variant fields (CORRECT approach):

When building a “Product Type” selector where each type (Smartphone, Laptop, Headphones) has its OWN set of fields, you MUST:

  1. Create a piece/create operation for each variant
  2. Reference those pieces in the componentChoice
// ✅ CORRECT — each choice variant has its own piece with the right fields
// Step 1: Create pieces for each variant
{ "intent": "piece/create", "identifier": "smartphone-spec", "name": "Smartphone Spec", "components": [
{ "id": "screen-size", "name": "Screen Size", "type": "numeric", "config": { "numeric": { "units": ["inch"] } } },
{ "id": "storage", "name": "Storage", "type": "numeric", "config": { "numeric": { "units": ["GB", "TB"] } } },
{ "id": "os", "name": "Operating System", "type": "selection", "config": { "selection": { "options": [{ "key": "ios", "value": "iOS" }, { "key": "android", "value": "Android" }] } } }
] }
{ "intent": "piece/create", "identifier": "laptop-spec", "name": "Laptop Spec", "components": [
{ "id": "screen-size", "name": "Screen Size", "type": "numeric", "config": { "numeric": { "units": ["inch"] } } },
{ "id": "ram", "name": "RAM", "type": "numeric", "config": { "numeric": { "units": ["GB"] } } },
{ "id": "processor", "name": "Processor", "type": "singleLine" }
] }
// Step 2: Reference them in componentChoice
{
"id": "product-type",
"name": "Product Type",
"type": "componentChoice",
"config": {
"componentChoice": {
"choices": [
{ "id": "smartphone", "name": "Smartphone", "type": "piece", "config": { "piece": { "identifier": "smartphone-spec" } } },
{ "id": "laptop", "name": "Laptop", "type": "piece", "config": { "piece": { "identifier": "laptop-spec" } } }
]
}
}
}

To nest structural components inside choices, use pieces as intermediaries:

// The piece itself can then contain contentChunk
{
"intent": "piece/create",
"identifier": "smartphone-spec",
"name": "Smartphone Spec",
"components": [
{
"type": "contentChunk", // ✓ allowed inside a piece
"components": [...]
}
]
}

Nesting Depth Limits for Structural Components

Structural components (Chunk, Piece, Choice, Multiple Choice) can be nested, but only up to 4 levels deep. At level 4, you can only use non-structural components (Single Line, Rich Text, Numeric, etc.).

Valid nesting patterns:

Level 1: Chunk OR Choice/Multiple Choice
└─ Level 2: Piece
└─ Level 3: Choice/Multiple Choice OR Piece OR Chunk
└─ Level 4: Piece OR Choice/Multiple Choice OR Chunk
└─ Level 5: Only non-structural components allowed
(Single Line, Rich Text, Numeric, Images, etc.)

Example — Maximum nesting depth:

Product Shape
└─ Level 1: page-sections (Chunk[], repeating)
└─ Level 2: section (Piece - e.g., "Hero Section")
└─ Level 3: hero-variant (Choice)
├─ Image Hero
│ └─ Level 4: cta-group (Piece)
│ └─ Level 5: headline (Single Line) ✓
│ └─ Level 5: button-url (Single Line) ✓
└─ Video Hero
└─ Level 4: video-options (Chunk)
└─ Level 5: video (Videos) ✓
└─ Level 5: autoplay (Switch) ✓

Why this limit exists:

  1. Performance — Deep nesting impacts query performance and API response times
  2. Complexity — Beyond 4 levels, content structures become hard to maintain and understand
  3. Editor UX — Deeply nested structures create confusing editing interfaces

Best practices:

  • Most use cases need only 2-3 levels (e.g., Multiple Choice → Piece → components)
  • If you hit the 4-level limit, consider flattening your structure or using Item Relations instead of nested components
  • Level 4 is for final structural organization before actual data fields

Multilingual & Localization

Every component has an isTranslatable flag that determines whether content can vary by language or is shared across all languages. This is critical for multi-market catalogues where some data is universal (GTINs, dimensions) while other data needs localization (descriptions, marketing copy).

How Localization Works

  • Languages are configured at tenant level (Settings → Languages) with unique codes (ISO 639-1 recommended: en, de, fr, no-nb, se, but not required)
  • Each component has an isTranslatable configuration you set when designing shapes
  • Editors switch between languages in the item edit view to add translated content
  • isTranslatable = true → component content can vary per language (localized)
  • isTranslatable = false → component content is shared across all languages (universal)
  • Built-in name field is translatable and creates the path leaf in the catalogue tree — if not defined for a language, you’ll see MISSING_NAME_FOR_ITEM as placeholder
  • Query language is specified at the top level of Catalogue API and Discovery API queries

Default Localization Settings by Component

Different components have different default isTranslatable values based on their typical use cases:

ComponentDefault isTranslatableTypical use case
Single Line✓ TrueTitles, subtitles, slogans, taglines
Rich Text✓ TrueDescriptions, marketing copy, specifications
Paragraph Collection✓ TrueBlog posts, landing pages, editorial content
Numeric✗ FalseDimensions, weights, ratings (universal values)
Properties Table✗ FalseTechnical specs with shared keys/values
Images✗ FalseProduct photos (same images, translate captions)
Videos✗ FalseProduct videos (same videos, translate captions)
Files✗ FalseShared downloads (or create separate per-market)
Selection✗ FalseShared options (size, color codes)
Switch✗ FalseBoolean flags (universal true/false)
Datetime✗ FalseUniversal dates (launch, expiration)
Location✗ FalseGeographic coordinates (universal)
Item Relation✗ FalseRelationships are shared, but related items can be translated
Grid Relation✗ FalseGrid references shared, grid content can be translated

Important: These are defaults when adding components. You can override them based on your specific use case.

Structural Components (Choice & Multiple Choice)

Choice and Multiple Choice components have special translation behavior:

When the structural component is NOT translatable:

  • The same choices are used across all languages (universal structure)
  • Components and Pieces INSIDE each choice CAN be translatable
  • Editors see the same choice options in all languages, but content within choices can vary

Example:

Product Shape
└── product-type (Multiple Choice, NOT translatable)
├── Physical Product (Piece)
│ ├── weight (Numeric, NOT translatable) → 1.2 kg (universal)
│ └── description (Rich Text, translatable) → Localized per language
└── Digital Product (Piece)
├── file-size (Numeric, NOT translatable) → 250 MB (universal)
└── description (Rich Text, translatable) → Localized per language

In this example, the choice between “Physical Product” and “Digital Product” is the same across all languages, but the descriptions inside can be translated.

When the structural component IS translatable:

  • The choices themselves become translatable (different choice options per language)
  • All components inside are ALWAYS translatable (cannot be shared)
  • This is uncommon — only use when the structure itself varies by market

Best practice: Keep Choice/Multiple Choice NOT translatable. Translate the content inside the choices instead. This maintains consistent structure across markets while allowing localized content.

Paragraph Collection Granular Control

Paragraph Collection allows per-field localization control. You can configure which parts should be translatable:

  • Title → typically translatable (headlines vary by market)
  • Body → typically translatable (content varies by market)
  • Images → typically shared (same image, maybe translate alt text)
  • Videos → typically shared (same video, maybe translate captions)

This granularity is useful for blog posts or editorial content where media is universal but text is localized.

Operations Config for paragraphCollection

CRITICAL: The multilingual property is REQUIRED (not optional) for paragraphCollection. It must be an array of paragraph part names that should support translations (lowercase), NOT language codes.

Valid values: "body", "title", "images", "videos", "structure"

Examples:

  • Everything localized: ["title", "body", "videos", "images"]
  • Only text localized: ["title", "body"]
  • Nothing localized (universal): []
{
"id": "body",
"name": "Body Content",
"type": "paragraphCollection",
"config": {
"paragraphCollection": {
"multilingual": ["title", "body", "images", "videos"]
}
}
}

Common mistakes:

  • Omitting multilingual entirely → Error: “expected array, received undefined”
  • Using language codes like ["en"] → Error: Invalid option: expected one of "body"|"images"|"title"|"videos"|"structure"
  • Using capitalized names like ["Title", "Body"] → Must use lowercase: ["title", "body"]

Localized vs Shared Content Patterns

When to use localized (isTranslatable = true):

  • Marketing copy and storytelling (titles, descriptions, taglines)
  • Editorial content (blog posts, articles, guides)
  • Customer-facing text that varies by culture or market
  • Legal disclaimers that must be in local language
  • SEO metadata (meta descriptions, page titles)

When to use shared (isTranslatable = false):

  • Universal product identifiers (GTIN, EAN, MPN, internal SKUs)
  • Technical specifications with numeric values (weight, dimensions, voltage)
  • Product codes and reference numbers
  • Structural/classification properties (material codes, category IDs)
  • Universal dates (product launch date, manufacturing date)
  • Geographic data (coordinates, store locations)

Mixed pattern example (common for products):

Product Shape
├── title (Single Line, translatable) → "Leather Jacket" / "Veste en Cuir"
├── description (Rich Text, translatable) → Localized marketing copy
├── gtin (Single Line, NOT translatable) → "1234567890123" (universal)
├── weight (Numeric, NOT translatable) → 1.2 kg (universal)
├── material (Item Relation → Material doc, NOT translatable)
│ └── Material document itself HAS translatable fields
│ ├── name (translatable) → "Leather" / "Cuir"
│ └── description (translatable) → Care instructions in each language
└── care-instructions (Rich Text, translatable) → Localized guidance

Semantic Bridge Pattern with Translations

The Semantic Classification Bridge pattern becomes extremely powerful with translations:

  • The Item Relation component itself is NOT translatable (relationship is universal)
  • The related document (Brand, Material, Certification) CAN have translatable components
  • Result: One relationship, many translated classification values

Example — Material classification:

Product Shape
└── materials (Chunk[], repeating, NOT translatable)
└── material (Item Relation → Material documents, NOT translatable)
Material Document Shape
├── name (Single Line, translatable)
│ └── en: "Organic Cotton" / fr: "Coton Biologique" / de: "Bio-Baumwolle"
├── description (Rich Text, translatable)
│ └── Localized care instructions, sustainability story
├── certification-logo (Images, NOT translatable)
│ └── Same logo image across all languages
└── material-code (Single Line, NOT translatable)
└── "MAT-001" (universal internal code)

Why this works:

  • Product links to Material once (universal relationship)
  • Material renders in customer’s language automatically
  • Update Material translation once, affects all products using it
  • Structural integrity maintained (same relationships across languages)

API Query Behavior

When querying the Catalogue API or Discovery API, you specify the desired language using the language parameter. This parameter can be passed:

  1. At the top level of queries (most common):

    • catalogue(path: "/shop/chair", language: "en")
    • grid(id: "...", language: "en")
    • topics(name: "Brands", language: "en")
    • productVariants(skus: [...], language: "en")
  2. On specific relationship fields (for nested language queries):

    • Image.topics(language: "en") — get topic associations in a specific language
    • ImageShowcase.items(language: "en") — get showcase items in a specific language
    • PriceList.products(language: "en") — get products from price list in a specific language

Example query with language parameter:

query GetProduct($path: String!, $language: String!) {
catalogue(path: $path, language: $language) {
name
... on Product {
variants {
sku
name
price
}
# Component fields
description {
plainText
}
# Relationship components can specify language
brand {
name # Automatically uses parent query language
}
}
}
}

For non-translatable components:

  • The default language’s content is returned for all queries
  • Ensures universal data (GTINs, dimensions, dates) is consistent across markets

For translatable components:

  • If translation exists for the requested language → returns that translation
  • If translation does NOT exist for the requested language → returns empty (null/empty string)
  • No automatic fallback to default language — Crystallize does not implement fallback logic
  • Fallback strategy is the storefront’s responsibility during development

Common storefront fallback strategies:

// Frontend decides how to handle missing translations
const title = product.title || product.defaultLanguageTitle || "Untitled";
const description = product.description || product.defaultLanguageDescription || "";

This gives you full control over fallback behavior based on your business rules (show default language? show “translation missing”? hide the field?).

Planning for Production

Critical: Changing isTranslatable settings after content is created has side effects.

When switching from localized to shared:

  • All language variations are discarded
  • Content defaults back to the default language value
  • All markets will see the same content (from default language)
  • This is irreversible — translations are permanently lost

When switching from shared to localized:

  • Existing shared content becomes the default language value
  • All non-default languages start empty (need new translations)
  • Editors must add translations for each market

Best practices:

  1. Plan translation strategy during shape design — decide which components need localization before creating content
  2. Structural components rarely need translation — properties, classifications, technical specs usually shared
  3. Test with 2-3 languages early — verify your translation strategy works before scaling to many markets
  4. Document your decisions — explain why certain components are/aren’t translatable for future maintainers
  5. Use Semantic Bridges for reusable translations — translate classification documents once, reuse everywhere
  6. Avoid changing isTranslatable in production — if needed, create new components and migrate content gradually

Localization Examples by Use Case

Blog post (maximum localization):

Blog Post Document
├── hero-image (Images, NOT translatable) → Same hero across languages
├── title (Single Line, translatable) → Localized headlines
├── author (Item Relation, NOT translatable) → Same author
│ └── Author document HAS translatable bio
├── publish-date (Date, NOT translatable) → Universal date
├── content (Paragraph Collection, translatable)
│ ├── title: translatable
│ ├── body: translatable
│ ├── images: shared
│ └── videos: shared
└── topic (Item Relation → Topic docs, NOT translatable)
└── Topic docs HAVE translatable names/descriptions

Technical product (minimal localization):

Industrial Equipment Product
├── model-number (Single Line, NOT translatable) → "XJ-3000"
├── marketing-name (Single Line, translatable) → "PowerFlow Pro" / "PowerFlow Pro"
├── dimensions (Chunk, NOT translatable)
│ ├── width (Numeric, NOT translatable) → 50 cm
│ ├── height (Numeric, NOT translatable) → 100 cm
│ └── depth (Numeric, NOT translatable) → 30 cm
├── voltage (Selection, NOT translatable) → "220V" (universal)
├── safety-certification (Item Relation, NOT translatable)
│ └── Certification doc HAS translatable descriptions
├── description (Rich Text, translatable) → Localized marketing copy
└── manual (Files, NOT translatable OR create separate per language)

Component Selection Flowchart

Is the field already built-in (name, SKU, price, stock, images, videos, attributes)?
→ Yes → No component needed — these are built-in on items/variants
Is the data a short text value?
→ Yes → Single Line
Is the data formatted/styled text?
→ Yes, single block → Rich Text
→ Yes, multi-section with media → Paragraph Collection
Is the data a number with a known metric?
→ Yes → Numeric (with unit)
Is the data arbitrary key-value pairs?
→ Yes → Properties Table
Is the data media (images, video, files)?
→ Images → Images
→ Video → Videos
→ Downloads → Files
Is the data a reference to another item?
→ Yes, to catalogue items → Item Relation
→ Yes, to a curated grid → Grid Relation
Is the data a choice from fixed options?
→ Yes, string labels → Selection
→ Yes, true/false → Switch
→ Yes, a date → Date
→ Yes, a location → Location
Does the data need grouping/structure?
→ Repeating group of fields → Chunk
→ One-of-many exclusive forms → Choice
→ Mix-and-match forms → Multiple Choice

Discoverability Rules

The discoverable flag determines whether a component is indexed in the Discovery API at all. This is not just about filtering — if a component is NOT marked discoverable, it will be completely absent from Discovery API responses.

Discoverable = true:

  • Component data is indexed and returned in Discovery API queries
  • Component becomes available as a filter/sort/facet field
  • Generated field name follows the convention: {componentId}_{subfield}_{type}

Discoverable = false:

  • Component data is NOT indexed in Discovery API
  • Component will NOT appear in Discovery API responses at all
  • Data is still available via Catalogue API and other APIs

When to Mark Components as Discoverable

Mark as discoverable when:

  • Customers need to filter/search by this field (color, size, brand, price range)
  • You want to sort results by this field (weight, rating, date)
  • You need faceted navigation on this field (categories, materials, features)
  • The component contains structured data relevant to product discovery

Examples:

  • Numeric components (price, weight, dimensions) → enables range filters
  • Selection components (color, size, material) → enables faceted filtering
  • Single Line (model number, GTIN) → enables exact-match filtering
  • Switch (featured, on-sale) → enables boolean filtering
  • Item Relations (brand, category) → enables filtering by related items

Do NOT mark as discoverable when:

  • Data is for internal use only and should not be exposed in public APIs (external system IDs, integration keys, internal workflow flags)
  • Content contains sensitive organizational information not meant to be shared outside the organization
  • Fields are used purely for backend integrations and should never appear on websites or customer-facing applications
  • Data is temporary or administrative (import status, internal notes, migration flags)

Common examples of non-discoverable fields:

  • external-system-id (Single Line) — ERP/CRM reference, internal only
  • internal-notes (Rich Text) — admin comments, not for public display
  • migration-status (Selection) — data migration tracking
  • integration-key (Single Line) — third-party system reference
  • internal-workflow-stage (Selection) — editorial workflow state
  • erp-product-code (Single Line) — back-office system integration
  • warehouse-location (Single Line) — internal logistics data

Typical use case: System integrations. When you integrate with ERP, CRM, WMS, or other backend systems, you often need to store reference IDs and integration metadata on catalogue items. Mark these fields as non-discoverable so they’re available in Core/PIM/Catalogue APIs for your integrations, but never exposed in the customer-facing Discovery API used by your website.

Remember: non-discoverable does NOT mean “hidden” — the data is still available in Core API, PIM API, and Catalogue API for authenticated backend use. Use permissions and access control for true security. The discoverable flag only controls whether data appears in the Discovery API (which is typically the public-facing search/filter API used on websites).

Discovery vs Catalogue API

  • Discovery API = fast search/filter engine with async indexing. Only includes discoverable component data. Eventually consistent — disconnected from database, updates propagate after a short delay.
  • Catalogue API = full content retrieval with direct database connection. Includes ALL component data regardless of discoverable flag. Always in sync — reflects changes immediately.

When to use each:

  • Discovery API — when you need fast search/filter/faceting and can tolerate eventual consistency (typically seconds to minutes). Works for listing pages AND product detail pages if real-time accuracy isn’t critical.
  • Catalogue API — when you need complete data or real-time accuracy. Essential for: admin interfaces, real-time inventory checks, editorial previews, integrations requiring guaranteed up-to-date data.

Important: Adding or removing discoverable components requires re-igniting the Discovery API for the schema changes to take effect. Plan discoverability at shape design time to minimize re-ignitions.

Component Validations

Every component type supports validation rules to enforce data integrity and guide editors. These act like database constraints, ensuring content quality and consistency.

Text Components

ComponentAvailable validations
Single LineMin/max character length, regex pattern, required/optional
Rich TextMin/max character length, required/optional
Paragraph CollectionMin/max number of paragraphs, required/optional

Example use cases:

  • Single Line with regex: enforce URL format, validate email addresses, require specific SKU patterns
  • Rich Text min length: ensure product descriptions are at least 100 characters
  • Paragraph Collection min: require at least 3 sections for blog posts

Numeric Components

ComponentAvailable validations
NumericMin/max value, decimal places, required/optional
Properties TableMin/max number of rows, required/optional

Example use cases:

  • Numeric min/max: weight must be between 0.1kg and 500kg, rating 1-5
  • Numeric decimal places: price to 2 decimals, weight to 3 decimals
  • Properties Table min rows: require at least 5 specifications per product

Media Components

ComponentAvailable validations
ImagesMin/max count, max file size, min/max dimensions, required
VideosMin/max count, max file size, max duration, required
FilesMin/max count, max file size, allowed file types, required

Example use cases:

  • Images: require at least 3 product photos, max 10, each under 5MB, min 800px wide
  • Videos: max 1 demo video, under 100MB, under 3 minutes
  • Files: allow only PDF/DOCX for datasheets, max 10MB per file

Selection Components

ComponentAvailable validations
SelectionMin/max selections, required/optional
SwitchNo validations (always boolean true/false)
DatetimeMin/max date range, required/optional
LocationRequired/optional

Example use cases:

  • Radio/Enum pattern: min=1, max=1, required → “Select a size” (S, M, L, XL) — exactly one required
  • Checkboxes pattern: min=0, max=5 → “Select up to 5 dietary preferences” — optional multi-select, can be null
  • Required multi-select: min=1, max=3 → “Select 1-3 colors” — at least one required, up to 3 allowed
  • Preselected defaults: Set isPreselected: true on commonly chosen options to prefill the selection for editors. Useful for default sizes, standard shipping modes, or common categories. Multiple options can be preselected.
  • Date range: launch date must be in the future, expiration within 2 years

Relationship Components

ComponentAvailable validations
Item RelationMin/max relations, accepted shape identifiers, required
Grid RelationMin/max grids, required/optional

Example use cases:

  • Item Relation: every product must link to exactly 1 brand (min=1, max=1, shapes=[“Brand”])
  • Item Relation: product can have 0-5 related products (min=0, max=5, shapes=[“Product”])
  • Grid Relation: landing page must feature at least 2 curated collections

Structural Components

ComponentAvailable validations
ChunkMin/max items (for repeating chunks), required
ChoiceRequired/optional
Multiple ChoiceMin/max choices selected, allowDuplicates (allow same choice multiple times), required/optional

Example use cases:

  • Chunk repeating: recipe must have at least 3 ingredients, kit must have 1-10 components
  • Multiple Choice for page builder: min=1 (at least one section), allowDuplicates: true (can repeat Banner section multiple times)
  • Multiple Choice for polymorphic product: min=1, max=3, allowDuplicates: false (product must have 1-3 detail Pieces, each unique)

Validation Best Practices

  1. Start lenient, tighten later — it’s easier to add validations than remove them after content is created
  2. Use validations to prevent common errors — regex for SKU format, min/max for ratings, required for critical fields
  3. Guide editors with clear validation messages — “Product description must be at least 100 characters to ensure quality”
  4. Combine with discoverability — fields used for filtering should have validations ensuring data consistency
  5. Test validations during shape design — create test items to verify validation rules work as intended

Pieces vs Chunks: When to Use Each

Decision Flowchart

Do you need to group fields together?

  1. Will this exact group be used in 2+ locations? (multiple shapes OR multiple pieces)

    • YES → Use a Piece (reusable)
    • NO → Continue to step 2
  2. Will this group repeat multiple times in the same location? (like a list)

    • YES → Use a Chunk with repeatable: true
    • NO → Use a Chunk with repeatable: false (or omit repeatable property)

Examples

ScenarioSolutionReasoning
SEO fields needed on products, articles, and landing pagesPieceUsed in 3+ shapes
Banner section used on landing pages and category pagesPieceUsed in 2+ shapes
Product ingredients (flour, sugar, eggs)Chunk (repeatable)List of items
Multiple shipping addressesChunk (repeatable)List of addresses
One-time metadata group (creation date + author + tags) on a single shapeChunk (non-repeatable)Only used once, not repeating
Contact info (email + phone + address) appearing only on About pageChunk (non-repeatable)Only used in one place, single occurrence
Video settings (autoplay + muted + loop) for a video componentChunk (non-repeatable)Single configuration group

Key Principles

  • Pieces are for reuse — same structure, multiple locations
  • Chunks are for grouping — related fields that belong together, whether repeating or not
  • Avoid unnecessary pieces — if a group only appears once, use a chunk (even non-repeatable)
  • Repeatable is optional — chunks don’t have to repeat; use repeatable: false for single-occurrence groups

Pieces: Reusable Component Groups

A Piece is a named collection of components that can be embedded into any shape. Use pieces when the same group of fields appears in multiple shapes.

When to create a Piece

  • The same 3+ components appear in 2 or more shapes OR pieces
  • You want to update the structure once and have it reflect everywhere
  • The group represents a distinct concept that will be reused (SEO, CTA, environmental data)

When NOT to create a Piece

  • The group only appears once in your entire content model → use a non-repeatable chunk instead
  • The fields don’t form a cohesive unit → keep as separate components
  • It’s just for organization without reuse → use a chunk

Common Piece patterns

Piece nameComponents insideUsed in
SEO Metadatameta-title (Single Line), meta-description (Rich Text), og-image (Images)All product, document, folder shapes
CTA Blockheadline (Single Line), body (Rich Text), button-label (Single Line), button-url (Single Line)Landing pages, category folders
Environmental Datacarbon-footprint (Numeric, kg CO₂), recyclable (Switch), certifications (Item Relation)Product shapes
Social Linkswebsite (Single Line), instagram (Single Line), twitter (Single Line)Author/brand document shapes

Page Builder Pattern

For flexible page builders where editors can add/remove/reorder sections, use Multiple Choice with Pieces:

Structure

Landing Page (Folder shape - folders can have children, documents cannot)
└── sections (Multiple Choice)
├── Banner (Piece)
│ ├── headline (Single Line)
│ ├── subheadline (Rich Text)
│ ├── background (Images)
│ └── cta (Chunk: label + url)
├── Testimonials (Piece)
│ ├── title (Single Line)
│ └── quotes (Chunk[], repeating)
│ ├── quote (Rich Text)
│ ├── author (Single Line)
│ └── photo (Images)
├── Product Grid (Piece)
│ ├── title (Single Line)
│ ├── products (Item Relation → Products)
│ └── columns (Selection: 2, 3, 4)
├── Text Block (Piece)
│ ├── title (Single Line)
│ ├── content (Rich Text)
│ └── alignment (Selection)
└── Video Section (Piece)
├── title (Single Line)
├── video (Videos)
└── caption (Rich Text)

Configuration

  • Min choices: 1 (page needs at least one section)
  • Max choices: unlimited (or set a limit like 20)
  • Only allow unique: false (editors can add multiple Banners or Text Blocks)
  • Required: yes

Why This Pattern Works

  1. Reusability — Pieces can be used across multiple page shapes (Landing Page, Category Page, etc.)
  2. Maintainability — Update a Piece once, changes reflect everywhere it’s used
  3. Flexibility — Editors compose pages by selecting which sections to include
  4. Repeatability — Same section type can appear multiple times (3 banners, 2 product grids)
  5. Order control — Frontend renders sections in the order editors selected them

Alternative: Paragraph Collection

For simpler page builders where all sections follow the same “title + body + media” structure, use Paragraph Collection instead. Use Multiple Choice + Pieces when section types have fundamentally different fields.

📖 Deep Dive: See page-builder-pattern.md for the complete Page Builder pattern reference — including the shared layout piece (per-block theming/width), localized structure for per-market merchandising, frontend rendering patterns, and a full architecture diagram based on the Furnitut reference implementation.

Polymorphic Product Pattern

For products that are similar but have varying details, use Multiple Choice with detail Pieces:

Structure

Product Shape
└── product-details (Multiple Choice)
├── Physical Attributes (Piece)
│ ├── dimensions (Chunk: width + height + depth)
│ ├── weight (Numeric, kg)
│ └── material (Item Relation → Material documents)
├── Digital License (Piece)
│ ├── license-type (Selection: Single-user, Multi-user, Enterprise)
│ ├── duration (Numeric, months)
│ └── download-link (Single Line)
├── Subscription Terms (Piece)
│ ├── billing-cycle (Selection: Monthly, Yearly)
│ ├── commitment (Numeric, months)
│ └── cancellation-policy (Rich Text)
└── Warranty Info (Piece)
├── duration (Numeric, years)
├── coverage (Rich Text)
└── terms-pdf (Files)

Configuration

  • Min choices: 1 (product must have at least one detail type)
  • Max choices: 3 (product can have multiple detail types but not all)
  • Only allow unique: true (each detail type can only be selected once)
  • Required: yes

Why This Pattern Works

  1. Avoid shape explosion — One product shape instead of separate shapes for Physical Product, Digital Product, Subscription Product
  2. Flexible combinations — A product can be physical + subscription, or digital + warranty
  3. Consistent editing — All products use the same shape, editors just select which details apply
  4. Type safety — Each detail type has its own validated fields

Content Modelling Guide

This guide provides comprehensive information on designing effective content models in Crystallize.

Understanding Built-in Fields vs Components

Every item in Crystallize has built-in fields that exist automatically:

Universal built-in fields (all items):

  • name - Item name (translatable, creates path in catalogue tree)
  • id - Unique identifier
  • language - Language code
  • tree - Catalogue tree position
  • topics - Topic classifications
  • createdAt - Creation timestamp
  • updatedAt - Last modification timestamp

Product variant built-in fields:

  • sku - Stock keeping unit (unique identifier)
  • images - Product photos
  • videos - Product videos
  • price - Pricing information
  • stock - Inventory levels
  • attributes - Additional variant attributes

Components are the additional, dynamic fields you add to shapes beyond these built-in fields. When designing shapes, you’re defining what extra information is needed beyond what Crystallize provides automatically.

Thinking Framework for Content Model Design

Start with Intent, Not Structure

Before creating shapes, ask:

  1. What are you selling/publishing? (products, articles, courses, etc.)
  2. How will customers discover it? (search, browse categories, filters)
  3. What makes each item unique? (specs, features, content)
  4. What’s shared across items? (brands, materials, classifications)
  5. How will it be presented? (product pages, grids, recommendations)

The Three-Layer Approach

Layer 1: Core Content (Shapes) The fundamental item types in your catalogue.

Products → Physical goods that can be purchased
Documents → Editorial content, marketing pages
Folders → Categories, collections, organizational structure

Layer 2: Classification (Topic Maps + Documents) How items are categorized and filtered.

Topic Maps → Simple, hierarchical classifications (flavor, color, size)
Classification Documents → Rich classifications that need content (brands, certifications, allergens)

Layer 3: Curation (Grids) Editorial selections independent of catalogue structure.

Grids → Homepage features, campaigns, seasonal collections

Decision Tree: Shape or Document?

Use a Product shape when:

  • Item can be purchased
  • Needs variants (size, color, subscription period)
  • Requires pricing, stock, SKUs
  • Needs cart/checkout integration

Use a Document shape when:

  • Content is editorial/informational
  • No purchasing required
  • Needs rich content with media
  • Part of classification system (brands, ingredients)

Use a Folder shape when:

  • Organizing other items hierarchically
  • Creating categories or collections
  • Building navigation structure

Industry-Specific Patterns

Fashion & Apparel

Shapes:

  • Product: Clothing Item
    • Variants: Size (XS-XXL), Color
    • Components: Material composition, care instructions, fit guide
    • Relations: Brand (document), Collection (document), Materials (documents)

Topic Maps:

  • Category: Tops, Bottoms, Shoes, Accessories
  • Season: Spring/Summer, Fall/Winter
  • Style: Casual, Formal, Sporty, Vintage

Classification Documents:

  • Brand: Logo, story, values, country of origin
  • Material: Description, care instructions, sustainability info
  • Collection: Theme, season, lookbook images

Food & Beverage

Shapes:

  • Product: Food Item
    • Variants: Size/Weight
    • Components: Nutrition facts, storage instructions, preparation
    • Relations: Allergens (documents), Ingredients (products), Certifications (documents)

Topic Maps:

  • Dietary: Vegan, Vegetarian, Gluten-Free, Organic
  • Meal Type: Breakfast, Lunch, Dinner, Snack
  • Cuisine: Italian, Asian, Mexican, Mediterranean

Classification Documents:

  • Allergen: Icon, severity level, regulatory info
  • Certification: Logo, issuing body, validity period
  • Ingredient: Nutrition data, origin, properties

Electronics & Technology

Shapes:

  • Product: Electronic Device
    • Variants: Configuration (storage, RAM, color)
    • Components: Technical specs (table), connectivity, dimensions
    • Relations: Compatible Accessories (products), Brand (document)

Topic Maps:

  • Category: Smartphones, Laptops, Tablets, Wearables
  • Operating System: iOS, Android, Windows, macOS
  • Connectivity: WiFi, Bluetooth, 5G, NFC

Classification Documents:

  • Brand: Logo, support info, warranty terms
  • Technology: Explanation, benefits, compatibility
  • Specification Standard: Definition, certification, compliance

B2B / Professional Services

Shapes:

  • Product: Service Package
    • Variants: Tier (Basic, Professional, Enterprise)
    • Components: Features list, deliverables, terms
    • Relations: Case Studies (documents), Industries (documents)

Topic Maps:

  • Industry: Healthcare, Finance, Retail, Manufacturing
  • Service Type: Consulting, Implementation, Support, Training
  • Expertise Level: Junior, Mid, Senior, Expert

Classification Documents:

  • Industry Vertical: Overview, challenges, solutions
  • Case Study: Client, challenge, solution, results
  • Certification: Credential, issuing body, validity

Publishing & Media

Shapes:

  • Product: Book/Course
    • Variants: Format (Hardcover, Paperback, eBook, Audio)
    • Components: Table of contents, sample chapter, reviews
    • Relations: Authors (documents), Publishers (documents), Series (documents)

Document: Article/Post

  • Components: Content blocks, author bio, related articles
  • Relations: Topics, Authors, Categories

Topic Maps:

  • Genre: Fiction, Non-Fiction, Biography, Technical
  • Difficulty: Beginner, Intermediate, Advanced, Expert
  • Language: English, Spanish, French, German

Classification Documents:

  • Author: Bio, photo, social links, bibliography
  • Publisher: Logo, description, catalogue
  • Series: Description, reading order, theme

Common Patterns by Use Case

E-commerce Fundamentals

Product with Variants (Size, Color)

Product Shape: Clothing
├── Description (Rich Text)
├── Material (Item Relation → Material documents)
├── Care Instructions (Rich Text)
├── Fit Guide (Piece)
└── Brand (Item Relation → Brand document)
Variant Components:
├── Size (built-in attribute)
├── Color (built-in attribute)
├── SKU (built-in)
└── Images (built-in)

Product Kits/Bundles

Product Shape: Gift Set
├── Description (Rich Text)
└── Contents (Chunk, repeating)
├── Product (Item Relation → Product)
├── Quantity (Numeric)
└── Customization Notes (Single Line)

Content-Heavy Sites

Blog with Categories and Tags

Document Shape: Blog Post
├── Hero Image (Images)
├── Content (Paragraph Collection)
├── Author (Item Relation → Author document)
├── SEO (Piece)
└── Related Posts (Item Relation → Blog Post)
Topic Maps:
- Category (hierarchical)
- Tags (flat)

Landing Pages with Sections

Folder Shape: Landing Page (folders can have children, documents cannot)
├── SEO (Piece)
└── Sections (Multiple Choice, repeating)
├── Hero Banner (Piece)
├── Feature Grid (Piece)
├── Testimonials (Piece)
├── CTA Block (Piece)
└── Product Showcase (Piece)

Product Configurators

Custom Furniture Builder

Product Shape: Configurable Furniture
├── Base Model (Item Relation → Base Product)
└── Options (Chunk, repeating)
├── Category (Selection: Material, Color, Hardware)
├── Choice (Item Relation → Option Product)
└── Price Modifier (Numeric)
Document Shape: Configuration Rule
├── Condition (Selection: requires, excludes, optional)
├── If Option (Item Relation → Option)
└── Then Option (Item Relation → Option)

Subscription Services

Tiered Subscription Plans

Product Shape: Subscription
Variants:
├── Plan Tier (attribute: Basic, Pro, Enterprise)
├── Billing Period (attribute: Monthly, Annual)
Components:
├── Features (Chunk, repeating)
│ ├── Feature Name (Single Line)
│ ├── Included (Boolean)
│ └── Limit (Numeric, optional)
└── Price Tiers (Paragraph Collection)

Multi-Language / Multi-Market

Global Product Catalogue

Product Shape: Global Product
├── Name (Single Line, translatable)
├── Description (Rich Text, translatable)
├── GTIN (Single Line, NOT translatable)
├── Dimensions (Numeric, NOT translatable)
├── Region Info (Chunk, repeating)
│ ├── Region (Selection)
│ ├── Availability (Boolean)
│ └── Regulatory Notes (Rich Text, translatable)
└── Market-Specific Content (Multiple Choice)
├── EU Piece (translated descriptions, certifications)
├── US Piece (FDA info, warnings)
└── APAC Piece (local certifications)

Anti-Patterns to Avoid

❌ Over-Structuring

Problem: Creating too many components when one would do.

Bad:
├── Street Name (Single Line)
├── Street Number (Single Line)
├── Apartment (Single Line)
├── City (Single Line)
├── State (Single Line)
├── ZIP (Single Line)
├── Country (Single Line)
Good:
└── Address (Rich Text or Chunk)
├── Street Address (Single Line)
├── City (Single Line)
├── Postal Code (Single Line)
└── Country (Selection)

❌ Using Item Relations When Topics Suffice

Problem: Creating classification documents when simple topics work.

Bad:
Document Shape: Color
├── Color Name
└── HEX Code
Product:
└── Colors (Item Relation → Color documents)
Good:
Topic Map: Colors
├── Red
├── Blue
└── Green
Product:
└── topics → Colors

When to use Item Relations over Topics:

  • Classification needs description, images, or rich content
  • Classification is multilingual with different translations
  • Classification itself needs metadata or attributes

❌ Hardcoding Enums as Selection Options

Problem: Using Selection components for values that may expand.

Bad (rigid):
Product:
└── Brand (Selection: Nike, Adidas, Puma)
Good (scalable):
Document Shape: Brand
├── Logo (Images)
├── Description (Rich Text)
└── Website (Single Line)
Product:
└── Brand (Item Relation → Brand document)

❌ Flat Component Lists

Problem: Not grouping related fields with Chunks.

Bad:
Product:
├── Ingredient 1 Name
├── Ingredient 1 Quantity
├── Ingredient 1 Unit
├── Ingredient 2 Name
├── Ingredient 2 Quantity
├── Ingredient 2 Unit
...
Good:
Product:
└── Ingredients (Chunk, repeating)
├── Name (Single Line)
├── Quantity (Numeric)
└── Unit (Selection)

❌ Mixing Variant Data with Product Data

Problem: Adding variant-specific fields to product shape.

Bad:
Product Shape:
├── Size (Selection) ← Should be variant attribute
├── Color (Selection) ← Should be variant attribute
└── SKU (Single Line) ← Built-in on variants
Good:
Product Shape:
└── Fit Guide (Rich Text) ← Product-level
Variant Attributes (built-in):
├── Size
├── Color
├── SKU
└── Images

❌ Not Planning for Localization Early

Problem: Adding multilingual support later requires data migration.

Bad (then needs fixing):
Product:
└── Description (Rich Text, NOT translatable)
Then need to migrate all content when expanding to new markets.
Good (planned):
Product:
└── Description (Rich Text, translatable from the start)

Validation Best Practices

Use Min/Max Constraints

Item Relations, Selection components, and numeric fields support validation:

{
"type": "itemRelations",
"config": {
"itemRelations": {
"acceptedShapeIdentifiers": ["brand"],
"minItems": 1,
"maxItems": 1
}
}
}

This enforces:

  • Editors must select at least 1 brand
  • Editors cannot select more than 1 brand
  • Only items with shape “brand” can be selected

Required vs Optional Fields

While Crystallize doesn’t have a global “required” flag, you can enforce requirements through:

  1. Item Relations: minItems: 1 makes the field required
  2. Selection: min: 1, max: 1 for required single selection
  3. Business logic: Validate in your application layer
  4. Editorial guidelines: Document required fields in shape descriptions

Accepted Shape Identifiers

Always restrict Item Relation components to specific shapes:

{
"id": "ingredients",
"type": "itemRelations",
"config": {
"itemRelations": {
"acceptedShapeIdentifiers": ["ingredient"],
"minItems": 1
}
}
}

Without acceptedShapeIdentifiers, editors could link any shape type, breaking your data model’s semantic meaning.

Performance Considerations

Component Count

  • Crystallize supports hundreds of components per shape
  • Keep shapes focused for better editor UX
  • Use pieces to share common field sets across shapes

Nesting Depth

  • Maximum 4 levels of nesting for structural components
  • Beyond 4 levels, use Item Relations instead
  • Deep nesting impacts query performance

Discovery API Optimization

  • Enable discovery only on fields used for filtering/search
  • Numeric fields with discovery enabled can be sorted/filtered
  • Rich Text discovery enables full-text search on that content

Migration Patterns

Adding New Components

Safe - existing items get null/empty values for new components.

Removing Components

Dangerous - data loss. Consider:

  1. Export data first
  2. Create new shape with migration path
  3. Gradually migrate content
  4. Archive old shape when complete

Changing Component Types

Not supported directly. Pattern:

  1. Add new component with different ID
  2. Migrate data via API
  3. Remove old component

Renaming Components

Use the identifier field:

  • Display name can change freely (editor-facing)
  • Identifier change requires data migration (API-facing)

References

createShape Mutation — Full API Reference

Use this reference to validate all required fields and config structures when calling the createShape mutation on the Core API.

Endpoint

POST https://api.crystallize.com/@{tenantIdentifier}

Auth headers: X-Crystallize-Access-Token-Id + X-Crystallize-Access-Token-Secret


Mutation Signature

mutation CreateShape($input: CreateShapeInput!) {
createShape(input: $input) {
... on Shape {
identifier
name
type
}
... on BasicError {
errorName
message
}
... on UnauthorizedError {
errorName
message
}
}
}

Result union: CreateShapeResult = Shape | ExperimentalFeaturesNotAvailableError | InvalidIdError | PieceIdentifierTakenError | UnauthorizedError | UnknownError


CreateShapeInput

{
name: String! // REQUIRED — display name
type: ShapeType! // REQUIRED — see ShapeType enum
identifier?: String // optional — auto-generated from name if omitted
components?: ComponentDefinitionInput[] // shape-level component definitions
variantComponents?: ComponentDefinitionInput[] // product-only: custom components on variants
meta?: { key: String!, value?: String }[] // arbitrary key-value metadata
}

ShapeType enum: document | folder | product

variantComponents is only meaningful on type: "product" shapes. Use it to add custom fields to product variants beyond the built-in sku, images, videos, price, stock, and attributes.


ComponentDefinitionInput

Used in both components and variantComponents arrays, and recursively inside structural component configs.

{
name: String! // REQUIRED — display name
type: ComponentType! // REQUIRED — see ComponentType enum
id?: String // optional — identifier used in API queries. Auto-generated from name if omitted. Prefer explicit IDs.
description?: String // optional — help text shown to editors
config?: ComponentConfigInput // optional — required for some types (see per-type rules below)
}

ComponentType enum: boolean | componentChoice | componentMultipleChoice | contentChunk | datetime | files | gridRelations | images | itemRelations | location | numeric | paragraphCollection | piece | propertiesTable | richText | selection | singleLine | videos


ComponentConfigInput — Per-Type Required Fields

The config object must match the component type. Only set the key that corresponds to the component type.

boolean

config: {
boolean: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
}
}

No required sub-fields.


singleLine

config: {
singleLine: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
min?: Int // min character length
max?: Int // max character length
pattern?: String // regex validation pattern
}
}

No required sub-fields.


richText

config: {
richText: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
min?: Int
max?: Int
}
}

No required sub-fields.


numeric

config: {
numeric: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
decimalPlaces?: Int
units?: String[] // list of allowed units editors can select (e.g. ["kg", "g", "lb"])
}
}

No required sub-fields.


datetime

config: {
datetime: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
format?: DateComponentFormat // "date" | "datetime" (default: datetime)
}
}

No required sub-fields.


location

config: {
location: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
}
}

No required sub-fields.


images

config: {
images: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
min?: Int // min number of images
max?: Int // max number of images
}
}

No required sub-fields.


videos

config: {
videos: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
min?: Int
max?: Int
}
}

No required sub-fields.


files

config: {
files: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
min?: Int
max?: Int
acceptedContentTypes?: {
contentType: String! // REQUIRED — MIME type e.g. "application/pdf"
extensionLabel?: String // e.g. "PDF"
}[]
maxFileSize?: {
size: Float! // REQUIRED
unit: FileSizeUnit! // REQUIRED — "Bytes" | "KiB" | "MiB" | "GiB"
}
}
}

Sub-fields contentType, size, and unit are required when their parent objects are provided.


selection

config: {
selection: {
options: { // REQUIRED — at least one option
key: String! // REQUIRED — API value (e.g. "red")
value: String! // REQUIRED — display label (e.g. "Red")
isPreselected?: Boolean
}[]
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
min?: Int // min number of selections
max?: Int // max number of selections
}
}

options is required (non-nullable array). Each option requires key (the API identifier) and value (the display label).


propertiesTable

config: {
propertiesTable: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
sections?: {
keys: String[]! // REQUIRED — predefined key names for this section
title?: String
}[]
}
}

keys is required when a section is provided. Omit sections entirely for fluid/dynamic keys (editors define keys at content time).


itemRelations

config: {
itemRelations: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
acceptedShapeIdentifiers?: String[] // STRONGLY RECOMMENDED — restrict which shapes can be linked
minItems?: Int
maxItems?: Int
minSkus?: Int // for linking specific variant SKUs
maxSkus?: Int
quickSelect?: {
folders: {
folderId: String! // REQUIRED
}[] // REQUIRED — at least one folder
view?: "nerdy" | "pretty"
}
}
}

Always set acceptedShapeIdentifiers — without it, editors can link any shape, breaking semantic meaning. The min/max fields (without Items suffix) are deprecated aliases — use minItems/maxItems instead.


gridRelations

config: {
gridRelations: {
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
min?: Int
max?: Int
}
}

No required sub-fields.


contentChunk

config: {
contentChunk: {
components: ComponentDefinitionInput[] // REQUIRED — MUST have at least 1 component. Empty chunks are invalid.
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
repeatable?: Boolean // true = editors can add multiple chunk instances
}
}

components is required (non-nullable) and must contain at least 1 entry — an empty components: [] is invalid and meaningless. Children MUST be pieces or regular (non-structural) components. Structural components (componentChoice, componentMultipleChoice, contentChunk) cannot be direct children.


componentChoice

config: {
componentChoice: {
choices: ComponentDefinitionInput[] // REQUIRED — min 2 choices (single choice is invalid)
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
}
}

choices is required (non-nullable). Minimum 2 choices — a single-choice componentChoice is invalid. Children MUST be pieces or regular components; not other structural components.


componentMultipleChoice

config: {
componentMultipleChoice: {
choices: ComponentDefinitionInput[] // REQUIRED — min 2 choices (single choice is invalid)
allowDuplicates?: Boolean // true = same choice can be selected multiple times
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
}
}

choices is required (non-nullable). Minimum 2 choices — a single-choice componentMultipleChoice is invalid. allowDuplicates (not repeatable) controls whether editors can add the same choice more than once (e.g., two Banner sections on one page). Children MUST be pieces or regular components; not other structural components.


piece

config: {
piece: {
identifier: String! // REQUIRED — the piece's identifier (created separately)
required?: Boolean
discoverable?: Boolean
multilingual?: Boolean
}
}

identifier is required — must match an existing piece’s identifier.


paragraphCollection

config: {
paragraphCollection: {
required?: Boolean
discoverable?: Boolean
multilingual: ("body" | "images" | "structure" | "title" | "videos")[] // REQUIRED: array of paragraph parts that are translatable (lowercase)
}
}

Important: multilingual is required (not optional). If all parts are localized, use ["title", "body", "videos", "images"]. For universal content (no localization), use []. No required sub-fields.


Structural Nesting Rules

NEVER nest structural components as direct children of other structural components:

Parent typeValid direct childrenInvalid direct children
contentChunkpieces, regular componentscomponentChoice, componentMultipleChoice, contentChunk
componentChoicepieces, regular componentscomponentChoice, componentMultipleChoice, contentChunk
componentMultipleChoicepieces, regular componentscomponentChoice, componentMultipleChoice, contentChunk

Maximum nesting depth: 4 levels. Level 5 and beyond may only contain non-structural components.


Full Example

mutation CreateProductShape {
createShape(
input: {
name: "Guitar"
type: product
identifier: "guitar"
components: [
{
id: "description"
name: "Description"
type: richText
config: { richText: { discoverable: true, multilingual: true } }
}
{
id: "brand"
name: "Brand"
type: itemRelations
config: { itemRelations: { acceptedShapeIdentifiers: ["brand"], minItems: 1, maxItems: 1 } }
}
{
id: "body-type"
name: "Body Type"
type: itemRelations
config: { itemRelations: { acceptedShapeIdentifiers: ["body-type"], maxItems: 1 } }
}
{
id: "finish"
name: "Finish"
type: selection
config: {
selection: {
options: [
{ key: "gloss", value: "Gloss" }
{ key: "satin", value: "Satin" }
{ key: "matte", value: "Matte" }
]
discoverable: true
}
}
}
{
id: "specs"
name: "Specifications"
type: contentChunk
config: {
contentChunk: {
repeatable: false
components: [
{
id: "weight"
name: "Weight"
type: numeric
config: { numeric: { units: ["kg", "lb"] } }
}
{
id: "scale-length"
name: "Scale Length"
type: numeric
config: { numeric: { units: ["mm", "inch"] } }
}
]
}
}
}
{ id: "seo", name: "SEO", type: piece, config: { piece: { identifier: "seo" } } }
]
variantComponents: [
{ id: "fret-count", name: "Fret Count", type: numeric, config: { numeric: { discoverable: true } } }
]
}
) {
... on Shape {
identifier
name
type
}
... on BasicError {
errorName
message
}
}
}

Common Errors

ErrorCause
InvalidIdErroridentifier contains invalid characters (use lowercase letters, numbers, hyphens)
PieceIdentifierTakenErrorThe identifier is already used by an existing piece
UnauthorizedErrorAccess token lacks write permissions
ExperimentalFeaturesNotAvailableErrorFeature not available on current plan

Design Patterns for Content Modelling

Crystallize provides five data modelling design patterns for structuring relationships and polymorphism in your content model. Each pattern solves a specific class of problem. The key is recognizing which problem you’re facing — then the pattern choice follows naturally.

Pattern Overview

PatternOne-line summaryCore mechanism
Semantic Classification BridgeShared, enrichable attributes as separate documentsItem Relation → Document
Quantised Classification BridgeRelationships with measurable quantitiesChunk (Relation + Numeric + Unit)
Conditional Classification BridgeRelationships with rules and dependenciesChunk (Relation + Rule selection)
Composite Classification BridgeRelationships that describe what the relationship meansChunk (Relation + Role/meaning selection)
Polymorphic ChoiceA single concept with mutually exclusive structural formsChoice component

Decision Tree: Which Pattern Do I Need?

START: You have a relationship between items.
Q1: Is the relationship just "this item has this attribute"?
(e.g., product has allergen, product has brand, product has material)
→ Yes → Is the attribute a simple label or does it need its own content?
→ Simple label → Use Topic Map or Selection component (no pattern needed)
→ Needs content (description, image, translations) → SEMANTIC BRIDGE
Q2: Does the relationship carry a quantity?
(e.g., recipe has 200g of flour, kit contains 4x screws)
→ Yes → QUANTISED BRIDGE
Q3: Does the relationship carry rules or logic?
(e.g., topping A requires topping B, part X excludes part Y)
→ Yes → CONDITIONAL BRIDGE
Q4: Does the relationship need to describe its own meaning?
(e.g., person is the "author" of this book, material is applied to the "lining")
→ Yes → COMPOSITE BRIDGE
Q5: No relationship — but a concept can take different structural forms?
(e.g., hero section is image-hero OR video-hero, product is guitar OR amplifier)
→ Yes → POLYMORPHIC CHOICE

1. Semantic Classification Bridge

The problem it solves

You have product attributes that are shared across many products, and those attributes need to be more than just a label. They need descriptions, images, translations, or other rich content. And you need to update the attribute once and have the change reflected everywhere.

Recognizing when you need it

  • Multiple products share the same attribute values (e.g., 50 products are all “Organic certified”)
  • The attribute value needs its own story (a brand has a logo and description, a material has care instructions)
  • You need to filter/search products by these attributes
  • You want to localize attribute values independently

How it works

  1. Create a Document shape for the classification (e.g., “Allergen”, “Brand”, “Material”)
  2. Create documents for each value (e.g., “Peanuts”, “Gluten”, “Dairy”)
  3. Add an Item Relation component to the product shape
  4. Configure the Item Relation component:
    • Accepted shapes: restrict to only the classification document shape (e.g., only “Allergen” documents)
    • Min relations: enforce minimum links (e.g., at least 1 brand required)
    • Max relations: limit how many (e.g., max 3 materials, or unlimited for allergens)
  5. Products link to the relevant classification documents

These configuration options act like foreign key constraints in a relational database, enforcing data integrity and guiding editors.

Product ──(Item Relation)──→ Classification Document
├── name
├── description (Rich Text)
├── image (Images)
└── metadata

Example: Product with Brand and Allergen classification

Step 1: Create classification document shapes

{
"identifier": "brand",
"name": "Brand",
"type": "document",
"components": [
{ "id": "logo", "name": "Logo", "type": "images" },
{ "id": "description", "name": "Description", "type": "richText" },
{ "id": "website", "name": "Website", "type": "singleLine" }
]
}
{
"identifier": "allergen",
"name": "Allergen",
"type": "document",
"components": [
{ "id": "icon", "name": "Icon", "type": "images" },
{ "id": "severity", "name": "Severity", "type": "singleLine" }
]
}

Step 2: Add Item Relation components to Product shape with acceptedShapeIdentifiers

{
"identifier": "product",
"name": "Product",
"type": "product",
"components": [
{
"id": "brand",
"name": "Brand",
"type": "itemRelations",
"config": {
"acceptedShapeIdentifiers": ["brand"],
"minItems": 1,
"maxItems": 1
}
},
{
"id": "allergens",
"name": "Allergens",
"type": "itemRelations",
"config": {
"acceptedShapeIdentifiers": ["allergen"],
"minItems": 0,
"maxItems": 999
}
}
]
}

Key configuration properties:

  • acceptedShapeIdentifiers — array of shape identifiers that can be linked. Enforces type safety.
  • minItems — minimum required links (0 = optional, 1+ = required)
  • maxItems — maximum allowed links (use high number like 999 for “unlimited”)

Without acceptedShapeIdentifiers, editors could accidentally link any shape type, breaking the semantic meaning of the relationship.

Real-world examples

DomainClassificationDocument shape fields
FoodAllergenname, icon, severity level, regulatory info
FashionMaterialname, image, care instructions, sustainability score
ElectronicsBrandname, logo, description, website, country of origin
BeautyIngredientname, INCI name, description, benefits, warnings
B2BCertificationname, logo, issuing body, validity period, PDF

When NOT to use it

  • The classification is just a simple label with no content needs → use Selection or Topic Map
  • The classification needs a quantity (200g of flour) → use Quantised Bridge instead
  • The classification needs rules (A requires B) → use Conditional Bridge instead

2. Quantised Classification Bridge

The problem it solves

You need to express that one item contains a measurable amount of another item. Not just “this recipe uses flour” but “this recipe uses 200g of flour”. Not just “this kit includes screws” but “this kit includes 4 screws”.

Recognizing when you need it

  • Items are compositions of other items with quantities
  • You need to calculate totals (nutrition sums, material costs, weight totals)
  • You want to show ingredient/component breakdowns on product pages
  • A product is a kit, bundle, recipe, assembly, or bill of materials

How it works

Add a repeating Chunk to the parent item’s shape. Each chunk entry contains:

  • Numeric — the quantity (200)
  • Selection or Single Line — the unit (g, pcs, m, L)
  • Item Relation — link to the ingredient/component product
    • Configure accepted shapes: restrict to only the component shape (e.g., only “Ingredient” products)
    • Configure min/max relations: typically min=1, max=1 per chunk entry (each entry links to exactly one item)
Parent Product
└── ingredients (Chunk[], repeating)
├── quantity (Numeric)
├── unit (Selection: g, kg, ml, L, pcs, m)
└── ingredient (Item Relation → Ingredient product)
↳ Accepted shapes: ["ingredient"]
↳ Min: 1, Max: 1

Example: Recipe with Ingredients

Step 1: Create Ingredient product shape

{
"identifier": "ingredient",
"name": "Ingredient",
"type": "product",
"components": [
{
"id": "nutritionInfo",
"name": "Nutrition Info",
"type": "propertiesTable"
},
{
"id": "allergens",
"name": "Allergens",
"type": "itemRelations",
"config": { "acceptedShapeIdentifiers": ["allergen"] }
}
]
}

Step 2: Add repeating Chunk to Recipe product shape

{
"identifier": "recipe",
"name": "Recipe",
"type": "product",
"components": [
{
"id": "ingredients",
"name": "Ingredients",
"type": "contentChunk",
"config": {
"contentChunk": {
"repeatable": true,
"components": [
{
"id": "quantity",
"name": "Quantity",
"type": "numeric"
},
{
"id": "unit",
"name": "Unit",
"type": "selection",
"config": {
"selection": {
"options": [
{ "key": "g", "value": "grams" },
{ "key": "ml", "value": "milliliters" },
{ "key": "pcs", "value": "pieces" }
]
}
}
},
{
"id": "ingredient",
"name": "Ingredient",
"type": "itemRelations",
"config": {
"itemRelations": {
"acceptedShapeIdentifiers": ["ingredient"],
"minItems": 1,
"maxItems": 1
}
}
}
]
}
}
}
]
}

Critical: The acceptedShapeIdentifiers: ["ingredient"] ensures editors can only select Ingredient products, not recipes or other shapes. This prevents data integrity issues.

Real-world examples

DomainParent itemLinked itemQuantity meaning
Food/RecipesRecipeIngredient200g of flour, 2 eggs
ManufacturingAssemblyPart4x M6 bolts, 1x motor
Furniture kitsFurniture kitComponent4x legs, 1x tabletop, 8x screws
NutritionMealIngredientEnables calorie/macro rollups
ConstructionMaterial listMaterial50m² of tiles, 25kg of adhesive

When NOT to use it

  • The relationship doesn’t need a quantity → use Semantic Bridge (just a link)
  • The relationship carries rules, not quantities → use Conditional Bridge
  • The relationship needs a role description → use Composite Bridge

3. Conditional Classification Bridge

The problem it solves

Relationships between items have rules: selecting option A requires option B, or selecting option A excludes option B. This is essential for product configurators, builders, and compatibility systems.

Recognizing when you need it

  • Products can be configured with interdependent options
  • Some combinations are invalid and should be prevented
  • You’re building a product configurator, builder, or wizard
  • Parts/accessories have compatibility requirements

How it works

Add a repeating Chunk where each entry stores the relation AND the rule:

Configurable Product
└── options (Chunk[], repeating)
├── part (Item Relation → Component product)
│ ↳ Accepted shapes: ["Component Part"]
│ ↳ Min: 1, Max: 1
├── rule (Selection: requires | excludes | optional)
└── depends-on (Item Relation → another Component product, optional)
↳ Accepted shapes: ["Component Part"]
↳ Min: 0, Max: 1

Common rule types:

  • Requires: selecting A demands that B is also selected
  • Excludes: selecting A prevents selecting B
  • Optional: no constraint, purely informational

Real-world examples

DomainScenarioRule
Pizza builderChili topping requires CheeseChili → requires → Cheese
Pizza builderVegan Cheese excludes Meat toppingsVegan Cheese → excludes → Meat
PC builderRTX 4090 requires 850W PSUGPU → requires → PSU with min spec
FurnitureWooden legs require matching screw setLegs → requires → Screw Set
CamerasEF-S lens excludes full-frame bodyLens → excludes → Body type
Car configSport package requires sport suspensionPackage → requires → Suspension

Frontend implications

The conditional bridge produces structured data that your frontend can interpret to:

  • Grey out incompatible options
  • Auto-add required dependencies
  • Show validation messages (“Chili requires Cheese”)
  • Build progressive configuration wizards

When NOT to use it

  • The relationship has no rules or dependencies → use Semantic or Quantised Bridge
  • The relationship needs to describe its role/meaning → use Composite Bridge
  • You just need one-of-many selection → use Polymorphic Choice

4. Composite Classification Bridge

The problem it solves

You need to link items together and describe what the relationship means. Not just “this book links to this person” but “this person is the illustrator of this book”. Not just “this jacket uses wool” but “wool is used in the lining”.

Recognizing when you need it

  • The same type of relationship can mean different things (person can be author, editor, or illustrator)
  • You need to display the relationship role in the UI (“Illustrated by”, “Written by”)
  • The meaning of the relationship matters for filtering, display, or integrations
  • You want structured, predictable data about why two items are connected

How it works

Add a repeating Chunk where each entry stores the relation AND a classification of what it means:

Book Product
└── contributors (Chunk[], repeating)
├── person (Item Relation → Author document)
│ ↳ Accepted shapes: ["Author", "Person"]
│ ↳ Min: 1, Max: 1
└── role (Selection: Writer | Illustrator | Editor | Translator)

The classification (role/meaning) can be a Selection (simple label) or an Item Relation to a classification document (if the role itself needs enrichment).

Real-world examples

DomainParent itemRelated itemRelationship meaning
PublishingBookPersonWriter, Illustrator, Editor
FashionJacketMaterialLining, Sleeve, Collar, Body
MusicAlbumPersonProducer, Vocalist, Guitarist
ConstructionBuildingCompanyArchitect, Contractor, Supplier
Modular sofaSofaComponentLegs, Cushion, Frame, Fabric

Example: Book with Contributors

Step 1: Create Author/Person document shape

{
"identifier": "author",
"name": "Author",
"type": "document",
"components": [
{ "id": "bio", "name": "Biography", "type": "richText" },
{ "id": "photo", "name": "Photo", "type": "images" },
{ "id": "website", "name": "Website", "type": "singleLine" }
]
}

Step 2: Add Contributors chunk to Book product shape

{
"identifier": "book",
"name": "Book",
"type": "product",
"components": [
{ "id": "description", "name": "Description", "type": "richText" },
{
"id": "contributors",
"name": "Contributors",
"type": "contentChunk",
"config": {
"contentChunk": {
"repeatable": true,
"components": [
{
"id": "person",
"name": "Person",
"type": "itemRelations",
"config": {
"itemRelations": {
"acceptedShapeIdentifiers": ["author"],
"minItems": 1,
"maxItems": 1
}
}
},
{
"id": "role",
"name": "Role",
"type": "selection",
"config": {
"selection": {
"options": [
{ "key": "writer", "value": "Writer" },
{ "key": "illustrator", "value": "Illustrator" },
{ "key": "editor", "value": "Editor" },
{ "key": "translator", "value": "Translator" }
]
}
}
}
]
}
}
}
]
}

Key insight: Each contributor entry links a person AND describes their role, enabling displays like “Written by Jane Doe” and “Illustrated by John Smith”.

When NOT to use it

  • All relationships have the same meaning → use Semantic Bridge (no role needed)
  • The relationship needs a quantity → use Quantised Bridge (or combine both)
  • The relationship carries rules → use Conditional Bridge

5. Polymorphic Choice

The problem it solves

A single semantic concept can take one of several mutually exclusive structural formspoly- (many) + morph (form). The meaning stays the same, but the internal fields change based on a deliberate selection. You want to handle this variation within a single shape rather than creating separate shapes. Crystallize implements this via the Choice (componentChoice) and Multiple Choice (componentMultipleChoice) component types.

Recognizing when you need it

  • A concept has multiple valid representations, but only one applies at a time
  • You want one shape for products that have different field sets (guitar vs amplifier vs pedal)
  • Page sections can be different layouts (image hero vs video hero vs text hero)
  • Product data follows a standard where each class has its own property set (ETIM, UNSPSC)

How it works

Add a Choice component to the shape. Define multiple named structures within the Choice, each with its own set of components. The editor selects exactly one.

Product Shape
└── product-type (Choice)
├── Guitar
│ ├── body-type (Selection: Solid, Semi-hollow, Hollow)
│ ├── strings (Numeric)
│ ├── scale-length (Numeric, cm)
│ └── pickup-config (Selection: SSS, HSS, HH)
├── Amplifier
│ ├── wattage (Numeric, W)
│ ├── channels (Numeric)
│ └── speaker-size (Numeric, inches)
└── Pedal
├── effect-type (Selection: Overdrive, Delay, Reverb, ...)
├── power-draw (Numeric, mA)
└── true-bypass (Switch)

Real-world examples

DomainChoice conceptVariants
Music gearProduct typeGuitar, Amplifier, Pedal, Accessory
Landing pagesHero sectionImage hero, Video hero, Text hero, Product hero
B2B industrialProduct classETIM class A, ETIM class B (each with own specs)
Real estateProperty typeApartment, House, Commercial, Land
FoodPreparation typeReady-to-eat, Cook-at-home, Raw ingredient

When to use Choice vs Multiple Choice

  • Choice → exactly one form, mutually exclusive. “This product IS a guitar.”
  • Multiple Choice → one or more forms can coexist. “This product has physical attributes AND a digital license.”

When NOT to use it

  • Products differ so fundamentally that they share almost no fields → use separate shapes
  • You just need a dropdown value, not different field sets → use Selection
  • The variation is about relationships, not field structure → use a bridge pattern

Advanced: Polymorphic Choice with Pieces

For large or reusable field sets, reference Pieces inside choice options instead of defining components inline. This makes variants more maintainable and reusable across shapes.

When to use this variant:

  • Choice variants have many components (10+ fields each)
  • The same variant appears in multiple choice components or shapes
  • You want to update variant fields in one place
  • You’re building reusable patterns across your model

Structure:

{
"id": "flower-type",
"name": "Flower Type",
"type": "choice",
"config": {
"choice": {
"choices": [
{
"id": "rose",
"name": "Rose",
"type": "piece",
"config": {
"piece": {
"identifier": "rose-attributes"
}
}
},
{
"id": "orchid",
"name": "Orchid",
"type": "piece",
"config": {
"piece": {
"identifier": "orchid-attributes"
}
}
}
]
}
}
}

Pieces (referenced above):

{
"identifier": "rose-attributes",
"name": "Rose Attributes",
"components": [
{ "id": "thorns", "name": "Has Thorns", "type": "boolean" },
{ "id": "petal-count", "name": "Petal Count", "type": "numeric" },
{ "id": "rose-type", "name": "Rose Type", "type": "selection", "config": {...} }
]
}

Benefits:

  • Cleaner JSON structure — choice definitions stay focused on selection
  • Pieces are reusable across multiple shapes and choices
  • Easier to maintain — update piece definition once, affects all uses
  • Better for model visualization tools (pieces show as distinct nodes)

Combining Patterns

Patterns can be combined on the same shape. A single product shape might use:

  • Semantic Bridge for brand and material classification
  • Quantised Bridge for ingredient composition
  • Polymorphic Choice for product-type-specific fields
  • Topic Maps for navigation categories

The patterns are not mutually exclusive — they solve different axes of the same content model.

Pattern Selection Summary

Your data looks like…Pattern
Products share enrichable attributesSemantic Classification Bridge
Items contain measured amounts of other itemsQuantised Classification Bridge
Options have compatibility rules and dependenciesConditional Classification Bridge
Relationships need to describe their meaning/roleComposite Classification Bridge
A concept takes one of several mutually exclusive formsPolymorphic Choice
Simple labels for filtering (no enrichment needed)Topic Maps or Selection (no pattern)

Crystallize Page Builder Design Pattern

Based on the Furnitut reference implementation — landing-page and category shapes in content-model.json


Overview

The Page Builder pattern in Crystallize lets content editors compose flexible page layouts from a curated set of reusable section types — without needing developer involvement for every new page. It is built on three Crystallize primitives working together:

  1. componentMultipleChoice — the “block slot” on a page shape that holds an ordered list of sections
  2. Pieces — self-contained, reusable section definitions (the actual blocks)
  3. A shared layout piece — a styling contract embedded in every block piece, giving editors per-block visual control

The pattern is intentionally editor-friendly: add blocks in any order, repeat the same block type multiple times, and control each block’s theme and width independently.


The Three Core Primitives

1. componentMultipleChoice — The Block Slot

This is the backbone of the page builder. A shape (e.g. landing-page, category) gets a single component named blocks of type componentMultipleChoice. Each choice in the config references a piece by identifier.

{
"id": "blocks",
"type": "componentMultipleChoice",
"name": "Blocks",
"config": {
"componentMultipleChoice": {
"multilingual": true,
"allowDuplicates": true,
"choices": [
{
"id": "banner",
"type": "piece",
"config": { "piece": { "identifier": "banner" } }
},
{
"id": "feature-highlights",
"type": "piece",
"config": { "piece": { "identifier": "feature-highlights" } }
},
{
"id": "product-slider",
"type": "piece",
"config": { "piece": { "identifier": "product-slider" } }
},
{
"id": "story-slider",
"type": "piece",
"config": { "piece": { "identifier": "story-slider" } }
},
{
"id": "picture-grid",
"type": "piece",
"config": { "piece": { "identifier": "picture-grid" } }
},
{
"id": "category-slider",
"type": "piece",
"config": { "piece": { "identifier": "category-slider" } }
}
]
}
}
}

Key flags:

  • allowDuplicates: true — editors can use the same block type multiple times on the same page (e.g. two separate banners)
  • multilingual: true (localized structure) — different languages/markets can have completely different blocks selected. See Localized Structure below.
  • min / max — optionally enforce minimum (e.g. 1 — page needs at least one section) and maximum number of blocks

2. Block Pieces — The Reusable Sections

Each block is a piece shape with its own component set. Pieces are defined once and referenced by multiple shapes — both landing-page and category share the exact same set of blocks in Furnitut.

Piece identifierWhat it rendersKey components
bannerFull-width hero/promotional sectiontitle, description, image, call-to-action, layout
feature-highlightsUSP grid (repeatable icon + headline + copy)usp (contentChunk, repeatable), layout
product-sliderCurated horizontal product carouseltitle, description, itemRelations → products, layout
story-sliderEditorial story carouseltitle, description, itemRelations → stories, layout
picture-gridFixed 4-image visual gridtitle, description, images (min/max: 4), layout
category-sliderCategory navigation carouseltitle, description, itemRelations → categories, layout

3. The layout Piece — Shared Styling Contract

Every block piece embeds the layout piece as a component. This gives editors per-block visual control without duplicating the config across pieces.

{
"identifier": "layout",
"components": [
{
"id": "display-width",
"type": "selection",
"config": {
"selection": {
"options": [
{ "key": "stretch", "value": "Stretch" },
{ "key": "contain", "value": "Contain" }
]
}
}
},
{
"id": "theme",
"type": "selection",
"config": {
"selection": {
"options": [
{ "key": "light", "value": "Light" },
{ "key": "dark", "value": "Dark" },
{ "key": "muted", "value": "Muted" },
{ "key": "pastel", "value": "Pastel" },
{ "key": "vivid", "value": "Vivid" }
]
}
}
},
{
"id": "background-media",
"type": "componentChoice",
"config": {
"componentChoice": {
"choices": [
{ "id": "image", "type": "images" },
{ "id": "video", "type": "videos" }
]
}
}
}
]
}
  • display-width: stretch (full bleed) or contain (max-width wrapper). Default: contain.
  • theme: Controls colour palette of the block. Default: light.
  • background-media: Optional image or video background — uses componentChoice for mutual exclusivity (one or the other, not both). Note: componentChoice is valid inside a piece; the nesting restriction only applies to direct children of structural components.

Localized Structure (multilingual)

The multilingual: true flag on componentMultipleChoice enables localized structure — called “Yes - choice varies per language” in the Crystallize PIM UI. This is the default and recommended setting for page builder blocks.

What it means

With localized structure enabled, each language/market gets its own independent selection and ordering of blocks. This is not just about translating text inside the same blocks — the entire page composition can differ:

MarketBlocks on landing page
EnglishHero Banner → Product Slider → Testimonials → CTA Banner
GermanHero Banner → Feature Highlights → Product Slider
FrenchVideo Section → Category Slider → Story Slider → Banner
JapaneseBanner → Picture Grid → Product Slider → Feature Highlights

Why this is the default recommendation

Experience shows that different markets rarely want identical page layouts. Common reasons:

  • Merchandising differences — The German market may prioritize a product launch that isn’t relevant in France
  • Seasonal campaigns — Summer campaign blocks in the Northern Hemisphere while Southern Hemisphere markets show winter content
  • Regulatory content — Some markets require specific disclosure sections
  • Cultural preferences — Image-heavy layouts for some markets, text-heavy for others
  • Product availability — Only show product sliders for categories available in that market

When you might NOT want localized structure

In rare cases, you want all languages to share the same block selection (only translating content within blocks):

  • A brand with strictly identical global page layouts enforced by HQ
  • Technical/documentation pages where structure must be consistent across languages
  • When the editorial team is small and manages all languages from one view

In these cases, set multilingual: false — but this is the exception, not the rule.


Supporting Patterns Within Pieces

componentChoice — Exclusive Alternatives

Used when a field should have one value from a set of structurally different types. Examples:

  • layout.background-media: image or video
  • story.media: image, shoppable-image, or video
  • menu-item.link: a raw URL or an item relation
{
"id": "background-media",
"type": "componentChoice",
"config": {
"componentChoice": {
"choices": [
{ "id": "image", "type": "images" },
{ "id": "video", "type": "videos" }
]
}
}
}

Use componentChoice (not componentMultipleChoice) when only one option should be active at a time.

contentChunk — Repeatable Inline Groups

Used inside a piece when you need an editor-managed list of structured items that don’t warrant their own shape. Examples:

  • feature-highlights.usp: repeatable {headline, description, icon} group
  • product.details: repeatable {title, description} tab/accordion content
  • call-to-action.action: repeatable {label, url} button list
{
"id": "usp",
"type": "contentChunk",
"config": {
"contentChunk": {
"repeatable": true,
"components": [
{ "id": "headline", "type": "singleLine" },
{ "id": "description", "type": "richText" },
{ "id": "icon", "type": "images" }
]
}
}
}

Use contentChunk for inline-repeated groups. Use itemRelations when those items need to exist as their own content items (e.g. products, stories, categories).

Shared Utility Pieces

Beyond layout, other pieces serve as shared field groups embedded across shapes:

PieceUsed byPurpose
call-to-actionbannerRepeatable {label, url} CTA buttons
metalanding-page, category, product, storySEO title, description, image
dimensionsproduct, product variantPhysical measurements (W/H/D/weight)

Pattern Architecture Diagram

landing-page (document shape)
└── blocks [componentMultipleChoice, allowDuplicates, multilingual]
├── banner (piece)
│ ├── title, description
│ ├── call-to-action (piece) → action [contentChunk, repeatable]
│ ├── banner image
│ └── layout (piece) → display-width, theme, background-media [componentChoice]
├── feature-highlights (piece)
│ ├── usp [contentChunk, repeatable] → headline, description, icon
│ └── layout (piece)
├── product-slider (piece)
│ ├── title, description
│ ├── products [itemRelations → product shape]
│ └── layout (piece)
├── story-slider (piece)
│ └── stories [itemRelations → story shape]
├── picture-grid (piece)
│ └── images (fixed 4)
└── category-slider (piece)
└── categories [itemRelations → category shape]
category (folder shape) ← same blocks component as landing-page
└── blocks [componentMultipleChoice] (same 6 choices)

Best Practices

Modelling

Keep block pieces focused. Each piece should represent one distinct visual section type. Avoid adding optional fields that turn one piece into two different things — make two pieces instead.

Always embed layout in every block piece. This is the styling contract. Editors expect to be able to control width and theme per block. If a block genuinely can’t vary (e.g. a fixed-dimension widget), document why layout is omitted rather than forgetting it.

Use allowDuplicates: true on the blocks field. Pages frequently need two banners (hero + mid-page CTA) or two product sliders (featured + related). Lock this down only if there is a real business reason to prevent it.

Use multilingual: true (localized structure) on the blocks field. This is the default in the Crystallize PIM UI and is the recommended setting. Different markets almost always need different page compositions for merchandising, campaigns, and cultural reasons. Only disable it when all languages must share identical block structure.

Use componentChoice for media fields (background-media, story media). It enforces the “image OR video” constraint at the data model level rather than relying on frontend logic.

Use contentChunk with repeatable: true for inline lists (USP items, CTA buttons, detail tabs). Use itemRelations when the items are independently managed content (products, stories, categories).

Frontend Rendering

Switch on block type to render. When consuming the blocks array via the API or GraphQL, the discriminator is the chosen piece identifier (or the component choice key). Map each identifier to its React/component.

// Example pattern
blocks.map((block) => {
switch (block.__typename || block.type) {
case "banner":
return <Banner {...block} />;
case "feature-highlights":
return <FeatureHighlights {...block} />;
case "product-slider":
return <ProductSlider {...block} />;
// ...
}
});

Apply layout values as wrapper props, not inside the block component itself. This keeps block components pure and reusable outside of the page builder context.

<BlockWrapper
displayWidth={block.layout.displayWidth} // "stretch" | "contain"
theme={block.layout.theme} // "light" | "dark" | ...
backgroundMedia={block.layout.backgroundMedia}
>
<Banner {...block} />
</BlockWrapper>

Handle allowDuplicates. The same piece type can appear more than once. Use index or a stable id (e.g. UUID generated at item creation) as the React key, not the block type name.

Schema Governance

Centralise shared pieces (layout, meta, call-to-action). If you need to add a new theme option (e.g. "brand"), you change it in one place and it propagates to all blocks automatically.

Add blocks by extending componentMultipleChoice choices — not by creating parallel block fields. A single blocks field keeps rendering logic, ordering, and multilingual handling unified.

Reuse the same blocks definition across shapes (landing-page and category in Furnitut are identical). Consider whether a category page and a landing page genuinely need different block sets, or whether shared blocks reduce maintenance.


When to Use This Pattern

✅ Pages where editors need to compose sections freely (home page, campaign pages, category pages, editorial pages) ✅ When the same visual components should be reusable across multiple page types ✅ When per-block theming/layout control is required ✅ When the number of section types is bounded and known (not user-generated) ✅ When different markets/languages need different page compositions (localized structure)

⛔ Deep product detail pages with fixed, structured layouts (use explicit components instead) ⛔ When section order is always fixed by business rules (a simple flat component set is clearer) ⛔ When blocks need complex inter-block relationships (e.g. block A controls block B)

Pieces Reference

Pieces in Crystallize are reusable sets of fields that can be embedded inside Shapes. They help avoid duplication and ensure consistency across your content model.

What Are Pieces?

Pieces are independent component groups that exist outside of shapes. Once created, they can be dragged into any shape like a regular component.

Key characteristics:

  • Created once, used many times
  • Changes to a piece affect all shapes using it
  • Can contain any components including chunks
  • Support localization

When to Use Pieces

Use pieces for content that:

  • Appears across multiple shapes
  • Needs consistent structure
  • Is managed separately from main content
  • Represents a reusable concept

Common Piece Examples

SEO Metadata

Standard SEO fields for all content:

SEO Metadata (Piece)
├── Meta Title (Single Line)
├── Meta Description (Rich Text, max 160 chars)
├── OG Image (Images, single)
├── OG Title (Single Line)
├── Canonical URL (Single Line)
└── No Index (Boolean)

Environmental Impact

Sustainability data for products:

Environmental Data (Piece)
├── Carbon Footprint (Numeric, kg CO2)
├── Recyclable (Boolean)
├── Recycled Content (Numeric, percentage)
├── Certifications (Chunk, Repeatable)
│ ├── Name (Single Line)
│ └── Logo (Images)
└── Disposal Instructions (Rich Text)

Banner/Hero

Promotional banner component:

Banner (Piece)
├── Headline (Single Line)
├── Subheadline (Rich Text)
├── Background (Images)
├── CTA Text (Single Line)
├── CTA Link (Single Line)
└── Theme (Choice: Light, Dark, Transparent)

Author

For editorial content:

Author (Piece)
├── Name (Single Line)
├── Avatar (Images)
├── Bio (Rich Text)
├── Social Links (Chunk, Repeatable)
│ ├── Platform (Single Line)
│ └── URL (Single Line)
└── Author Page (Item Relation → Document)

Product Visualizer

3D/AR product visualization:

Product Visualizer (Piece)
├── 3D Model (Files, .glb/.gltf)
├── AR Enabled (Boolean)
├── Initial Rotation (Numeric, degrees)
├── Background Color (Single Line, hex)
└── Hotspots (Chunk, Repeatable)
├── Label (Single Line)
├── X Position (Numeric)
├── Y Position (Numeric)
└── Description (Rich Text)

Creating a Piece

  1. Navigate to Settings → Shapes
  2. Scroll to the Pieces section
  3. Click + to create new piece
  4. Give it a descriptive name
  5. Add components using drag and drop
  6. Configure each component
  7. Click Update to save

Using Pieces in Shapes

  1. Open the shape you want to modify
  2. In the right panel, find your piece
  3. Drag and drop it into the shape
  4. Position as needed
  5. Save the shape

Piece Configuration

Inside a Piece

Each component can have:

  • Name and identifier
  • Localization settings
  • Repeatability (for chunks)
  • Validation rules

When Embedded in Shape

Additional options:

  • Override display name
  • Set as required
  • Position in component order

Best Practices

  1. Name clearly - “SEO Metadata” not “SEO”
  2. Single responsibility - One piece = one concept
  3. Plan for reuse - Design pieces to work in multiple contexts
  4. Keep pieces small - Large pieces are harder to maintain
  5. Document usage - Note which shapes use each piece
  6. Version carefully - Changes affect all shapes using the piece

Pieces vs Chunks

FeaturePieceChunk
Reusable across shapes
Can be repeatable❌ (embed in chunk for this)
Lives inSettings → ShapesInside a shape
Changes affectAll shapes using itOnly that shape

API Access

Pieces appear in the API as nested objects:

{
catalogue(path: "/products/coffee") {
... on Product {
components {
id
content {
... on ParagraphCollectionContent {
paragraphs {
title
body
}
}
}
}
}
}
}

Shapes Reference

Shapes in Crystallize are blueprints that define how content and products are structured. They specify which fields (components) are available when editors create items in the catalogue.

Shape Types

Product Shape

Used for sellable items with variants, pricing, and stock.

Built-in capabilities:

  • Product variants with SKUs
  • Pricing per variant
  • Stock management
  • Variant attributes (size, color, etc.)

Example use cases:

  • Physical products (clothing, furniture)
  • Digital products (software, downloads)
  • Subscription products
  • Configurable products

variantComponents — Custom Fields on Variants

Product shapes support variantComponents in addition to components. These are custom component definitions that appear on each product variant rather than on the product itself.

Use variantComponents when you need variant-level data beyond the built-in SKU, images, videos, price, stock, and attributes — for example:

  • Variant-specific certifications (e.g., per-color safety certification)
  • Variant-level specs that differ per variant (e.g., battery capacity per size)
  • Custom variant identifiers (e.g., manufacturer variant codes)
mutation {
createShape(
input: {
name: "Phone"
type: product
components: [
# Product-level components
{ id: "description", name: "Description", type: richText }
{
id: "brand"
name: "Brand"
type: itemRelations
config: { itemRelations: { acceptedShapeIdentifiers: ["brand"], maxItems: 1 } }
}
]
variantComponents: [
# Variant-level custom components
{
id: "battery-capacity"
name: "Battery Capacity"
type: numeric
config: { numeric: { units: ["mAh"], discoverable: true } }
}
{
id: "color-name"
name: "Color Name"
type: singleLine
config: { singleLine: { multilingual: true } }
}
]
}
) {
... on Shape {
identifier
}
}
}

Note: Do not use variantComponents for fields that are the same across all variants — those belong in components (product level).

Document Shape

Used for editorial and marketing content. Documents are leaf nodes and cannot have children.

Example use cases:

  • Blog posts / Articles
  • About pages
  • FAQ sections
  • News articles
  • Brand pages

Important: Documents cannot have children in the catalogue tree. If you need structure (e.g., landing pages with sub-pages), use a Folder shape instead.

Folder Shape

Used to organize the catalogue hierarchy. Folders can have children.

Example use cases:

  • Categories
  • Collections
  • Landing pages (when structure/children needed)
  • Campaigns
  • Sections

Folders can be nested to create hierarchical navigation.

Components

Components are the atomic fields that make up shapes.

Basic Components

ComponentDescriptionAPI Type
Single LineShort text (max ~256 chars)singleLine
Rich TextFormatted HTML contentrichText
NumericNumbers with optional unitsnumeric
BooleanTrue/false valuesboolean
DatetimeDate and time pickerdatetime
LocationGPS coordinateslocation

Media Components

ComponentDescriptionAPI Type
ImagesImage gallery with variantsimages
VideosVideo filesvideos
FilesAny file typefiles

Images are automatically optimized and served in WebP/AVIF formats with responsive sizes.

Structural Components

Chunk

Groups related fields together. Can be marked as repeatable.

Specifications (Chunk, Repeatable)
├── Weight (Numeric, kg)
├── Height (Numeric, cm)
└── Material (Single Line)

Choice

Polymorphic field where editors select ONE option from predefined choices.

Guitar Specs (Choice)
├── Electric Specs
│ ├── Pickup Type (Single Line)
│ └── Wattage (Numeric)
└── Acoustic Specs
├── Body Wood (Single Line)
└── Soundhole Style (Single Line)

Multiple Choice

Like Choice, but multiple options can be selected simultaneously.

Relation Components

ComponentDescriptionUse Case
Item RelationLinks to catalogue itemsRelated products, cross-sells
Grid RelationLinks to gridsFeatured collections
Topic RelationLinks to topicsCategories, tags

Component Settings

Identifier

Unique key used in API queries. Auto-generated from name but customizable.

  • Use camelCase: productDescription
  • Keep stable once in production
  • Must be unique within the shape

Localization

Enable to allow translation into multiple languages. When enabled:

  • Content can be entered per language
  • API queries require language parameter
  • Missing translations return null

Discoverability

Controls whether the field appears in Discovery API:

  • Enabled: Field is searchable and filterable
  • Disabled: Field only in Catalogue API

Use for fields that need search/filter capabilities.

Repeatability (in Chunks)

When enabled on a chunk, editors can add multiple instances:

Ingredients (Chunk, Repeatable)
├── Name (Single Line)
├── Amount (Numeric)
└── Unit (Single Line)
Result:
- Flour, 500, grams
- Sugar, 200, grams
- Eggs, 3, pieces

Creating a Shape

  1. Go to Settings → Shapes
  2. Click + icon
  3. Select shape type
  4. Drag components from right panel
  5. Configure each component
  6. Click Update to save

Shape Best Practices

  1. Plan before building - Sketch your data model first
  2. Use semantic names - productDescription not field1
  3. Group with chunks - Keep related fields together
  4. Make fields discoverable - Enable for searchable attributes
  5. Consider localization - Enable early if multilingual
  6. Keep shapes focused - One shape per content type
  7. Version carefully - Changing shapes affects existing content

Taxonomies Reference

Taxonomies in Crystallize include Topic Maps for classification and Grids for curated collections.

Topic Maps

Topic Maps define classifications and taxonomies that can be assigned to products, documents, folders, and media assets.

Topics vs Catalogue Hierarchy

AspectCatalogue (Folders)Topics
StructureSingle hierarchyMulti-dimensional
DefinesWhere item livesWhat item is about
NavigationPrimary navigationFaceted filtering
AssignmentOne parent onlyMultiple topics

Why Use Topics?

  1. Classification - Add structured tags to any item
  2. Multi-dimensional navigation - Faceted browsing
  3. Search & filtering - Power Discovery API filters
  4. Consistency - Central taxonomy across teams

Creating Topic Maps

  1. Navigate to Topic Maps in left menu
  2. Click + to create new topic map
  3. Name it clearly (e.g., “Flavour”, “Origin”, “Material”)
  4. Add child topics to build hierarchy

Topic Hierarchy

Topics can be nested at multiple levels:

Flavour (Topic Map)
├── Fruity
│ ├── Citrus
│ │ ├── Lemon
│ │ ├── Lime
│ │ └── Orange
│ └── Berry
│ ├── Strawberry
│ └── Blueberry
├── Roasty
│ ├── Burnt Sugar
│ └── Smoky
│ ├── Dark Smoke
│ └── Light Smoke
└── Nutty
├── Almond
└── Hazelnut

Topic Features

Localization: Each topic can be translated, enabling multilingual taxonomies.

Visual Organization: Drag and drop to restructure topics visually.

Hierarchy Depth: No limit on nesting depth. Design for your use case.

Assigning Topics

Topics are assigned via:

  • Catalogue UI when editing items
  • Topic Relation component in shapes
  • API mutations

Querying Topics

In Discovery API:

{
search(filter: { type: PRODUCT, topics: { path: { equals: "/flavour/fruity/citrus" } } }) {
edges {
node {
name
topics {
name
path
}
}
}
}
}

Topic Aggregations

Get facet counts:

{
search(filter: { type: PRODUCT }) {
aggregations {
topics {
path
name
count
}
}
}
}

Grids

Grids are curated collections of catalogue items, independent of the main hierarchy.

Why Use Grids?

  1. Curated selections - Highlight products for campaigns
  2. Flexible layout - Freeform visual arrangement
  3. Cross-category grouping - Combine items from different areas
  4. Reusability - Use same grid in multiple places

Grid Use Cases

  • Homepage hero/featured sections
  • Seasonal promotions
  • Editor’s picks
  • Newsletter content
  • Landing page components

Creating a Grid

  1. Navigate to Grids in left menu
  2. Click + to create new grid
  3. Name it descriptively (e.g., “Summer Sale”, “Editor’s Picks”)
  4. Add items via search or catalogue browser
  5. Switch to Edit Layout mode to arrange
  6. Click Publish when ready

Grid Layout

Grids support freeform layouts:

  • Resize items individually
  • Create visual hierarchy
  • Mix content types (products + documents)
  • Maintain layout across API responses

Grid Relations

Use Grid Relation component in shapes to reference grids:

Homepage (Document Shape)
├── Hero Banner (Piece)
├── Featured Products (Grid Relation)
├── Latest Posts (Grid Relation)
└── Newsletter Signup (Piece)

Querying Grids

{
grid(id: "grid-id") {
name
rows {
columns {
item {
name
path
... on Product {
defaultVariant {
price
}
}
}
layout {
colspan
rowspan
}
}
}
}
}

Grid Best Practices

  1. Name semantically - “Summer Campaign 2024” not “Grid 1”
  2. Keep focused - One theme per grid
  3. Consider layout - Design for frontend display
  4. Update regularly - Refresh campaign grids
  5. Use for merchandising - Cross-sell, upsell opportunities

Topics vs Grids

FeatureTopicsGrids
PurposeClassificationCuration
StructureHierarchical taxonomyFlat collection with layout
AssignmentTopics assigned to itemsItems placed in grid
DiscoveryPowers filtering/facetsEditorial selection
Use caseNavigation, searchMerchandising, campaigns

Combined Usage

Topics and grids work together:

  1. Use topics to classify all products by attribute
  2. Use grids to create curated selections for campaigns
  3. Filter by topic in Discovery API for category pages
  4. Display grids for promotional areas


Crystallize AI