content-model
npx skills add https://github.com/crystallizeapi/ai --skill content-modelCrystallize 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:
- What are you selling or publishing? Products, articles, courses, services?
- How will customers discover items? Browse categories, search, filter by attributes?
- What makes each item unique? Specs, features, editorial content, variants?
- What’s shared across items? Brands, materials, certifications, authors?
- Do you need multiple languages/markets? Which fields vary by language?
- What’s the scale? Number of products, categories, content pages?
- 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:
- What is the item? → Choose shape type (Product, Document, Folder)
- What data does it need? → Add components (see Component Decision Guide)
- How does it relate to others? → Use Item Relations with
acceptedShapeIdentifiers - Is it part of a classification? → Consider design patterns (see below)
Core Concepts
The Five Building Blocks
- Shapes - Blueprints defining item structure (product/document/folder)
- Components - Atomic fields added to shapes (text, numbers, media, relations)
- Pieces - Reusable field sets embedded across shapes
- Topic Maps - Hierarchical taxonomies for classification
- Grids - Curated editorial collections
Shape Types
| Type | Purpose | Has Variants | Can Have Children | Example |
|---|---|---|---|---|
| Product | Sellable items with pricing | Yes | No | Shoes, Subscription |
| Document | Editorial content (leaf nodes) | No | No | Article, Brand, FAQ |
| Folder | Catalogue hierarchy organization | No | Yes | Category, 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 inconfig.numeric.units. - Properties Table → Arbitrary key-value specs
”I need to link to other items”
- Item Relation → Link to catalogue items (products, documents, folders)
- Always use
acceptedShapeIdentifiersto restrict which shapes can be linked - Set
minItems/maxItemsfor cardinality constraints
- Always use
- 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
typefield - choices with notypeor with a rawcomponentsarray are invalid API structures - When each choice needs multiple fields → create a
piece/upsertoperation 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
componentsarray and notypefield — 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 componentsExample - 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?
| Need | Pattern | Structure |
|---|---|---|
| Rich classification (brand, allergen, cert) | Semantic Bridge | Item Relation → Document |
| Relationship with quantity (recipe, BOM, kit) | Quantised Bridge | Chunk(Numeric + Item Relation) |
| Relationship with role (contributor, usage) | Composite Bridge | Chunk(Item Relation + Selection) |
| Relationship with rules (configurator) | Conditional Bridge | Chunk(Item Relation + Rule Type) |
| Single concept, multiple forms (instrument) | Polymorphic Choice | Choice/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:
-
Similarity threshold: Two or more product/folder shapes share 50%+ of their components
- Example: Plant and Vase both have description and SEO components
-
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)
-
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:
-
Fundamentally different purposes: Items serve completely different business functions
- Product vs Folder (different shape types)
- Apparel vs Digital Download (different fulfillment, no pricing overlap)
-
Low component overlap: Less than 30% of components are shared
- Consolidation would create a confusing, bloated shape
-
Different business logic: Items require distinct workflows, permissions, or integrations
- Separate checkout flows
- Different inventory systems
- Distinct pricing models
-
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-typewith options “plant”, “vase”, “bundle”
Step 3: Extract variant-specific attributes into pieces
- Create separate pieces for each variant’s unique components
- Example:
plant-attributespiece,vase-attributespiece
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:
- Phase 1: Create variant-specific pieces (plant-attributes, vase-attributes)
- Phase 2: Create consolidated shape container
- Phase 3: Add components to variant pieces
- 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 mapProduct 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
- Use normalcase for identifiers - Shape and component identifiers should be lowercase with hyphens (e.g.,
product-page,hero-section) - Be concise, use hierarchy - Let the structure provide context. Use
titlenotproduct-page-hero-section-title - Don’t repeat container names - Within a piece called
seo, usetitlenotseo-title. The piece name provides context. - Use semantic names - Name components for what they represent, not how they’re displayed
- Avoid redundant prefixes - Bad: “Meta Title”, “Meta Description”. Good: “Title”, “Description” (when inside SEO piece)
- 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
- Start with intent, not structure - Understand what you’re modeling before creating shapes
- Use pieces for shared fields - SEO, environmental data, common metadata
- Always restrict item relations - Use
acceptedShapeIdentifiersto enforce type safety - Set min/max constraints - Guide editors with cardinality rules (minItems, maxItems)
- Keep shapes focused - One shape per logical content type
- 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
- Use chunks for grouping - Related fields that repeat together
- Choose topics vs documents wisely - Simple labels → topics, rich content → documents
- Plan localization early - Enable
isTranslatablefrom the start - Mark fields discoverable - Enable for filtering/search in Discovery API
- 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
- Create empty pieces →
intent: "piece/upsert"(identifier + name, NOcomponents) - Create empty shapes →
intent: "shape/upsert"(identifier + name + type, NOcomponents/variantComponents) - Add components to pieces →
intent: "piece/upsert"(identifier + name +components) - Add components to shapes →
intent: "shape/upsert"(identifier + name + type +components/variantComponents)
REQUIRED FIELDS:
- All upsert operations MUST include
identifierandname - 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:
- Validate with
build-mass-operationtool — Run the operations array through the MCP tool and fix any schema errors. This is the source of truth. acceptedShapeIdentifierson everyitemRelationscomponent — no untyped relations- No structural nesting violations —
contentChunk,componentChoice,componentMultipleChoicecannot be direct children of each other - Every
contentChunkhas at least 1 component — empty chunks are invalid - Every
componentChoice/componentMultipleChoicehas at least 2 choices — single-choice is invalid - No custom price/stock components on product shapes — these are built-in on variants
paragraphCollectionconfig always hasmultilingualarray — this is required, not optional (use[]for no localization,["title", "body", "images", "videos"]for full localization)datetimeconfig uses only valid fields —required,discoverable,multilingualonly. There is noformatfield.filesconfig: ifacceptedContentTypesis provided, each entry needscontentType(required). IfmaxFileSizeis provided, bothsizeandunitare required.- Naming conventions — all identifiers use lowercase-with-hyphens (no camelCase, no underscores)
- Reusable SEO piece — create an SEO piece and reference it from multiple shapes
- Localization strategy — translatable for marketing text, non-translatable for universal data (codes, dimensions)
References
- Create Shape API Reference - Full mutation type reference with all required fields, config structures, and validation rules for
createShape - Content Modelling Guide - Comprehensive design framework, industry patterns, validation
- Components Reference - All 15+ component types, nesting rules, localization
- Design Patterns Reference - 5 bridge patterns with complete JSON examples
- Shapes Reference - Shape types, variantComponents, and creation process
- Pieces Reference - Reusable field sets
- Taxonomies Reference - Topic Maps and Grids
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”
| Scenario | Component |
|---|---|
| Short, single-value text (subtitle, GTIN, EAN, MPN, ISBN, URL) | Single Line |
| Formatted content with headings, links, lists, bold | Rich Text |
| Long-form editorial with mixed media sections | Paragraph Collection |
- Single Line → structured, unformatted, indexable. Ideal for labels, codes, and short metadata. Note: item
nameand variantskuare 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”
| Scenario | Component |
|---|---|
| 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.unitsas 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"] } } }
- Units config: define the list of valid unit options in
- 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”
| Scenario | Component |
|---|---|
| Product photos, banners, galleries | Images |
| Product demos, tutorials, promo videos | Videos |
| 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.
”I need to link to other items”
| Scenario | Component |
|---|---|
| Link to another catalogue item (product, document, folder) | Item Relation |
| Link to a curated grid of items | Grid 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.
- Configuration options (like relational database constraints):
- 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”
| Scenario | Component |
|---|---|
| Predefined dropdown or multi-select (color, size, material) | Selection |
| Boolean flag (featured, in-stock, certified) | Switch |
| A date or timestamp | Datetime |
| Geographic coordinates | Location |
- Selection → dropdown, radio buttons, or checkboxes with predefined options. Each option has a
key(API value) andvalue(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 }—keyis used in queries/filters,valueis 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”
| Scenario | Component |
|---|---|
| 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: trueto 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.
- Can be repeatable: Enable
- 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) — whenfalse, each choice can only be selected once; whentrue, 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 separatepiece/createoperation
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:
- Create a
piece/createoperation for each variant - 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:
- Performance — Deep nesting impacts query performance and API response times
- Complexity — Beyond 4 levels, content structures become hard to maintain and understand
- 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
isTranslatableconfiguration 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_ITEMas 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:
| Component | Default isTranslatable | Typical use case |
|---|---|---|
| Single Line | ✓ True | Titles, subtitles, slogans, taglines |
| Rich Text | ✓ True | Descriptions, marketing copy, specifications |
| Paragraph Collection | ✓ True | Blog posts, landing pages, editorial content |
| Numeric | ✗ False | Dimensions, weights, ratings (universal values) |
| Properties Table | ✗ False | Technical specs with shared keys/values |
| Images | ✗ False | Product photos (same images, translate captions) |
| Videos | ✗ False | Product videos (same videos, translate captions) |
| Files | ✗ False | Shared downloads (or create separate per-market) |
| Selection | ✗ False | Shared options (size, color codes) |
| Switch | ✗ False | Boolean flags (universal true/false) |
| Datetime | ✗ False | Universal dates (launch, expiration) |
| Location | ✗ False | Geographic coordinates (universal) |
| Item Relation | ✗ False | Relationships are shared, but related items can be translated |
| Grid Relation | ✗ False | Grid 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 languageIn 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
multilingualentirely → 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 guidanceSemantic 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:
-
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")
-
On specific relationship fields (for nested language queries):
Image.topics(language: "en")— get topic associations in a specific languageImageShowcase.items(language: "en")— get showcase items in a specific languagePriceList.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 translationsconst 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:
- Plan translation strategy during shape design — decide which components need localization before creating content
- Structural components rarely need translation — properties, classifications, technical specs usually shared
- Test with 2-3 languages early — verify your translation strategy works before scaling to many markets
- Document your decisions — explain why certain components are/aren’t translatable for future maintainers
- Use Semantic Bridges for reusable translations — translate classification documents once, reuse everywhere
- 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/descriptionsTechnical 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 ChoiceDiscoverability 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 onlyinternal-notes(Rich Text) — admin comments, not for public displaymigration-status(Selection) — data migration trackingintegration-key(Single Line) — third-party system referenceinternal-workflow-stage(Selection) — editorial workflow stateerp-product-code(Single Line) — back-office system integrationwarehouse-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
| Component | Available validations |
|---|---|
| Single Line | Min/max character length, regex pattern, required/optional |
| Rich Text | Min/max character length, required/optional |
| Paragraph Collection | Min/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
| Component | Available validations |
|---|---|
| Numeric | Min/max value, decimal places, required/optional |
| Properties Table | Min/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
| Component | Available validations |
|---|---|
| Images | Min/max count, max file size, min/max dimensions, required |
| Videos | Min/max count, max file size, max duration, required |
| Files | Min/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
| Component | Available validations |
|---|---|
| Selection | Min/max selections, required/optional |
| Switch | No validations (always boolean true/false) |
| Datetime | Min/max date range, required/optional |
| Location | Required/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: trueon 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
| Component | Available validations |
|---|---|
| Item Relation | Min/max relations, accepted shape identifiers, required |
| Grid Relation | Min/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
| Component | Available validations |
|---|---|
| Chunk | Min/max items (for repeating chunks), required |
| Choice | Required/optional |
| Multiple Choice | Min/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
- Start lenient, tighten later — it’s easier to add validations than remove them after content is created
- Use validations to prevent common errors — regex for SKU format, min/max for ratings, required for critical fields
- Guide editors with clear validation messages — “Product description must be at least 100 characters to ensure quality”
- Combine with discoverability — fields used for filtering should have validations ensuring data consistency
- 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?
-
Will this exact group be used in 2+ locations? (multiple shapes OR multiple pieces)
- YES → Use a Piece (reusable)
- NO → Continue to step 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)
- YES → Use a Chunk with
Examples
| Scenario | Solution | Reasoning |
|---|---|---|
| SEO fields needed on products, articles, and landing pages | Piece | Used in 3+ shapes |
| Banner section used on landing pages and category pages | Piece | Used in 2+ shapes |
| Product ingredients (flour, sugar, eggs) | Chunk (repeatable) | List of items |
| Multiple shipping addresses | Chunk (repeatable) | List of addresses |
| One-time metadata group (creation date + author + tags) on a single shape | Chunk (non-repeatable) | Only used once, not repeating |
| Contact info (email + phone + address) appearing only on About page | Chunk (non-repeatable) | Only used in one place, single occurrence |
| Video settings (autoplay + muted + loop) for a video component | Chunk (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: falsefor 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 name | Components inside | Used in |
|---|---|---|
| SEO Metadata | meta-title (Single Line), meta-description (Rich Text), og-image (Images) | All product, document, folder shapes |
| CTA Block | headline (Single Line), body (Rich Text), button-label (Single Line), button-url (Single Line) | Landing pages, category folders |
| Environmental Data | carbon-footprint (Numeric, kg CO₂), recyclable (Switch), certifications (Item Relation) | Product shapes |
| Social Links | website (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
- Reusability — Pieces can be used across multiple page shapes (Landing Page, Category Page, etc.)
- Maintainability — Update a Piece once, changes reflect everywhere it’s used
- Flexibility — Editors compose pages by selecting which sections to include
- Repeatability — Same section type can appear multiple times (3 banners, 2 product grids)
- 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
layoutpiece (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
- Avoid shape explosion — One product shape instead of separate shapes for Physical Product, Digital Product, Subscription Product
- Flexible combinations — A product can be physical + subscription, or digital + warranty
- Consistent editing — All products use the same shape, editors just select which details apply
- 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 identifierlanguage- Language codetree- Catalogue tree positiontopics- Topic classificationscreatedAt- Creation timestampupdatedAt- Last modification timestamp
Product variant built-in fields:
sku- Stock keeping unit (unique identifier)images- Product photosvideos- Product videosprice- Pricing informationstock- Inventory levelsattributes- 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:
- What are you selling/publishing? (products, articles, courses, etc.)
- How will customers discover it? (search, browse categories, filters)
- What makes each item unique? (specs, features, content)
- What’s shared across items? (brands, materials, classifications)
- 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 purchasedDocuments → Editorial content, marketing pagesFolders → Categories, collections, organizational structureLayer 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 collectionsDecision 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 → ColorsWhen 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:
- Item Relations:
minItems: 1makes the field required - Selection:
min: 1, max: 1for required single selection - Business logic: Validate in your application layer
- 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:
- Export data first
- Create new shape with migration path
- Gradually migrate content
- Archive old shape when complete
Changing Component Types
Not supported directly. Pattern:
- Add new component with different ID
- Migrate data via API
- Remove old component
Renaming Components
Use the identifier field:
- Display name can change freely (editor-facing)
- Identifier change requires data migration (API-facing)
References
- Components Reference - All component types and configuration
- Design Patterns Reference - Bridge patterns and advanced modeling
- Shapes Reference - Shape types and creation
- Pieces Reference - Reusable field sets
- Taxonomies Reference - Topic Maps and Grids
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 type | Valid direct children | Invalid direct children |
|---|---|---|
contentChunk | pieces, regular components | componentChoice, componentMultipleChoice, contentChunk |
componentChoice | pieces, regular components | componentChoice, componentMultipleChoice, contentChunk |
componentMultipleChoice | pieces, regular components | componentChoice, 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
| Error | Cause |
|---|---|
InvalidIdError | identifier contains invalid characters (use lowercase letters, numbers, hyphens) |
PieceIdentifierTakenError | The identifier is already used by an existing piece |
UnauthorizedError | Access token lacks write permissions |
ExperimentalFeaturesNotAvailableError | Feature 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
| Pattern | One-line summary | Core mechanism |
|---|---|---|
| Semantic Classification Bridge | Shared, enrichable attributes as separate documents | Item Relation → Document |
| Quantised Classification Bridge | Relationships with measurable quantities | Chunk (Relation + Numeric + Unit) |
| Conditional Classification Bridge | Relationships with rules and dependencies | Chunk (Relation + Rule selection) |
| Composite Classification Bridge | Relationships that describe what the relationship means | Chunk (Relation + Role/meaning selection) |
| Polymorphic Choice | A single concept with mutually exclusive structural forms | Choice 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 CHOICE1. 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
- Create a Document shape for the classification (e.g., “Allergen”, “Brand”, “Material”)
- Create documents for each value (e.g., “Peanuts”, “Gluten”, “Dairy”)
- Add an Item Relation component to the product shape
- 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)
- 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) └── metadataExample: 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
| Domain | Classification | Document shape fields |
|---|---|---|
| Food | Allergen | name, icon, severity level, regulatory info |
| Fashion | Material | name, image, care instructions, sustainability score |
| Electronics | Brand | name, logo, description, website, country of origin |
| Beauty | Ingredient | name, INCI name, description, benefits, warnings |
| B2B | Certification | name, 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: 1Example: 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
| Domain | Parent item | Linked item | Quantity meaning |
|---|---|---|---|
| Food/Recipes | Recipe | Ingredient | 200g of flour, 2 eggs |
| Manufacturing | Assembly | Part | 4x M6 bolts, 1x motor |
| Furniture kits | Furniture kit | Component | 4x legs, 1x tabletop, 8x screws |
| Nutrition | Meal | Ingredient | Enables calorie/macro rollups |
| Construction | Material list | Material | 50m² 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: 1Common 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
| Domain | Scenario | Rule |
|---|---|---|
| Pizza builder | Chili topping requires Cheese | Chili → requires → Cheese |
| Pizza builder | Vegan Cheese excludes Meat toppings | Vegan Cheese → excludes → Meat |
| PC builder | RTX 4090 requires 850W PSU | GPU → requires → PSU with min spec |
| Furniture | Wooden legs require matching screw set | Legs → requires → Screw Set |
| Cameras | EF-S lens excludes full-frame body | Lens → excludes → Body type |
| Car config | Sport package requires sport suspension | Package → 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
| Domain | Parent item | Related item | Relationship meaning |
|---|---|---|---|
| Publishing | Book | Person | Writer, Illustrator, Editor |
| Fashion | Jacket | Material | Lining, Sleeve, Collar, Body |
| Music | Album | Person | Producer, Vocalist, Guitarist |
| Construction | Building | Company | Architect, Contractor, Supplier |
| Modular sofa | Sofa | Component | Legs, 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 forms — poly- (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
| Domain | Choice concept | Variants |
|---|---|---|
| Music gear | Product type | Guitar, Amplifier, Pedal, Accessory |
| Landing pages | Hero section | Image hero, Video hero, Text hero, Product hero |
| B2B industrial | Product class | ETIM class A, ETIM class B (each with own specs) |
| Real estate | Property type | Apartment, House, Commercial, Land |
| Food | Preparation type | Ready-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 attributes | Semantic Classification Bridge |
| Items contain measured amounts of other items | Quantised Classification Bridge |
| Options have compatibility rules and dependencies | Conditional Classification Bridge |
| Relationships need to describe their meaning/role | Composite Classification Bridge |
| A concept takes one of several mutually exclusive forms | Polymorphic 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-pageandcategoryshapes incontent-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:
componentMultipleChoice— the “block slot” on a page shape that holds an ordered list of sections- Pieces — self-contained, reusable section definitions (the actual blocks)
- A shared
layoutpiece — 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 identifier | What it renders | Key components |
|---|---|---|
banner | Full-width hero/promotional section | title, description, image, call-to-action, layout |
feature-highlights | USP grid (repeatable icon + headline + copy) | usp (contentChunk, repeatable), layout |
product-slider | Curated horizontal product carousel | title, description, itemRelations → products, layout |
story-slider | Editorial story carousel | title, description, itemRelations → stories, layout |
picture-grid | Fixed 4-image visual grid | title, description, images (min/max: 4), layout |
category-slider | Category navigation carousel | title, 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) orcontain(max-width wrapper). Default:contain.theme: Controls colour palette of the block. Default:light.background-media: Optional image or video background — usescomponentChoicefor mutual exclusivity (one or the other, not both). Note:componentChoiceis 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:
| Market | Blocks on landing page |
|---|---|
| English | Hero Banner → Product Slider → Testimonials → CTA Banner |
| German | Hero Banner → Feature Highlights → Product Slider |
| French | Video Section → Category Slider → Story Slider → Banner |
| Japanese | Banner → 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 videostory.media: image, shoppable-image, or videomenu-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}groupproduct.details: repeatable{title, description}tab/accordion contentcall-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:
| Piece | Used by | Purpose |
|---|---|---|
call-to-action | banner | Repeatable {label, url} CTA buttons |
meta | landing-page, category, product, story | SEO title, description, image |
dimensions | product, product variant | Physical 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 patternblocks.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
- Navigate to Settings → Shapes
- Scroll to the Pieces section
- Click + to create new piece
- Give it a descriptive name
- Add components using drag and drop
- Configure each component
- Click Update to save
Using Pieces in Shapes
- Open the shape you want to modify
- In the right panel, find your piece
- Drag and drop it into the shape
- Position as needed
- 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
- Name clearly - “SEO Metadata” not “SEO”
- Single responsibility - One piece = one concept
- Plan for reuse - Design pieces to work in multiple contexts
- Keep pieces small - Large pieces are harder to maintain
- Document usage - Note which shapes use each piece
- Version carefully - Changes affect all shapes using the piece
Pieces vs Chunks
| Feature | Piece | Chunk |
|---|---|---|
| Reusable across shapes | ✅ | ❌ |
| Can be repeatable | ❌ (embed in chunk for this) | ✅ |
| Lives in | Settings → Shapes | Inside a shape |
| Changes affect | All shapes using it | Only 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 } } } } } }}Related Links
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
variantComponentsfor fields that are the same across all variants — those belong incomponents(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
| Component | Description | API Type |
|---|---|---|
| Single Line | Short text (max ~256 chars) | singleLine |
| Rich Text | Formatted HTML content | richText |
| Numeric | Numbers with optional units | numeric |
| Boolean | True/false values | boolean |
| Datetime | Date and time picker | datetime |
| Location | GPS coordinates | location |
Media Components
| Component | Description | API Type |
|---|---|---|
| Images | Image gallery with variants | images |
| Videos | Video files | videos |
| Files | Any file type | files |
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
| Component | Description | Use Case |
|---|---|---|
| Item Relation | Links to catalogue items | Related products, cross-sells |
| Grid Relation | Links to grids | Featured collections |
| Topic Relation | Links to topics | Categories, 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, piecesCreating a Shape
- Go to Settings → Shapes
- Click + icon
- Select shape type
- Drag components from right panel
- Configure each component
- Click Update to save
Shape Best Practices
- Plan before building - Sketch your data model first
- Use semantic names -
productDescriptionnotfield1 - Group with chunks - Keep related fields together
- Make fields discoverable - Enable for searchable attributes
- Consider localization - Enable early if multilingual
- Keep shapes focused - One shape per content type
- Version carefully - Changing shapes affects existing content
Related Links
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
| Aspect | Catalogue (Folders) | Topics |
|---|---|---|
| Structure | Single hierarchy | Multi-dimensional |
| Defines | Where item lives | What item is about |
| Navigation | Primary navigation | Faceted filtering |
| Assignment | One parent only | Multiple topics |
Why Use Topics?
- Classification - Add structured tags to any item
- Multi-dimensional navigation - Faceted browsing
- Search & filtering - Power Discovery API filters
- Consistency - Central taxonomy across teams
Creating Topic Maps
- Navigate to Topic Maps in left menu
- Click + to create new topic map
- Name it clearly (e.g., “Flavour”, “Origin”, “Material”)
- 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 └── HazelnutTopic 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?
- Curated selections - Highlight products for campaigns
- Flexible layout - Freeform visual arrangement
- Cross-category grouping - Combine items from different areas
- 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
- Navigate to Grids in left menu
- Click + to create new grid
- Name it descriptively (e.g., “Summer Sale”, “Editor’s Picks”)
- Add items via search or catalogue browser
- Switch to Edit Layout mode to arrange
- 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
- Name semantically - “Summer Campaign 2024” not “Grid 1”
- Keep focused - One theme per grid
- Consider layout - Design for frontend display
- Update regularly - Refresh campaign grids
- Use for merchandising - Cross-sell, upsell opportunities
Topics vs Grids
| Feature | Topics | Grids |
|---|---|---|
| Purpose | Classification | Curation |
| Structure | Hierarchical taxonomy | Flat collection with layout |
| Assignment | Topics assigned to items | Items placed in grid |
| Discovery | Powers filtering/facets | Editorial selection |
| Use case | Navigation, search | Merchandising, campaigns |
Combined Usage
Topics and grids work together:
- Use topics to classify all products by attribute
- Use grids to create curated selections for campaigns
- Filter by topic in Discovery API for category pages
- Display grids for promotional areas
Related Links
Crystallize AI