Copied to clipboard!

Bulk Operations

Creating Multiple Content Items

Bulk content creation is essential when importing content from external sources or setting up initial content structure. Vibe CMS provides efficient functions for creating multiple items at once.

Via MCP Tools

Use the create_content MCP tool with a loop to create multiple items:

// Create multiple blog posts
const items = [
  { title: "Getting Started", slug: "getting-started" },
  { title: "Advanced Features", slug: "advanced-features" },
  { title: "Best Practices", slug: "best-practices" }
];

for (const post of items) {
  const result = await callTool('create_content', {
    collection_slug: 'blog-posts',
    description: `Post: ${post.title}`,
    status: 'draft'
  });
  
  // Update with content data
  await callTool('update_content_translation', {
    content_item_id: result.id,
    locale: 'en-US',
    data: {
      title: post.title,
      slug: post.slug,
      body: '...'
    }
  });
}

Via Backend API

For server-side bulk operations, use the create_content_item and validate_and_upsert_translation functions:

from app.services.supabase import SupabaseService

async def bulk_create_posts(supabase: SupabaseService, posts: list[dict]):
    created_items = []
    
    for post in posts:
        # Create content item
        result = await supabase.call_rpc('create_content_item', {
            'p_project_id': project_id,
            'p_collection_id': collection_id,
            'p_description': post.get('title'),
            'p_status': 'draft'
        })
        
        item_id = result[0]['id']
        
        # Upsert translation with validation
        await supabase.call_rpc('validate_and_upsert_translation', {
            'p_content_item_id': item_id,
            'p_locale': 'en-US',
            'p_data': post.get('data', {}),
            'p_status': 'draft'
        })
        
        created_items.append(item_id)
    
    return created_items

Batch Status Updates

Update multiple items to a new status atomically:

// Update multiple items to published status
async function bulkPublish(contentItemIds, locale) {
  for (const itemId of contentItemIds) {
    await callTool('update_content_translation', {
      content_item_id: itemId,
      locale: locale,
      status: 'published'
    });
  }
}

The validate_and_upsert_translation function (used internally) automatically handles:

  • Required field validation
  • Schema compatibility checking
  • Null field prevention for required fields
  • Data cleanup to match current collection schema

Batch Translations

Create translations for multiple items across locales:

// Translate 50 items from English to Spanish
const items = await callTool('content', {
  collection_slug: 'products'
});

for (const item of items.items) {
  // Get existing English version
  const enContent = await callTool('content', {
    collection_slug: 'products',
    content_item_id: item.id,
    locale: 'en-US'
  });
  
  // Call translation service (e.g., via LLM or translation API)
  const translatedData = await translateContent(
    enContent.translations[0].data,
    'en-US',
    'es-ES'
  );
  
  // Create Spanish translation
  await callTool('update_content_translation', {
    content_item_id: item.id,
    locale: 'es-ES',
    data: translatedData
  });
}

Content Migration

Exporting Content

Export content from a collection for migration or backup:

async function exportCollectionContent(collectionSlug) {
  const content = await callTool('content', {
    collection_slug: collectionSlug
  });
  
  const exported = [];
  
  for (const item of content.items) {
    const fullItem = await callTool('content', {
      collection_slug: collectionSlug,
      content_item_id: item.id
    });
    
    exported.push({
      id: item.id,
      description: fullItem.description,
      status: fullItem.status,
      translations: fullItem.translations
    });
  }
  
  return JSON.stringify(exported, null, 2);
}

Importing Content

Import exported content into a new project:

async function importContent(data, targetCollectionSlug) {
  const parsed = JSON.parse(data);
  const results = {
    created: 0,
    failed: 0,
    errors: []
  };
  
  for (const item of parsed) {
    try {
      // Create new item
      const newItem = await callTool('create_content', {
        collection_slug: targetCollectionSlug,
        description: item.description,
        status: item.status
      });
      
      // Create translations
      for (const translation of item.translations) {
        await callTool('update_content_translation', {
          content_item_id: newItem.id,
          locale: translation.locale,
          data: translation.data
        });
      }
      
      results.created++;
    } catch (error) {
      results.failed++;
      results.errors.push({
        item: item.id,
        error: error.message
      });
    }
  }
  
  return results;
}

Moving Content Between Collections

Migrate content while updating field mappings:

