Chapter 8: Content Operations
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:
- Marks draft version as published (
is_draft = FALSE) - Updates commit message if provided
- Clears
cms_content_items_publishedtable for the project - Copies all published-status items from draft to published table
- Cleans data to match current collection schema
- Updates
cms_published_versionspointer - Creates new draft version (copy from just-published version)
- 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:
- Validates the target version number exists
- Sets target version as published in
cms_published_versions - Clears
cms_content_items_publishedtable - Copies all items from target version to published table
- Cleans data to match current collection schema
- Creates new draft (copy from target version)
- 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
- Use collections strategically - Group related content together
- Standardize descriptions - Use consistent naming: "Product: X" or "Post: Title"
- Plan locales early - Define all supported locales before bulk operations
- Validate schemas - Ensure collection fields match your data before import
Version Management
- Descriptive commit messages - Include context: "Updated pricing and features for Q4 campaign"
- Test in draft - Always validate content changes in draft before publishing
- Regular snapshots - Publish versions at logical intervals (e.g., after major updates)
- Archive old versions - Keep version history manageable by archiving obsolete versions
Performance
- Batch operations - Group updates to reduce API calls
- Use appropriate statuses - Don't publish incomplete translations
- Clean up regularly - Archive and delete obsolete content items
- Monitor version size - Large versions with many items may impact publish performance
Security
- Validate before import - Check imported content for malicious scripts
- Audit translations - Verify machine translations before publishing
- Control version access - Only team members should access version management
- 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