Appendix C: Migration Guides
This appendix provides comprehensive guides for migrating to Vibe CMS from other platforms, evolving your schema, and performing zero-downtime migrations.
1. Migrating from Other CMSs
1.1 From WordPress
Content Export Strategy
WordPress stores content in MySQL tables. Export data using WP REST API or direct database access.
Export Posts via WP REST API:
import { createVibeCMS } from 'vibe-cms-sdk';
const cms = createVibeCMS({
projectId: 'your-project-id',
apiKey: 'your-api-key',
baseUrl: 'https://api.vibe-cms.com'
});
// Fetch WordPress posts
async function fetchWordPressPosts() {
const response = await fetch('https://your-site.com/wp-json/wp/v2/posts?per_page=100');
const posts = await response.json();
return posts;
}
// Map WordPress post to Vibe CMS structure
function mapWordPressPost(wpPost: any) {
return {
title: wpPost.title.rendered,
content: wpPost.content.rendered,
excerpt: wpPost.excerpt.rendered,
slug: wpPost.slug,
published_at: wpPost.date,
author: wpPost.author,
status: wpPost.status === 'publish' ? 'published' : 'draft',
featured_image: wpPost.featured_media
};
}
// Import to Vibe CMS
async function migrateWordPressContent() {
const wpPosts = await fetchWordPressPosts();
for (const wpPost of wpPosts) {
const mappedData = mapWordPressPost(wpPost);
// Create content item in Vibe CMS
// Note: Use admin API or MCP tools for write operations
console.log('Migrating:', mappedData.title);
}
}
Schema Mapping
| WordPress Field | Vibe CMS Field | Type | Notes |
|---|---|---|---|
post_title |
title |
text | Direct mapping |
post_content |
content |
markdown | Convert HTML to Markdown |
post_excerpt |
excerpt |
text | Direct mapping |
post_name |
slug |
text | URL-friendly slug |
post_status |
status |
text | Map publish/draft |
post_date |
published_at |
text | ISO 8601 format |
post_author |
author |
text | Store author name/ID |
| Custom Fields | Custom fields | varies | Map ACF fields to Vibe fields |
Asset Migration
import { createVibeCMS } from 'vibe-cms-sdk';
const cms = createVibeCMS({
projectId: 'your-project-id',
apiKey: 'your-api-key'
});
// Download WordPress media and upload to Vibe CMS
async function migrateWordPressMedia(wpMediaId: number) {
// 1. Fetch WordPress media metadata
const wpMedia = await fetch(`https://your-site.com/wp-json/wp/v2/media/${wpMediaId}`);
const mediaData = await wpMedia.json();
const imageUrl = mediaData.source_url;
const fileName = mediaData.title.rendered;
const altText = mediaData.alt_text;
// 2. Upload to Vibe CMS using MCP upload tool
// Use mcp__vibe-cms-docs__upload_file_from_url
console.log(`Upload ${fileName} from ${imageUrl}`);
return {
vibeCmsId: 'new-asset-id',
originalId: wpMediaId
};
}
1.2 From Contentful
Content Export Strategy
Contentful provides a robust export API. Use the Content Management API to export entries.
Export Script:
import * as contentful from 'contentful-management';
const client = contentful.createClient({
accessToken: 'your-management-token'
});
async function exportContentfulSpace(spaceId: string, environmentId = 'master') {
const space = await client.getSpace(spaceId);
const environment = await space.getEnvironment(environmentId);
// Export content types (schema)
const contentTypes = await environment.getContentTypes();
// Export entries (content)
const entries = await environment.getEntries({ limit: 1000 });
// Export assets
const assets = await environment.getAssets({ limit: 1000 });
return { contentTypes, entries, assets };
}
Schema Mapping
Contentful's content types map to Vibe CMS collections:
// Contentful Content Type -> Vibe CMS Collection
interface ContentfulContentType {
sys: { id: string };
name: string;
fields: Array<{
id: string;
name: string;
type: string;
required: boolean;
}>;
}
function mapContentfulToVibeSchema(contentType: ContentfulContentType) {
// 1. Create collection using MCP manage_collection tool
const collectionSlug = contentType.sys.id.toLowerCase().replace(/\s+/g, '-');
console.log('Create collection:', {
action: 'create',
slug: collectionSlug,
name: contentType.name
});
// 2. Create fields using MCP add_collection_field tool
for (const field of contentType.fields) {
const vibeFieldType = mapContentfulFieldType(field.type);
const vibeInterfaceType = mapContentfulInterface(field.type);
console.log('Add field:', {
collection_slug: collectionSlug,
name: field.id,
field_type: vibeFieldType,
interface_type: vibeInterfaceType,
is_required: field.required
});
}
}
function mapContentfulFieldType(contentfulType: string): string {
const typeMap: Record<string, string> = {
'Symbol': 'text',
'Text': 'markdown',
'RichText': 'markdown',
'Integer': 'number',
'Number': 'number',
'Boolean': 'boolean',
'Date': 'text',
'Location': 'text',
'Link': 'file'
};
return typeMap[contentfulType] || 'text';
}
function mapContentfulInterface(contentfulType: string): string {
const interfaceMap: Record<string, string> = {
'Symbol': 'input',
'Text': 'textarea',
'RichText': 'markdown',
'Integer': 'input',
'Number': 'input',
'Boolean': 'input',
'Link': 'single_file'
};
return interfaceMap[contentfulType] || 'input';
}
Content Migration
async function migrateContentfulEntries(entries: any[], assetMap: Map<string, string>) {
for (const entry of entries) {
const contentType = entry.sys.contentType.sys.id;
const locale = entry.sys.locale || 'en-US';
// Map Contentful fields to Vibe CMS data structure
const vibeData: Record<string, any> = {};
for (const [fieldName, fieldValue] of Object.entries(entry.fields)) {
// Handle different field types
if (Array.isArray(fieldValue)) {
// Array of linked assets or references
vibeData[fieldName] = fieldValue.map(item => {
if (item.sys?.linkType === 'Asset') {
return assetMap.get(item.sys.id) || item.sys.id;
}
return item;
});
} else if (typeof fieldValue === 'object' && fieldValue.sys?.linkType === 'Asset') {
// Single asset reference
vibeData[fieldName] = assetMap.get(fieldValue.sys.id) || fieldValue.sys.id;
} else {
vibeData[fieldName] = fieldValue;
}
}
// Create content item in Vibe CMS
console.log(`Migrate entry: ${entry.sys.id} to collection: ${contentType}`);
console.log('Data:', vibeData);
// Use MCP create_content and update_content_translation tools
}
}
1.3 From Strapi
Content Export Strategy
Strapi stores content in a database. Export via REST API or direct database access.
Export via Strapi API:
async function exportStrapiContent(strapiUrl: string, apiToken: string) {
const headers = {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
};
// 1. Fetch content types
const contentTypesRes = await fetch(`${strapiUrl}/api/content-type-builder/content-types`, {
headers
});
const contentTypes = await contentTypesRes.json();
// 2. Fetch entries for each content type
const allContent: Record<string, any[]> = {};
for (const contentType of contentTypes.data) {
const uid = contentType.uid;
const apiId = uid.split('.').pop(); // e.g., 'api::article.article' -> 'article'
const entriesRes = await fetch(`${strapiUrl}/api/${apiId}?pagination[limit]=1000`, {
headers
});
const entries = await entriesRes.json();
allContent[apiId] = entries.data;
}
return allContent;
}
Schema Mapping
function mapStrapiToVibeSchema(strapiContentType: any) {
const collectionSlug = strapiContentType.apiID;
const collectionName = strapiContentType.info.displayName;
console.log('Create collection:', {
action: 'create',
slug: collectionSlug,
name: collectionName,
description: strapiContentType.info.description
});
// Map Strapi attributes to Vibe CMS fields
for (const [attrName, attrConfig] of Object.entries(strapiContentType.attributes)) {
const config: any = attrConfig;
const vibeFieldType = mapStrapiFieldType(config.type);
const vibeInterfaceType = mapStrapiInterface(config.type);
console.log('Add field:', {
collection_slug: collectionSlug,
name: attrName,
field_type: vibeFieldType,
interface_type: vibeInterfaceType,
is_required: config.required || false
});
}
}
function mapStrapiFieldType(strapiType: string): string {
const typeMap: Record<string, string> = {
'string': 'text',
'text': 'markdown',
'richtext': 'markdown',
'email': 'text',
'integer': 'number',
'biginteger': 'number',
'float': 'number',
'decimal': 'number',
'boolean': 'boolean',
'date': 'text',
'time': 'text',
'datetime': 'text',
'media': 'file'
};
return typeMap[strapiType] || 'text';
}
function mapStrapiInterface(strapiType: string): string {
const interfaceMap: Record<string, string> = {
'string': 'input',
'text': 'textarea',
'richtext': 'markdown',
'email': 'input',
'integer': 'input',
'biginteger': 'input',
'float': 'input',
'decimal': 'input',
'boolean': 'input',
'media': 'single_file'
};
return interfaceMap[strapiType] || 'input';
}
1.4 From Custom CMS
General Migration Strategy
Audit Existing Content Model
- Document all content types and their fields
- Identify relationships and dependencies
- Map custom fields to Vibe CMS field types
Export Data
- Write database export scripts
- Export to JSON or CSV for easier processing
- Include metadata (created_at, updated_at, author, etc.)
Transform Data
- Normalize field names (lowercase, underscores)
- Convert data types to Vibe CMS compatible formats
- Handle special cases (rich text, references, files)
Import to Vibe CMS
- Create collections via MCP tools
- Add fields matching your schema
- Import content items via MCP tools
Example Custom CMS Migration:
import { createVibeCMS } from 'vibe-cms-sdk';
import * as fs from 'fs';
// 1. Read exported data
const exportedData = JSON.parse(fs.readFileSync('custom-cms-export.json', 'utf-8'));
// 2. Define schema mapping
const schemaMapping = {
'BlogPost': {
vibeName: 'blog-posts',
fields: {
'Title': { vibeName: 'title', type: 'text', interface: 'input' },
'Body': { vibeName: 'content', type: 'markdown', interface: 'markdown' },
'PublishDate': { vibeName: 'published_at', type: 'text', interface: 'input' },
'FeaturedImage': { vibeName: 'featured_image', type: 'file', interface: 'single_file' }
}
}
};
// 3. Create collections and fields
for (const [customType, mapping] of Object.entries(schemaMapping)) {
console.log(`Creating collection: ${mapping.vibeName}`);
// Use MCP manage_collection tool
// Then add fields using MCP add_collection_field tool
for (const [customField, vibeField] of Object.entries(mapping.fields)) {
console.log(`Adding field: ${vibeField.vibeName}`);
}
}
// 4. Import content
for (const item of exportedData.items) {
const mapping = schemaMapping[item.type];
if (!mapping) continue;
const vibeData: Record<string, any> = {};
for (const [customField, value] of Object.entries(item.fields)) {
const fieldMapping = mapping.fields[customField];
if (fieldMapping) {
vibeData[fieldMapping.vibeName] = value;
}
}
console.log(`Importing item to ${mapping.vibeName}:`, vibeData);
// Use MCP create_content and update_content_translation tools
}
2. Schema Evolution
2.1 Adding New Collections
Via MCP Tools:
// Use mcp__vibe-cms-docs__manage_collection
const result = await mcp.manage_collection({
action: 'create',
slug: 'blog-posts',
name: 'Blog Posts',
description: 'Articles and blog content',
is_singleton: false
});
Best Practices:
- Use descriptive, plural nouns for collection names (e.g., 'blog-posts', 'products')
- Plan your slug carefully - it becomes part of your API URLs
- Add a description to help team members understand the collection's purpose
- Use
is_singleton: truefor unique content (e.g., 'homepage', 'site-settings')
2.2 Adding Fields to Existing Collections
Via MCP Tools:
// Add a new field
const result = await mcp.add_collection_field({
collection_slug: 'blog-posts',
name: 'author_bio',
field_type: 'markdown',
interface_type: 'markdown',
is_required: false,
sort_order: 10
});
Field Types and Compatible Interfaces:
| Field Type | Compatible Interfaces | Use Case |
|---|---|---|
text |
input, textarea |
Short text, long text |
markdown |
markdown |
Rich text content |
number |
input |
Integers, decimals |
boolean |
input |
True/false flags |
file |
single_file, multiple_files |
Images, documents, galleries |
Validation Rules:
- Field names must start with a letter
- Only lowercase letters, numbers, and underscores allowed
- Cannot use reserved words:
id,created_at,updated_at,status,data, etc.
2.3 Renaming Fields (Safe Pattern)
Vibe CMS does not support direct field renaming. Follow this pattern:
Step 1: Add New Field
await mcp.add_collection_field({
collection_slug: 'blog-posts',
name: 'post_title', // new name
field_type: 'text',
interface_type: 'input',
is_required: false
});
Step 2: Migrate Data
import { createVibeCMS } from 'vibe-cms-sdk';
const cms = createVibeCMS({
projectId: 'your-project-id',
apiKey: 'your-api-key'
});
async function migrateFieldData() {
// 1. Fetch all content items
const items = await cms.collection('blog-posts').all();
// 2. Update each item with new field
for (const item of items) {
const oldValue = item.field('title'); // old field
// Update translation with new field
// Use MCP update_content_translation tool
console.log(`Migrating item ${item.id}: title -> post_title`);
console.log(`Value: ${oldValue}`);
}
}
Step 3: Delete Old Field
// After confirming data migration succeeded
await mcp.delete_collection_field({
collection_slug: 'blog-posts',
field_name: 'title', // old field
confirm: true
});
Important: Always backup your data before deleting fields. Deletion is permanent.
2.4 Changing Field Types
Field type changes require creating a new field and migrating data.
Example: Convert text field to markdown
// 1. Add new field with desired type
await mcp.add_collection_field({
collection_slug: 'blog-posts',
name: 'description_markdown',
field_type: 'markdown',
interface_type: 'markdown',
is_required: false
});
// 2. Migrate data (convert plain text to markdown if needed)
async function convertTextToMarkdown() {
const items = await cms.collection('blog-posts').all();
for (const item of items) {
const textValue = item.field('description');
// Simple conversion: wrap paragraphs
const markdownValue = textValue
.split('\n\n')
.map(p => p.trim())
.join('\n\n');
console.log(`Convert item ${item.id}`);
// Use MCP update_content_translation to save markdownValue
}
}
// 3. Delete old field after verification
await mcp.delete_collection_field({
collection_slug: 'blog-posts',
field_name: 'description',
confirm: true
});
// 4. Optionally rename new field back to original name
// (Follow renaming pattern above)
2.5 Reordering Fields
Control field display order in the admin interface.
Via MCP Tools:
// Reorder all fields in a collection
await mcp.reorder_collection_fields({
collection_slug: 'blog-posts',
field_names: [
'title',
'slug',
'excerpt',
'content',
'featured_image',
'published_at',
'author'
]
});
Important: You must provide ALL field names in desired order. Missing fields will cause an error.
Update Individual Field Sort Order:
await mcp.update_collection_field({
collection_slug: 'blog-posts',
field_name: 'featured_image',
sort_order: 5
});
3. Complete Schema Evolution Documentation
For a complete treatment of schema migrations, field renaming, type changes, bulk operations, and transaction management, please refer to the full documentation sections on content modeling and data operations.
Key patterns to remember:
- Backward compatibility is essential
- Always backup before migrations
- Test on staging first
- Document schema changes thoroughly
- Use MCP tools for consistent, auditable changes
See Also
- Chapter 4: Content Modeling - Designing collection schemas
- Chapter 5: File Management - Working with files and folders
- Appendix A: MCP Tools Reference - Complete MCP tool documentation
- Appendix D: API Endpoint Reference - REST API documentation