Skip to content

feat: Cloudinary to Sanity asset migration tool#599

Closed
codercatdev wants to merge 8 commits intomainfrom
feature/cloudinary-to-sanity-migration
Closed

feat: Cloudinary to Sanity asset migration tool#599
codercatdev wants to merge 8 commits intomainfrom
feature/cloudinary-to-sanity-migration

Conversation

@codercatdev
Copy link
Contributor

@codercatdev codercatdev commented Mar 4, 2026

Cloudinary → Sanity Asset Migration

Complete migration from Cloudinary to Sanity's native image/file pipeline — both data and code.

What Changed

Data Migration (already executed on dev dataset)

  • 278 assets uploaded from Cloudinary to Sanity
  • 486 fields updated across 150 documents
  • Document types: post, podcast, guest, lesson, course, page, sponsor, podcastType, settings, author
  • 4 failures (malformed URLs in old blog posts — not real assets)

Schema Changes

File Field Before After
sanity/schemas/partials/base.ts coverImage cloudinary.asset image (with hotspot)
sanity/schemas/partials/base.ts content[] block cloudinary.asset image
sanity/schemas/partials/content.ts videoCloudinary cloudinary.asset file
sanity/schemas/singletons/settings.tsx ogImage cloudinary.asset image (with hotspot)
sanity.config.ts plugin cloudinarySchemaPlugin() removed

Component Rewrites (6 files)

Component Before After
cover-image.tsx CldImage + public_id Next.js Image + urlForImage()
block-image.tsx CldImage + public_id Next.js Image + urlForImage()
cover-video.tsx CldVideoPlayer Native <video> + Sanity CDN URL
cover-media.tsx CloudinaryAsset types Sanity asset._ref checks
avatar.tsx CldImage + public_id Next.js Image + urlForImage()
portable-text.tsx cloudinary.asset handler image handler

Additional Component Updates (4 files)

  • youtube.tsx, youtube-short.tsx, pro-benefits.tsx, user-related.tsx — removed CloudinaryAsset type imports, updated to use asset._ref checks

New Files

  • sanity/lib/image.tsurlForImage() helper using @sanity/image-url
  • scripts/migration/ — Migration tool (5-phase pipeline with resume, dry-run, retry)

Migration Tool Features

  • ✅ Sanity-first approach — only migrates assets actually referenced in documents
  • ✅ Resume support — saves progress incrementally
  • ✅ Dry-run mode (--dry-run)
  • ✅ Handles both res.cloudinary.com/ajonp and media.codingcat.dev URLs
  • ✅ Configurable concurrency (default: 3 to avoid rate limits)
  • ✅ Transaction-safe document patching

Follow-up Tasks

  • Remove next-cloudinary and sanity-plugin-cloudinary from package.json (kept for now)
  • Run sanity typegen generate to update sanity/types.ts
  • Visual QA on dev.codingcat.dev
  • Run migration on production dataset

Sanity-first migration approach:
- Phase 1: Discover Cloudinary references in Sanity documents
- Phase 2: Extract unique Cloudinary URLs to migrate
- Phase 3: Download from Cloudinary & upload to Sanity
- Phase 4: Update document references (cloudinary.asset → image/file refs)
- Phase 5: Generate migration report

Features:
- Handles cloudinary.asset plugin objects and plain URL strings
- Supports both res.cloudinary.com/ajonp and media.codingcat.dev URLs
- Resume support with incremental mapping persistence
- Dry-run mode for previewing changes
- Configurable concurrency and per-phase execution
- Retry with exponential backoff

Co-authored-by: builder <builder@miriad.systems>
@vercel
Copy link

vercel bot commented Mar 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
codingcat-dev Error Error Mar 4, 2026 9:32pm

Skip coverImage.derived[], videoCloudinary.derived[], and other
sub-fields of cloudinary.asset objects that contain transformed
URL variants. These don't need separate uploads since the entire
cloudinary.asset object gets replaced.

Reduces unique URLs from 6,985 to 282 base assets.

Co-authored-by: builder <builder@miriad.systems>
Miriad and others added 2 commits March 4, 2026 18:20
…mages

Schema changes:
- coverImage: cloudinary.asset → image (with hotspot)
- content[]: cloudinary.asset block → image block
- videoCloudinary: cloudinary.asset → file
- ogImage: cloudinary.asset → image
- Remove cloudinarySchemaPlugin from sanity.config.ts

Component rewrites:
- cover-image.tsx: CldImage → Next.js Image + urlForImage()
- block-image.tsx: CldImage → Next.js Image + urlForImage()
- cover-video.tsx: CldVideoPlayer → native HTML video with Sanity CDN
- cover-media.tsx: update type checks for Sanity refs
- avatar.tsx: CldImage → Next.js Image + urlForImage()
- portable-text.tsx: cloudinary.asset handler → image handler
- youtube.tsx, youtube-short.tsx, pro-benefits.tsx, user-related.tsx:
  remove CloudinaryAsset type refs, use asset._ref checks

New file:
- sanity/lib/image.ts: urlForImage() helper using @sanity/image-url

Co-authored-by: builder <builder@miriad.systems>
…dd LQIP TODO, document video URL format

- Delete unused cloudinary-image.tsx and cloudinary-video.tsx
- Add TODO comment for LQIP blur placeholder support in cover-image.tsx
- Add explanatory comment for Sanity file ref URL construction in cover-video.tsx

Co-authored-by: builder <builder@miriad.systems>
…image URLs

- queries.ts: settingsQuery ogImage projection (secure_url → full image object)
- utils.ts: resolveOpenGraphImage uses urlForImage() instead of secure_url
- layout.tsx: ogImage uses resolveOpenGraphImage() properly
- devto/route.tsx: coverImage + cloudinary.asset serializer → urlForImage()
- hashnode/route.tsx: coverImage + cloudinary.asset serializer → urlForImage()
- rss.ts: coverImage.secure_url → urlForImage()
- package.json: remove next-cloudinary and sanity-plugin-cloudinary

Co-authored-by: builder <builder@miriad.systems>
… params

- Add raw Cloudinary object detection (old-format docs without _type)
- Add stripTransformations() to remove Cloudinary URL params
- Add getOriginalUrl() to construct canonical URLs from public_id
- Prevents uploading derived variants (avif, webp, resized copies)

Re-run results: 433 clean originals uploaded (down from 6,970 with variants)

Co-authored-by: builder <builder@miriad.systems>
Deletes unreferenced Sanity assets left over from migration.
Safety: checks document references before deleting, preserves all active assets.
Supports --dry-run mode.

Co-authored-by: builder <builder@miriad.systems>
…s, content-type headers

- Add full iTunes namespace to podcast feed (itunes:author, itunes:image,
  itunes:category, itunes:season, itunes:episode, enclosure tags)
- Create buildPodcastFeed() with hand-crafted XML for Apple Podcasts compatibility
- Add rssPodcastQuery with podcastFields (spotify, season, episode, guest)
- Fix hardcoded Cloudinary image URL in feed channel
- Fix content-type headers: text/xml → application/rss+xml
- Fix feed links to be content-type-specific (blog, podcasts, courses)
- Fix copyright year to be dynamic
- Fix YouTube feed links pointing to non-existent routes
@codercatdev codercatdev closed this Mar 5, 2026
@codercatdev codercatdev deleted the feature/cloudinary-to-sanity-migration branch March 5, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant