# Validate your import without a frontend

https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/validate-migration

Run a script that checks document counts, singleton existence, map integrity, dangling references, and SEO coverage before wiring up any frontend.

---

## Navigation

**Course:** [Migrating from Webflow to Sanity](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity) · [View as markdown](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity.md)

**Previous:** [Set up draft mode and visual preview](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/draft-mode-visual-preview) · **Next:** [Launch checklist and DNS transfer](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/launch-checklist-dns)

---

## Course Contents

1. [Introduction](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/introduction-to-webflow-migration)
2. [Audit your Webflow content](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/audit-webflow-content)
3. [Recognize Webflow's content modeling patterns](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/recognize-content-modeling-patterns)
4. [Export from Webflow and connect the Data API](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/export-webflow-connect-api)
5. [Initialize Sanity and install the migration toolkit](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/initialize-sanity-migration-toolkit)
6. [Map collections to Sanity document types](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/map-collections-to-schemas)
7. [Build a page builder with a sections array](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/page-builder-sections-array)
8. [Model custom-template pages as singletons](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/custom-template-singletons)
9. [Model navigation in Sanity](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/model-navigation)
10. [Create a siteSettings singleton](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/site-settings-singleton)
11. [Handle internal links in Portable Text](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/internal-links-portable-text)
12. [Fetch your collection items from the API](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/fetch-collection-items-api)
13. [Run the asset pre-upload pass](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/asset-pre-upload)
14. [Resolve reference fields from API data](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/resolve-reference-fields)
15. [Convert rich text to Portable Text](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/convert-rich-text-portable-text)
16. [Extract static page content using the page DOM API](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/extract-static-page-content)
17. [Import content with the two-pass pattern](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/two-pass-import)
18. [Validate your import in Studio](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/validate-import-studio)
19. [Set up forms, redirects, and sitemap](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/forms-redirects-sitemap)
20. [Set up draft mode and visual preview](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/draft-mode-visual-preview)
21. **Validate your import without a frontend** *(current)*
22. [Launch checklist and DNS transfer](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/launch-checklist-dns)
23. [AI prompt library and resources](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity/appendix-prompt-library-resources)

---

You don't need a running Next.js app to know whether the migration is healthy. This script queries Sanity directly and checks the things that break silently: weak references that never got resolved, CMS items with empty SEO, singleton documents that weren't imported, and artifact files that scripts downstream depend on.



Save it as `scripts/validate-migration.js` and run it from the migration repo root:



```bash
node scripts/validate-migration.js
```

```javascript:scripts/validate-migration.js
// scripts/validate-migration.js
// Run after import.js, resolve-refs.js, and generate-url-map.js.
// Requires: id-map.json, url-map.json, asset-map.json, webflow/api/collections.json
// Env: SANITY_PROJECT_ID, SANITY_DATASET, SANITY_TOKEN

import { createClient } from '@sanity/client'
import fs from 'fs'
import path from 'path'

const client = createClient({
  projectId: process.env.SANITY_PROJECT_ID,
  dataset: process.env.SANITY_DATASET,
  token: process.env.SANITY_TOKEN,
  apiVersion: '2024-01-01',
  useCdn: false,
})

// ─── Config ───────────────────────────────────────────────────────────────────

// Update these to match your schema types and singleton document IDs.
const CMS_TYPES = ['post', 'author', 'caseStudy', 'teamMember'] // your collection types
const SINGLETON_IDS = ['homePage', 'aboutPage', 'pricingPage'] // fixed _id for each singleton
const SEO_TYPES = ['post', 'caseStudy'] // CMS types that must have seo.metaTitle

// ─── Helpers ──────────────────────────────────────────────────────────────────

let passed = 0
let failed = 0
let warnings = 0

function pass(label) {
  console.log(`  ✓  ${label}`)
  passed++
}

function fail(label, detail = '') {
  console.error(`  ✗  ${label}${detail ? ` — ${detail}` : ''}`)
  failed++
}

function warn(label, detail = '') {
  console.warn(`  ⚠  ${label}${detail ? ` — ${detail}` : ''}`)
  warnings++
}

function loadJson(filePath) {
  if (!fs.existsSync(filePath)) return null
  return JSON.parse(fs.readFileSync(filePath, 'utf8'))
}

// ─── Checks ───────────────────────────────────────────────────────────────────

async function checkDocumentCounts() {
  console.log('\n── Document counts ──')
  const collections = loadJson('webflow/api/collections.json')
  if (!collections) { warn('webflow/api/collections.json not found — skipping count check'); return }

  const idMap = loadJson('output/id-map.json') ?? {}
  const importedIds = Object.values(idMap)

  for (const collection of collections.collections ?? []) {
    const itemFile = `webflow/api/items/${collection.id}.ndjson`
    if (!fs.existsSync(itemFile)) continue
    const lines = fs.readFileSync(itemFile, 'utf8').trim().split('\n').filter(Boolean)
    const webflowCount = lines.length
    const sanityCount = importedIds.filter(id => id.startsWith(`${collection.slug}-`)).length
    if (sanityCount >= webflowCount) {
      pass(`${collection.displayName}: ${sanityCount}/${webflowCount} documents imported`)
    } else {
      fail(`${collection.displayName}: only ${sanityCount}/${webflowCount} documents in id-map`)
    }
  }
}

async function checkSingletons() {
  console.log('\n── Singleton documents ──')
  for (const docId of SINGLETON_IDS) {
    const doc = await client.fetch(`*[_id == $id][0]{ _id, _type }`, { id: docId })
    if (doc) {
      pass(`${docId} exists (_type: ${doc._type})`)
    } else {
      fail(`${docId} not found in dataset`)
    }
  }
}

async function checkWeakRefs() {
  console.log('\n── Unresolved placeholder references ──')
  // Placeholder refs from Pass 1 use _weak: true and _ref starting with wf_
  const weakRefs = await client.fetch(
    `*[references(*[_id match "wf_*"]._id)]{ _id, _type }`
  )
  if (weakRefs.length === 0) {
    pass('No documents reference placeholder wf_* IDs')
  } else {
    fail(`${weakRefs.length} documents still reference placeholder IDs`, 'run resolve-refs.js again')
    weakRefs.slice(0, 5).forEach(d => console.error(`       ${d._type} / ${d._id}`))
    if (weakRefs.length > 5) console.error(`       …and ${weakRefs.length - 5} more`)
  }
}

async function checkSeoPopulated() {
  console.log('\n── SEO coverage ──')
  for (const typeName of SEO_TYPES) {
    const [total, missing] = await Promise.all([
      client.fetch(`count(*[_type == $t])`, { t: typeName }),
      client.fetch(`count(*[_type == $t && !defined(seo.metaTitle)])`, { t: typeName }),
    ])
    if (missing === 0) {
      pass(`${typeName}: all ${total} documents have seo.metaTitle`)
    } else if (missing < total * 0.1) {
      warn(`${typeName}: ${missing}/${total} documents missing seo.metaTitle`)
    } else {
      fail(`${typeName}: ${missing}/${total} documents missing seo.metaTitle`, 'check detail template mapping')
    }
  }
}

async function checkCmsTypeFields() {
  console.log('\n── CMS type field presence ──')
  for (const typeName of CMS_TYPES) {
    const [total, missingSlug, missingName] = await Promise.all([
      client.fetch(`count(*[_type == $t])`, { t: typeName }),
      client.fetch(`count(*[_type == $t && !defined(slug.current)])`, { t: typeName }),
      client.fetch(`count(*[_type == $t && !defined(name)])`, { t: typeName }),
    ])
    if (total === 0) { warn(`${typeName}: no documents found`); continue }
    if (missingSlug === 0) {
      pass(`${typeName}: all ${total} documents have slug.current`)
    } else {
      fail(`${typeName}: ${missingSlug}/${total} documents missing slug.current`)
    }
    if (missingName === 0) {
      pass(`${typeName}: all ${total} documents have name`)
    } else {
      warn(`${typeName}: ${missingName}/${total} documents missing name (check fieldData mapping)`)
    }
  }
}

async function checkArtifacts() {
  console.log('\n── Artifact files ──')
  const required = [
    ['output/id-map.json', 'Maps Webflow IDs to Sanity IDs'],
    ['output/url-map.json', 'Maps Webflow paths to Sanity _id'],
    ['output/asset-map.json', 'Maps CDN/local image URLs to Sanity asset IDs'],
  ]
  const optional = [
    ['output/sitemap.xml', 'Sitemap for submission to Google Search Console'],
    ['output/redirects.json', 'Redirect rules (Enterprise API)'],
    ['output/forms-audit.json', 'Forms requiring replacement'],
  ]

  for (const [filePath, label] of required) {
    if (fs.existsSync(filePath)) {
      const size = fs.statSync(filePath).size
      if (size < 10) {
        fail(`${filePath} exists but is empty`, label)
      } else {
        pass(`${filePath} present (${(size / 1024).toFixed(1)} KB) — ${label}`)
      }
    } else {
      fail(`${filePath} missing`, label)
    }
  }

  for (const [filePath, label] of optional) {
    if (fs.existsSync(filePath)) {
      pass(`${filePath} present — ${label}`)
    } else {
      warn(`${filePath} not found — ${label}`)
    }
  }
}

async function checkAssetMapCoverage() {
  console.log('\n── Asset map coverage ──')
  const assetMap = loadJson('output/asset-map.json')
  if (!assetMap) { warn('asset-map.json not found — skipping'); return }
  const entries = Object.keys(assetMap).length
  if (entries === 0) {
    fail('asset-map.json is empty — pre-upload-assets.js may not have run')
  } else {
    pass(`asset-map.json has ${entries} entries`)
  }

  // Spot-check: do any Sanity documents still reference CDN URLs directly?
  const docsWithCdnUrls = await client.fetch(
    `count(*[@ match "uploads-ssl.webflow.com" || @ match "website-files.com"])`
  )
  if (docsWithCdnUrls === 0) {
    pass('No documents reference Webflow CDN URLs directly')
  } else {
    warn(`${docsWithCdnUrls} documents may still reference Webflow CDN URLs`)
  }
}

// ─── Run ──────────────────────────────────────────────────────────────────────

async function run() {
  console.log(`\nValidating migration — dataset: ${process.env.SANITY_DATASET}\n`)
  await checkDocumentCounts()
  await checkSingletons()
  await checkWeakRefs()
  await checkSeoPopulated()
  await checkCmsTypeFields()
  await checkArtifacts()
  await checkAssetMapCoverage()

  console.log('\n─────────────────────────────────────────')
  console.log(`  ${passed} passed   ${warnings} warnings   ${failed} failed`)
  if (failed > 0) {
    console.error('\nFix the failures above before proceeding to launch.')
    process.exit(1)
  } else if (warnings > 0) {
    console.warn('\nWarnings are non-blocking but worth reviewing.')
  } else {
    console.log('\nAll checks passed. Ready for launch checklist.')
  }
}

run().catch(err => { console.error(err); process.exit(1) })
```

**Customize before running:**



- `CMS_TYPES` — your Sanity collection document types

- `SINGLETON_IDS` — the fixed `_id` strings for each singleton page

- `SEO_TYPES` — which types must have `seo.metaTitle` before launch


The script exits with code 1 if any required check fails, so you can run it in CI or as part of a pre-launch script. Warnings are printed but non-blocking.



**What each check covers:**



The document count check compares NDJSON line counts from your Webflow export to entries in `id-map.json`. A mismatch means some items from that collection weren't imported. Usually a transform function that threw and was silently caught.



The weak reference check queries for any document that still references an ID starting with `wf_`. Those placeholders are written in Pass 1 and should all be swapped out by `resolve-refs.js`. If any remain, the ref resolution script either didn't run or encountered an ID that wasn't in `id-map.json`.



The SEO check uses a 10% threshold rather than zero-tolerance — a handful of items with unusual `fieldData` is common and fixable post-launch, but 40% missing is a structural problem with your detail template mapping.



---

## Related Resources

- [Full course as markdown](https://www.sanity.io/learn/course/migrating-content-from-webflow-to-sanity.md)
- [All courses and lessons](https://www.sanity.io/learn/sitemap.md)
- [Complete content for LLMs](https://www.sanity.io/learn/llms-full.txt)

<AgentInstructions>

## Submitting Feedback

If you encounter incorrect, outdated, or confusing documentation on this page, submit feedback:

POST https://www.sanity.io/learn/resource/feedback

```json
{
  "path": "/learn/course/migrating-content-from-webflow-to-sanity/validate-migration.md",
  "feedback": "Description of the issue"
}
```

Only submit feedback when you have something specific and actionable to report.

</AgentInstructions>
