Skip to main content

Validation overview

Content validation checklist

Completeness check

  • All content migrated - Compare counts with source system
  • No missing pages - Verify every page from inventory exists
  • No missing media - Confirm all images and files transferred
  • Metadata preserved - Check publish dates, authors, categories
  • Relationships intact - Verify references and links work

Quality scan

  • Visual inspection - Scroll through migrated content
  • Formatting correct - Headers, paragraphs, lists render properly
  • No broken formatting - No HTML tags showing as text
  • Character encoding - Special characters display correctly
  • No placeholder content - All fields have real content
  • Internal links work - Links between pages resolve correctly
  • External links work - Links to external sites are valid
  • No broken links - Use a link checker tool
  • Correct destinations - Links go to intended pages
  • Anchor links work - In-page navigation functions
Possible link outcomes:
  1. ✅ Link works correctly
  2. ⚠️ Link to unmigrated content (fix before go-live)
  3. ⚠️ Link to source system (update to new URL)
  4. ❌ Broken link (must fix)

Media validation

Image checklist

  • All images load - No broken image icons
  • Correct image shown - Right images on right pages
  • Image quality acceptable - No blurry or distorted images
  • Alt text preserved - Accessibility text migrated
  • CDN URLs used - Images served from ButterCMS CDN

SEO validation

SEO checklist

  • Meta titles preserved - SEO titles migrated correctly
  • Meta descriptions preserved - Descriptions migrated correctly
  • URL structure maintained - Or proper redirects in place
  • Canonical URLs correct - Pointing to right pages
  • No duplicate content - Old URLs retired or redirected
  • Sitemap updated - Reflects new URL structure
  • Robots.txt correct - Allows crawling of new content

SEO monitoring

After migration, monitor these metrics:
MetricToolWhat to Watch
RankingsGoogle Search ConsoleAny significant drops
IndexingGoogle Search ConsolePages being indexed
404 ErrorsGoogle Search ConsoleBroken URLs
Organic TrafficGoogle AnalyticsTraffic changes
Crawl ErrorsGoogle Search ConsoleCrawl issues

Redirect validation

async function validateRedirects(redirectMap) {
  const issues = [];

  for (const [oldUrl, newUrl] of Object.entries(redirectMap)) {
    try {
      const response = await fetch(oldUrl, { redirect: 'manual' });

      if (response.status !== 301 && response.status !== 302) {
        issues.push({
          url: oldUrl,
          expected: 'redirect',
          actual: response.status
        });
      } else {
        const location = response.headers.get('location');
        if (!location.includes(newUrl)) {
          issues.push({
            url: oldUrl,
            expected: newUrl,
            actual: location
          });
        }
      }
    } catch (error) {
      issues.push({
        url: oldUrl,
        error: error.message
      });
    }
  }

  return issues;
}

Performance validation

Performance checklist

  • Page load time - Compare to baseline
  • Time to First Byte - API response times
  • Core Web Vitals - LCP, FID, CLS metrics
  • Mobile performance - Test on mobile devices
  • CDN working - Images served from CDN

Performance comparison

MetricPre-MigrationPost-MigrationTarget
Page Load___ ms___ ms< 3000ms
TTFB___ ms___ ms< 600ms
LCP___ s___ s< 2.5s
FID___ ms___ ms< 100ms
CLS______< 0.1

Functional testing

User acceptance testing

Conduct UAT with real users:
  1. Content team review - Have editors verify content accuracy
  2. Developer review - Verify API responses match expectations
  3. Stakeholder review - Business owners approve key pages
  4. QA review - Systematic testing of all functionality

Comparison report template

# Migration Validation Report

## Summary
- **Migration Date:** YYYY-MM-DD
- **Source System:** [WordPress/Contentful/etc]
- **Validation Date:** YYYY-MM-DD
- **Overall Status:** ✅ PASSED / ❌ ISSUES FOUND

## Content Counts
| Content Type | Expected | Migrated | Difference |
|--------------|----------|----------|------------|
| Blog Posts | 150 | 150 | 0 |
| Pages | 25 | 25 | 0 |
| Authors | 5 | 5 | 0 |
| Categories | 12 | 12 | 0 |

## Quality Checks
- [ ] ✅ All content visually inspected
- [ ] ✅ No broken formatting found
- [ ] ✅ No encoding issues
- [ ] ✅ All media loading correctly

## SEO Validation
- [ ] ✅ Meta titles preserved
- [ ] ✅ Meta descriptions preserved
- [ ] ✅ Redirects working
- [ ] ✅ Sitemap updated

## Performance
- [ ] ✅ Page load within acceptable range
- [ ] ✅ API response times acceptable

## Issues Found
| # | Severity | Description | Resolution |
|---|----------|-------------|------------|
| 1 | Low | Minor formatting in 3 posts | Fixed |

## Sign-off
- [ ] Content Team: _________________ Date: _______
- [ ] Development: _________________ Date: _______
- [ ] QA: __________________________ Date: _______

Monitoring after go-live

First 24-48 hours

  • Monitor error logs
  • Check for 404 errors
  • Verify CDN is serving content
  • Watch for user-reported issues

First week

  • Compare traffic to pre-migration baseline
  • Monitor search rankings
  • Check indexing status
  • Review user feedback

First month

  • Full performance comparison
  • SEO impact assessment
  • User adoption metrics
  • Identify any remaining issues

Rollback procedures

If critical issues are found:

Quick rollback (DNS-based)

  1. Point DNS back to old system
  2. Document all issues found
  3. Plan fixes before re-attempting

Partial rollback

  1. Identify problematic content
  2. Restore from backup or re-import
  3. Verify fixes before full cutover