async function migrateToNewCollection(
  sourceCollectionSlug,
  targetCollectionSlug,
  fieldMappings
) {
  const source = await callTool('content', {
    collection_slug: sourceCollectionSlug
  });
  
  const migrationResults = [];
  
  for (const item of source.items) {
    const fullItem = await callTool('content', {
      collection_slug: sourceCollectionSlug,
      content_item_id: item.id
    });
    
    // Create target item
    const targetItem = await callTool('create_content', {
      collection_slug: targetCollectionSlug,
      description: fullItem.description,
      status: fullItem.status
    });
    
    // Map and create translations
    for (const translation of fullItem.translations) {
      const mappedData = {};
      
      // Apply field mappings
      for (const [sourceField, targetField] of Object.entries(fieldMappings)) {
        if (sourceField in translation.data) {
          mappedData[targetField] = translation.data[sourceField];
        }
      }
      
      await callTool('update_content_translation', {
        content_item_id: targetItem.id,
        locale: translation.locale,
        data: mappedData
      });
    }
    
    migrationResults.push({
      sourceId: item.id,
      targetId: targetItem.id,
      status: 'success'
    });
  }
  
  return migrationResults;
}

Version Control

Understanding Draft Versions

Every project has exactly one draft version at any time. The draft version is:

  • The working copy where all content edits occur
  • Automatically created when you publish a version
  • Always named "Version N" where N is the sequential version number
  • Linked to its parent version via parent_version_id

Content items use a composite primary key (id, version_id) allowing the same logical content to exist across multiple versions with different values.

Version Lifecycle

1. DRAFT PHASE
   └─ Edit content in draft version
   └─ Create/update/delete items and translations
   └─ Test and validate

2. PUBLISH
   └─ Call publish_draft()
   └─ Draft becomes published (is_draft = FALSE)
   └─ Content synced to cms_content_items_published table
   └─ New draft automatically created from published content

3. PUBLISHED STATE
   └─ Version is immutable (cannot be edited)
   └─ Served as current public version
   └─ Can be rolled back to if needed

4. ARCHIVE
   └─ Old versions can be archived
   └─ Cannot archive draft or published versions

Querying Versions

List all versions for a project with metadata:

async function listProjectVersions(projectId) {
  // Uses get_versions() function - JWT only
  const versions = await callManagedFunction('get_versions', {
    p_project_id: projectId
  });
  
  return versions.map(v => ({
    id: v.id,
    name: v.version_name,
    number: v.version_number,
    isDraft: v.is_draft,
    isPublished: v.is_published,
    parentVersionNumber: v.parent_version_number,
    commitMessage: v.commit_message,
    contentCount: v.content_count,
    createdAt: v.created_at,
    archivedAt: v.archived_at
  }));
}

Creating Versions Programmatically

While the primary workflow uses automatic draft creation, you can create versions manually:

# Via backend - JWT only, no API key support
async def create_new_version(supabase, project_id, copy_from_version_id):
    result = await supabase.call_rpc('create_version', {
        'p_project_id': project_id,
        'p_copy_from_version_id': copy_from_version_id,
        'p_commit_message': 'Manual version creation',
        'p_created_by': current_user_id
    })
    
    return result  # Returns new version ID

Rollback and Restore

Publishing the Draft Version

Publish the draft version and automatically create a new draft:

async function publishContent(projectId, commitMessage = null) {
  // JWT-only function
  const publishedVersionId = await callManagedFunction('publish_draft', {
    p_project_id: projectId,
    p_commit_message: commitMessage,
    p_published_by: currentUserId
  });
  
  return {
    publishedVersionId,
    message: 'Draft published successfully. New draft created automatically.'
  };
}

The publish_draft() function:

  1. Marks draft version as published (is_draft = FALSE)
  2. Updates commit message if provided
  3. Clears cms_content_items_published table for the project
  4. Copies all published-status items from draft to published table
  5. Cleans data to match current collection schema
  6. Updates cms_published_versions pointer
  7. Creates new draft version (copy from just-published version)
  8. Returns the published version ID

Rolling Back to Previous Version

Restore a previous version as the current published version:

async function rollbackToVersion(projectId, targetVersionNumber) {
  // JWT-only function
  const newDraftVersionNumber = await callManagedFunction('rollback_to_version', {
    p_project_id: projectId,
    p_target_version_number: targetVersionNumber,
    p_user_id: currentUserId
  });
  
  return {
    targetVersionNumber,
    newDraftVersionNumber,
    message: `Rolled back to version ${targetVersionNumber}. New draft created as version ${newDraftVersionNumber}.`
  };
}

