> ## 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.

# Map content to ButterCMS models

> Plan how your source content maps to ButterCMS content types and fields, then build and validate transformation scripts.

## Understanding the mapping process

```mermaid theme={null}
flowchart LR
    subgraph Source["Source Schema"]
        S1["Fields"]
        S2["Types"]
        S3["Relationships"]
        S1 --> S2 --> S3
    end
    subgraph Mapping["Mapping Rules"]
        M1["Type convert"]
        M2["Restructure"]
        M3["Clean/Format"]
        M1 --> M2 --> M3
    end
    subgraph Target["ButterCMS Schema"]
        T1["Page Types"]
        T2["Collections"]
        T3["Components"]
        T1 --> T2 --> T3
    end
    Source --> Mapping --> Target
```

## 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:

```markdown theme={null}
# 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
```

***

## Create transformation rules

### Field transformations

```javascript theme={null}
// 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

```javascript theme={null}
// 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:**

1. First, migrate Collections (authors, categories, tags)
2. Note the slugs or IDs of created items
3. Then migrate Pages/Posts with references to those items

```javascript theme={null}
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:

```javascript theme={null}
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;
}
```

### Handling media assets

```javascript theme={null}
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

```javascript theme={null}
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];
}
```

## Validate transformations

Always validate transformed data before uploading:

```javascript theme={null}
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;
  }
}
```

## Data transformation tools

### jq (JSON processing)

```bash theme={null}
# 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)

```python theme={null}
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)
```
