Documentation Index
Fetch the complete documentation index at: https://buttercms.com/docs/llms.txt
Use this file to discover all available pages before exploring further.
Understanding the mapping process
Design target schema
Map source content to ButterCMS content types:
Choosing the right ButterCMS content type
| Source Content | Best ButterCMS Match | Reason |
|---|
| Blog posts | Blog Engine | Built-in author, categories, tags, SEO |
| Landing pages | Page Type | Custom fields, components, SEO |
| Product pages | Page Type | Custom schema per product type |
| Team members | Collection | Reference data used across pages |
| Categories | Collection | Reusable taxonomy |
| Testimonials | Collection | Referenced by multiple pages |
| FAQs | Collection | Structured Q&A data |
| Settings | Collection | Site-wide configuration |
| Navigation | Collection | Menu structure data |
Mapping field types
| Source Field Type | ButterCMS Field Type | Transformation Notes |
|---|
| Plain text | Short Text | Truncate if exceeds limits |
| Long text | Long Text or WYSIWYG | Choose based on formatting needs |
| Rich text/HTML | WYSIWYG | Preserve HTML, clean up if needed |
| Integer | Number | Direct mapping |
| Decimal | Number | Direct mapping |
| Boolean | Checkbox | Direct mapping |
| Date | Date | Convert to ISO format |
| DateTime | Date | May lose time component |
| Single select | Dropdown | Define options in schema |
| Multi-select | Repeater or Reference | Depends on use case |
| Single image | Media | Upload to CDN |
| Gallery | Repeater with Media | One media field per item |
| File | Media | Upload to CDN |
| Single reference | Reference (One-to-One) | Map to collection/page |
| Multiple references | Reference (One-to-Many) | Map to collection/page |
| JSON/Object | Long Text or Repeater | Store as JSON or flatten |
| Geolocation | Short Text | Store as “lat,lng” string |
Mapping documentation template
Document your mappings for team reference:
# Content Mapping Documentation
## Blog Posts
### Source: WordPress Post
### Target: ButterCMS Page Type "blog_post"
| Source Field | Target Field | Transformation |
|-------------|--------------|----------------|
| post_title | title | Direct mapping |
| post_content | body | Clean shortcodes |
| post_excerpt | summary | Generate if empty |
| post_date | publish_date | ISO format |
| post_status | status | Map to published/draft |
| post_name | slug | Direct mapping |
| featured_media | featured_image | Resolve to URL |
| author | author | Map ID to slug |
| categories | categories | Map IDs to slugs |
| tags | tags | Map IDs to slugs |
### Notes
- Author must exist in blog_authors collection before post migration
- Categories must exist in blog_categories collection before post migration
- Images are automatically served via ButterCMS CDN
// transformation-rules.js
const transformations = {
// Text transformations
text: {
// Truncate to max length
truncate: (value, maxLength) => {
if (!value || value.length <= maxLength) return value;
return value.substring(0, maxLength - 3) + '...';
},
// Clean HTML tags
stripHtml: (value) => {
return value?.replace(/<[^>]*>/g, '') || '';
},
// Generate slug from title
slugify: (value) => {
return value
?.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '') || '';
}
},
// HTML transformations
html: {
// Fix relative URLs to absolute
fixUrls: (html, baseUrl) => {
return html?.replace(
/(?:src|href)="(\/[^"]+)"/g,
(match, path) => match.replace(path, baseUrl + path)
) || '';
},
// Remove WordPress shortcodes
removeShortcodes: (html) => {
return html?.replace(/\[[^\]]+\]/g, '') || '';
},
// Clean empty paragraphs
cleanEmpty: (html) => {
return html?.replace(/<p>\s*<\/p>/g, '') || '';
}
},
// Date transformations
date: {
// Convert to ISO format
toISO: (value) => {
const date = new Date(value);
return isNaN(date) ? null : date.toISOString();
},
// Parse various formats
parse: (value, format) => {
// Add custom parsing logic for specific formats
return new Date(value).toISOString();
}
},
// Reference transformations
reference: {
// Convert IDs to slugs
idToSlug: (id, lookupTable) => {
return lookupTable[id] || null;
},
// Ensure array for one-to-many
ensureArray: (value) => {
if (!value) return [];
return Array.isArray(value) ? value : [value];
}
},
// Media transformations
media: {
// Ensure HTTPS
secureUrl: (url) => {
return url?.replace(/^http:/, 'https:') || '';
},
// Extract CDN URL
extractCdn: (mediaObject) => {
return mediaObject?.url || mediaObject?.src || mediaObject || '';
}
}
};
Content type transformers
// transformers.js
class BlogPostTransformer {
constructor(lookups) {
this.authorLookup = lookups.authors || {};
this.categoryLookup = lookups.categories || {};
this.tagLookup = lookups.tags || {};
}
transform(sourcePost) {
return {
"page-type": "blog_post",
status: this.mapStatus(sourcePost.status),
title: sourcePost.title,
slug: this.generateSlug(sourcePost),
fields: {
// Core fields
title: sourcePost.title,
body: this.transformBody(sourcePost.content),
summary: this.generateSummary(sourcePost),
// Date fields
publish_date: transformations.date.toISO(sourcePost.published_at),
// References
author: this.transformAuthor(sourcePost.author),
categories: this.transformCategories(sourcePost.categories),
tags: this.transformTags(sourcePost.tags),
// SEO fields
seo: {
title: sourcePost.meta_title || sourcePost.title,
description: sourcePost.meta_description || this.generateSummary(sourcePost),
og_image: transformations.media.secureUrl(sourcePost.featured_image)
}
}
};
}
mapStatus(sourceStatus) {
const statusMap = {
'publish': 'published',
'published': 'published',
'draft': 'draft',
'pending': 'draft',
'private': 'draft'
};
return statusMap[sourceStatus] || 'draft';
}
generateSlug(post) {
return post.slug || transformations.text.slugify(post.title);
}
transformBody(content) {
let html = content || '';
// Clean up common issues
html = transformations.html.removeShortcodes(html);
html = transformations.html.cleanEmpty(html);
return html;
}
generateSummary(post) {
if (post.excerpt) return post.excerpt;
// Extract from body
const stripped = transformations.text.stripHtml(post.content);
return transformations.text.truncate(stripped, 160);
}
transformAuthor(author) {
if (!author) return null;
// Convert ID to slug
if (typeof author === 'number' || typeof author === 'string') {
return this.authorLookup[author] || null;
}
// Already an object
return author.slug || transformations.text.slugify(author.name);
}
transformCategories(categories) {
if (!categories) return [];
return categories
.map(cat => {
if (typeof cat === 'number' || typeof cat === 'string') {
return this.categoryLookup[cat];
}
return cat.slug || transformations.text.slugify(cat.name);
})
.filter(Boolean);
}
transformTags(tags) {
if (!tags) return [];
return tags
.map(tag => {
if (typeof tag === 'number' || typeof tag === 'string') {
return this.tagLookup[tag];
}
return tag.slug || transformations.text.slugify(tag.name);
})
.filter(Boolean);
}
}
Handle special cases
Relationship mapping
References require special handling during migration:
Migration order matters:
- First, migrate Collections (authors, categories, tags)
- Note the slugs or IDs of created items
- Then migrate Pages/Posts with references to those items
async function migrateWithReferences(data) {
const lookups = {};
// Step 1: Migrate authors collection
console.log('Migrating authors...');
lookups.authors = {};
for (const author of data.authors) {
const result = await createCollectionItem('blog_authors', {
en: {
name: author.name,
slug: author.slug || slugify(author.name),
bio: author.bio
}
});
lookups.authors[author.id] = result.data.fields.slug;
}
// Step 2: Migrate categories
console.log('Migrating categories...');
lookups.categories = {};
for (const category of data.categories) {
const result = await createCollectionItem('blog_categories', {
en: {
name: category.name,
slug: category.slug || slugify(category.name)
}
});
lookups.categories[category.id] = result.data.fields.slug;
}
// Step 3: Migrate posts with references
console.log('Migrating posts...');
const transformer = new BlogPostTransformer(lookups);
for (const post of data.posts) {
const butterPost = transformer.transform(post);
await createPage(butterPost);
}
}
Handling rich text content
Rich text often needs cleanup during migration:
const cheerio = require('cheerio');
function cleanRichText(html) {
const $ = cheerio.load(html);
// Remove inline styles
$('[style]').removeAttr('style');
// Remove empty elements
$('p:empty, span:empty, div:empty').remove();
// Fix image sources
$('img').each((i, el) => {
const src = $(el).attr('src');
if (src && !src.startsWith('http')) {
$(el).attr('src', 'https://example.com' + src);
}
});
// Remove WordPress-specific classes
$('[class*="wp-"]').removeClass(function(i, className) {
return className.split(' ').filter(c => c.startsWith('wp-')).join(' ');
});
// Convert shortcodes to proper HTML
// [button url="..." text="..."] -> <a href="..." class="button">...</a>
let result = $.html();
result = result.replace(
/\[button url="([^"]+)" text="([^"]+)"\]/g,
'<a href="$1" class="button">$2</a>'
);
return result;
}
async function migrateMediaAssets(sourceMedia) {
const mediaMapping = {};
for (const media of sourceMedia) {
// Option 1: Use URL directly (ButterCMS will serve via CDN)
mediaMapping[media.id] = media.url;
// Option 2: Download and upload to ButterCMS Media Library
// (If you need to ensure all assets are in ButterCMS)
// Media field URLs uploaded via Write API are saved to the media library.
}
return mediaMapping;
}
// Use in content transformation
function transformImageField(imageId, mediaMapping) {
const url = mediaMapping[imageId];
if (!url) return null;
// Ensure HTTPS
return url.replace(/^http:/, 'https:');
}
Handling localized content
function transformLocalizedContent(sourceContent, locales) {
const result = {};
for (const locale of locales) {
// Map source locale code to ButterCMS locale
const butterLocale = mapLocaleCode(locale.code);
// Get localized fields
result[butterLocale] = {
title: sourceContent.title[locale.code] || sourceContent.title.default,
body: sourceContent.body[locale.code] || sourceContent.body.default,
// ... other fields
};
}
return result;
}
function mapLocaleCode(sourceCode) {
// Map common locale formats
const mapping = {
'en_US': 'en',
'en-us': 'en',
'de_DE': 'de',
'de-de': 'de',
'fr_FR': 'fr',
'fr-fr': 'fr'
};
return mapping[sourceCode] || sourceCode.split(/[-_]/)[0];
}
Always validate transformed data before uploading:
class TransformationValidator {
constructor(schema) {
this.schema = schema;
this.errors = [];
}
validate(transformedData) {
this.errors = [];
// Check required fields
for (const [field, config] of Object.entries(this.schema)) {
if (config.required && !transformedData.fields?.[field]) {
this.errors.push(`Missing required field: ${field}`);
}
}
// Check field types
for (const [field, value] of Object.entries(transformedData.fields || {})) {
const expectedType = this.schema[field]?.type;
if (expectedType && !this.checkType(value, expectedType)) {
this.errors.push(`Invalid type for ${field}: expected ${expectedType}`);
}
}
// Check slug format
if (transformedData.slug && !/^[a-z0-9-]+$/.test(transformedData.slug)) {
this.errors.push(`Invalid slug format: ${transformedData.slug}`);
}
// Check reference validity
for (const [field, config] of Object.entries(this.schema)) {
if (config.type === 'reference') {
const value = transformedData.fields?.[field];
if (value && !this.validateReference(value, config)) {
this.errors.push(`Invalid reference for ${field}`);
}
}
}
return this.errors.length === 0;
}
checkType(value, expectedType) {
switch (expectedType) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number';
case 'boolean':
return typeof value === 'boolean';
case 'array':
return Array.isArray(value);
case 'date':
return !isNaN(new Date(value));
default:
return true;
}
}
validateReference(value, config) {
if (config.oneToMany) {
return Array.isArray(value);
}
return typeof value === 'string';
}
getErrors() {
return this.errors;
}
}
jq (JSON processing)
# Extract and transform JSON
cat export.json | jq '.posts | map({
"page-type": "blog_post",
title: .title,
slug: .slug,
fields: {
title: .title,
body: .content
}
})' > buttercms_import.json
Python pandas (CSV processing)
import pandas as pd
import json
# Read CSV
df = pd.read_csv('content.csv')
# Transform to ButterCMS format
pages = []
for _, row in df.iterrows():
pages.append({
'page-type': 'blog_post',
'title': row['title'],
'slug': row['slug'],
'status': 'draft',
'fields': {
'title': row['title'],
'body': row['content'],
'summary': row['excerpt']
}
})
# Save for import
with open('import.json', 'w') as f:
json.dump({'pages': pages}, f, indent=2)