The rollback_to_version() function:

  1. Validates the target version number exists
  2. Sets target version as published in cms_published_versions
  3. Clears cms_content_items_published table
  4. Copies all items from target version to published table
  5. Cleans data to match current collection schema
  6. Creates new draft (copy from target version)
  7. Returns new draft version number

Rollback Guarantees

Vibe CMS provides safe rollback with these guarantees:

// Safe rollback workflow
async function safeRollback(projectId, targetVersion) {
  try {
    // 1. Get current state
    const currentVersions = await listProjectVersions(projectId);
    const targetVersionInfo = currentVersions.find(v => v.number === targetVersion);
    
    if (!targetVersionInfo) {
      throw new Error(`Version ${targetVersion} not found`);
    }
    
    // 2. Cannot rollback draft or current published
    if (targetVersionInfo.isDraft) {
      throw new Error('Cannot rollback to draft version');
    }
    
    if (targetVersionInfo.isPublished) {
      throw new Error('Already at this version');
    }
    
    // 3. Perform rollback
    const result = await rollbackToVersion(projectId, targetVersion);
    
    // 4. Verify rollback
    const newState = await listProjectVersions(projectId);
    const newPublished = newState.find(v => v.isPublished);
    
    if (newPublished.number !== targetVersion) {
      throw new Error('Rollback verification failed');
    }
    
    return { success: true, ...result };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

Content Duplication

Duplicating Individual Items

Clone a content item with all translations:

async function duplicateContentItem(sourceItemId, collectionSlug) {
  // Get source item with all translations
  const source = await callTool('content', {
    collection_slug: collectionSlug,
    content_item_id: sourceItemId
  });
  
  // Create new item
  const newItem = await callTool('create_content', {
    collection_slug: collectionSlug,
    description: `${source.description} (Copy)`,
    status: source.status
  });
  
  // Duplicate all translations
  for (const translation of source.translations) {
    await callTool('update_content_translation', {
      content_item_id: newItem.id,
      locale: translation.locale,
      data: JSON.parse(JSON.stringify(translation.data)) // Deep clone
    });
  }
  
  return {
    original: sourceItemId,
    duplicate: newItem.id,
    translationsCount: source.translations.length
  };
}

Creating Templates

Save a content item as a template for future use:

async function saveAsTemplate(
  sourceItemId,
  collectionSlug,
  templateName
) {
  const source = await callTool('content', {
    collection_slug: collectionSlug,
    content_item_id: sourceItemId
  });
  
  // Save to local storage or database
  const template = {
    name: templateName,
    collectionSlug,
    description: source.description,
    translations: source.translations.map(t => ({
      locale: t.locale,
      data: t.data
    }))
  };
  
  localStorage.setItem(`template:${templateName}`, JSON.stringify(template));
  
  return {
    templateName,
    saved: true,
    translatableLocales: source.translations.map(t => t.locale)
  };
}

async function createFromTemplate(templateName, collectionSlug, newDescription) {
  const template = JSON.parse(localStorage.getItem(`template:${templateName}`));
  
  if (!template) {
    throw new Error(`Template "${templateName}" not found`);
  }
  
  // Create new item from template
  const newItem = await callTool('create_content', {
    collection_slug: collectionSlug,
    description: newDescription,
    status: 'draft'
  });
  
  // Create translations
  for (const translation of template.translations) {
    await callTool('update_content_translation', {
      content_item_id: newItem.id,
      locale: translation.locale,
      data: translation.data
    });
  }
  
  return newItem.id;
}

Bulk Duplication

Duplicate multiple items at once:

async function bulkDuplicate(
  sourceItemIds,
  collectionSlug,
  suffixPattern = ' (Copy)'
) {
  const results = [];
  
  for (const itemId of sourceItemIds) {
    try {
      const result = await duplicateContentItem(itemId, collectionSlug);
      results.push({
        sourceId: itemId,
        duplicateId: result.duplicate,
        status: 'success'
      });
    } catch (error) {
      results.push({
        sourceId: itemId,
        status: 'failed',
        error: error.message
      });
    }
  }
  
  return {
    total: sourceItemIds.length,
    successful: results.filter(r => r.status === 'success').length,
    failed: results.filter(r => r.status === 'failed').length,
    results
  };
}

Advanced Queries

Complex Filtering

Filter content items by multiple criteria:

async function advancedQuery(collectionSlug, filters) {
  const allItems = await callTool('content', {
    collection_slug: collectionSlug
  });
  
  let results = allItems.items;
  
  // Status filter
  if (filters.statuses) {
    results = results.filter(item => filters.statuses.includes(item.status));
  }
  
  // Content filter (search translations)
  if (filters.searchTerm) {
    const term = filters.searchTerm.toLowerCase();
    results = results.filter(item => {
      const fullItem = callTool('content', {
        collection_slug: collectionSlug,
        content_item_id: item.id
      });
      
      return JSON.stringify(fullItem).toLowerCase().includes(term);
    });
  }
  
  // Locale filter
  if (filters.locale) {
    results = results.filter(item => {
      const fullItem = callTool('content', {
        collection_slug: collectionSlug,
        content_item_id: item.id,
        locale: filters.locale
      });
      
      return fullItem.translations.some(t => t.locale === filters.locale);
    });
  }
  
  // Date range filter
  if (filters.createdAfter || filters.createdBefore) {
    results = results.filter(item => {
      const createdAt = new Date(item.created_at);
      
      if (filters.createdAfter && createdAt < new Date(filters.createdAfter)) {
        return false;
      }
      
      if (filters.createdBefore && createdAt > new Date(filters.createdBefore)) {
        return false;
      }
      
      return true;
    });
  }
  
  return results;
}

Sorting and Pagination

Organize results for efficient presentation:

function sortAndPaginate(items, sortBy = 'created_at', order = 'desc', page = 1, pageSize = 20) {
  // Sort
  const sorted = [...items].sort((a, b) => {
    const aVal = a[sortBy];
    const bVal = b[sortBy];
    
    if (typeof aVal === 'string') {
      return order === 'asc' 
        ? aVal.localeCompare(bVal)
        : bVal.localeCompare(aVal);
    }
    
    return order === 'asc'
      ? aVal - bVal
      : bVal - aVal;
  });
  
  // Paginate
  const totalPages = Math.ceil(sorted.length / pageSize);
  const startIdx = (page - 1) * pageSize;
  const endIdx = startIdx + pageSize;
  
  return {
    items: sorted.slice(startIdx, endIdx),
    pagination: {
      page,
      pageSize,
      total: sorted.length,
      totalPages,
      hasNext: page < totalPages,
      hasPrev: page > 1
    }
  };
}

Full-Text Search

Search content across all fields:

async function fullTextSearch(collectionSlug, searchTerm, locales = null) {
  const allItems = await callTool('content', {
    collection_slug: collectionSlug
  });
  
  const results = [];
  const term = searchTerm.toLowerCase();
  
  for (const item of allItems.items) {
    const fullItem = await callTool('content', {
      collection_slug: collectionSlug,
      content_item_id: item.id
    });
    
    for (const translation of fullItem.translations) {
      // Skip if specific locales requested and this isn't one
      if (locales && !locales.includes(translation.locale)) {
        continue;
      }
      
      // Search in all fields
      const matches = searchInObject(translation.data, term);
      
      if (matches.length > 0) {
        results.push({
          itemId: item.id,
          description: fullItem.description,
          locale: translation.locale,
          matches: matches,
          relevance: matches.length
        });
      }
    }
  }
  
  // Sort by relevance
  return results.sort((a, b) => b.relevance - a.relevance);
}

function searchInObject(obj, term, path = '') {
  const matches = [];
  
  for (const [key, value] of Object.entries(obj)) {
    const currentPath = path ? `${path}.${key}` : key;
    
    if (typeof value === 'string' && value.toLowerCase().includes(term)) {
      matches.push({ field: currentPath, value });
    } else if (typeof value === 'object' && value !== null) {
      matches.push(...searchInObject(value, term, currentPath));
    }
  }
  
  return matches;
}

Batch Retrieval with Caching

Efficiently fetch multiple items with caching:

class ContentCache {
  constructor(collectionSlug) {
    this.collectionSlug = collectionSlug;
    this.cache = new Map();
    this.cacheAge = new Map();
    this.maxCacheAge = 5 * 60 * 1000; // 5 minutes
  }
  
  async getItem(itemId) {
    // Check cache
    const cached = this.cache.get(itemId);
    const age = Date.now() - (this.cacheAge.get(itemId) || 0);
    
    if (cached && age < this.maxCacheAge) {
      return cached;
    }
    
    // Fetch fresh
    const item = await callTool('content', {
      collection_slug: this.collectionSlug,
      content_item_id: itemId
    });
    
    this.cache.set(itemId, item);
    this.cacheAge.set(itemId, Date.now());
    
    return item;
  }
  
  async getItems(itemIds) {
    return Promise.all(itemIds.map(id => this.getItem(id)));
  }
  
  invalidate(itemId = null) {
    if (itemId) {
      this.cache.delete(itemId);
      this.cacheAge.delete(itemId);
    } else {
      this.cache.clear();
      this.cacheAge.clear();
    }
  }
}

Best Practices

Content Organization

  1. Use collections strategically - Group related content together
  2. Standardize descriptions - Use consistent naming: "Product: X" or "Post: Title"
  3. Plan locales early - Define all supported locales before bulk operations
  4. Validate schemas - Ensure collection fields match your data before import

Version Management

  1. Descriptive commit messages - Include context: "Updated pricing and features for Q4 campaign"
  2. Test in draft - Always validate content changes in draft before publishing
  3. Regular snapshots - Publish versions at logical intervals (e.g., after major updates)
  4. Archive old versions - Keep version history manageable by archiving obsolete versions

Performance

  1. Batch operations - Group updates to reduce API calls
  2. Use appropriate statuses - Don't publish incomplete translations
  3. Clean up regularly - Archive and delete obsolete content items
  4. Monitor version size - Large versions with many items may impact publish performance

Security

  1. Validate before import - Check imported content for malicious scripts
  2. Audit translations - Verify machine translations before publishing
  3. Control version access - Only team members should access version management
  4. Backup before major changes - Publish backup version before large migrations

Troubleshooting

Common Issues

"Draft version not found"

Cause: Project lost its draft version (shouldn't happen in normal operation) Solution: Contact support - this indicates a critical issue requiring database recovery

"Unknown field" validation error

Cause: Content data includes a field not defined in collection schema Solution: Check collection fields or update import data to match schema

// Debug: List valid fields
async function debugCollectionSchema(collectionSlug) {
  const items = await callTool('content', {
    collection_slug: collectionSlug
  });
  
  if (items.items.length > 0) {
    const sample = await callTool('content', {
      collection_slug: collectionSlug,
      content_item_id: items.items[0].id
    });
    
    console.log('Available fields:', Object.keys(sample.translations[0].data));
  }
}

"Cannot delete required field"

Cause: Attempted to remove a required field value in an update Solution: All required fields must have values; use null only for optional fields

"Translation already exists"

Cause: Attempted to create translation for locale that already exists Solution: Use update_content_translation instead to update existing translation

Publish takes too long

Cause: Large number of items (1000+) or complex schema Solution: Increase timeout, split into smaller batches, or optimize collection schema

Validation Errors

// Robust error handling for operations
async function operationWithFallback(operation, fallback) {
  try {
    return await operation();
  } catch (error) {
    if (error.message.includes('Unknown field')) {
      console.error('Schema mismatch:', error);
      return fallback();
    }
    
    if (error.message.includes('Missing required field')) {
      console.error('Incomplete data:', error);
      throw new Error('Content missing required fields. Check schema.');
    }
    
    if (error.message.includes('Access denied')) {
      console.error('Permission issue:', error);
      throw new Error('You do not have permission for this operation.');
    }
    
    throw error;
  }
}

Performance Debugging

async function benchmarkOperation(operation, name) {
  const start = Date.now();
  const result = await operation();
  const duration = Date.now() - start;
  
  console.log(`${name}: ${duration}ms`);
  
  if (duration > 5000) {
    console.warn(`${name} exceeded 5s threshold`);
  }
  
  return { result, duration };
}

// Usage
await benchmarkOperation(
  () => publishContent(projectId, 'Publish test'),
  'Publish Draft'
);

Next Steps

Now that you understand content operations:

  • Practice bulk operations with test data
  • Experiment with version control workflows
  • Implement advanced queries for your use cases
  • Build migration scripts for content imports

Continue to: Chapter 9: Multi-language Content