Data recovery

  1. Use ButterCMS revision history if available
  2. Re-import from source backup
  3. Contact ButterCMS support for assistance

Automated validation

Automated validation script

// validation.js
const Butter = require('buttercms');

const BUTTERCMS_READ_TOKEN = process.env.BUTTERCMS_READ_TOKEN;
const butter = Butter(BUTTERCMS_READ_TOKEN);

class MigrationValidator {
  constructor() {
    this.issues = [];
    this.stats = {
      pagesChecked: 0,
      collectionsChecked: 0,
      mediaChecked: 0,
      issuesFound: 0
    };
  }

  async validatePages(pageType, expectedCount) {
    console.log(`Validating ${pageType} pages...`);

    const response = await butter.page.list(pageType, { page_size: 100 });
    const pages = response.data.data;

    // Check count
    if (pages.length !== expectedCount) {
      this.addIssue('count', `Expected ${expectedCount} ${pageType} pages, found ${pages.length}`);
    }

    // Validate each page
    for (const page of pages) {
      await this.validatePage(page);
      this.stats.pagesChecked++;
    }
  }

  async validatePage(page) {
    const issues = [];

    // Check required fields
    if (!page.fields.title) {
      issues.push('Missing title');
    }

    // Check for empty body
    if (page.fields.body && page.fields.body.trim() === '') {
      issues.push('Empty body content');
    }

    // Check for broken images
    if (page.fields.featured_image) {
      const imageValid = await this.checkImageUrl(page.fields.featured_image);
      if (!imageValid) {
        issues.push(`Broken image: ${page.fields.featured_image}`);
      }
    }

    // Check references
    if (page.fields.author && !page.fields.author.slug) {
      issues.push('Invalid author reference');
    }

    // Check for encoding issues
    const content = JSON.stringify(page.fields);
    if (content.includes('�') || content.includes('&amp;amp;')) {
      issues.push('Possible encoding issues');
    }

    if (issues.length > 0) {
      this.addIssue('page', {
        slug: page.slug,
        title: page.fields.title,
        issues
      });
    }
  }

  async checkImageUrl(url) {
    try {
      const response = await fetch(url, { method: 'HEAD' });
      return response.ok;
    } catch {
      return false;
    }
  }

  async validateCollection(collectionKey, expectedCount) {
    console.log(`Validating ${collectionKey} collection...`);

    const response = await butter.content.retrieve([collectionKey]);
    const items = response.data.data[collectionKey] || [];

    if (items.length !== expectedCount) {
      this.addIssue('count', `Expected ${expectedCount} ${collectionKey} items, found ${items.length}`);
    }

    this.stats.collectionsChecked++;
  }

  addIssue(type, details) {
    this.issues.push({ type, details, timestamp: new Date().toISOString() });
    this.stats.issuesFound++;
  }

  getReport() {
    return {
      stats: this.stats,
      issues: this.issues,
      passed: this.issues.length === 0
    };
  }
}

// Usage
async function runValidation() {
  const validator = new MigrationValidator();

  // Validate pages
  await validator.validatePages('blog_post', 150);  // Expected 150 posts
  await validator.validatePages('landing_page', 10); // Expected 10 landing pages

  // Validate collections
  await validator.validateCollection('blog_authors', 5);
  await validator.validateCollection('blog_categories', 12);

  // Get report
  const report = validator.getReport();

  console.log('\n=== Validation Report ===');
  console.log(`Pages checked: ${report.stats.pagesChecked}`);
  console.log(`Collections checked: ${report.stats.collectionsChecked}`);
  console.log(`Issues found: ${report.stats.issuesFound}`);

  if (!report.passed) {
    console.log('\nIssues:');
    for (const issue of report.issues) {
      console.log(`- ${issue.type}: ${JSON.stringify(issue.details)}`);
    }
  } else {
    console.log('\n✅ All validations passed!');
  }

  return report;
}

runValidation().catch(console.error);

Media validation script

async function validateMedia(pages) {
  const brokenMedia = [];

  for (const page of pages) {
    const mediaUrls = extractMediaUrls(page.fields);

    for (const url of mediaUrls) {
      const isValid = await checkUrl(url);
      if (!isValid) {
        brokenMedia.push({
          page: page.slug,
          url: url
        });
      }
    }
  }

  return brokenMedia;
}

function extractMediaUrls(fields, urls = []) {
  for (const [key, value] of Object.entries(fields)) {
    // Check if images use ButterCMS CDN (recommended) or external URLs
    if (typeof value === 'string' && value.includes('cdn.buttercms.com')) {
      urls.push(value);
    } else if (typeof value === 'object' && value !== null) {
      extractMediaUrls(value, urls);
    }
  }
  return urls;
}

async function checkUrl(url) {
  try {
    const response = await fetch(url, { method: 'HEAD' });
    return response.ok;
  } catch {
    return false;
  }
}

API integration tests

describe('ButterCMS API Integration', () => {
  test('can fetch blog posts', async () => {
    const response = await butter.post.list();
    expect(response.data.data.length).toBeGreaterThan(0);
  });

  test('can fetch single page', async () => {
    const response = await butter.page.retrieve('*', 'homepage');
    expect(response.data.data).toBeDefined();
    expect(response.data.data.fields.title).toBeDefined();
  });

  test('can fetch collection', async () => {
    const response = await butter.content.retrieve(['blog_categories']);
    expect(response.data.data.blog_categories.length).toBeGreaterThan(0);
  });

  test('localized content available', async () => {
    const response = await butter.page.retrieve('*', 'homepage', { locale: 'de' });
    expect(response.data.data.fields.title).toBeDefined();
  });
});