From 02c21e76b90990a8a85658e9127a633d2816ba85 Mon Sep 17 00:00:00 2001 From: Miriad Date: Wed, 4 Mar 2026 17:25:20 +0000 Subject: [PATCH 1/8] feat: add Cloudinary to Sanity asset migration tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 8 + scripts/migration/README.md | 203 +++++++ scripts/migration/env-example.txt | 14 + scripts/migration/migrate.mjs | 949 ++++++++++++++++++++++++++++++ scripts/migration/package.json | 23 + 5 files changed, 1197 insertions(+) create mode 100644 scripts/migration/README.md create mode 100644 scripts/migration/env-example.txt create mode 100644 scripts/migration/migrate.mjs create mode 100644 scripts/migration/package.json diff --git a/.gitignore b/.gitignore index 76a41667..3a42b9c7 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,11 @@ next-env.d.ts # Firebase debug files firebase-debug.log firebase-debug.*.logpackage-lock.json + +# Migration tool generated files +scripts/migration/discovered-references.json +scripts/migration/unique-cloudinary-urls.json +scripts/migration/asset-mapping.json +scripts/migration/migration-report.json +scripts/migration/node_modules/ +scripts/migration/.env diff --git a/scripts/migration/README.md b/scripts/migration/README.md new file mode 100644 index 00000000..f6af0643 --- /dev/null +++ b/scripts/migration/README.md @@ -0,0 +1,203 @@ +# Cloudinary → Sanity Asset Migration (Sanity-First) + +A production-grade Node.js tool that migrates Cloudinary assets to Sanity using +a **Sanity-first** approach: it starts by scanning your Sanity documents to +discover which Cloudinary assets are actually referenced, then migrates only +those assets and rewrites all references. + +## Why Sanity-First? + +The previous approach enumerated **all** Cloudinary assets and uploaded them +blindly. This was wasteful because: + +- Many Cloudinary assets may not be referenced by any Sanity document +- It uploaded assets that were never needed, wasting time and storage +- It couldn't handle the Sanity Cloudinary plugin's `cloudinary.asset` type + +The new approach: + +1. **Discovers** what's actually used in Sanity +2. **Extracts** a deduplicated list of Cloudinary URLs +3. **Migrates** only what's needed +4. **Updates** all references in-place +5. **Reports** a full summary + +--- + +## Prerequisites + +| Requirement | Why | +|---|---| +| **Node.js ≥ 18** | Native `fetch` support & ES-module compatibility | +| **Sanity project** | Project ID, dataset name, and a **write-enabled** API token | + +> **Note:** Cloudinary API credentials are no longer required! The script +> downloads assets directly from their public URLs. You only need Cloudinary +> credentials if your assets are private/restricted. + +## Quick Start + +```bash +# 1. Install dependencies +cd migration +npm install + +# 2. Create your .env from the template +cp env-example.txt .env +# Then fill in your real credentials + +# 3. Run the full migration (dry-run first!) +npm run migrate:dry-run + +# 4. Run for real +npm run migrate +``` + +## Environment Variables + +Copy `env-example.txt` to `.env` and fill in: + +| Variable | Required | Description | +|---|---|---| +| `SANITY_PROJECT_ID` | ✅ | Sanity project ID | +| `SANITY_DATASET` | ✅ | Sanity dataset (e.g. `production`) | +| `SANITY_TOKEN` | ✅ | Sanity API token with **write** access | +| `CLOUDINARY_CLOUD_NAME` | | Cloudinary cloud name (default: `ajonp`) | +| `CONCURRENCY` | | Max parallel uploads (default: `5`) | +| `DRY_RUN` | | Set to `true` to preview without writing | + +## CLI Flags + +```bash +node migrate.mjs # Full migration, all phases +node migrate.mjs --dry-run # Preview mode — no writes +node migrate.mjs --phase=1 # Run only Phase 1 +node migrate.mjs --phase=1,2 # Run Phases 1 & 2 +node migrate.mjs --phase=3,4 # Run Phases 3 & 4 (uses cached data) +node migrate.mjs --concurrency=10 # Override parallel upload limit +``` + +## What Each Phase Does + +### Phase 1 — Discover Cloudinary References in Sanity + +Scans **all** Sanity documents (excluding built-in asset types) to find any +that reference Cloudinary. Handles two types of references: + +#### `cloudinary.asset` objects (Sanity Cloudinary Plugin) + +The [sanity-plugin-cloudinary](https://github.com/sanity-io/sanity-plugin-cloudinary) +stores assets as objects with `_type: "cloudinary.asset"` containing fields like +`public_id`, `secure_url`, `resource_type`, `format`, etc. + +#### Plain URL strings + +Any string field containing: +- `res.cloudinary.com/ajonp` (standard Cloudinary URL) +- `media.codingcat.dev` (custom CNAME domain) + +This includes both standalone URL fields and URLs embedded in text/markdown content. + +**Output:** `discovered-references.json` — list of documents with their Cloudinary references. + +### Phase 2 — Extract Unique Cloudinary URLs + +Deduplicates all discovered references into a unique list of Cloudinary asset +URLs that need to be migrated. Tracks which documents reference each URL. + +**Output:** `unique-cloudinary-urls.json` — deduplicated URL list with metadata: +```json +{ + "cloudinaryUrl": "https://res.cloudinary.com/ajonp/image/upload/v123/folder/photo.jpg", + "cloudinaryPublicId": "folder/photo", + "resourceType": "image", + "sourceDocIds": ["doc-abc", "doc-def"] +} +``` + +### Phase 3 — Download & Upload Assets + +Downloads each unique Cloudinary asset and uploads it to Sanity's asset pipeline. + +**Output:** `asset-mapping.json` — mapping between Cloudinary and Sanity: +```json +{ + "cloudinaryUrl": "https://res.cloudinary.com/ajonp/image/upload/v123/folder/photo.jpg", + "cloudinaryPublicId": "folder/photo", + "sanityAssetId": "image-abc123-1920x1080-jpg", + "sanityUrl": "https://cdn.sanity.io/images/{projectId}/{dataset}/abc123-1920x1080.jpg", + "sourceDocIds": ["doc-abc", "doc-def"] +} +``` + +- **Resume support**: assets already in the mapping are skipped automatically. +- Retries failed downloads/uploads up to 3× with exponential back-off. + +### Phase 4 — Update References + +Patches Sanity documents to replace Cloudinary references with Sanity references: + +| Reference Type | Action | +|---|---| +| `cloudinary.asset` object | Replaced with `{ _type: "image", asset: { _type: "reference", _ref: "..." } }` | +| Full URL string | Replaced with Sanity CDN URL | +| Embedded URL in text | URL swapped inline within the text | + +All patches are applied inside **transactions** for atomicity (one transaction per document). + +### Phase 5 — Report + +Prints a summary to the console and writes a detailed report: + +``` +══════════════════════════════════════════════════════════ + MIGRATION SUMMARY +══════════════════════════════════════════════════════════ + Documents with refs: 42 + Total references found: 128 + cloudinary.asset objects: 35 + URL string fields: 61 + Embedded URLs in text: 32 + Unique Cloudinary URLs: 87 + Assets uploaded to Sanity: 87 + Document fields updated: 128 + Errors: 0 +══════════════════════════════════════════════════════════ +``` + +**Output:** `migration-report.json` + +## Generated Files + +| File | Phase | Description | +|---|---|---| +| `discovered-references.json` | 1 | Documents with Cloudinary references | +| `unique-cloudinary-urls.json` | 2 | Deduplicated Cloudinary URLs to migrate | +| `asset-mapping.json` | 3 | Cloudinary → Sanity asset mapping | +| `migration-report.json` | 5 | Full migration report | + +## Resuming an Interrupted Migration + +The script is fully resumable: + +1. **Phase 1** is skipped if `discovered-references.json` exists. +2. **Phase 2** is skipped if `unique-cloudinary-urls.json` exists. +3. **Phase 3** skips any asset already present in `asset-mapping.json`. +4. **Phases 4–5** are idempotent — re-running them is safe. + +To start completely fresh, delete the generated JSON files: + +```bash +rm -f discovered-references.json unique-cloudinary-urls.json asset-mapping.json migration-report.json +``` + +## Troubleshooting + +| Problem | Fix | +|---|---| +| `401 Unauthorized` from Sanity | Check `SANITY_TOKEN` has write permissions | +| Download fails for private assets | Add Cloudinary credentials to `.env` and modify the download logic | +| Script hangs | Check network; the script logs progress for every asset | +| Partial migration | Just re-run — resume picks up where it left off | +| `cloudinary.asset` not detected | Ensure the field has `_type: "cloudinary.asset"` in the document | +| Custom CNAME not detected | Add your domain to `CLOUDINARY_PATTERNS` in the script | diff --git a/scripts/migration/env-example.txt b/scripts/migration/env-example.txt new file mode 100644 index 00000000..81cbd856 --- /dev/null +++ b/scripts/migration/env-example.txt @@ -0,0 +1,14 @@ +# Sanity credentials (required) +SANITY_PROJECT_ID=your_project_id +SANITY_DATASET=dev +SANITY_TOKEN=your_sanity_token_with_write_access + +# Cloudinary cloud name (optional, defaults to "ajonp") +CLOUDINARY_CLOUD_NAME=ajonp + +# Migration options (all optional) +# Max parallel uploads (default: 5) +CONCURRENCY=5 + +# Set to "true" to preview changes without writing anything +DRY_RUN=false diff --git a/scripts/migration/migrate.mjs b/scripts/migration/migrate.mjs new file mode 100644 index 00000000..0b26c4ef --- /dev/null +++ b/scripts/migration/migrate.mjs @@ -0,0 +1,949 @@ +#!/usr/bin/env node + +/** + * Cloudinary → Sanity Asset Migration Tool (Sanity-First Approach) + * + * Instead of enumerating ALL Cloudinary assets, this script starts by + * scanning Sanity documents to discover which Cloudinary assets are + * actually referenced, then migrates only those. + * + * Usage: + * node migrate.mjs # full migration + * node migrate.mjs --dry-run # preview only + * node migrate.mjs --phase=1,2 # run specific phases + * node migrate.mjs --concurrency=10 # override parallelism + */ + +import 'dotenv/config'; +import { createClient } from '@sanity/client'; +import fetch from 'node-fetch'; +import pLimit from 'p-limit'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// ─── Resolve __dirname for ES modules ──────────────────────────────────────── +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// ─── File paths ────────────────────────────────────────────────────────────── +const DISCOVERY_FILE = path.join(__dirname, 'discovered-references.json'); +const UNIQUE_URLS_FILE = path.join(__dirname, 'unique-cloudinary-urls.json'); +const MAPPING_FILE = path.join(__dirname, 'asset-mapping.json'); +const REPORT_FILE = path.join(__dirname, 'migration-report.json'); + +// ─── Parse CLI flags ───────────────────────────────────────────────────────── +function parseArgs() { + const args = process.argv.slice(2); + const flags = { + dryRun: false, + phases: null, // null = all phases + concurrency: null, // null = use env or default + }; + + for (const arg of args) { + if (arg === '--dry-run') { + flags.dryRun = true; + } else if (arg.startsWith('--phase=')) { + flags.phases = arg.replace('--phase=', '').split(',').map(Number); + } else if (arg.startsWith('--concurrency=')) { + flags.concurrency = parseInt(arg.replace('--concurrency=', ''), 10); + } + } + + return flags; +} + +const FLAGS = parseArgs(); + +// ─── Configuration ─────────────────────────────────────────────────────────── +const DRY_RUN = FLAGS.dryRun || process.env.DRY_RUN === 'true'; +const CONCURRENCY = FLAGS.concurrency || parseInt(process.env.CONCURRENCY, 10) || 5; +const CLOUD_NAME = process.env.CLOUDINARY_CLOUD_NAME || 'ajonp'; +const SANITY_PROJECT_ID = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || process.env.SANITY_PROJECT_ID; +const SANITY_DATASET = process.env.NEXT_PUBLIC_SANITY_DATASET || process.env.SANITY_DATASET || 'production'; +const SANITY_TOKEN = process.env.SANITY_API_WRITE_TOKEN || process.env.SANITY_TOKEN; +const API_VERSION = '2024-01-01'; + +// URL patterns that indicate a Cloudinary reference +const CLOUDINARY_PATTERNS = [ + `res.cloudinary.com/${CLOUD_NAME}`, + 'media.codingcat.dev', +]; + +// ─── Validate env ──────────────────────────────────────────────────────────── +function validateEnv() { + const required = [ + ['SANITY_PROJECT_ID', SANITY_PROJECT_ID], + ['SANITY_TOKEN', SANITY_TOKEN], + ]; + + const missing = required.filter(([, val]) => !val).map(([name]) => name); + if (missing.length > 0) { + console.error(`❌ Missing required environment variables: ${missing.join(', ')}`); + console.error(' Copy env-example.txt to .env and fill in your credentials.'); + process.exit(1); + } +} + +// ─── Configure Sanity client ───────────────────────────────────────────────── +function initSanityClient() { + return createClient({ + projectId: SANITY_PROJECT_ID, + dataset: SANITY_DATASET, + token: SANITY_TOKEN, + apiVersion: API_VERSION, + useCdn: false, + }); +} + +// ─── Utility: logging ──────────────────────────────────────────────────────── +function log(phase, message) { + const prefix = DRY_RUN ? '[DRY-RUN] ' : ''; + console.log(`${prefix}[Phase ${phase}] ${message}`); +} + +function logInfo(message) { + const prefix = DRY_RUN ? '[DRY-RUN] ' : ''; + console.log(`${prefix}${message}`); +} + +// ─── Utility: retry with exponential back-off ──────────────────────────────── +async function withRetry(fn, { retries = 3, baseDelay = 1000, label = 'operation' } = {}) { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + return await fn(); + } catch (err) { + if (attempt === retries) { + console.error(` ✗ ${label} failed after ${retries} attempts: ${err.message}`); + throw err; + } + const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 500; + console.warn(` ⚠ ${label} attempt ${attempt} failed (${err.message}), retrying in ${Math.round(delay)}ms…`); + await sleep(delay); + } + } +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +// ─── Utility: safe JSON file I/O ───────────────────────────────────────────── +async function readJsonFile(filePath) { + try { + const raw = await fs.readFile(filePath, 'utf-8'); + return JSON.parse(raw); + } catch { + return null; + } +} + +async function writeJsonFile(filePath, data) { + await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8'); +} + +// ─── Utility: MIME type from format / extension ────────────────────────────── +function guessMimeType(url) { + const mimeMap = { + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + gif: 'image/gif', + webp: 'image/webp', + svg: 'image/svg+xml', + avif: 'image/avif', + tiff: 'image/tiff', + bmp: 'image/bmp', + ico: 'image/x-icon', + mp4: 'video/mp4', + webm: 'video/webm', + mov: 'video/quicktime', + avi: 'video/x-msvideo', + mp3: 'audio/mpeg', + wav: 'audio/wav', + ogg: 'audio/ogg', + pdf: 'application/pdf', + json: 'application/json', + csv: 'text/csv', + txt: 'text/plain', + zip: 'application/zip', + }; + + if (url) { + const ext = url.split('?')[0].split('.').pop()?.toLowerCase(); + if (ext && mimeMap[ext]) return mimeMap[ext]; + } + + return 'application/octet-stream'; +} + +// ─── Utility: determine resource type from URL or MIME ─────────────────────── +function guessResourceType(url) { + const mime = guessMimeType(url); + if (mime.startsWith('image/')) return 'image'; + if (mime.startsWith('video/')) return 'video'; + return 'file'; +} + +// ─── Utility: extract filename from a URL ──────────────────────────────────── +function extractFilenameFromUrl(url) { + try { + const pathname = new URL(url).pathname; + const segments = pathname.split('/'); + return segments[segments.length - 1] || 'unnamed'; + } catch { + return 'unnamed'; + } +} + +// ─── Utility: extract public_id from a Cloudinary URL ──────────────────────── +function extractPublicIdFromUrl(url) { + // Standard Cloudinary URL: + // https://res.cloudinary.com/{cloud}/{resource_type}/upload/v{version}/{public_id}.{format} + // Custom CNAME: + // https://media.codingcat.dev/{resource_type}/upload/v{version}/{public_id}.{format} + try { + const u = new URL(url); + const pathname = u.pathname; + + // Find the upload/ segment and take everything after it + const uploadIdx = pathname.indexOf('/upload/'); + if (uploadIdx === -1) { + // Try without /upload/ — might be a fetch or other delivery type + const parts = pathname.split('/').filter(Boolean); + // Remove cloud name if present, resource_type, type + return parts.slice(-1)[0]?.split('.')[0] || null; + } + + let afterUpload = pathname.substring(uploadIdx + '/upload/'.length); + + // Strip version prefix (v1234567890/) + afterUpload = afterUpload.replace(/^v\d+\//, ''); + + // Strip file extension + const lastDot = afterUpload.lastIndexOf('.'); + if (lastDot > 0) { + afterUpload = afterUpload.substring(0, lastDot); + } + + return afterUpload || null; + } catch { + return null; + } +} + +// ─── Utility: check if a string contains a Cloudinary reference ────────────── +function containsCloudinaryRef(str) { + if (typeof str !== 'string') return false; + return CLOUDINARY_PATTERNS.some((p) => str.includes(p)); +} + +// ─── Utility: extract all Cloudinary URLs from a string ────────────────────── +function extractCloudinaryUrls(str) { + const standardRegex = new RegExp( + `https?://res\\.cloudinary\\.com/${escapeRegex(CLOUD_NAME)}/[^\\s"'<>)\\]]+`, + 'g' + ); + const cnameRegex = /https?:\/\/media\.codingcat\.dev\/[^\s"'<>)\]]+/g; + + const standardMatches = str.match(standardRegex) || []; + const cnameMatches = str.match(cnameRegex) || []; + return [...new Set([...standardMatches, ...cnameMatches])]; +} + +function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +// ─── Utility: build Sanity CDN URL from asset ID ───────────────────────────── +function sanityAssetUrl(assetId) { + // Sanity asset IDs look like: image-abc123-1920x1080-jpg + // CDN URL: https://cdn.sanity.io/images/{projectId}/{dataset}/{hash}-{dims}.{ext} + const parts = assetId.split('-'); + if (parts.length < 3) return null; + + const type = parts[0]; // 'image' or 'file' + const assetType = type === 'image' ? 'images' : 'files'; + const idParts = parts.slice(1); + const ext = idParts.pop(); + const filenamePart = idParts.join('-') + '.' + ext; + + return `https://cdn.sanity.io/${assetType}/${SANITY_PROJECT_ID}/${SANITY_DATASET}/${filenamePart}`; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// PHASE 1: Discover Cloudinary References in Sanity +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Recursively walk an object and find all Cloudinary references. + * Handles both: + * - cloudinary.asset objects (from the Sanity Cloudinary plugin) + * - Plain URL strings containing Cloudinary domains + * + * Returns an array of: + * { path, type: 'cloudinary.asset'|'url'|'embedded', value, url, publicId } + */ +function findCloudinaryRefs(obj, currentPath = '') { + const results = []; + + if (obj === null || obj === undefined) return results; + + // Check for cloudinary.asset plugin objects + if (typeof obj === 'object' && !Array.isArray(obj) && obj._type === 'cloudinary.asset') { + const url = obj.secure_url || obj.url || null; + const publicId = obj.public_id || (url ? extractPublicIdFromUrl(url) : null); + + // If no URL in the object, construct one from public_id + let resolvedUrl = url; + if (!resolvedUrl && publicId && obj.resource_type && obj.format) { + resolvedUrl = `https://res.cloudinary.com/${CLOUD_NAME}/${obj.resource_type}/upload/${publicId}.${obj.format}`; + } + + if (resolvedUrl || publicId) { + results.push({ + path: currentPath, + type: 'cloudinary.asset', + value: obj, + url: resolvedUrl, + publicId: publicId, + resourceType: obj.resource_type || 'image', + }); + } + return results; // Don't recurse into cloudinary.asset children + } + + if (typeof obj === 'string') { + if (containsCloudinaryRef(obj)) { + const urls = extractCloudinaryUrls(obj); + const isFullUrl = obj.trim().startsWith('http') && !obj.includes(' ') && urls.length === 1; + + if (isFullUrl) { + results.push({ + path: currentPath, + type: 'url', + value: obj, + url: urls[0], + publicId: extractPublicIdFromUrl(urls[0]), + }); + } else if (urls.length > 0) { + // Embedded URL(s) in text content + for (const u of urls) { + results.push({ + path: currentPath, + type: 'embedded', + value: obj, + url: u, + publicId: extractPublicIdFromUrl(u), + }); + } + } + } + return results; + } + + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + results.push(...findCloudinaryRefs(obj[i], `${currentPath}[${i}]`)); + } + return results; + } + + if (typeof obj === 'object') { + for (const [key, value] of Object.entries(obj)) { + const nextPath = currentPath ? `${currentPath}.${key}` : key; + results.push(...findCloudinaryRefs(value, nextPath)); + } + } + + return results; +} + +async function phase1_discoverReferences(sanityClient) { + log(1, '── Discovering Cloudinary references in Sanity documents ──'); + + // Check for cached discovery + const cached = await readJsonFile(DISCOVERY_FILE); + if (cached && Array.isArray(cached) && cached.length > 0) { + log(1, `Found cached discovery with ${cached.length} documents. Delete ${DISCOVERY_FILE} to re-scan.`); + return cached; + } + + // Fetch all documents (excluding Sanity's own asset types) + log(1, 'Fetching all documents from Sanity…'); + + let allDocs = []; + const batchSize = 2000; + let lastId = ''; + + while (true) { + const paginatedQuery = `*[ + _type != "sanity.imageAsset" && + _type != "sanity.fileAsset" && + _id > $lastId + ] | order(_id asc) [0...${batchSize}] { ... }`; + + const batch = await withRetry( + () => sanityClient.fetch(paginatedQuery, { lastId }), + { label: `Fetch Sanity documents batch after ${lastId || 'start'}` } + ); + + if (!batch || batch.length === 0) break; + + allDocs = allDocs.concat(batch); + lastId = batch[batch.length - 1]._id; + log(1, ` Fetched ${allDocs.length} documents so far…`); + + if (batch.length < batchSize) break; + } + + log(1, `Total documents fetched: ${allDocs.length}`); + + // Scan each document for Cloudinary references + const docsWithRefs = []; + + for (const doc of allDocs) { + const refs = findCloudinaryRefs(doc); + if (refs.length > 0) { + docsWithRefs.push({ + _id: doc._id, + _type: doc._type, + refs, + }); + } + } + + log(1, `Found ${docsWithRefs.length} documents with Cloudinary references`); + + let cloudinaryAssetCount = 0; + let urlCount = 0; + let embeddedCount = 0; + + for (const d of docsWithRefs) { + log(1, ` ${d._type} (${d._id}): ${d.refs.length} reference(s)`); + for (const r of d.refs) { + log(1, ` ${r.path} [${r.type}] → ${r.url || r.publicId || '(no url)'}`); + if (r.type === 'cloudinary.asset') cloudinaryAssetCount++; + else if (r.type === 'url') urlCount++; + else if (r.type === 'embedded') embeddedCount++; + } + } + + log(1, `\n Breakdown: ${cloudinaryAssetCount} cloudinary.asset objects, ${urlCount} URL fields, ${embeddedCount} embedded URLs`); + + // Save to disk for resume + if (!DRY_RUN) { + await writeJsonFile(DISCOVERY_FILE, docsWithRefs); + log(1, `Saved discovery to ${DISCOVERY_FILE}`); + } + + return docsWithRefs; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// PHASE 2: Extract Unique Cloudinary URLs +// ═══════════════════════════════════════════════════════════════════════════════ + +async function phase2_extractUniqueUrls(docsWithRefs) { + log(2, '── Extracting unique Cloudinary URLs ──'); + + // Check for cached URL list + const cached = await readJsonFile(UNIQUE_URLS_FILE); + if (cached && Array.isArray(cached) && cached.length > 0) { + log(2, `Found cached URL list with ${cached.length} unique URLs. Delete ${UNIQUE_URLS_FILE} to re-extract.`); + return cached; + } + + // Build a map: url → { url, publicId, resourceType, sourceDocIds } + const urlMap = new Map(); + + for (const doc of docsWithRefs) { + for (const ref of doc.refs) { + const url = ref.url; + if (!url) continue; + + if (urlMap.has(url)) { + // Add this doc as another source + const entry = urlMap.get(url); + if (!entry.sourceDocIds.includes(doc._id)) { + entry.sourceDocIds.push(doc._id); + } + } else { + urlMap.set(url, { + cloudinaryUrl: url, + cloudinaryPublicId: ref.publicId || extractPublicIdFromUrl(url), + resourceType: ref.resourceType || guessResourceType(url), + sourceDocIds: [doc._id], + }); + } + } + } + + const uniqueUrls = Array.from(urlMap.values()); + + log(2, `Total unique Cloudinary URLs to migrate: ${uniqueUrls.length}`); + + // Log breakdown by resource type + const byType = {}; + for (const u of uniqueUrls) { + byType[u.resourceType] = (byType[u.resourceType] || 0) + 1; + } + for (const [type, count] of Object.entries(byType)) { + log(2, ` ${type}: ${count}`); + } + + // Save to disk for resume + if (!DRY_RUN) { + await writeJsonFile(UNIQUE_URLS_FILE, uniqueUrls); + log(2, `Saved unique URL list to ${UNIQUE_URLS_FILE}`); + } + + return uniqueUrls; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// PHASE 3: Download & Upload Assets to Sanity +// ═══════════════════════════════════════════════════════════════════════════════ + +async function phase3_downloadAndUpload(uniqueUrls) { + log(3, '── Downloading & uploading assets to Sanity ──'); + log(3, `Concurrency: ${CONCURRENCY} | Total unique URLs: ${uniqueUrls.length}`); + + // Load existing mapping for resume + let mapping = (await readJsonFile(MAPPING_FILE)) || []; + const existingUrls = new Set(mapping.map((m) => m.cloudinaryUrl)); + + const toProcess = uniqueUrls.filter((u) => !existingUrls.has(u.cloudinaryUrl)); + log(3, `Skipping ${uniqueUrls.length - toProcess.length} already-migrated assets`); + log(3, `Assets to migrate: ${toProcess.length}`); + + if (toProcess.length === 0) { + log(3, 'Nothing to do — all assets already migrated.'); + return mapping; + } + + const limit = pLimit(CONCURRENCY); + let completed = 0; + let errors = 0; + const errorDetails = []; + + const tasks = toProcess.map((entry) => + limit(async () => { + const { cloudinaryUrl, cloudinaryPublicId, resourceType, sourceDocIds } = entry; + const filename = extractFilenameFromUrl(cloudinaryUrl); + const mimeType = guessMimeType(cloudinaryUrl); + const isImage = resourceType === 'image'; + + try { + // Step 1: Download from Cloudinary + const downloadRes = await withRetry( + () => fetch(cloudinaryUrl), + { label: `Download ${cloudinaryPublicId || cloudinaryUrl}` } + ); + + if (!downloadRes.ok) { + throw new Error(`Download failed: HTTP ${downloadRes.status} for ${cloudinaryUrl}`); + } + + const buffer = await downloadRes.buffer(); + + if (DRY_RUN) { + completed++; + log(3, ` [${completed}/${toProcess.length}] Would upload: ${cloudinaryPublicId || cloudinaryUrl} (${(buffer.length / 1024).toFixed(1)} KB)`); + return; + } + + // Step 2: Upload to Sanity + const assetType = isImage ? 'images' : 'files'; + const uploadUrl = `https://${SANITY_PROJECT_ID}.api.sanity.io/v${API_VERSION}/assets/${assetType}/${SANITY_DATASET}?filename=${encodeURIComponent(filename)}`; + + const uploadRes = await withRetry( + async () => { + const res = await fetch(uploadUrl, { + method: 'POST', + headers: { + 'Content-Type': mimeType, + Authorization: `Bearer ${SANITY_TOKEN}`, + }, + body: buffer, + }); + + if (!res.ok) { + const body = await res.text(); + throw new Error(`Upload failed: HTTP ${res.status} — ${body}`); + } + + return res.json(); + }, + { label: `Upload ${cloudinaryPublicId || cloudinaryUrl}` } + ); + + const sanityDoc = uploadRes.document; + const sanityUrl = sanityAssetUrl(sanityDoc._id); + + const mappingEntry = { + cloudinaryUrl, + cloudinaryPublicId: cloudinaryPublicId || null, + sanityAssetId: sanityDoc._id, + sanityUrl: sanityUrl || sanityDoc.url, + sourceDocIds, + }; + + mapping.push(mappingEntry); + + // Persist mapping incrementally for resume safety + await writeJsonFile(MAPPING_FILE, mapping); + + completed++; + log(3, ` [${completed}/${toProcess.length}] ✓ ${cloudinaryPublicId || cloudinaryUrl} → ${sanityDoc._id}`); + } catch (err) { + errors++; + errorDetails.push({ cloudinaryUrl, cloudinaryPublicId, error: err.message }); + console.error(` ✗ [${completed + errors}/${toProcess.length}] Failed: ${cloudinaryPublicId || cloudinaryUrl} — ${err.message}`); + } + }) + ); + + await Promise.all(tasks); + + log(3, `\nPhase 3 complete: ${completed} uploaded, ${errors} errors`); + if (errorDetails.length > 0) { + log(3, 'Errors:'); + for (const e of errorDetails) { + log(3, ` - ${e.cloudinaryPublicId || e.cloudinaryUrl}: ${e.error}`); + } + } + + return mapping; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// PHASE 4: Update References in Sanity Documents +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Given a Cloudinary URL, find the matching Sanity asset in the mapping. + */ +function findMappingForUrl(url, mapping) { + // Try exact URL match first + let entry = mapping.find((m) => m.cloudinaryUrl === url); + if (entry) return entry; + + // Try matching by public_id extracted from the URL + const publicId = extractPublicIdFromUrl(url); + if (publicId) { + entry = mapping.find((m) => m.cloudinaryPublicId === publicId); + if (entry) return entry; + + // Fuzzy: check if the URL contains the mapping's public_id + for (const m of mapping) { + if (m.cloudinaryPublicId && url.includes(m.cloudinaryPublicId)) { + return m; + } + } + } + + // Try matching by filename as last resort + const urlFilename = url.split('/').pop()?.split('?')[0]; + if (urlFilename) { + for (const m of mapping) { + const mappingFilename = m.cloudinaryUrl.split('/').pop()?.split('?')[0]; + if (urlFilename === mappingFilename) { + return m; + } + } + } + + return null; +} + +async function phase4_updateReferences(sanityClient, docsWithRefs, mapping) { + log(4, '── Updating Cloudinary references in Sanity documents ──'); + + if (docsWithRefs.length === 0) { + log(4, 'No documents to update.'); + return []; + } + + const changes = []; + let updatedDocs = 0; + let updatedFields = 0; + let skippedFields = 0; + + for (const docInfo of docsWithRefs) { + const { _id: docId, _type: docType, refs } = docInfo; + let transaction = sanityClient.transaction(); + let hasPatches = false; + + for (const ref of refs) { + const { path: fieldPath, type: refType, url: refUrl, value } = ref; + + if (!refUrl) { + log(4, ` ⚠ No URL for reference at ${docId}.${fieldPath} — skipping`); + skippedFields++; + continue; + } + + const mappingEntry = findMappingForUrl(refUrl, mapping); + + if (!mappingEntry) { + log(4, ` ⚠ No mapping found for URL: ${refUrl} (in ${docId} at ${fieldPath})`); + skippedFields++; + continue; + } + + const sanityId = mappingEntry.sanityAssetId; + const cdnUrl = mappingEntry.sanityUrl || sanityAssetUrl(sanityId); + + if (refType === 'cloudinary.asset') { + // ── Replace entire cloudinary.asset object with Sanity image/file reference ── + const isImage = (ref.resourceType || 'image') === 'image'; + const refObj = isImage + ? { + _type: 'image', + asset: { + _type: 'reference', + _ref: sanityId, + }, + } + : { + _type: 'file', + asset: { + _type: 'reference', + _ref: sanityId, + }, + }; + + const change = { + docId, + docType, + fieldPath, + action: 'replace_cloudinary_asset', + from: refUrl, + to: sanityId, + }; + changes.push(change); + + if (!DRY_RUN) { + transaction = transaction.patch(docId, (p) => p.set({ [fieldPath]: refObj })); + hasPatches = true; + } + + log(4, ` ${docId}: ${fieldPath} [cloudinary.asset] → ${isImage ? 'image' : 'file'} ref ${sanityId}`); + updatedFields++; + + } else if (refType === 'url') { + // ── Full URL field — replace with Sanity CDN URL ── + const newUrl = cdnUrl || refUrl; + + const change = { + docId, + docType, + fieldPath, + action: 'replace_url', + from: refUrl, + to: newUrl, + }; + changes.push(change); + + if (!DRY_RUN && cdnUrl) { + transaction = transaction.patch(docId, (p) => p.set({ [fieldPath]: newUrl })); + hasPatches = true; + } + + log(4, ` ${docId}: ${fieldPath} [url] → ${newUrl}`); + updatedFields++; + + } else if (refType === 'embedded') { + // ── Embedded URL in text — replace inline ── + const newValue = value.replace( + new RegExp(escapeRegex(refUrl), 'g'), + cdnUrl || refUrl + ); + + const change = { + docId, + docType, + fieldPath, + action: 'replace_embedded_url', + from: refUrl, + to: cdnUrl || refUrl, + }; + changes.push(change); + + if (!DRY_RUN && cdnUrl) { + transaction = transaction.patch(docId, (p) => p.set({ [fieldPath]: newValue })); + hasPatches = true; + } + + log(4, ` ${docId}: ${fieldPath} [embedded] → replaced URL`); + updatedFields++; + } + } + + // Commit the transaction for this document + if (hasPatches && !DRY_RUN) { + try { + await withRetry( + () => transaction.commit(), + { label: `Commit patches for ${docId}` } + ); + updatedDocs++; + log(4, ` ✓ Committed changes for ${docId}`); + } catch (err) { + console.error(` ✗ Failed to commit changes for ${docId}: ${err.message}`); + changes.push({ + docId, + docType, + action: 'error', + error: err.message, + }); + } + } else if (DRY_RUN) { + updatedDocs++; + } + } + + log(4, `\nPhase 4 complete: ${updatedDocs} documents, ${updatedFields} fields updated, ${skippedFields} skipped`); + return changes; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// PHASE 5: Report +// ═══════════════════════════════════════════════════════════════════════════════ + +async function phase5_report(docsWithRefs, uniqueUrls, mapping, changes) { + log(5, '── Migration Report ──'); + + const totalRefs = docsWithRefs.reduce((sum, d) => sum + d.refs.length, 0); + const cloudinaryAssetRefs = docsWithRefs.reduce( + (sum, d) => sum + d.refs.filter((r) => r.type === 'cloudinary.asset').length, + 0 + ); + const urlRefs = docsWithRefs.reduce( + (sum, d) => sum + d.refs.filter((r) => r.type === 'url').length, + 0 + ); + const embeddedRefs = docsWithRefs.reduce( + (sum, d) => sum + d.refs.filter((r) => r.type === 'embedded').length, + 0 + ); + + const report = { + timestamp: new Date().toISOString(), + dryRun: DRY_RUN, + summary: { + totalDocumentsWithRefs: docsWithRefs.length, + totalReferencesFound: totalRefs, + cloudinaryAssetObjects: cloudinaryAssetRefs, + urlStringRefs: urlRefs, + embeddedUrlRefs: embeddedRefs, + uniqueCloudinaryUrls: uniqueUrls.length, + assetsUploaded: mapping.length, + fieldsUpdated: changes.filter((c) => c.action !== 'error').length, + errors: changes.filter((c) => c.action === 'error').length, + }, + assetMapping: mapping, + documentChanges: changes, + }; + + console.log('\n══════════════════════════════════════════════════════════'); + console.log(' MIGRATION SUMMARY'); + console.log('══════════════════════════════════════════════════════════'); + console.log(` Mode: ${DRY_RUN ? 'DRY RUN (no changes written)' : 'LIVE'}`); + console.log(` Documents with refs: ${report.summary.totalDocumentsWithRefs}`); + console.log(` Total references found: ${report.summary.totalReferencesFound}`); + console.log(` cloudinary.asset objects: ${report.summary.cloudinaryAssetObjects}`); + console.log(` URL string fields: ${report.summary.urlStringRefs}`); + console.log(` Embedded URLs in text: ${report.summary.embeddedUrlRefs}`); + console.log(` Unique Cloudinary URLs: ${report.summary.uniqueCloudinaryUrls}`); + console.log(` Assets uploaded to Sanity: ${report.summary.assetsUploaded}`); + console.log(` Document fields updated: ${report.summary.fieldsUpdated}`); + console.log(` Errors: ${report.summary.errors}`); + console.log('══════════════════════════════════════════════════════════\n'); + + if (!DRY_RUN) { + await writeJsonFile(REPORT_FILE, report); + logInfo(`Detailed report saved to ${REPORT_FILE}`); + } else { + logInfo('Dry run — no report file written. Remove --dry-run to execute.'); + } + + return report; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// MAIN +// ═══════════════════════════════════════════════════════════════════════════════ + +async function main() { + console.log('╔══════════════════════════════════════════════════════════╗'); + console.log('║ Cloudinary → Sanity Asset Migration (Sanity-First) ║'); + console.log('╚══════════════════════════════════════════════════════════╝'); + console.log(); + + if (DRY_RUN) { + console.log('🔍 DRY RUN MODE — no changes will be written\n'); + } + + validateEnv(); + const sanityClient = initSanityClient(); + + const shouldRun = (phase) => !FLAGS.phases || FLAGS.phases.includes(phase); + + let docsWithRefs = []; + let uniqueUrls = []; + let mapping = []; + let changes = []; + + // ── Phase 1: Discover Cloudinary references in Sanity ── + if (shouldRun(1)) { + docsWithRefs = await phase1_discoverReferences(sanityClient); + } else { + docsWithRefs = (await readJsonFile(DISCOVERY_FILE)) || []; + logInfo(`Loaded ${docsWithRefs.length} discovered docs from cache (Phase 1 skipped)`); + } + + // ── Phase 2: Extract unique Cloudinary URLs ── + if (shouldRun(2)) { + uniqueUrls = await phase2_extractUniqueUrls(docsWithRefs); + } else { + uniqueUrls = (await readJsonFile(UNIQUE_URLS_FILE)) || []; + logInfo(`Loaded ${uniqueUrls.length} unique URLs from cache (Phase 2 skipped)`); + } + + // ── Phase 3: Download & Upload ── + if (shouldRun(3)) { + mapping = await phase3_downloadAndUpload(uniqueUrls); + } else { + mapping = (await readJsonFile(MAPPING_FILE)) || []; + logInfo(`Loaded ${mapping.length} mappings from cache (Phase 3 skipped)`); + } + + // ── Phase 4: Update references ── + if (shouldRun(4)) { + // Ensure we have discovery data even if Phase 1 was skipped + if (docsWithRefs.length === 0 && !shouldRun(1)) { + docsWithRefs = (await readJsonFile(DISCOVERY_FILE)) || []; + if (docsWithRefs.length === 0) { + log(4, 'No discovery data available — running Phase 1 now…'); + docsWithRefs = await phase1_discoverReferences(sanityClient); + } + } + // Ensure we have mapping data even if Phase 3 was skipped + if (mapping.length === 0 && !shouldRun(3)) { + mapping = (await readJsonFile(MAPPING_FILE)) || []; + } + changes = await phase4_updateReferences(sanityClient, docsWithRefs, mapping); + } + + // ── Phase 5: Report (always runs) ── + await phase5_report(docsWithRefs, uniqueUrls, mapping, changes); +} + +// ── Run ────────────────────────────────────────────────────────────────────── +main().catch((err) => { + console.error('\n💥 Fatal error:', err); + process.exit(1); +}); diff --git a/scripts/migration/package.json b/scripts/migration/package.json new file mode 100644 index 00000000..fb41b2de --- /dev/null +++ b/scripts/migration/package.json @@ -0,0 +1,23 @@ +{ + "name": "cloudinary-to-sanity-migration", + "version": "1.0.0", + "description": "Migration tool to copy referenced Cloudinary assets to Sanity and update document references", + "type": "module", + "scripts": { + "migrate": "node migrate.mjs", + "migrate:dry-run": "node migrate.mjs --dry-run", + "migrate:phase1": "node migrate.mjs --phase=1", + "migrate:phase2": "node migrate.mjs --phase=2", + "migrate:phase3": "node migrate.mjs --phase=3", + "migrate:phase4": "node migrate.mjs --phase=4" + }, + "dependencies": { + "@sanity/client": "^6.24.1", + "node-fetch": "^2.7.0", + "dotenv": "^16.4.7", + "p-limit": "^3.1.0" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file From 41b30a32eb18131fd2eeabf4f0e4eb194af4172c Mon Sep 17 00:00:00 2001 From: Miriad Date: Wed, 4 Mar 2026 18:10:43 +0000 Subject: [PATCH 2/8] fix: filter derived Cloudinary URLs from migration 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 --- scripts/migration/migrate.mjs | 9 + scripts/migration/package-lock.json | 346 ++++++++++++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 scripts/migration/package-lock.json diff --git a/scripts/migration/migrate.mjs b/scripts/migration/migrate.mjs index 0b26c4ef..3bbe66cf 100644 --- a/scripts/migration/migrate.mjs +++ b/scripts/migration/migrate.mjs @@ -316,6 +316,15 @@ function findCloudinaryRefs(obj, currentPath = '') { if (typeof obj === 'string') { if (containsCloudinaryRef(obj)) { + // Skip URLs that are inside cloudinary.asset sub-fields (derived, url, secure_url) + // These will be replaced when the parent cloudinary.asset object is swapped out + const skipPatterns = ['.derived[', '.secure_url', '.url']; + const isSubField = skipPatterns.some((p) => currentPath.includes(p)) && + (currentPath.includes('coverImage') || currentPath.includes('videoCloudinary') || currentPath.includes('ogImage')); + if (isSubField) { + return results; + } + const urls = extractCloudinaryUrls(obj); const isFullUrl = obj.trim().startsWith('http') && !obj.includes(' ') && urls.length === 1; diff --git a/scripts/migration/package-lock.json b/scripts/migration/package-lock.json new file mode 100644 index 00000000..6a87f292 --- /dev/null +++ b/scripts/migration/package-lock.json @@ -0,0 +1,346 @@ +{ + "name": "cloudinary-to-sanity-migration", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cloudinary-to-sanity-migration", + "version": "1.0.0", + "dependencies": { + "@sanity/client": "^6.24.1", + "dotenv": "^16.4.7", + "node-fetch": "^2.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@sanity/client": { + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/@sanity/client/-/client-6.29.1.tgz", + "integrity": "sha512-BQRCMeDlBxwnMbFtB61HUxFf9aSb4HNVrpfrC7IFVqFf4cwcc3o5H8/nlrL9U3cDFedbe4W0AXt1mQzwbY/ljw==", + "license": "MIT", + "dependencies": { + "@sanity/eventsource": "^5.0.2", + "get-it": "^8.6.7", + "rxjs": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sanity/eventsource": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sanity/eventsource/-/eventsource-5.0.2.tgz", + "integrity": "sha512-/B9PMkUvAlUrpRq0y+NzXgRv5lYCLxZNsBJD2WXVnqZYOfByL9oQBV7KiTaARuObp5hcQYuPfOAVjgXe3hrixA==", + "license": "MIT", + "dependencies": { + "@types/event-source-polyfill": "1.0.5", + "@types/eventsource": "1.1.15", + "event-source-polyfill": "1.0.31", + "eventsource": "2.0.2" + } + }, + "node_modules/@types/event-source-polyfill": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/event-source-polyfill/-/event-source-polyfill-1.0.5.tgz", + "integrity": "sha512-iaiDuDI2aIFft7XkcwMzDWLqo7LVDixd2sR6B4wxJut9xcp/Ev9bO4EFg4rm6S9QxATLBj5OPxdeocgmhjwKaw==", + "license": "MIT" + }, + "node_modules/@types/eventsource": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz", + "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==", + "license": "MIT" + }, + "node_modules/@types/follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@types/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/decompress-response": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-7.0.0.tgz", + "integrity": "sha512-6IvPrADQyyPGLpMnUh6kfKiqy7SrbXbjoUuZ90WMBJKErzv2pCiwlGEXjRX9/54OnTq+XFVnkOnOMzclLI5aEA==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/event-source-polyfill": { + "version": "1.0.31", + "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.31.tgz", + "integrity": "sha512-4IJSItgS/41IxN5UVAVuAyczwZF7ZIEsM1XAoUzIHA6A+xzusEZUutdXz2Nr+MQPLxfTiCvqE79/C8HT8fKFvA==", + "license": "MIT" + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/get-it": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/get-it/-/get-it-8.7.0.tgz", + "integrity": "sha512-uong/+jOz0GiuIWIUJXp2tnQKgQKukC99LEqOxLckPUoHYoerQbV6vC0Tu+/pSgk0tgHh1xX2aJtCk4y35LLLg==", + "license": "MIT", + "dependencies": { + "@types/follow-redirects": "^1.14.4", + "decompress-response": "^7.0.0", + "follow-redirects": "^1.15.9", + "is-retry-allowed": "^2.2.0", + "through2": "^4.0.2", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "license": "MIT", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} From 0dc796af529b08b20ade83c4ac74a719fe113d9c Mon Sep 17 00:00:00 2001 From: Miriad Date: Wed, 4 Mar 2026 18:20:56 +0000 Subject: [PATCH 3/8] feat: update schema and components from Cloudinary to Sanity native images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- components/avatar.tsx | 107 ++++++++++--------------- components/block-image.tsx | 73 +++++++---------- components/cover-image.tsx | 90 ++++++++------------- components/cover-media.tsx | 47 ++++++----- components/cover-video.tsx | 65 ++++++++------- components/portable-text.tsx | 2 +- components/pro-benefits.tsx | 4 +- components/user-related.tsx | 5 +- components/youtube-short.tsx | 6 +- components/youtube.tsx | 6 +- sanity.config.ts | 2 - sanity/lib/image.ts | 9 +++ sanity/schemas/partials/base.ts | 11 +-- sanity/schemas/partials/content.ts | 4 +- sanity/schemas/singletons/settings.tsx | 3 +- 15 files changed, 195 insertions(+), 239 deletions(-) create mode 100644 sanity/lib/image.ts diff --git a/components/avatar.tsx b/components/avatar.tsx index 5e9722eb..efb070f6 100644 --- a/components/avatar.tsx +++ b/components/avatar.tsx @@ -1,74 +1,55 @@ "use client"; -import { CldImage } from "next-cloudinary"; +import Image from "next/image"; import type { Author } from "@/sanity/types"; import Link from "next/link"; -import { stegaClean } from "@sanity/client/stega"; +import { urlForImage } from "@/sanity/lib/image"; interface Props { - name?: string; - href?: string; - coverImage: Exclude | undefined; - imgSize?: string; - width?: number; - height?: number; + name?: string; + href?: string; + coverImage: Exclude | undefined; + imgSize?: string; + width?: number; + height?: number; } export default function Avatar({ - name, - href, - coverImage, - imgSize, - width, - height, + name, + href, + coverImage, + imgSize, + width, + height, }: Props) { - const source = stegaClean(coverImage); - if (!href && source?.public_id) { - return ( -
- -
- ); - } - if (href && source?.public_id) { - return ( - - {source?.public_id && ( -
- -
- )} - {name && ( -
- {name} -
- )} - - ); - } - return <>; + const imageUrl = coverImage?.asset?._ref + ? urlForImage(coverImage)?.width(width || 48).height(height || 48).url() + : null; + + if (!imageUrl) return <>; + + const imageElement = ( +
+ {coverImage?.alt +
+ ); + + if (!href) return imageElement; + + return ( + + {imageElement} + {name && ( +
+ {name} +
+ )} + + ); } diff --git a/components/block-image.tsx b/components/block-image.tsx index ea301ab4..4f29a32d 100644 --- a/components/block-image.tsx +++ b/components/block-image.tsx @@ -1,52 +1,35 @@ -import type { CloudinaryAsset } from "@/sanity/types"; -import CloudinaryImage from "@/components/cloudinary-image"; +import Image from "next/image"; +import { urlForImage } from "@/sanity/lib/image"; -import { getCldImageUrl } from "next-cloudinary"; - -interface CoverImageProps { - image: CloudinaryAsset; +interface BlockImageProps { + image: any; } -export default async function BlockImage(props: CoverImageProps) { - const { image: originalImage } = props; +export default function BlockImage(props: BlockImageProps) { + const { image } = props; - let image; - if (originalImage?.public_id) { - const imageUrl = getCldImageUrl({ - src: originalImage.public_id, - width: 100, - }); - const response = await fetch(imageUrl); - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - const base64 = buffer.toString("base64"); - const dataUrl = `data:${response.type};base64,${base64}`; + const imageUrl = image?.asset?._ref + ? urlForImage(image)?.width(1920).height(1080).url() + : null; - image = ( - - ); - } else { - image =
; - } + if (!imageUrl) { + return ( +
+
+
+ ); + } - return ( -
- {image} -
- ); + return ( +
+ {image?.alt +
+ ); } diff --git a/components/cover-image.tsx b/components/cover-image.tsx index 51b508a5..7f0b506a 100644 --- a/components/cover-image.tsx +++ b/components/cover-image.tsx @@ -1,64 +1,42 @@ -import type { CloudinaryAsset } from "@/sanity/types"; -import CloudinaryImage from "@/components/cloudinary-image"; -import { getCldImageUrl } from "next-cloudinary"; -import { stegaClean } from "@sanity/client/stega"; +import Image from "next/image"; +import { urlForImage } from "@/sanity/lib/image"; interface CoverImageProps { - image: CloudinaryAsset | null | undefined; - priority?: boolean; - className?: string; - width?: number; - height?: number; - quality?: number | `${number}`; + image: any; + priority?: boolean; + className?: string; + width?: number; + height?: number; + quality?: number; } -export default async function CoverImage(props: CoverImageProps) { - const { - image: originalImage, - priority, - className, - width, - height, - quality, - } = props; +export default function CoverImage(props: CoverImageProps) { + const { image, priority, className, width, height, quality } = props; - const source = stegaClean(originalImage); + const imageUrl = image?.asset?._ref + ? urlForImage(image)?.width(width || 1920).height(height || 1080).quality(quality || 80).url() + : null; - const getImageUrl = async (src: string) => { - const imageUrl = getCldImageUrl({ - src, - width: 100, - }); - const response = await fetch(imageUrl); - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - const base64 = buffer.toString("base64"); - return `data:${response.type};base64,${base64}`; - }; + if (!imageUrl) { + return ( +
+
+
+ ); + } - let image: JSX.Element | undefined; - if (source?.public_id) { - image = ( - - ); - } else { - image =
; - } - - return ( -
- {image} -
- ); + return ( +
+ {image?.alt +
+ ); } diff --git a/components/cover-media.tsx b/components/cover-media.tsx index 58ee229e..e9ad9d91 100644 --- a/components/cover-media.tsx +++ b/components/cover-media.tsx @@ -1,37 +1,36 @@ -import type { CloudinaryAsset } from "@/sanity/types"; import dynamic from "next/dynamic"; const YouTube = dynamic(() => - import("@/components/youtube").then((mod) => mod.YouTube), + import("@/components/youtube").then((mod) => mod.YouTube), ); const CoverImage = dynamic(() => import("@/components/cover-image")); const CoverVideo = dynamic(() => import("@/components/cover-video")); export interface CoverMediaProps { - cloudinaryImage: CloudinaryAsset | null | undefined; - cloudinaryVideo: CloudinaryAsset | null | undefined; - youtube: string | null | undefined; - className?: string; + cloudinaryImage: any; + cloudinaryVideo: any; + youtube: string | null | undefined; + className?: string; } export default function CoverMedia(props: CoverMediaProps) { - const { cloudinaryImage, cloudinaryVideo, youtube, className } = props; + const { cloudinaryImage, cloudinaryVideo, youtube, className } = props; - if (cloudinaryVideo?.public_id) { - return ( - - ); - } - if (youtube) { - return ( - - ); - } - return ( - - ); + if (cloudinaryVideo?.asset?._ref) { + return ( + + ); + } + if (youtube) { + return ( + + ); + } + return ( + + ); } diff --git a/components/cover-video.tsx b/components/cover-video.tsx index 5a530278..47699649 100644 --- a/components/cover-video.tsx +++ b/components/cover-video.tsx @@ -1,36 +1,43 @@ -import type { CloudinaryAsset } from "@/sanity/types"; -import CloudinaryVideo from "@/components/cloudinary-video"; - interface CoverVideoProps { - cloudinaryVideo: CloudinaryAsset | null | undefined; - className?: string; + cloudinaryVideo: any; + className?: string; } export default function CoverVideo(props: CoverVideoProps) { - const { cloudinaryVideo, className } = props; + const { cloudinaryVideo, className } = props; + + // After migration, cloudinaryVideo is { _type: "file", asset: { _ref: "file-xxx-ext" } } + // We need to construct the URL from the asset ref + const assetRef = cloudinaryVideo?.asset?._ref; + + if (!assetRef) { + return ( +
+
+
+ ); + } - const video = cloudinaryVideo?.public_id ? ( - - ) : ( -
- ); + // Convert file-{hash}-{ext} to URL + // file-abc123-mp4 -> https://cdn.sanity.io/files/{projectId}/{dataset}/abc123.mp4 + const parts = assetRef.split('-'); + const ext = parts.pop(); + const hash = parts.slice(1).join('-'); + const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID; + const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET; + const videoUrl = `https://cdn.sanity.io/files/${projectId}/${dataset}/${hash}.${ext}`; - return ( -
- {video} -
- ); + return ( +
+ +
+ ); } diff --git a/components/portable-text.tsx b/components/portable-text.tsx index f3d269bc..c6ca858c 100644 --- a/components/portable-text.tsx +++ b/components/portable-text.tsx @@ -39,7 +39,7 @@ export default function CustomPortableText({ const components: PortableTextComponents = { // TODO: make this more dynamic types: { - "cloudinary.asset": ({ value }) => , + image: ({ value }) => , code: ({ value }) => , codepen: ({ value }) => , codesandbox: ({ value }) => , diff --git a/components/pro-benefits.tsx b/components/pro-benefits.tsx index a4053482..a00ee545 100644 --- a/components/pro-benefits.tsx +++ b/components/pro-benefits.tsx @@ -3,14 +3,14 @@ import { useEffect, useState } from "react"; import GoPro from "./user-go-pro"; import Link from "next/link"; import CoverImage from "./cover-image"; -import type { CloudinaryAsset } from "@/sanity/types"; + import { Button } from "./ui/button"; import { useRouter, useSearchParams } from "next/navigation"; export default function ProBenefits({ coverImage, }: { - coverImage: CloudinaryAsset; + coverImage: any; }) { const [showGoPro, setShowGoPro] = useState(false); const router = useRouter(); diff --git a/components/user-related.tsx b/components/user-related.tsx index a77dffff..6d01edcb 100644 --- a/components/user-related.tsx +++ b/components/user-related.tsx @@ -1,6 +1,5 @@ import type { AuthorQueryWithRelatedResult, - CloudinaryAsset, GuestQueryResult, } from "@/sanity/types"; @@ -29,7 +28,7 @@ export default async function UserRelated( title: string; slug: string | null; excerpt: string | null; - coverImage: CloudinaryAsset | null; + coverImage: any; date: string; }>, ) => { @@ -69,7 +68,7 @@ export default async function UserRelated( title: string; slug: string | null; excerpt: string | null; - coverImage: CloudinaryAsset | null; + coverImage: any; date: string; }>; if (!contents?.length) { diff --git a/components/youtube-short.tsx b/components/youtube-short.tsx index bf0f8d47..dcd2d8e9 100644 --- a/components/youtube-short.tsx +++ b/components/youtube-short.tsx @@ -1,12 +1,12 @@ import Image from "next/image"; import { youtubeParser } from "@/lib/utils"; -import type { CloudinaryAsset } from "@/sanity/types"; + import CoverImage from "@/components/cover-image"; import { YouTubeShortEmbed } from "./youtube-short-embed"; export function YouTubeShort(props: { youtube: string; - image?: CloudinaryAsset | null | undefined; + image?: any; className?: string; isActive?: boolean; }) { @@ -15,7 +15,7 @@ export function YouTubeShort(props: { return ( - {image?.public_id ? ( + {image?.asset?._ref ? ( ) : ( diff --git a/components/youtube.tsx b/components/youtube.tsx index b0409abe..c1921ac7 100644 --- a/components/youtube.tsx +++ b/components/youtube.tsx @@ -1,12 +1,12 @@ import Image from "next/image"; import { youtubeParser } from "@/lib/utils"; -import type { CloudinaryAsset } from "@/sanity/types"; + import CoverImage from "@/components/cover-image"; import { YouTubeEmbed } from "./youtube-embed"; export function YouTube(props: { youtube: string; - image?: CloudinaryAsset | null | undefined; + image?: any; className?: string; }) { const { youtube, image, className } = props; @@ -17,7 +17,7 @@ export function YouTube(props: { return ( - {image?.public_id ? ( + {image?.asset?._ref ? ( ) : ( diff --git a/sanity.config.ts b/sanity.config.ts index 0915bb2d..ee5d433f 100644 --- a/sanity.config.ts +++ b/sanity.config.ts @@ -4,7 +4,6 @@ */ import { visionTool } from "@sanity/vision"; import { type PluginOptions, defineConfig } from "sanity"; -import { cloudinarySchemaPlugin } from "sanity-plugin-cloudinary"; // import { tags } from "sanity-plugin-tags"; import { codeInput } from "@sanity/code-input"; // import { table } from "@sanity/table"; // optional: add @sanity/table for studio table UI @@ -213,7 +212,6 @@ export default defineConfig({ // Sets up AI Assist with preset prompts // https://www.sanity.io/docs/ai-assistPcli assistWithPresets(), - cloudinarySchemaPlugin(), media(), // table(), // enable when @sanity/table is installed // tags(), diff --git a/sanity/lib/image.ts b/sanity/lib/image.ts new file mode 100644 index 00000000..148a41a3 --- /dev/null +++ b/sanity/lib/image.ts @@ -0,0 +1,9 @@ +import createImageUrlBuilder from "@sanity/image-url"; +import { dataset, projectId } from "./api"; + +const imageBuilder = createImageUrlBuilder({ projectId, dataset }); + +export function urlForImage(source: any) { + if (!source?.asset?._ref) return undefined; + return imageBuilder.image(source); +} diff --git a/sanity/schemas/partials/base.ts b/sanity/schemas/partials/base.ts index 8ab064ff..c5179406 100644 --- a/sanity/schemas/partials/base.ts +++ b/sanity/schemas/partials/base.ts @@ -1,6 +1,6 @@ import { format, parseISO } from "date-fns"; import { defineArrayMember, defineField, defineType } from "sanity"; -import { SiCloudinary } from "react-icons/si"; +import { ImageIcon } from "@sanity/icons"; //Custom Editor for markdown paste import input from "../../components/BlockEditor"; @@ -25,7 +25,8 @@ const baseType = defineType({ defineField({ name: "coverImage", title: "Cover Image", - type: "cloudinary.asset", + type: "image", + options: { hotspot: true }, validation: (rule) => rule.required(), }), defineField({ @@ -82,9 +83,9 @@ const baseType = defineType({ }), //Plugins defineArrayMember({ - type: "cloudinary.asset", - title: "Cloudinary", - icon: SiCloudinary, + type: "image", + title: "Image", + icon: ImageIcon, }), defineArrayMember({ type: "code", diff --git a/sanity/schemas/partials/content.ts b/sanity/schemas/partials/content.ts index 9f01f8ec..99854ee4 100644 --- a/sanity/schemas/partials/content.ts +++ b/sanity/schemas/partials/content.ts @@ -20,8 +20,8 @@ const content = defineType({ ...baseType.fields, defineField({ name: "videoCloudinary", - title: "Cloudinary Video", - type: "cloudinary.asset", + title: "Video", + type: "file", }), defineField({ name: "youtube", diff --git a/sanity/schemas/singletons/settings.tsx b/sanity/schemas/singletons/settings.tsx index 5fdbf7b8..46620d0b 100644 --- a/sanity/schemas/singletons/settings.tsx +++ b/sanity/schemas/singletons/settings.tsx @@ -84,7 +84,8 @@ export default defineType({ defineField({ name: "ogImage", title: "Open Graph Image", - type: "cloudinary.asset", + type: "image", + options: { hotspot: true }, description: "Displayed on social cards and search engine results.", }), ], From c9b2d563d0535d768a6b2fa3fd9ceacee3b56b34 Mon Sep 17 00:00:00 2001 From: Miriad Date: Wed, 4 Mar 2026 18:24:42 +0000 Subject: [PATCH 4/8] =?UTF-8?q?chore:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20delete=20dead=20Cloudinary=20components,=20add=20LQ?= =?UTF-8?q?IP=20TODO,=20document=20video=20URL=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- components/cloudinary-image.tsx | 25 ------------------------- components/cloudinary-video.tsx | 13 ------------- components/cover-image.tsx | 4 ++++ components/cover-video.tsx | 4 ++-- 4 files changed, 6 insertions(+), 40 deletions(-) delete mode 100644 components/cloudinary-image.tsx delete mode 100644 components/cloudinary-video.tsx diff --git a/components/cloudinary-image.tsx b/components/cloudinary-image.tsx deleted file mode 100644 index 83e7255e..00000000 --- a/components/cloudinary-image.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import { - CldImage as CldImageDefault, - type CldImageProps, -} from "next-cloudinary"; - -const CldImage = (props: CldImageProps) => { - const dev = process.env.NODE_ENV !== "production"; - return ( - - ); -}; - -export default CldImage; diff --git a/components/cloudinary-video.tsx b/components/cloudinary-video.tsx deleted file mode 100644 index 52f309c8..00000000 --- a/components/cloudinary-video.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { - CldVideoPlayer as CldVideoPlayerDefault, - type CldVideoPlayerProps, -} from "next-cloudinary"; -import "next-cloudinary/dist/cld-video-player.css"; - -const CldVideoPlayer = (props: CldVideoPlayerProps) => { - return ; -}; - -export default CldVideoPlayer; diff --git a/components/cover-image.tsx b/components/cover-image.tsx index 7f0b506a..f803a6c1 100644 --- a/components/cover-image.tsx +++ b/components/cover-image.tsx @@ -17,6 +17,10 @@ export default function CoverImage(props: CoverImageProps) { ? urlForImage(image)?.width(width || 1920).height(height || 1080).quality(quality || 80).url() : null; + // TODO: Add LQIP blur placeholder for progressive loading. Options: + // 1. Query Sanity for lqip metadata: image.asset->metadata.lqip + // 2. Use a tiny base64 placeholder generated at build time + if (!imageUrl) { return (
diff --git a/components/cover-video.tsx b/components/cover-video.tsx index 47699649..b294fa50 100644 --- a/components/cover-video.tsx +++ b/components/cover-video.tsx @@ -18,8 +18,8 @@ export default function CoverVideo(props: CoverVideoProps) { ); } - // Convert file-{hash}-{ext} to URL - // file-abc123-mp4 -> https://cdn.sanity.io/files/{projectId}/{dataset}/abc123.mp4 + // Sanity file asset references follow the format: file-{id}-{extension} + // e.g., "file-abc123def456-mp4" → https://cdn.sanity.io/files/{projectId}/{dataset}/abc123def456.mp4 const parts = assetRef.split('-'); const ext = parts.pop(); const hash = parts.slice(1).join('-'); From 7e5c775b41224f243b3ee48c33d80ca5d9e37d9c Mon Sep 17 00:00:00 2001 From: Miriad Date: Wed, 4 Mar 2026 18:31:14 +0000 Subject: [PATCH 5/8] fix: replace all remaining Cloudinary runtime references with Sanity image URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/(main)/layout.tsx | 4 ++-- app/api/devto/route.tsx | 13 ++++++++----- app/api/hashnode/route.tsx | 10 ++++++++-- lib/rss.ts | 3 ++- package.json | 2 -- sanity/lib/queries.ts | 4 +--- sanity/lib/utils.ts | 6 +++--- 7 files changed, 24 insertions(+), 18 deletions(-) diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx index c81d6244..80ebc9e7 100644 --- a/app/(main)/layout.tsx +++ b/app/(main)/layout.tsx @@ -13,6 +13,7 @@ import * as demo from "@/sanity/lib/demo"; import { sanityFetch } from "@/sanity/lib/live"; import { settingsQuery } from "@/sanity/lib/queries"; import { cn } from "@/lib/utils"; +import { resolveOpenGraphImage } from "@/sanity/lib/utils"; import { ThemeProvider } from "@/components/theme-provider"; import Link from "next/link"; import { Button } from "@/components/ui/button"; @@ -56,8 +57,7 @@ export async function generateMetadata(): Promise { const title = settings?.title || demo.title; const description = settings?.description || demo.description; - // const ogImage = resolveOpenGraphImage(settings?.ogImage); - const ogImage = settings?.ogImage?.secure_url; + const ogImage = resolveOpenGraphImage(settings?.ogImage); return { title: { template: `%s | ${title}`, diff --git a/app/api/devto/route.tsx b/app/api/devto/route.tsx index 89e22bcf..a4c1ade9 100644 --- a/app/api/devto/route.tsx +++ b/app/api/devto/route.tsx @@ -3,6 +3,7 @@ import { podcastQuery, postQuery } from "@/sanity/lib/queries"; import { isValidSignature, SIGNATURE_HEADER_NAME } from "@sanity/webhook"; import toMarkdown from "@sanity/block-content-to-markdown"; import { createClient } from "next-sanity"; +import { urlForImage } from "@/sanity/lib/utils"; const secret = process.env.PRIVATE_SYNDICATE_WEBOOK_SECRET; import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api"; @@ -81,10 +82,7 @@ const formatPodcast = async (_type: string, slug: string) => { title: podcast.title, published: true, tags: ["webdev", "javascript", "beginners"], - main_image: podcast?.coverImage?.secure_url?.replace( - "upload/", - "upload/b_rgb:5e1186,c_pad,w_1000,h_420/", - ), + main_image: urlForImage(podcast?.coverImage)?.width(1000).height(420).url() || "", canonical_url: `https://codingcat.dev/${podcast._type}/${podcast.slug}`, description: podcast?.excerpt || "", organization_id: "1009", @@ -239,7 +237,12 @@ const serializers = { types: { code: (props: any) => "```" + props?.node?.language + "\n" + props?.node?.code + "\n```", - "cloudinary.asset": (props: any) => `![](${props?.node?.secure_url})`, + image: (props: any) => { + const url = props?.node?.asset?._ref + ? urlForImage(props.node)?.url() + : ""; + return `![](${url})`; + }, codepen: (props: any) => `{% codepen ${props?.node?.url} %}`, codesandbox: (props: any) => `{% codesandbox ${props?.node?.url?.split("https://codesandbox.io/p/sandbox/")?.at(-1)} %}`, diff --git a/app/api/hashnode/route.tsx b/app/api/hashnode/route.tsx index 0f9ecb51..8795cb8a 100644 --- a/app/api/hashnode/route.tsx +++ b/app/api/hashnode/route.tsx @@ -3,6 +3,7 @@ import { podcastQuery, postQuery } from "@/sanity/lib/queries"; import { isValidSignature, SIGNATURE_HEADER_NAME } from "@sanity/webhook"; import toMarkdown from "@sanity/block-content-to-markdown"; import { createClient } from "next-sanity"; +import { urlForImage } from "@/sanity/lib/utils"; import { apiVersion, dataset, projectId, studioUrl } from "@/sanity/lib/api"; const secret = process.env.PRIVATE_SYNDICATE_WEBOOK_SECRET; @@ -110,7 +111,7 @@ const formatPodcast = async (_type: string, slug: string) => { }, ], coverImageOptions: { - coverImageURL: podcast?.coverImage?.secure_url, + coverImageURL: urlForImage(podcast?.coverImage)?.width(1600).height(840).url() || "", }, originalArticleURL: `https://codingcat.dev/${podcast._type}/${podcast.slug}`, contentMarkdown: ` @@ -344,7 +345,12 @@ const serializers = { types: { code: (props: any) => "```" + props?.node?.language + "\n" + props?.node?.code + "\n```", - "cloudinary.asset": (props: any) => `![](${props?.node?.secure_url})`, + image: (props: any) => { + const url = props?.node?.asset?._ref + ? urlForImage(props.node)?.url() + : ""; + return `![](${url})`; + }, codepen: (props: any) => `{% codepen ${props?.node?.url} %}`, codesandbox: (props: any) => `{% codesandbox ${props?.node?.url?.split("https://codesandbox.io/p/sandbox/")?.at(-1)} %}`, diff --git a/lib/rss.ts b/lib/rss.ts index 37a83472..fa85a29d 100644 --- a/lib/rss.ts +++ b/lib/rss.ts @@ -3,6 +3,7 @@ import { sanityFetch } from "@/sanity/lib/live"; import type { RssQueryResult } from "@/sanity/types"; import { rssQuery } from "@/sanity/lib/queries"; import { toHTML } from "@portabletext/to-html"; +import { urlForImage } from "@/sanity/lib/utils"; const productionDomain = process.env.VERCEL_PROJECT_PRODUCTION_URL; const site = productionDomain @@ -55,7 +56,7 @@ export async function buildFeed(params: { item.content && Array.isArray(item.content) ? toHTML(item.content) : "", link: `${site}/${item._type}/${item.slug}`, description: `${item.excerpt}`, - image: item.coverImage?.secure_url || feed.items.at(0)?.image, + image: urlForImage(item.coverImage)?.width(1200).height(630).url() || feed.items.at(0)?.image, date: item.date ? new Date(item.date) : new Date(), id: item._id, author: item.author diff --git a/package.json b/package.json index e0d35676..78fd915a 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,6 @@ "micromark": "^4.0.2", "micromark-extension-gfm-table": "^2.0.0", "next": "^16.1.6", - "next-cloudinary": "^6.17.5", "next-sanity": "^12.1.0", "next-themes": "^0.4.6", "nextjs-toploader": "^3.9.17", @@ -104,7 +103,6 @@ "remotion": "^4.0.431", "resend": "^6.9.3", "sanity": "^5.12.0", - "sanity-plugin-cloudinary": "^1.4.1", "sanity-plugin-media": "^4.1.1", "server-only": "^0.0.1", "sonner": "^2.0.7", diff --git a/sanity/lib/queries.ts b/sanity/lib/queries.ts index c7b7663b..5ab48f1d 100644 --- a/sanity/lib/queries.ts +++ b/sanity/lib/queries.ts @@ -4,9 +4,7 @@ export const docCount = groq`count(*[_type == $type])`; export const settingsQuery = groq`*[_type == "settings"][0]{ ..., - ogImage{ - secure_url - } + ogImage }`; // Partials diff --git a/sanity/lib/utils.ts b/sanity/lib/utils.ts index 5e5f5eab..5dbe1f48 100644 --- a/sanity/lib/utils.ts +++ b/sanity/lib/utils.ts @@ -17,12 +17,12 @@ export const urlForImage = (source: any) => { }; export function resolveOpenGraphImage(image: any, width = 1920, height = 1080) { - if (!image || !image?.secure_url) return; - const url = image?.secure_url; + if (!image?.asset?._ref) return; + const url = urlForImage(image)?.width(width).height(height).url(); if (!url) return; return { url, - alt: image?.context?.custom?.alt || "CodingCat.dev Image", + alt: image?.alt || "CodingCat.dev Image", width, height, }; From 9c18b5bc1513b576c1129af8d4c77b3a5991aefa Mon Sep 17 00:00:00 2001 From: Miriad Date: Wed, 4 Mar 2026 19:14:08 +0000 Subject: [PATCH 6/8] fix: migration script downloads only originals, strips transformation 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 --- scripts/migration/migrate.mjs | 87 ++- scripts/migration/migration-output.log | 53 ++ scripts/migration/phase4-output.log | 941 +++++++++++++++++++++++++ 3 files changed, 1068 insertions(+), 13 deletions(-) create mode 100644 scripts/migration/migration-output.log create mode 100644 scripts/migration/phase4-output.log diff --git a/scripts/migration/migrate.mjs b/scripts/migration/migrate.mjs index 3bbe66cf..28d0cbf9 100644 --- a/scripts/migration/migrate.mjs +++ b/scripts/migration/migrate.mjs @@ -309,11 +309,29 @@ function findCloudinaryRefs(obj, currentPath = '') { url: resolvedUrl, publicId: publicId, resourceType: obj.resource_type || 'image', + format: obj.format || null, }); } return results; // Don't recurse into cloudinary.asset children } + // Check for raw Cloudinary objects (old format without _type) + if (typeof obj === 'object' && !Array.isArray(obj) && obj.public_id && (obj.secure_url || obj.url) && !obj._type) { + const url = obj.secure_url || obj.url || null; + const publicId = obj.public_id; + if (url) { + results.push({ + path: currentPath, + type: 'raw-cloudinary-object', + url: url, + publicId: publicId, + resourceType: obj.resource_type || 'image', + format: obj.format || null, + }); + } + return results; // Don't recurse into raw Cloudinary object children (derived[], etc.) + } + if (typeof obj === 'string') { if (containsCloudinaryRef(obj)) { // Skip URLs that are inside cloudinary.asset sub-fields (derived, url, secure_url) @@ -426,6 +444,7 @@ async function phase1_discoverReferences(sanityClient) { log(1, `Found ${docsWithRefs.length} documents with Cloudinary references`); let cloudinaryAssetCount = 0; + let rawCloudinaryCount = 0; let urlCount = 0; let embeddedCount = 0; @@ -434,12 +453,13 @@ async function phase1_discoverReferences(sanityClient) { for (const r of d.refs) { log(1, ` ${r.path} [${r.type}] → ${r.url || r.publicId || '(no url)'}`); if (r.type === 'cloudinary.asset') cloudinaryAssetCount++; + else if (r.type === 'raw-cloudinary-object') rawCloudinaryCount++; else if (r.type === 'url') urlCount++; else if (r.type === 'embedded') embeddedCount++; } } - log(1, `\n Breakdown: ${cloudinaryAssetCount} cloudinary.asset objects, ${urlCount} URL fields, ${embeddedCount} embedded URLs`); + log(1, `\n Breakdown: ${cloudinaryAssetCount} cloudinary.asset objects, ${rawCloudinaryCount} raw Cloudinary objects, ${urlCount} URL fields, ${embeddedCount} embedded URLs`); // Save to disk for resume if (!DRY_RUN) { @@ -450,6 +470,26 @@ async function phase1_discoverReferences(sanityClient) { return docsWithRefs; } +// ─── Utility: strip Cloudinary transformations from URL ────────────────────── +function stripTransformations(url) { + // Cloudinary URL format: .../upload/[transformations/]v{version}/{public_id}.{ext} + // Strip everything between /upload/ and /v{version}/ + return url.replace( + /(\/upload\/)((?:[a-z_][a-z0-9_,:]+(?:\/|$))*)(v\d+\/)/i, + '$1$3' + ); +} + +// ─── Utility: get canonical original URL for a Cloudinary reference ────────── +function getOriginalUrl(ref) { + if (ref.publicId && ref.resourceType) { + const ext = ref.format || (ref.resourceType === 'video' ? 'mp4' : 'png'); + return `https://media.codingcat.dev/${ref.resourceType}/upload/${ref.publicId}.${ext}`; + } + // Fallback: strip transformations from the URL + return stripTransformations(ref.url); +} + // ═══════════════════════════════════════════════════════════════════════════════ // PHASE 2: Extract Unique Cloudinary URLs // ═══════════════════════════════════════════════════════════════════════════════ @@ -469,20 +509,22 @@ async function phase2_extractUniqueUrls(docsWithRefs) { for (const doc of docsWithRefs) { for (const ref of doc.refs) { - const url = ref.url; - if (!url) continue; + if (!ref.url) continue; - if (urlMap.has(url)) { + // Get the canonical original URL (strips transformations, uses CNAME) + const originalUrl = getOriginalUrl(ref); + + if (urlMap.has(originalUrl)) { // Add this doc as another source - const entry = urlMap.get(url); + const entry = urlMap.get(originalUrl); if (!entry.sourceDocIds.includes(doc._id)) { entry.sourceDocIds.push(doc._id); } } else { - urlMap.set(url, { - cloudinaryUrl: url, - cloudinaryPublicId: ref.publicId || extractPublicIdFromUrl(url), - resourceType: ref.resourceType || guessResourceType(url), + urlMap.set(originalUrl, { + cloudinaryUrl: originalUrl, + cloudinaryPublicId: ref.publicId || extractPublicIdFromUrl(originalUrl), + resourceType: ref.resourceType || guessResourceType(originalUrl), sourceDocIds: [doc._id], }); } @@ -634,11 +676,24 @@ async function phase3_downloadAndUpload(uniqueUrls) { /** * Given a Cloudinary URL, find the matching Sanity asset in the mapping. */ -function findMappingForUrl(url, mapping) { +function findMappingForUrl(url, mapping, refPublicId) { // Try exact URL match first let entry = mapping.find((m) => m.cloudinaryUrl === url); if (entry) return entry; + // Try matching by the ref's own publicId (from the Cloudinary object) + if (refPublicId) { + entry = mapping.find((m) => m.cloudinaryPublicId === refPublicId); + if (entry) return entry; + } + + // Try matching by stripped/canonical URL + const strippedUrl = stripTransformations(url); + if (strippedUrl !== url) { + entry = mapping.find((m) => m.cloudinaryUrl === strippedUrl); + if (entry) return entry; + } + // Try matching by public_id extracted from the URL const publicId = extractPublicIdFromUrl(url); if (publicId) { @@ -694,7 +749,7 @@ async function phase4_updateReferences(sanityClient, docsWithRefs, mapping) { continue; } - const mappingEntry = findMappingForUrl(refUrl, mapping); + const mappingEntry = findMappingForUrl(refUrl, mapping, ref.publicId); if (!mappingEntry) { log(4, ` ⚠ No mapping found for URL: ${refUrl} (in ${docId} at ${fieldPath})`); @@ -705,8 +760,8 @@ async function phase4_updateReferences(sanityClient, docsWithRefs, mapping) { const sanityId = mappingEntry.sanityAssetId; const cdnUrl = mappingEntry.sanityUrl || sanityAssetUrl(sanityId); - if (refType === 'cloudinary.asset') { - // ── Replace entire cloudinary.asset object with Sanity image/file reference ── + if (refType === 'cloudinary.asset' || refType === 'raw-cloudinary-object') { + // ── Replace entire cloudinary.asset or raw Cloudinary object with Sanity image/file reference ── const isImage = (ref.resourceType || 'image') === 'image'; const refObj = isImage ? { @@ -830,6 +885,10 @@ async function phase5_report(docsWithRefs, uniqueUrls, mapping, changes) { (sum, d) => sum + d.refs.filter((r) => r.type === 'cloudinary.asset').length, 0 ); + const rawCloudinaryRefs = docsWithRefs.reduce( + (sum, d) => sum + d.refs.filter((r) => r.type === 'raw-cloudinary-object').length, + 0 + ); const urlRefs = docsWithRefs.reduce( (sum, d) => sum + d.refs.filter((r) => r.type === 'url').length, 0 @@ -846,6 +905,7 @@ async function phase5_report(docsWithRefs, uniqueUrls, mapping, changes) { totalDocumentsWithRefs: docsWithRefs.length, totalReferencesFound: totalRefs, cloudinaryAssetObjects: cloudinaryAssetRefs, + rawCloudinaryObjects: rawCloudinaryRefs, urlStringRefs: urlRefs, embeddedUrlRefs: embeddedRefs, uniqueCloudinaryUrls: uniqueUrls.length, @@ -864,6 +924,7 @@ async function phase5_report(docsWithRefs, uniqueUrls, mapping, changes) { console.log(` Documents with refs: ${report.summary.totalDocumentsWithRefs}`); console.log(` Total references found: ${report.summary.totalReferencesFound}`); console.log(` cloudinary.asset objects: ${report.summary.cloudinaryAssetObjects}`); + console.log(` raw Cloudinary objects: ${report.summary.rawCloudinaryObjects}`); console.log(` URL string fields: ${report.summary.urlStringRefs}`); console.log(` Embedded URLs in text: ${report.summary.embeddedUrlRefs}`); console.log(` Unique Cloudinary URLs: ${report.summary.uniqueCloudinaryUrls}`); diff --git a/scripts/migration/migration-output.log b/scripts/migration/migration-output.log new file mode 100644 index 00000000..be39c9c1 --- /dev/null +++ b/scripts/migration/migration-output.log @@ -0,0 +1,53 @@ +╔══════════════════════════════════════════════════════════╗ +║ Cloudinary → Sanity Asset Migration (Sanity-First) ║ +╚══════════════════════════════════════════════════════════╝ + +[Phase 1] ── Discovering Cloudinary references in Sanity documents ── +(node:15211) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. +(Use `node --trace-deprecation ...` to show where the warning was created) +[Phase 1] Found cached discovery with 451 documents. Delete /home/daytona/codingcat.dev/scripts/migration/discovered-references.json to re-scan. +[Phase 2] ── Extracting unique Cloudinary URLs ── +[Phase 2] Found cached URL list with 436 unique URLs. Delete /home/daytona/codingcat.dev/scripts/migration/unique-cloudinary-urls.json to re-extract. +[Phase 3] ── Downloading & uploading assets to Sanity ── +[Phase 3] Concurrency: 3 | Total unique URLs: 436 +[Phase 3] Skipping 388 already-migrated assets +[Phase 3] Assets to migrate: 48 + ✗ [1/48] Failed: b_rgb:5e1186,c_pad,w_1000,h_420/$%7Bpage — Download failed: HTTP 404 for https://media.codingcat.dev/image/upload/b_rgb:5e1186,c_pad,w_1000,h_420/${page?.coverPhoto?.public_id}`, + ✗ [2/48] Failed: w/_500/v1556553295/ajonp-ajonp-com/18-rxfire-svelte-cats/RxFire/_Svelt — Download failed: HTTP 400 for https://res.cloudinary.com/ajonp/image/upload/w\_500/v1556553295/ajonp-ajonp-com/18-rxfire-svelte-cats/RxFire\_Svelt.webp + ✗ [3/48] Failed: q/_auto/ajonp-ajonp-com/17-rxfire-react-cats/RxFire/_3 — Download failed: HTTP 404 for https://res.cloudinary.com/ajonp/image/upload/q\_auto/ajonp-ajonp-com/17-rxfire-react-cats/RxFire\_3.webp +[Phase 3] [1/48] ✓ main-codingcatdev-photo/fosawiikzx30ajcilo2a → image-5febb50ae39284c22bdb43dc525ae5fdaedbaa9a-1920x1080-png +[Phase 3] [2/48] ✓ main-codingcatdev-photo/q2eng4nciqybq8clwg6k → image-2d68f449c62af8ea1ac6f369c8e9c5f99777f574-1920x1080-png +[Phase 3] [3/48] ✓ main-codingcatdev-photo/dz2owgunrjb8wa7vzzbu → image-7d3977f54bf16c87c46a1beef484cada62c16923-1920x1080-png +[Phase 3] [4/48] ✓ main-codingcatdev-photo/ome6ihlaksocf2rtzfhe → image-c010f3b5b43aaa88556f5fb6123d61f2ffa01b01-1920x1080-png +[Phase 3] [5/48] ✓ main-codingcatdev-photo/x8ncnxweooiat7vzpwke → image-2e3a02b625146e6099fde4c9dff438a1933e1b4d-1920x1080-png +[Phase 3] [6/48] ✓ main-codingcatdev-photo/veso6actvfkxmpkcid8f → image-9e5538fac8ed46b66e3e3e50d1347669c826a7d9-1920x1080-png +[Phase 3] [7/48] ✓ main-codingcatdev-photo/ni4t43qfcdab90gojuve → image-2fea4cd44d8d8fd09f3f3ef0af1a01072103f69d-1920x1080-png +[Phase 3] [8/48] ✓ main-codingcatdev-photo/zbahldu0x4ihuimeczfq → image-212028e3fef8f64e9471806d7d26d0999a93063e-1920x1080-png +[Phase 3] [9/48] ✓ main-codingcatdev-photo/zrgetssjgnguqj4yclfp → image-da2928acb25d72f7023b4d120c5c1daf6aca3e34-1920x1080-png +[Phase 3] [10/48] ✓ main-codingcatdev-photo/u8at848k5o9mdgnpxv5k → image-3afec45da64654865d96bd2673880ca6a2e16a74-1920x1080-png +[Phase 3] [11/48] ✓ main-codingcatdev-photo/vbex1zxomeoo0wjzyhhr → image-97e9213b6bff5259b7f12fc69908f300fcee99c5-1920x1080-png +[Phase 3] [12/48] ✓ main-codingcatdev-photo/ogluu84watt3zu63gbf8 → image-f3e1a12bddee913446b424e66fbbd75220fdb5a6-1920x1080-png +[Phase 3] [13/48] ✓ main-codingcatdev-photo/vz7ramuqpbyhcu3azajy → image-9be71c92a88bc5fb120fa8dcc3ef8f1b6581ecb7-1920x1080-png +[Phase 3] [14/48] ✓ main-codingcatdev-photo/tvksoc43u6exibz6fmzv → image-eb3c49cfe38d7a95e1d8a47289387b571f697e02-1920x1080-png +[Phase 3] [15/48] ✓ main-codingcatdev-photo/csxroq0lxevn4zbqdqks → image-ecc37088c7cd9ce37c1790a8fc6393c956271cf6-1920x1080-png +[Phase 3] [16/48] ✓ main-codingcatdev-photo/pvjydzcbs39pwocebmsd → image-ee6b49a7ff8b7966607a7e397a1b0bb91a3b742c-1920x1080-png +[Phase 3] [17/48] ✓ main-codingcatdev-photo/eeyfwyuldrn87o1i59ji → image-ba7df7dd2d5e5ad0a058f96583bcfa2664b21d44-1920x1080-png +[Phase 3] [18/48] ✓ main-codingcatdev-photo/stdloblfbnlgf4pm3ze5 → image-0f0c46e79995ee69acf9a45fde9165e8319642d3-1920x1080-png +[Phase 3] [19/48] ✓ main-codingcatdev-photo/hyopbplzvjnobn4nxe3v → image-135fd1bd7295a7173ec25208317befeb9fda3ab6-1920x1080-png +[Phase 3] [20/48] ✓ main-codingcatdev-photo/inhxfdhdrfjmnvcfzwyi → image-3933e331d455eb073f0105de1faf0588ff478a3f-1920x1080-png +[Phase 3] [21/48] ✓ main-codingcatdev-photo/vwb6zojoyln2d6oxudz8 → image-368eb23766409dbbf6025172f5e7178875833875-1920x1080-png +[Phase 3] [22/48] ✓ main-codingcatdev-photo/skkbgf5bix76a04zhzqp → image-c0ceae6b08b279593092c23b3de9ca33c50cbcfc-1920x1080-png +[Phase 3] [23/48] ✓ main-codingcatdev-photo/ecqrelydm7ykl8xup5xg → image-628c0895869e7f3e49a9253a5c5f72f2b8ffecb9-1920x1080-png +[Phase 3] [24/48] ✓ main-codingcatdev-photo/wveq4jasspmsywiqnglu → image-ca193facbbc516ab21b53ab5bf94d8a48c2d007d-1920x1080-png +[Phase 3] [25/48] ✓ main-codingcatdev-photo/pjkucvrzkdfkjyqa1nwu → image-53e9ff3d86e5cefe91650bcaaf1bdb04232aa619-1920x1080-png +[Phase 3] [26/48] ✓ main-codingcatdev-photo/hn8dumtsubllbz9xyqh6 → image-a153842b3a5b54b7f72326ea0027885ec4a1493e-1920x1080-png +[Phase 3] [27/48] ✓ main-codingcatdev-photo/cpg2clvczhzzvetwruul → image-9723fa4063e2cdf122862c1bdbd95225bc7ce5cd-1920x1080-png +[Phase 3] [28/48] ✓ main-codingcatdev-photo/omzc5ridsuuxgxgtvt7o → image-995ecf3d8e7e286b1d3650c4ef34c8f1a87df7f7-1920x1080-png +[Phase 3] [29/48] ✓ main-codingcatdev-photo/i6qzbmbxegit9nebc44s → image-933224e9d3b7fd471896c99dcdf340de2d6a42fc-1920x1080-png +[Phase 3] [30/48] ✓ main-codingcatdev-photo/b2ryikx5b9x5dq27anok → image-2691a24b52e6a655f49338a9602998b9d2e2d9c1-1920x1080-png +[Phase 3] [31/48] ✓ main-codingcatdev-photo/oogq3stsiqvbzsswaatr → image-149aa0d3ba55935a4bfb8c84913643403d424458-1920x1080-png +[Phase 3] [32/48] ✓ main-codingcatdev-photo/ilnwzjko76hr0lddwxk5 → image-a2e8a72e363dcb5866adebe2b62d47cb09425321-1920x1080-png +[Phase 3] [33/48] ✓ main-codingcatdev-photo/scjp26pt4hdxicpvsebs → image-f74d827f531e688c8ac022cdf695bdebb067ab34-1920x1080-png +[Phase 3] [34/48] ✓ main-codingcatdev-photo/ygqhzxyhtfpfilzskglf → image-05af6c518affbc8643ef1db5e67afaa65a4a0d61-1920x1080-png +[Phase 3] [35/48] ✓ main-codingcatdev-photo/ilpfshoaxdhnwemlsfld → image-0753d61f0436a4d52a78c893aaabdee468aac58c-1920x1080-png +[Phase 3] [36/48] ✓ main-codingcatdev-photo/mfzmkc6ohyyuhmgfdmru → image-e43fbdceae2ad533c194fae83cbf21f99115cf2e-1920x1080-png diff --git a/scripts/migration/phase4-output.log b/scripts/migration/phase4-output.log new file mode 100644 index 00000000..ef730098 --- /dev/null +++ b/scripts/migration/phase4-output.log @@ -0,0 +1,941 @@ +╔══════════════════════════════════════════════════════════╗ +║ Cloudinary → Sanity Asset Migration (Sanity-First) ║ +╚══════════════════════════════════════════════════════════╝ + +(node:17128) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. +(Use `node --trace-deprecation ...` to show where the warning was created) +Loaded 451 discovered docs from cache (Phase 1 skipped) +Loaded 436 unique URLs from cache (Phase 2 skipped) +Loaded 433 mappings from cache (Phase 3 skipped) +[Phase 4] ── Updating Cloudinary references in Sanity documents ── +[Phase 4] LzgcvqoNSwz5R8loykZxhb: coverImage [cloudinary.asset] → image ref image-7965dae0f320d14f3fd8b52994f65c86f455a615-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykZxhb +[Phase 4] LzgcvqoNSwz5R8loykZxzt: coverImage [cloudinary.asset] → image ref image-0d3b4005c9cd6dc3b7b51aae56943182563d1e56-1280x720-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykZxzt +[Phase 4] LzgcvqoNSwz5R8loykZyIB: coverImage [cloudinary.asset] → image ref image-1f6038abccbaede1ec4c60c240733609ee211af7-1920x1081-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykZyIB +[Phase 4] LzgcvqoNSwz5R8loykZyQ1: coverImage [cloudinary.asset] → image ref image-f0e7e772bc610f27a7269613b9bb01bb36f6b37f-1280x720-jpg +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykZyQ1 +[Phase 4] LzgcvqoNSwz5R8loykZyvN: coverImage [cloudinary.asset] → image ref image-2bec4e832ae6677d0518302aa83a395fe2a756d4-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykZyvN +[Phase 4] LzgcvqoNSwz5R8loykZzVx: content[17].code [embedded] → replaced URL +[Phase 4] LzgcvqoNSwz5R8loykZzVx: content[20].code [embedded] → replaced URL +[Phase 4] LzgcvqoNSwz5R8loykZzVx: coverImage [cloudinary.asset] → image ref image-2e9e6b14f0cdfd1b769040d6f4091a067f6b764d-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykZzVx +[Phase 4] LzgcvqoNSwz5R8loykZzj1: coverImage [cloudinary.asset] → image ref image-34b3725b9674233c321f2a0faaaf84e330dacfa1-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykZzj1 +[Phase 4] LzgcvqoNSwz5R8loykZzyh: coverImage [cloudinary.asset] → image ref image-df6dd08d0bb9e547c94b914020c41dcb4bdef7bf-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykZzyh +[Phase 4] LzgcvqoNSwz5R8loyka06X: coverImage [cloudinary.asset] → image ref image-63a8aa2e0a2e32226487e6e4c30034b7d017fd3c-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loyka06X +[Phase 4] LzgcvqoNSwz5R8loyka0MD: coverImage [cloudinary.asset] → image ref image-fef737e0e6b97819ae74804020afe80d5a85c3ed-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loyka0MD +[Phase 4] LzgcvqoNSwz5R8loyka0bt: coverImage [cloudinary.asset] → image ref image-bd7313f585f351350e268ef2837404638bbb70ba-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loyka0bt +[Phase 4] LzgcvqoNSwz5R8loyka1S9: coverImage [cloudinary.asset] → image ref image-92adbc842c19879beb03a6291c6437fe7ea9dac6-1200x631-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loyka1S9 +[Phase 4] LzgcvqoNSwz5R8loyka1hp: coverImage [cloudinary.asset] → image ref image-61d3051adb65658075dd48c570494e75a6e1acc3-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loyka1hp +[Phase 4] LzgcvqoNSwz5R8loykbNpB: coverImage [cloudinary.asset] → image ref image-b7bf10d252cce23951926b2d4e8d505a83d6431b-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykbNpB +[Phase 4] LzgcvqoNSwz5R8loykbNx1: coverImage [cloudinary.asset] → image ref image-9dfe1e4e41c08378e929d24032cd4920ae9be1f1-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykbNx1 +[Phase 4] LzgcvqoNSwz5R8loykbO4r: coverImage [cloudinary.asset] → image ref image-505d16c179debdb3d6532a27d5445fca63b69ec1-1920x1080-jpg +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykbO4r +[Phase 4] LzgcvqoNSwz5R8loykbOKX: coverImage [cloudinary.asset] → image ref image-f6001fcb1c89412f9f07461e85e3c96746407654-1920x1080-jpg +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykbOKX +[Phase 4] LzgcvqoNSwz5R8loykbOSN: coverImage [cloudinary.asset] → image ref image-87be5d9397ed05133670c8a545b977fe89b8aa3f-1920x1080-jpg +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykbOSN +[Phase 4] LzgcvqoNSwz5R8loykbRUV: coverImage [cloudinary.asset] → image ref image-067dff4dbf8adb9e92c7181e0c90ffb1bc74c737-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykbRUV +[Phase 4] LzgcvqoNSwz5R8loykbRex: coverImage [cloudinary.asset] → image ref image-bd85deaddc4e91de3e9dbea3ea15856b7d07e41f-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykbRex +[Phase 4] LzgcvqoNSwz5R8loykbTQh: coverImage [cloudinary.asset] → image ref image-1e92174af717e4ec9db01ea726d96b2a2ce894e1-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykbTQh +[Phase 4] LzgcvqoNSwz5R8loykbUMB: coverImage [cloudinary.asset] → image ref image-d86e2c48006bfa59f5c8b1fc3223ad8cefb9b67b-1920x1080-png +[Phase 4] ✓ Committed changes for LzgcvqoNSwz5R8loykbUMB +[Phase 4] RIgC5r4TJ0hCGOzxHszCcX: coverImage [cloudinary.asset] → image ref image-f25e504779cb6ebb7c0dd7df367f2c7490e17784-735x735-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszCcX +[Phase 4] RIgC5r4TJ0hCGOzxHszD1w: coverImage [cloudinary.asset] → image ref image-a606dfaa381aab08ddbe16a33fe1d3e1fb37c927-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszD1w +[Phase 4] RIgC5r4TJ0hCGOzxHszL3q: coverImage [cloudinary.asset] → image ref image-35a3ce2a02dfebf2e6812f35c32698496f4ce242-573x573-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszL3q +[Phase 4] RIgC5r4TJ0hCGOzxHszLOA: coverImage [cloudinary.asset] → image ref image-59db6c0fa3f364d548b1f8962d27cd7ef1896c15-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszLOA +[Phase 4] RIgC5r4TJ0hCGOzxHszLiU: coverImage [cloudinary.asset] → image ref image-05cd4349da2a9ed49d28b486fa8af6c6922359a3-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszLiU +[Phase 4] RIgC5r4TJ0hCGOzxHszM2o: coverImage [cloudinary.asset] → image ref image-5343deda15f92279be09bcc59fad799ffdd1885b-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszM2o +[Phase 4] RIgC5r4TJ0hCGOzxHszMN8: coverImage [cloudinary.asset] → image ref image-9755c8584666030505dd517a2e12b07db700e40a-1000x1000-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszMN8 +[Phase 4] RIgC5r4TJ0hCGOzxHszMhS: coverImage [cloudinary.asset] → image ref image-b9761a49dbb70a3d9c35cc65c070fa55ceca434a-450x450-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszMhS +[Phase 4] RIgC5r4TJ0hCGOzxHszN1m: coverImage [cloudinary.asset] → image ref image-0d8a0de5d9948c76157cf6d38ee1cf4d1d2b4fc0-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszN1m +[Phase 4] RIgC5r4TJ0hCGOzxHszOfO: coverImage [cloudinary.asset] → image ref image-b250e4e6e60441f58a4950056249095033540e2f-512x512-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszOfO +[Phase 4] RIgC5r4TJ0hCGOzxHszP4n: coverImage [cloudinary.asset] → image ref image-b53571cba6aace5abe132f142a9e83d0585e8674-320x320-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszP4n +[Phase 4] RIgC5r4TJ0hCGOzxHszPUC: coverImage [cloudinary.asset] → image ref image-ba88aa5933f722a8be8d14730046d569ee532479-371x371-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszPUC +[Phase 4] RIgC5r4TJ0hCGOzxHszRCt: coverImage [cloudinary.asset] → image ref image-a88b6523217fe32a7112ec9fbc2a9cb849e424d3-450x450-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszRCt +[Phase 4] RIgC5r4TJ0hCGOzxHszRcI: coverImage [cloudinary.asset] → image ref image-7a42edb655e17a08e5b939c0e31ef34444ffcad1-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszRcI +[Phase 4] RIgC5r4TJ0hCGOzxHszRrX: coverImage [cloudinary.asset] → image ref image-36508fa891c0da9ff9eb6d1e159a7bcdbd4be4dd-5120x2880-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszRrX +[Phase 4] RIgC5r4TJ0hCGOzxHszSBr: coverImage [cloudinary.asset] → image ref image-e83e030700fdbd4a42e6d2988e8679987c5fdae7-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszSBr +[Phase 4] RIgC5r4TJ0hCGOzxHszSR6: coverImage [cloudinary.asset] → image ref image-fb11557bcedccd470325eac095e32aebf77b5d32-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszSR6 +[Phase 4] RIgC5r4TJ0hCGOzxHszSgL: coverImage [cloudinary.asset] → image ref image-d0a1c3a612fbdc0e16a6abe7546b73b1b23a06f1-800x800-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszSgL +[Phase 4] RIgC5r4TJ0hCGOzxHszSva: coverImage [cloudinary.asset] → image ref image-4b02409981ed4c7dec4617ee7b3e6d7cd361feb7-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszSva +[Phase 4] RIgC5r4TJ0hCGOzxHszTAp: coverImage [cloudinary.asset] → image ref image-061af39d6258cb0e022235141ed45488fd95bdb4-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszTAp +[Phase 4] RIgC5r4TJ0hCGOzxHszTV9: coverImage [cloudinary.asset] → image ref image-abcbf9fffd0d3645b6c87ab31e7c30837a4cec2c-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszTV9 +[Phase 4] RIgC5r4TJ0hCGOzxHszTkO: coverImage [cloudinary.asset] → image ref image-4317219ac0c1f192d953db4954ff03c3cdb07aae-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszTkO +[Phase 4] RIgC5r4TJ0hCGOzxHszUEs: coverImage [cloudinary.asset] → image ref image-230d235863fd6fd647c62b49c33704aa8c6f795d-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszUEs +[Phase 4] RIgC5r4TJ0hCGOzxHszUU7: coverImage [cloudinary.asset] → image ref image-c99f5407f0047ad1b1d76fd5c7a9d3455e276701-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszUU7 +[Phase 4] RIgC5r4TJ0hCGOzxHszUjM: coverImage [cloudinary.asset] → image ref image-55ad343d455a4be319272467ec57f321494326e8-1000x1000-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszUjM +[Phase 4] RIgC5r4TJ0hCGOzxHszUyb: coverImage [cloudinary.asset] → image ref image-1c0a57e774043b3c56527b2501005e73b8ff0d05-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszUyb +[Phase 4] RIgC5r4TJ0hCGOzxHszVDq: coverImage [cloudinary.asset] → image ref image-8dd704389e9e72ca5b2b99dd1bb3b507ebfd133a-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszVDq +[Phase 4] RIgC5r4TJ0hCGOzxHszVT5: coverImage [cloudinary.asset] → image ref image-a0f7d9014e7d7e1e2dde49b16f1657e513a81687-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszVT5 +[Phase 4] RIgC5r4TJ0hCGOzxHszVnP: coverImage [cloudinary.asset] → image ref image-085399fc2fc397d5c93fe79bba9018df0b1f1769-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszVnP +[Phase 4] RIgC5r4TJ0hCGOzxHszYkJ: coverImage [cloudinary.asset] → image ref image-d8c6e9eaa7839d09d0983b1a8811de364227a0dd-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszYkJ +[Phase 4] RIgC5r4TJ0hCGOzxHszaNv: coverImage [cloudinary.asset] → image ref image-244b7773994cd05a53e1f50d146ce2315e947019-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszaNv +[Phase 4] RIgC5r4TJ0hCGOzxHszaiF: coverImage [cloudinary.asset] → image ref image-9d65222646c1e0ead7ff45a6e3fb1d5d6e897f4b-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszaiF +[Phase 4] RIgC5r4TJ0hCGOzxHszbCj: coverImage [cloudinary.asset] → image ref image-1cb5b0bac76a6c2fa4495ca526a90e9d83aa3b9c-200x200-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszbCj +[Phase 4] RIgC5r4TJ0hCGOzxHszbRy: coverImage [cloudinary.asset] → image ref image-faca4dce9e59b23992e47fd850fca9048458aa0d-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszbRy +[Phase 4] RIgC5r4TJ0hCGOzxHszbhD: coverImage [cloudinary.asset] → image ref image-f32b792357c379cca8cfe202082e9e06c4508931-1778x1778-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszbhD +[Phase 4] RIgC5r4TJ0hCGOzxHszbwS: coverImage [cloudinary.asset] → image ref image-4d7ca1d2cd2dc400432eb69f351f220d719798e0-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszbwS +[Phase 4] RIgC5r4TJ0hCGOzxHszcBh: coverImage [cloudinary.asset] → image ref image-c30cbf92e50931e8c1b45b60c89d5e413f54d30c-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszcBh +[Phase 4] RIgC5r4TJ0hCGOzxHszcQw: coverImage [cloudinary.asset] → image ref image-f32bb9e78ec8f878e052b0c758fe8f44d3d48b4d-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszcQw +[Phase 4] RIgC5r4TJ0hCGOzxHszcgB: coverImage [cloudinary.asset] → image ref image-22340b8b23c003bb68da2bd583925dd80ae52c13-460x460-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszcgB +[Phase 4] RIgC5r4TJ0hCGOzxHszcvQ: coverImage [cloudinary.asset] → image ref image-3aeb0e8d7f77cd8955e460bbd1880c24bbbf09b3-400x400-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszcvQ +[Phase 4] RIgC5r4TJ0hCGOzxHszdAf: coverImage [cloudinary.asset] → image ref image-8aafc72c95aaac76e5b948ee7f423339d9982887-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszdAf +[Phase 4] RIgC5r4TJ0hCGOzxHszdUz: coverImage [cloudinary.asset] → image ref image-a96a6bf2ac87f22bc4f4927a6a35e5f2e00ce6df-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszdUz +[Phase 4] RIgC5r4TJ0hCGOzxHszdkE: coverImage [cloudinary.asset] → image ref image-1d521fad31fda359f0ab59015f0c394ce78b18b6-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszdkE +[Phase 4] RIgC5r4TJ0hCGOzxHsze4Y: coverImage [cloudinary.asset] → image ref image-49e74e306a32fae03047de7851452eaf3eb21ffe-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHsze4Y +[Phase 4] RIgC5r4TJ0hCGOzxHszeTx: coverImage [cloudinary.asset] → image ref image-22f10807fe22f98c285541557036357e8f12455a-450x450-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszeTx +[Phase 4] RIgC5r4TJ0hCGOzxHszejC: coverImage [cloudinary.asset] → image ref image-4175cbb2ed39d2fe264c29ec8027ca7655a4da07-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszejC +[Phase 4] RIgC5r4TJ0hCGOzxHszeyR: coverImage [cloudinary.asset] → image ref image-793dd0a62c0c18273c2964c02e4433f36c1eef7f-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszeyR +[Phase 4] RIgC5r4TJ0hCGOzxHszgWy: coverImage [cloudinary.asset] → image ref image-1c0145ff9f0b4d1b427aa38fb8b431a7b6d0d1c5-560x560-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszgWy +[Phase 4] RIgC5r4TJ0hCGOzxHsziAa: coverImage [cloudinary.asset] → image ref image-beeb7bb8583b2a498569305315a98bb54d6259b0-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHsziAa +[Phase 4] RIgC5r4TJ0hCGOzxHszjYx: coverImage [cloudinary.asset] → image ref image-95d598287c62f68b136aa0b4948119e7f25f5bd8-256x256-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszjYx +[Phase 4] RIgC5r4TJ0hCGOzxHszml6: coverImage [cloudinary.asset] → image ref image-b8a1fa2d0f802bcd820660855ea74eb5377d1ad8-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszml6 +[Phase 4] RIgC5r4TJ0hCGOzxHszn0L: coverImage [cloudinary.asset] → image ref image-177e6807655a437951913865c33544bd22c3532d-460x460-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszn0L +[Phase 4] RIgC5r4TJ0hCGOzxHsznFa: coverImage [cloudinary.asset] → image ref image-d4ab017bc874c5ae376061692fcde3d3df819fab-800x800-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHsznFa +[Phase 4] RIgC5r4TJ0hCGOzxHsznUp: coverImage [cloudinary.asset] → image ref image-be9303bd97800ee9c125d69293ecc17207d0913f-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHsznUp +[Phase 4] RIgC5r4TJ0hCGOzxHsznk4: coverImage [cloudinary.asset] → image ref image-1b2614bd091b93ad55167e1e767025991ed798fa-800x800-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHsznk4 +[Phase 4] RIgC5r4TJ0hCGOzxHsznzJ: coverImage [cloudinary.asset] → image ref image-6550c4bb327bfb8707731c726bf6ba54bc2cbb30-400x400-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHsznzJ +[Phase 4] RIgC5r4TJ0hCGOzxHszoEY: coverImage [cloudinary.asset] → image ref image-59ca7b7197fd50d278e5aea21b050ba06c5d6ddd-399x399-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszoEY +[Phase 4] RIgC5r4TJ0hCGOzxHszoTn: coverImage [cloudinary.asset] → image ref image-a9a858ed6f6986e1e8c9f56774977c33e59655f5-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszoTn +[Phase 4] RIgC5r4TJ0hCGOzxHszoj2: coverImage [cloudinary.asset] → image ref image-09db48512d85ef8b26ac66ae5d6b96bb2fd72b9b-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszoj2 +[Phase 4] RIgC5r4TJ0hCGOzxHszoyH: coverImage [cloudinary.asset] → image ref image-fae142034961e906a3ba51a691ff2288f7fc54bf-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszoyH +[Phase 4] RIgC5r4TJ0hCGOzxHszpDW: coverImage [cloudinary.asset] → image ref image-2f960c1621286fe593ab6d6d64c09579cf81d13f-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszpDW +[Phase 4] RIgC5r4TJ0hCGOzxHszpSl: coverImage [cloudinary.asset] → image ref image-b84519ff6dd0fd9180afff97fc2134d353f1213d-2048x2048-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszpSl +[Phase 4] RIgC5r4TJ0hCGOzxHszy9J: coverImage [cloudinary.asset] → image ref image-907e6de82c78df0ecda5e04479e99f89ce5d4733-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszy9J +[Phase 4] RIgC5r4TJ0hCGOzxHszyTd: coverImage [cloudinary.asset] → image ref image-d2131d51fab0c178751ee53662fd91308f0b4cef-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHszyTd +[Phase 4] RIgC5r4TJ0hCGOzxHt01LS: coverImage [cloudinary.asset] → image ref image-90cf06895a09a91b0772875ad24a4c14630efdfc-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt01LS +[Phase 4] RIgC5r4TJ0hCGOzxHt01ah: coverImage [cloudinary.asset] → image ref image-7f4460cba3e46e4ca1ccce671b4cf9777cb9a64e-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt01ah +[Phase 4] RIgC5r4TJ0hCGOzxHt01pw: coverImage [cloudinary.asset] → image ref image-e0d26d888bd81d6f5df2bd67ee300a0c120fe596-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt01pw +[Phase 4] RIgC5r4TJ0hCGOzxHt025B: coverImage [cloudinary.asset] → image ref image-cb25ffe8ea6e2beff889e7aec2b3a046ce484bec-399x399-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt025B +[Phase 4] RIgC5r4TJ0hCGOzxHt02KQ: coverImage [cloudinary.asset] → image ref image-bae4c44f9bed7c0c255b338cf93b4e498ef540a8-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt02KQ +[Phase 4] RIgC5r4TJ0hCGOzxHt02Zf: coverImage [cloudinary.asset] → image ref image-b37427a9adc4115262883ee6553ca06fb107d5c1-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt02Zf +[Phase 4] RIgC5r4TJ0hCGOzxHt02ou: coverImage [cloudinary.asset] → image ref image-109db365496d9f14fd47539e40e8cb863b35c5ed-219x219-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt02ou +[Phase 4] RIgC5r4TJ0hCGOzxHt0349: coverImage [cloudinary.asset] → image ref image-00ea7f8673cf16bf30024ac4e60b87b05823ace4-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0349 +[Phase 4] RIgC5r4TJ0hCGOzxHt03JO: coverImage [cloudinary.asset] → image ref image-f5c9c9c4a7f82f041384c1236da12d56243acfb2-500x500-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt03JO +[Phase 4] RIgC5r4TJ0hCGOzxHt0437: coverImage [cloudinary.asset] → image ref image-cc8b70b59d816013c6496036c3b701fec58c4c8a-2448x3264-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0437 +[Phase 4] RIgC5r4TJ0hCGOzxHt04IM: coverImage [cloudinary.asset] → image ref image-fb22051cf3cd28e4273f93307a3154a1c12c8bb0-399x399-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt04IM +[Phase 4] RIgC5r4TJ0hCGOzxHt04hl: coverImage [cloudinary.asset] → image ref image-e8a706a5e02eecc46287f52f7e5699e867fc0332-172x172-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt04hl +[Phase 4] RIgC5r4TJ0hCGOzxHt04x0: coverImage [cloudinary.asset] → image ref image-7249d115b2c7af054326d8745ded15dd93e829c3-512x512-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt04x0 +[Phase 4] RIgC5r4TJ0hCGOzxHt05CF: coverImage [cloudinary.asset] → image ref image-c55d97a4d89f37d457d157bdeff3139e021f39e6-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt05CF +[Phase 4] RIgC5r4TJ0hCGOzxHt05gj: coverImage [cloudinary.asset] → image ref image-187de87ef877c457f4175531bb65f385cfb58dbf-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt05gj +[Phase 4] RIgC5r4TJ0hCGOzxHt05vy: coverImage [cloudinary.asset] → image ref image-401bc5776efbdf50246733bac2e261b999b551d4-200x200-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt05vy +[Phase 4] RIgC5r4TJ0hCGOzxHt06BD: coverImage [cloudinary.asset] → image ref image-e0d76b2ff5883a51386dd531b898f9944a1cb4be-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt06BD +[Phase 4] RIgC5r4TJ0hCGOzxHt06QS: coverImage [cloudinary.asset] → image ref image-5369c17cfd4e3570bb6df09a36b841950ec1987d-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt06QS +[Phase 4] RIgC5r4TJ0hCGOzxHt06fh: coverImage [cloudinary.asset] → image ref image-60586388e0d425e92b080aaa94e31caadfe67c7d-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt06fh +[Phase 4] RIgC5r4TJ0hCGOzxHt06uw: coverImage [cloudinary.asset] → image ref image-4cb6fbe1c4602cb2ff1af1f3bdfb5274206abb30-500x500-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt06uw +[Phase 4] RIgC5r4TJ0hCGOzxHt07AB: coverImage [cloudinary.asset] → image ref image-e8d579febfcc78f0a79e246e4aa0a663a8ce8efa-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt07AB +[Phase 4] RIgC5r4TJ0hCGOzxHt07PQ: coverImage [cloudinary.asset] → image ref image-07ca814149a3c7a732fedad1cababaf61411cb0b-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt07PQ +[Phase 4] RIgC5r4TJ0hCGOzxHt07ef: coverImage [cloudinary.asset] → image ref image-a895d6b063239ed00853ffab04a8e85f0394a5cb-450x450-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt07ef +[Phase 4] RIgC5r4TJ0hCGOzxHt07tu: coverImage [cloudinary.asset] → image ref image-8653b813000a531a083397d97e8665dbd8161990-460x460-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt07tu +[Phase 4] RIgC5r4TJ0hCGOzxHt0899: coverImage [cloudinary.asset] → image ref image-1b899cd24d2f8f0abb0073195fb14dc4a5743b32-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0899 +[Phase 4] RIgC5r4TJ0hCGOzxHt08OO: coverImage [cloudinary.asset] → image ref image-44d4266fc68c3dfb54c66dd2a40f44e33ca5e189-389x389-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt08OO +[Phase 4] RIgC5r4TJ0hCGOzxHt09XW: coverImage [cloudinary.asset] → image ref image-879a0b33c46a89e20220aa9db32cfd67d7e79d62-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt09XW +[Phase 4] RIgC5r4TJ0hCGOzxHt09ml: coverImage [cloudinary.asset] → image ref image-bbb504849129a95ed2775009ba78cfc3452d1628-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt09ml +[Phase 4] RIgC5r4TJ0hCGOzxHt0A20: coverImage [cloudinary.asset] → image ref image-a318328e619c1352b175e5b3d10562b5821e568b-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0A20 +[Phase 4] RIgC5r4TJ0hCGOzxHt0AHF: coverImage [cloudinary.asset] → image ref image-b231ed0cc840f471a5916c2c5d03e4bcb094dc3c-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0AHF +[Phase 4] RIgC5r4TJ0hCGOzxHt0AbZ: coverImage [cloudinary.asset] → image ref image-51176a30dd08e030ca56b07050b9ca4ab569a4c4-362x362-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0AbZ +[Phase 4] RIgC5r4TJ0hCGOzxHt0Aqo: coverImage [cloudinary.asset] → image ref image-024716658cd5ebec11629edc7b2c84e7a918a4da-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0Aqo +[Phase 4] RIgC5r4TJ0hCGOzxHt0B63: coverImage [cloudinary.asset] → image ref image-fe2833f4fa8aba77c460f47d47d67dcae6f74edd-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0B63 +[Phase 4] RIgC5r4TJ0hCGOzxHt0BLI: coverImage [cloudinary.asset] → image ref image-aa46f81af46be15eb4c6372375c438a1c900cbd8-247x247-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0BLI +[Phase 4] RIgC5r4TJ0hCGOzxHt0BaX: coverImage [cloudinary.asset] → image ref image-fad34d9f5f6d2d81c0b07f7f9d0e20638290e712-800x800-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0BaX +[Phase 4] RIgC5r4TJ0hCGOzxHt0Bpm: coverImage [cloudinary.asset] → image ref image-89279095e11db22fd0bcddcfbf290df76395e173-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0Bpm +[Phase 4] RIgC5r4TJ0hCGOzxHt0C51: coverImage [cloudinary.asset] → image ref image-f12a67194a2dd34b5498c8a5aba5e6ea44740284-388x388-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0C51 +[Phase 4] RIgC5r4TJ0hCGOzxHt0CKG: coverImage [cloudinary.asset] → image ref image-a0343d7b1b3a9df7807a0affe5302caab16f0a28-352x352-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0CKG +[Phase 4] RIgC5r4TJ0hCGOzxHt0EcW: coverImage [cloudinary.asset] → image ref image-c82cfe3099e45abf458b1babae0148f5ba1d8deb-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0EcW +[Phase 4] RIgC5r4TJ0hCGOzxHt0Erl: coverImage [cloudinary.asset] → image ref image-5f831515bf32ac2c91cd18e778c4622392a393e7-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0Erl +[Phase 4] RIgC5r4TJ0hCGOzxHt0F70: coverImage [cloudinary.asset] → image ref image-d71b8845be9022ad95add5355189fa21e81c6bfb-800x800-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0F70 +[Phase 4] RIgC5r4TJ0hCGOzxHt0FMF: coverImage [cloudinary.asset] → image ref image-7a9da2e49f138e15c8ecd6baad9b25c0ba03656c-391x391-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0FMF +[Phase 4] RIgC5r4TJ0hCGOzxHt0FbU: coverImage [cloudinary.asset] → image ref image-f653113aab8579d08b1946b6a4c9473e9ef1c799-2056x2643-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0FbU +[Phase 4] RIgC5r4TJ0hCGOzxHt0Fvo: coverImage [cloudinary.asset] → image ref image-9b085404dd4aa58aebb3f4ff3ecaafaa3c0b9d05-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0Fvo +[Phase 4] RIgC5r4TJ0hCGOzxHt0GB3: coverImage [cloudinary.asset] → image ref image-7af6aa6690ac48f6601ba6e352133988aa6bc8fa-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0GB3 +[Phase 4] RIgC5r4TJ0hCGOzxHt0GQI: coverImage [cloudinary.asset] → image ref image-3a3c31c394fe86516575c264b23af0a34d7f2440-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0GQI +[Phase 4] RIgC5r4TJ0hCGOzxHt0Gkc: coverImage [cloudinary.asset] → image ref image-4e6b56ba2a00d2d3eaf5fbd67e780a5977f1254d-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0Gkc +[Phase 4] RIgC5r4TJ0hCGOzxHt0H4w: coverImage [cloudinary.asset] → image ref image-5d99a3c07383ecb79b27a4a1b23911c4809de756-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0H4w +[Phase 4] RIgC5r4TJ0hCGOzxHt0HPG: coverImage [cloudinary.asset] → image ref image-1cf8ec7bcf05415c6f2f7a4f23e711da1ec3c9ae-397x397-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0HPG +[Phase 4] RIgC5r4TJ0hCGOzxHt0HeV: coverImage [cloudinary.asset] → image ref image-189680c79aa859bec60d11f9a23760c7649f1ad8-485x485-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0HeV +[Phase 4] RIgC5r4TJ0hCGOzxHt0Hyp: coverImage [cloudinary.asset] → image ref image-a9d7749053ea2e5e4df8a402fc3adebb81fb2145-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0Hyp +[Phase 4] RIgC5r4TJ0hCGOzxHt0IJ9: coverImage [cloudinary.asset] → image ref image-6697458f84d1b10ce595b451f4f612af5911a79f-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0IJ9 +[Phase 4] RIgC5r4TJ0hCGOzxHt0IdT: coverImage [cloudinary.asset] → image ref image-2396412af067817dc75c57ca795de2154923270a-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0IdT +[Phase 4] RIgC5r4TJ0hCGOzxHt0UgK: coverImage [cloudinary.asset] → image ref image-c62723671f45393131cf552976d6ee36569da91a-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0UgK +[Phase 4] RIgC5r4TJ0hCGOzxHt0uzD: coverImage [cloudinary.asset] → image ref image-1e9e44f1065c00f43cc8a55c30df00590c5a69a3-399x399-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt0uzD +[Phase 4] RIgC5r4TJ0hCGOzxHt1Hvn: coverImage [cloudinary.asset] → image ref image-f3772eea722b4a92ff9dc88cd2da5eb0e98c1f34-400x400-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt1Hvn +[Phase 4] RIgC5r4TJ0hCGOzxHt1ll9: coverImage [cloudinary.asset] → image ref image-b48c241fcab85ce8b251a727f2d7b41db8775832-460x460-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt1ll9 +[Phase 4] RIgC5r4TJ0hCGOzxHt25LQ: coverImage [cloudinary.asset] → image ref image-a41356f038e003b81b8b60a14c1a01f5331ef112-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt25LQ +[Phase 4] RIgC5r4TJ0hCGOzxHt2aeE: coverImage [cloudinary.asset] → image ref image-c6a431a8d4d6ab16f573757400a70fba65389c13-400x400-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt2aeE +[Phase 4] RIgC5r4TJ0hCGOzxHt31Rb: coverImage [cloudinary.asset] → image ref image-bd1b3b96384748b4f5f49c58b12b059164723ab2-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt31Rb +[Phase 4] RIgC5r4TJ0hCGOzxHt3Lvl: coverImage [cloudinary.asset] → image ref image-6ecd7c7c4fda57a33aab89e5d8fa5826bbc3b27a-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt3Lvl +[Phase 4] RIgC5r4TJ0hCGOzxHt3MG5: coverImage [cloudinary.asset] → image ref image-df0e1134ca61ac15d90e055b8b1565d01e320450-300x300-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt3MG5 +[Phase 4] RIgC5r4TJ0hCGOzxHt3MVK: coverImage [cloudinary.asset] → image ref image-823439b188dd8855591b33dc33237ac597cbf9eb-337x337-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt3MVK +[Phase 4] RIgC5r4TJ0hCGOzxHt3Mpe: coverImage [cloudinary.asset] → image ref image-032d7a59e62272fe72334328476a96d29a8f9509-256x256-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt3Mpe +[Phase 4] RIgC5r4TJ0hCGOzxHt3fqM: coverImage [cloudinary.asset] → image ref image-82ad5508dda59364de5299039c587eb675c91478-512x512-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt3fqM +[Phase 4] RIgC5r4TJ0hCGOzxHt3g5b: coverImage [cloudinary.asset] → image ref image-690883896dc78206b0a89df3069df7d9afc60c5f-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt3g5b +[Phase 4] RIgC5r4TJ0hCGOzxHt3pGd: coverImage [cloudinary.asset] → image ref image-53fe649cea82db4e42c511ce4be624694d871303-800x800-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt3pGd +[Phase 4] RIgC5r4TJ0hCGOzxHt3yMa: coverImage [cloudinary.asset] → image ref image-258b1dfc015aa7a9de3c8884f7f581706d8f4825-800x800-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt3yMa +[Phase 4] RIgC5r4TJ0hCGOzxHt3ybp: coverImage [cloudinary.asset] → image ref image-d353da9c34912440bfedbd3b062ec1fdf74f0875-600x600-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt3ybp +[Phase 4] RIgC5r4TJ0hCGOzxHt3yr4: coverImage [cloudinary.asset] → image ref image-3524e1eff548cb5205b748e5a9c6bf77e89b733f-450x449-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt3yr4 +[Phase 4] RIgC5r4TJ0hCGOzxHt3zBO: coverImage [cloudinary.asset] → image ref image-2a9e07bf96ac313ecddae26fbf12521b5c59a020-960x960-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt3zBO +[Phase 4] RIgC5r4TJ0hCGOzxHt48CG: coverImage [cloudinary.asset] → image ref image-eeb735d21975ae2df75dc7cf00bd2e637221a974-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt48CG +[Phase 4] RIgC5r4TJ0hCGOzxHt4BTU: coverImage [cloudinary.asset] → image ref image-28a0a46f0800a68d9d0d2607861ff4a2f7b125ec-583x561-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4BTU +[Phase 4] RIgC5r4TJ0hCGOzxHt4XR6: coverImage [cloudinary.asset] → image ref image-a39dadde6cc6a2b8497b8ffd542b38003da755a2-460x460-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4XR6 +[Phase 4] RIgC5r4TJ0hCGOzxHt4XlQ: coverImage [cloudinary.asset] → image ref image-0e4aee236ca43f5b54020a6b81e9a3ed8d3a9047-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4XlQ +[Phase 4] RIgC5r4TJ0hCGOzxHt4Y5k: coverImage [cloudinary.asset] → image ref image-db10c153f9abf7548cfa563374d76b1710855551-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4Y5k +[Phase 4] RIgC5r4TJ0hCGOzxHt4YfJ: coverImage [cloudinary.asset] → image ref image-fd761ba9c31650f505efc96d02664fdd6cb7fdf2-460x460-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4YfJ +[Phase 4] RIgC5r4TJ0hCGOzxHt4Yzd: coverImage [cloudinary.asset] → image ref image-8b725623f2e12c6234eaf99fe5b471f7297ac920-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4Yzd +[Phase 4] RIgC5r4TJ0hCGOzxHt4ZjM: coverImage [cloudinary.asset] → image ref image-30f313181033882f72f257a46ca604492febeebb-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4ZjM +[Phase 4] RIgC5r4TJ0hCGOzxHt4aYA: coverImage [cloudinary.asset] → image ref image-d8c8ffb4e971862c6ab26b07e832129155034f80-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4aYA +[Phase 4] RIgC5r4TJ0hCGOzxHt4bCo: coverImage [cloudinary.asset] → image ref image-4925120ab6041c935bc8c40f4e2177ef208ae45f-460x460-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4bCo +[Phase 4] RIgC5r4TJ0hCGOzxHt4bhI: coverImage [cloudinary.asset] → image ref image-1b251874623e529a13c395dec2d0cd7911c17b3e-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4bhI +[Phase 4] RIgC5r4TJ0hCGOzxHt4cR1: coverImage [cloudinary.asset] → image ref image-87110138fd07656e0ba6bd8a531a16868263e5f0-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4cR1 +[Phase 4] RIgC5r4TJ0hCGOzxHt4eZ7: coverImage [cloudinary.asset] → image ref image-822995112c621b246fe5bf7e2ced9b04690bfa19-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4eZ7 +[Phase 4] RIgC5r4TJ0hCGOzxHt4hvQ: coverImage [cloudinary.asset] → image ref image-5bb9f2f2c6d616b8b376f89503a536011b9a3524-512x512-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4hvQ +[Phase 4] RIgC5r4TJ0hCGOzxHt4lHj: coverImage [cloudinary.asset] → image ref image-f8e6279a30eadc3278fba57ae12c04386da0f9dd-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4lHj +[Phase 4] RIgC5r4TJ0hCGOzxHt4lh8: coverImage [cloudinary.asset] → image ref image-33bdea9e6a5a3f2302d3b88eae74e77023dbfac7-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4lh8 +[Phase 4] RIgC5r4TJ0hCGOzxHt4r1N: coverImage [cloudinary.asset] → image ref image-aff31fdf1931b534ea6eb4e69388b3f2a5a7aaf2-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4r1N +[Phase 4] RIgC5r4TJ0hCGOzxHt4w1I: coverImage [cloudinary.asset] → image ref image-ba92ba6b3024e536837bc55ee0f59f63bde09239-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt4w1I +[Phase 4] RIgC5r4TJ0hCGOzxHt50CP: coverImage [cloudinary.asset] → image ref image-7356c03082ff5c65875a53628155e39365c4d185-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt50CP +[Phase 4] RIgC5r4TJ0hCGOzxHt51q1: coverImage [cloudinary.asset] → image ref image-f973fda11222ab93927d42a644e1cd088c1922b7-400x351-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt51q1 +[Phase 4] RIgC5r4TJ0hCGOzxHt52AL: coverImage [cloudinary.asset] → image ref image-cce6379caf670e828253e19987e50c69b7bb6232-700x634-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt52AL +[Phase 4] RIgC5r4TJ0hCGOzxHt52Uf: coverImage [cloudinary.asset] → image ref image-d099a598f8869fd68695d3ca430da0c6cf6519c9-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt52Uf +[Phase 4] RIgC5r4TJ0hCGOzxHt55CK: coverImage [cloudinary.asset] → image ref image-e8d12f13db294d3038f36e06182de01e0e1f5ac0-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt55CK +[Phase 4] RIgC5r4TJ0hCGOzxHt5Qpc: coverImage [cloudinary.asset] → image ref image-96980c0a51b4dd356f6f22a649a83357311c99a9-1024x1024-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt5Qpc +[Phase 4] RIgC5r4TJ0hCGOzxHt5W4m: coverImage [cloudinary.asset] → image ref image-9f1c40b518c922f06956fa4826be738b3b5c60dd-400x400-jpg +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxHt5W4m +[Phase 4] RIgC5r4TJ0hCGOzxIBAHUD: coverImage [cloudinary.asset] → image ref image-92d1ea81c6e3e4c965e5ade2255dccffbecac7bb-1920x1080-png +[Phase 4] RIgC5r4TJ0hCGOzxIBAHUD: videoCloudinary [cloudinary.asset] → file ref file-d69ad282ae229a5c9432d709ce525de8ccb594ca-mp4 +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAHUD +[Phase 4] RIgC5r4TJ0hCGOzxIBAHjS: coverImage [cloudinary.asset] → image ref image-2a349ce52bff79f28e8a573f43077970f7eb6dc6-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAHjS +[Phase 4] RIgC5r4TJ0hCGOzxIBAHyh: coverImage [cloudinary.asset] → image ref image-c7267000053c90d86990079ebe3650b57f83447f-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAHyh +[Phase 4] RIgC5r4TJ0hCGOzxIBAIDw: coverImage [cloudinary.asset] → image ref image-2780a6fc5ef85ab5fcabe8d086bd74b4f883ddfc-1920x1080-png +[Phase 4] RIgC5r4TJ0hCGOzxIBAIDw: videoCloudinary [cloudinary.asset] → file ref image-2780a6fc5ef85ab5fcabe8d086bd74b4f883ddfc-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAIDw +[Phase 4] RIgC5r4TJ0hCGOzxIBAITB: coverImage [cloudinary.asset] → image ref image-afa5376ac209d31c149e2ff410e64c4160a84806-1920x1080-png +[Phase 4] RIgC5r4TJ0hCGOzxIBAITB: videoCloudinary [cloudinary.asset] → file ref file-9cdfdcf834f1e30fc3c0fee74f3cbe1d21a599ff-mp4 +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAITB +[Phase 4] RIgC5r4TJ0hCGOzxIBAIiQ: coverImage [cloudinary.asset] → image ref image-c04b17d48181bcc93c6c29f3d02776dede7c770c-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAIiQ +[Phase 4] RIgC5r4TJ0hCGOzxIBAIxf: coverImage [cloudinary.asset] → image ref image-17077024019db9679dc31436b7b7515e22673c26-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAIxf +[Phase 4] RIgC5r4TJ0hCGOzxIBAJCu: coverImage [cloudinary.asset] → image ref image-59902a0c5636f9a49671aa8e4928a998150db8b1-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAJCu +[Phase 4] RIgC5r4TJ0hCGOzxIBAJS9: coverImage [cloudinary.asset] → image ref image-1230d146654fd7a17a6a9ecaf6aefac4154b12ae-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAJS9 +[Phase 4] RIgC5r4TJ0hCGOzxIBAJhO: coverImage [cloudinary.asset] → image ref image-9f9dbeb66a3e9f6f2b698423b83a9fef9de8d8b1-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAJhO +[Phase 4] RIgC5r4TJ0hCGOzxIBAJwd: coverImage [cloudinary.asset] → image ref image-d50b57c3597cbcaecc285e7be769af410ff94856-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAJwd +[Phase 4] RIgC5r4TJ0hCGOzxIBAKBs: coverImage [cloudinary.asset] → image ref image-74f8cbe73a288b60ad02b700a5c2757c0d59b1f5-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAKBs +[Phase 4] RIgC5r4TJ0hCGOzxIBAKR7: coverImage [cloudinary.asset] → image ref image-d6057d0e030de250287d19cae1bef0e735fa188d-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAKR7 +[Phase 4] RIgC5r4TJ0hCGOzxIBAKgM: coverImage [cloudinary.asset] → image ref image-deca83794dfb41e5ec607afa546ddcd1fdc504dd-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAKgM +[Phase 4] RIgC5r4TJ0hCGOzxIBAKvb: coverImage [cloudinary.asset] → image ref image-d8ba83e3cbc693832fcc3419148eed9c47efb7a1-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAKvb +[Phase 4] RIgC5r4TJ0hCGOzxIBALAq: coverImage [cloudinary.asset] → image ref image-5bd2e2d479ac5a78e11a3af75523d6e40b2fa358-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBALAq +[Phase 4] RIgC5r4TJ0hCGOzxIBALQ5: coverImage [cloudinary.asset] → image ref image-91a55d00d95d4f1fd657ecc81510390fc3a3e956-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBALQ5 +[Phase 4] RIgC5r4TJ0hCGOzxIBALfK: coverImage [cloudinary.asset] → image ref image-f45bdcd180e2b7158e4deae3989a6e5aba0cec7e-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBALfK +[Phase 4] RIgC5r4TJ0hCGOzxIBALuZ: coverImage [cloudinary.asset] → image ref image-1f51d8be9e369365716a991ddf3eb72fffb640dd-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBALuZ +[Phase 4] RIgC5r4TJ0hCGOzxIBAM9o: coverImage [cloudinary.asset] → image ref image-9f756cf83c8d865170e175081c84993e557e88ee-1920x1084-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAM9o +[Phase 4] RIgC5r4TJ0hCGOzxIBAMP3: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAMP3 +[Phase 4] RIgC5r4TJ0hCGOzxIBAMeI: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAMeI +[Phase 4] RIgC5r4TJ0hCGOzxIBAMtX: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAMtX +[Phase 4] RIgC5r4TJ0hCGOzxIBAN8m: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAN8m +[Phase 4] RIgC5r4TJ0hCGOzxIBANO1: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBANO1 +[Phase 4] RIgC5r4TJ0hCGOzxIBANdG: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBANdG +[Phase 4] RIgC5r4TJ0hCGOzxIBANsV: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBANsV +[Phase 4] RIgC5r4TJ0hCGOzxIBAO7k: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAO7k +[Phase 4] RIgC5r4TJ0hCGOzxIBAOMz: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAOMz +[Phase 4] RIgC5r4TJ0hCGOzxIBAOcE: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAOcE +[Phase 4] RIgC5r4TJ0hCGOzxIBAOrT: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAOrT +[Phase 4] RIgC5r4TJ0hCGOzxIBAP6i: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAP6i +[Phase 4] RIgC5r4TJ0hCGOzxIBAPLx: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAPLx +[Phase 4] RIgC5r4TJ0hCGOzxIBAPbC: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAPbC +[Phase 4] RIgC5r4TJ0hCGOzxIBAPqR: coverImage [cloudinary.asset] → image ref image-be314b2ba96969840c1e0a3db978768c9a491ede-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAPqR +[Phase 4] RIgC5r4TJ0hCGOzxIBAQ5g: coverImage [cloudinary.asset] → image ref image-72ca81aa1ccdd077811aeed38de3eeefa8b66860-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAQ5g +[Phase 4] RIgC5r4TJ0hCGOzxIBAQKv: coverImage [cloudinary.asset] → image ref image-b044c2beb2a931aec7e0e43a165fa02cd41b1f0f-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAQKv +[Phase 4] RIgC5r4TJ0hCGOzxIBAQpP: coverImage [cloudinary.asset] → image ref image-6339543c6125be353037074c53064b8ec54e80d8-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAQpP +[Phase 4] RIgC5r4TJ0hCGOzxIBARJt: coverImage [cloudinary.asset] → image ref image-a0b269fceac7ec44e3abdc1184a82316c95a8f42-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBARJt +[Phase 4] RIgC5r4TJ0hCGOzxIBARjI: coverImage [cloudinary.asset] → image ref image-e5e13cc945db42c6e018a6fda8b3239668f17514-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBARjI +[Phase 4] RIgC5r4TJ0hCGOzxIBASDm: coverImage [cloudinary.asset] → image ref image-f32a94b713420a9b9592967bb2f35225408e466d-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBASDm +[Phase 4] RIgC5r4TJ0hCGOzxIBAST1: coverImage [cloudinary.asset] → image ref image-5072aa556fe642bd1e6b30afb26580f045d2a32d-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAST1 +[Phase 4] RIgC5r4TJ0hCGOzxIBASxV: coverImage [cloudinary.asset] → image ref image-9fb6e4347552d8cca0502f8a85cba3f7165bffb1-1921x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBASxV +[Phase 4] RIgC5r4TJ0hCGOzxIBATCk: coverImage [cloudinary.asset] → image ref image-02525d90e4d91151568532e41eabc52982d4c281-1921x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBATCk +[Phase 4] RIgC5r4TJ0hCGOzxIBATRz: coverImage [cloudinary.asset] → image ref image-3eb78ac179d7090f3f2d11ac0274bde2296db5d8-3840x2160-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBATRz +[Phase 4] RIgC5r4TJ0hCGOzxIBAThE: coverImage [cloudinary.asset] → image ref image-27dce90082f7b793aaffde1a71aec4a8f59a75e2-3840x2160-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBAThE +[Phase 4] RIgC5r4TJ0hCGOzxIBDguH: coverImage [cloudinary.asset] → image ref image-c89927147885230ea6b3b5802bd574097b4d36b4-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBDguH +[Phase 4] RIgC5r4TJ0hCGOzxIBDh9W: coverImage [cloudinary.asset] → image ref image-d515a6644917ffad9109cb5b2b7aa639c470ccc4-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBDh9W +[Phase 4] RIgC5r4TJ0hCGOzxIBDhe0: coverImage [cloudinary.asset] → image ref image-5a871ff23135a64d2c34d45418c24d88453724e8-1920x1084-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBDhe0 +[Phase 4] RIgC5r4TJ0hCGOzxIBDiXt: coverImage [cloudinary.asset] → image ref image-5e283fb0d0ffe55c9c179aca147a1c03dd88d599-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBDiXt +[Phase 4] RIgC5r4TJ0hCGOzxIBDin8: coverImage [cloudinary.asset] → image ref image-e229da54c6839b8d967125167ef9108ecd6fb935-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBDin8 +[Phase 4] RIgC5r4TJ0hCGOzxIBDj2N: coverImage [cloudinary.asset] → image ref image-af1c94461826c80c797c64b2633b7ea33e056960-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBDj2N +[Phase 4] RIgC5r4TJ0hCGOzxIBDjm6: coverImage [cloudinary.asset] → image ref image-07703a0d8102dd092c5f4a691bc08579a7514921-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBDjm6 +[Phase 4] RIgC5r4TJ0hCGOzxIBDkLf: coverImage [cloudinary.asset] → image ref image-75346ad2ae21a468aa2c2933847030d639cc62c2-1920x1080-png +[Phase 4] ✓ Committed changes for RIgC5r4TJ0hCGOzxIBDkLf +[Phase 4] T68Tm1I9oo5QeAPCgsFbM0: coverImage [cloudinary.asset] → image ref image-1564a576c0c19e1b857c9bfba3cfb6364e7af223-2400x1350-jpg +[Phase 4] ✓ Committed changes for T68Tm1I9oo5QeAPCgsFbM0 +[Phase 4] T68Tm1I9oo5QeAPCgsFbWk: coverImage [cloudinary.asset] → image ref image-75f6a8d12374228519245c55e64705f535a26d8a-1000x1000-png +[Phase 4] ✓ Committed changes for T68Tm1I9oo5QeAPCgsFbWk +[Phase 4] T68Tm1I9oo5QeAPCgsFbhU: coverImage [cloudinary.asset] → image ref image-d1043302179e97d6f0f9755ced51228ecb53c577-4800x2700-png +[Phase 4] ✓ Committed changes for T68Tm1I9oo5QeAPCgsFbhU +[Phase 4] T68Tm1I9oo5QeAPCgsFbsE: coverImage [cloudinary.asset] → image ref image-9cfc1e48b8a113a50197b5d105273ed0d2328775-1200x630-png +[Phase 4] ✓ Committed changes for T68Tm1I9oo5QeAPCgsFbsE +[Phase 4] T68Tm1I9oo5QeAPCgsFc2y: coverImage [cloudinary.asset] → image ref image-058754a4a1b521789ae5582b3c8ac8943e0f29b0-2400x1350-png +[Phase 4] ✓ Committed changes for T68Tm1I9oo5QeAPCgsFc2y +[Phase 4] T68Tm1I9oo5QeAPCgsFcDi: coverImage [cloudinary.asset] → image ref image-e355e7e90e37726637596011069c154ae515da9f-1200x675-png +[Phase 4] ✓ Committed changes for T68Tm1I9oo5QeAPCgsFcDi +[Phase 4] T68Tm1I9oo5QeAPCgsFcOS: coverImage [cloudinary.asset] → image ref image-b6b01384aa98407d8fcc5b726afc2553445cffcf-1200x630-jpg +[Phase 4] ✓ Committed changes for T68Tm1I9oo5QeAPCgsFcOS +[Phase 4] ZKdo3vLUqcb4sYrehuNXvu: coverImage [cloudinary.asset] → image ref image-1c0145ff9f0b4d1b427aa38fb8b431a7b6d0d1c5-560x560-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYrehuNXvu +[Phase 4] ZKdo3vLUqcb4sYrehuNY56: coverImage [cloudinary.asset] → image ref image-b84519ff6dd0fd9180afff97fc2134d353f1213d-2048x2048-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYrehuNY56 +[Phase 4] ZKdo3vLUqcb4sYrehuNYEI: coverImage [cloudinary.asset] → image ref image-fd761ba9c31650f505efc96d02664fdd6cb7fdf2-460x460-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYrehuNYEI +[Phase 4] ZKdo3vLUqcb4sYreiCMQ7Y: coverImage [cloudinary.asset] → image ref image-57d7c7e556e5efe60ba10ca6eec7f8914320f1d5-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMQ7Y +[Phase 4] ZKdo3vLUqcb4sYreiCMQiK: coverImage [cloudinary.asset] → image ref image-d80cf5e4fdcbfdfe921245486cfd04e343266110-1792x1024-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMQiK +[Phase 4] ZKdo3vLUqcb4sYreiCMQua: coverImage [cloudinary.asset] → image ref image-9ccf273f94f1ef952b47e1d75a12c3a73dfe5751-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMQua +[Phase 4] ZKdo3vLUqcb4sYreiCMRG2: coverImage [cloudinary.asset] → image ref image-64b1cdf840e96840ef3fd00a8692a88c8f0ddac2-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMRG2 +[Phase 4] ZKdo3vLUqcb4sYreiCMRkg: coverImage [cloudinary.asset] → image ref image-d4babb7ffb9548d3bd4406f7d65f901678ff876b-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMRkg +[Phase 4] ZKdo3vLUqcb4sYreiCMSOW: coverImage [cloudinary.asset] → image ref image-4de2886f45d64abc1a19a86840c19c552349aee1-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMSOW +[Phase 4] ZKdo3vLUqcb4sYreiCMU4i: coverImage [cloudinary.asset] → image ref image-96c43bbc92799a978c14990f9953f458b92fa290-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMU4i +[Phase 4] ZKdo3vLUqcb4sYreiCMUN6: coverImage [cloudinary.asset] → image ref image-c27886125a50aba7e81aaf12868fe6b2616833fd-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMUN6 +[Phase 4] ZKdo3vLUqcb4sYreiCMUcQ: coverImage [cloudinary.asset] → image ref image-b9d36640932af8763bb084b031469fa8fe452594-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMUcQ +[Phase 4] ⚠ No mapping found for URL: https://res.cloudinary.com/ajonp/image/upload/w\_500/v1556553295/ajonp-ajonp-com/18-rxfire-svelte-cats/RxFire\_Svelt.webp (in ZKdo3vLUqcb4sYreiCMUrk at content[28].code) +[Phase 4] ZKdo3vLUqcb4sYreiCMUrk: coverImage [cloudinary.asset] → image ref image-8f112a33677dfbfc7253c60d157baf363e13ebe6-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMUrk +[Phase 4] ⚠ No mapping found for URL: https://res.cloudinary.com/ajonp/image/upload/q\_auto/ajonp-ajonp-com/17-rxfire-react-cats/RxFire\_3.webp (in ZKdo3vLUqcb4sYreiCMVA8 at content[26].code) +[Phase 4] ZKdo3vLUqcb4sYreiCMVA8: coverImage [cloudinary.asset] → image ref image-14c859eab1d212d8e8bfa6c43bcfe965cec07a11-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMVA8 +[Phase 4] ZKdo3vLUqcb4sYreiCMVSW: coverImage [cloudinary.asset] → image ref image-8c7fe3a0cf30fffe584e5b4397a438d7ffd8f8c9-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMVSW +[Phase 4] ZKdo3vLUqcb4sYreiCMVku: coverImage [cloudinary.asset] → image ref image-c75d764c39a0f828614f8b848199965957a145cb-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMVku +[Phase 4] ZKdo3vLUqcb4sYreiCMW3I: coverImage [cloudinary.asset] → image ref image-24001bc9917ea437753ef7f05175529d542a0c24-1200x631-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMW3I +[Phase 4] ⚠ No mapping found for URL: https://media.codingcat.dev/image/upload/b_rgb:5e1186,c_pad,w_1000,h_420/${page?.coverPhoto?.public_id}`, (in ZKdo3vLUqcb4sYreiCMWCU at content[12].code) +[Phase 4] ZKdo3vLUqcb4sYreiCMWCU: coverImage [cloudinary.asset] → image ref image-bbf6e5212bcec055b854494b3fdac46312e35be8-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMWCU +[Phase 4] ZKdo3vLUqcb4sYreiCMWLg: coverImage [cloudinary.asset] → image ref image-b01b75ceb28c40b64967c97bd208a896fbf58e5f-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMWLg +[Phase 4] ZKdo3vLUqcb4sYreiCMWUs: coverImage [cloudinary.asset] → image ref image-519a1c72b7a3bee30d02a44395e4dec6aa3f5dd2-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMWUs +[Phase 4] ZKdo3vLUqcb4sYreiCMWe4: coverImage [cloudinary.asset] → image ref image-df234899a342359c323e924fd218f8c83b377f79-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMWe4 +[Phase 4] ZKdo3vLUqcb4sYreiCMWwS: coverImage [cloudinary.asset] → image ref image-a7b78d1d7cc92b18602ac3107130fa1ebcef1b38-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMWwS +[Phase 4] ZKdo3vLUqcb4sYreiCMXvk: coverImage [cloudinary.asset] → image ref image-491be315d1db6904132e898767ed88eee4e98374-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMXvk +[Phase 4] ZKdo3vLUqcb4sYreiCMY4w: coverImage [cloudinary.asset] → image ref image-1a0babc20fd7892a4f1e5441a10be57b6f0c87a2-1280x720-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMY4w +[Phase 4] ZKdo3vLUqcb4sYreiCMYNK: coverImage [cloudinary.asset] → image ref image-53c49dccf49b9f616b4ea5e95b814ff31cd322a6-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMYNK +[Phase 4] ZKdo3vLUqcb4sYreiCMYWW: coverImage [cloudinary.asset] → image ref image-899f30ff4efc79aa36a3a66c87734a18765de8e3-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMYWW +[Phase 4] ZKdo3vLUqcb4sYreiCMYfi: coverImage [cloudinary.asset] → image ref image-0a974b403f056384c923b5decaff392d6af61e95-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMYfi +[Phase 4] ZKdo3vLUqcb4sYreiCMYou: coverImage [cloudinary.asset] → image ref image-980bd32c6a71afd0d03b1f86797206d7236805fe-1280x720-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMYou +[Phase 4] ZKdo3vLUqcb4sYreiCMZ7I: coverImage [cloudinary.asset] → image ref image-80a3eb15d10366ed52ae81fa950b2b83a4079bdd-1280x720-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMZ7I +[Phase 4] ZKdo3vLUqcb4sYreiCMZPg: coverImage [cloudinary.asset] → image ref image-40e2e69cf5d937ae73414f4edf4ceb65f94b1dab-2880x1920-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMZPg +[Phase 4] ZKdo3vLUqcb4sYreiCMZi4: coverImage [cloudinary.asset] → image ref image-587167d5142eb1aa4f0a73f0ed9764af76ce80f1-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMZi4 +[Phase 4] ZKdo3vLUqcb4sYreiCMZrG: coverImage [cloudinary.asset] → image ref image-ada94a15455f9eb7527bd88eff6330802f323896-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMZrG +[Phase 4] ZKdo3vLUqcb4sYreiCMa0S: coverImage [cloudinary.asset] → image ref image-f8e90b34289c2581385bb41cc5fe1f77877d6583-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMa0S +[Phase 4] ZKdo3vLUqcb4sYreiCMaIq: coverImage [cloudinary.asset] → image ref image-76efa3c45e46b0a601d56043694656ed40e42f72-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMaIq +[Phase 4] ZKdo3vLUqcb4sYreiCMabE: coverImage [cloudinary.asset] → image ref image-e682b3bf07ba93789f8cb8940b7482be76274a97-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMabE +[Phase 4] ZKdo3vLUqcb4sYreiCMatc: coverImage [cloudinary.asset] → image ref image-e6c8ab66ce8c7ef5714ad3ed650d13f3f8a6cc61-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMatc +[Phase 4] ZKdo3vLUqcb4sYreiCMb2o: coverImage [cloudinary.asset] → image ref image-acce9e900e103ce76fae5592744d9b12e584806b-1280x720-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMb2o +[Phase 4] ZKdo3vLUqcb4sYreiCMbLC: coverImage [cloudinary.asset] → image ref image-82ca2d5baaf3e0a4898b9697f661ffbe771d7003-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMbLC +[Phase 4] ZKdo3vLUqcb4sYreiCMbUO: coverImage [cloudinary.asset] → image ref image-7713255dd24259b737b575b89d1fe7508d28c0c6-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMbUO +[Phase 4] ZKdo3vLUqcb4sYreiCMbda: coverImage [cloudinary.asset] → image ref image-48b44d21f8d9fa4fc35015d36006f9649df617b0-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMbda +[Phase 4] ZKdo3vLUqcb4sYreiCMbmm: coverImage [cloudinary.asset] → image ref image-f13e901f6157a042ba6ceb021a00f56788286990-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMbmm +[Phase 4] ZKdo3vLUqcb4sYreiCMc5A: coverImage [cloudinary.asset] → image ref image-a9715bc8ef51cab8ed422e81c810d42b1fe086ef-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMc5A +[Phase 4] ZKdo3vLUqcb4sYreiCMcEM: coverImage [cloudinary.asset] → image ref image-a5a76ed7073d6a46a8eaf21d825a1ce8d6d8cd9c-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMcEM +[Phase 4] ZKdo3vLUqcb4sYreiCMcNY: coverImage [cloudinary.asset] → image ref image-cb9f7a182c2d4a6fe5461277874cfcff5f3eb11b-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMcNY +[Phase 4] ZKdo3vLUqcb4sYreiCMcWk: coverImage [cloudinary.asset] → image ref image-dfd267cc99ce0abcb6098421614161996ce5ca2b-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMcWk +[Phase 4] ZKdo3vLUqcb4sYreiCMcm4: coverImage [cloudinary.asset] → image ref image-01546d01f7b69c501bc27be3337d984479b3d12a-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMcm4 +[Phase 4] ZKdo3vLUqcb4sYreiCMcvG: coverImage [cloudinary.asset] → image ref image-2a94a53b557fd4658a909349ac9e5a111468bbbb-1280x720-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMcvG +[Phase 4] ZKdo3vLUqcb4sYreiCMd4S: coverImage [cloudinary.asset] → image ref image-49d6dcd4606357dda5859df9a9a0262551d18248-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMd4S +[Phase 4] ZKdo3vLUqcb4sYreiCMdDe: coverImage [cloudinary.asset] → image ref image-8eab24e984aaaec11a3b5749b4ef590dedd909fa-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMdDe +[Phase 4] ZKdo3vLUqcb4sYreiCMdMq: coverImage [cloudinary.asset] → image ref image-9c29ad58644138a83445910e2b6472a4a4042f45-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMdMq +[Phase 4] ZKdo3vLUqcb4sYreiCMdW2: coverImage [cloudinary.asset] → image ref image-5f08488112c192d358c8d324e25be28f70b1c98d-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMdW2 +[Phase 4] ZKdo3vLUqcb4sYreiCMdfE: coverImage [cloudinary.asset] → image ref image-29d28af2e253529714e7afeff6a85873c17fb497-1280x720-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMdfE +[Phase 4] ZKdo3vLUqcb4sYreiCMeSG: coverImage [cloudinary.asset] → image ref image-38edf2a2efddfe4743b57270cde3592fda9dfe71-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMeSG +[Phase 4] ZKdo3vLUqcb4sYreiCMebS: coverImage [cloudinary.asset] → image ref image-2d05e8d1908651e9d626214672be473a7f85dcd4-1280x720-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMebS +[Phase 4] ZKdo3vLUqcb4sYreiCMeke: coverImage [cloudinary.asset] → image ref image-ccbc8203df1de782371c3ee852dbc5f647c83225-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMeke +[Phase 4] ZKdo3vLUqcb4sYreiCMh1c: coverImage [cloudinary.asset] → image ref image-ccbc8203df1de782371c3ee852dbc5f647c83225-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCMh1c +[Phase 4] ZKdo3vLUqcb4sYreiCP8fY: coverImage [cloudinary.asset] → image ref image-722f9b9e39ba3f6fae594361052ea606757ddd82-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCP8fY +[Phase 4] ZKdo3vLUqcb4sYreiCP8ok: coverImage [cloudinary.asset] → image ref image-bd1ce599334db95c5d1809b6b015624f422ddf2a-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCP8ok +[Phase 4] ZKdo3vLUqcb4sYreiCP8xw: coverImage [cloudinary.asset] → image ref image-343e2649e927e4e44c23609ae4ebaca2ad2b05d0-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCP8xw +[Phase 4] ZKdo3vLUqcb4sYreiCP978: coverImage [cloudinary.asset] → image ref image-098510938a91e58b35d879525fa3171828adbfac-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCP978 +[Phase 4] ZKdo3vLUqcb4sYreiCP9GK: coverImage [cloudinary.asset] → image ref image-34f2211036f7229d80aca4446a3af20e7be99c9a-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCP9GK +[Phase 4] ZKdo3vLUqcb4sYreiCP9PW: coverImage [cloudinary.asset] → image ref image-be4ab3b7d1d31d0354fa6889511f4e172adcb40f-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCP9PW +[Phase 4] ZKdo3vLUqcb4sYreiCP9r6: coverImage [cloudinary.asset] → image ref image-abca4c1a426820206a97cd3aa20d730620ca0850-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCP9r6 +[Phase 4] ZKdo3vLUqcb4sYreiCPA0I: coverImage [cloudinary.asset] → image ref image-91553c5a23390467ddeff3b49326b8bffee905f1-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPA0I +[Phase 4] ZKdo3vLUqcb4sYreiCPA9U: coverImage [cloudinary.asset] → image ref image-9b0c2fdb08ada42536b9bc7c851dd490a3fbbba8-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPA9U +[Phase 4] ZKdo3vLUqcb4sYreiCPAIg: coverImage [cloudinary.asset] → image ref image-dd397fb605327c30582d305c5e39af48dadcc4b5-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPAIg +[Phase 4] ZKdo3vLUqcb4sYreiCPARs: coverImage [cloudinary.asset] → image ref image-e30ed0fae3f33f21034b0235d98ebe493c7e63df-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPARs +[Phase 4] ZKdo3vLUqcb4sYreiCPAb4: coverImage [cloudinary.asset] → image ref image-080cc7d50b421ea224c29b5e74d82f458b7fd785-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPAb4 +[Phase 4] ZKdo3vLUqcb4sYreiCPAkG: coverImage [cloudinary.asset] → image ref image-2d988a2d4124c96fc9da1c73fd32e6f3f25dacda-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPAkG +[Phase 4] ZKdo3vLUqcb4sYreiCPAtS: coverImage [cloudinary.asset] → image ref image-1cf673a085537be26c6ea5e0e0089fe8188bfbe8-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPAtS +[Phase 4] ZKdo3vLUqcb4sYreiCPB2e: coverImage [cloudinary.asset] → image ref image-80a9c7f1e7bcd5a2d24c707626afb433eb96f03b-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPB2e +[Phase 4] ZKdo3vLUqcb4sYreiCPBBq: coverImage [cloudinary.asset] → image ref image-e39b2780e402821b829bfa52d358a0e7e1440080-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPBBq +[Phase 4] ZKdo3vLUqcb4sYreiCPBL2: coverImage [cloudinary.asset] → image ref image-778341ddfe02498b535de1cbad23abcb61fcaef5-960x540-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPBL2 +[Phase 4] ZKdo3vLUqcb4sYreiCPBUE: coverImage [cloudinary.asset] → image ref image-7ebc6accfa015ca6226eca6d42480866b1926418-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPBUE +[Phase 4] ZKdo3vLUqcb4sYreiCPCB8: coverImage [cloudinary.asset] → image ref image-cc425b6346b3a804a920d5f90b51f9584921748e-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPCB8 +[Phase 4] ZKdo3vLUqcb4sYreiCPCNO: coverImage [cloudinary.asset] → image ref image-fac29485bb3dba8b272f5fdf7ac0b3a5b99d0f5a-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPCNO +[Phase 4] ZKdo3vLUqcb4sYreiCPCci: coverImage [cloudinary.asset] → image ref image-636b03849b7273c74831e810cd995159ca697072-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPCci +[Phase 4] ZKdo3vLUqcb4sYreiCPClu: coverImage [cloudinary.asset] → image ref image-37d9408851ff7a6ac51bdfaf513fc0b4600cf10c-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPClu +[Phase 4] ZKdo3vLUqcb4sYreiCPCv6: coverImage [cloudinary.asset] → image ref image-049fbf0855028b07cc7d2d2e16c72d1174ded37b-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPCv6 +[Phase 4] ZKdo3vLUqcb4sYreiCPD4I: coverImage [cloudinary.asset] → image ref image-6dad0f7e032c1029f6e94198ec0df9883bae49f4-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPD4I +[Phase 4] ZKdo3vLUqcb4sYreiCPDDU: coverImage [cloudinary.asset] → image ref image-13b3650dfe32395065b2e91425e0046e9f1dbb93-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPDDU +[Phase 4] ZKdo3vLUqcb4sYreiCPDMg: coverImage [cloudinary.asset] → image ref image-54f923647199c5ed4e257a81ddbae1ace404e5ad-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPDMg +[Phase 4] ZKdo3vLUqcb4sYreiCPDVs: coverImage [cloudinary.asset] → image ref image-6febc461be283cbf1dfaa57c5927260cd853204b-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPDVs +[Phase 4] ZKdo3vLUqcb4sYreiCPDf4: coverImage [cloudinary.asset] → image ref image-8a10ce4a328dc24b0b795fb2238d9b81049635a3-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPDf4 +[Phase 4] ZKdo3vLUqcb4sYreiCPDoG: coverImage [cloudinary.asset] → image ref image-ccd389df9b063e5f31a242b1e0ea3ed4d99d3993-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPDoG +[Phase 4] ZKdo3vLUqcb4sYreiCPDxS: coverImage [cloudinary.asset] → image ref image-1ae2c394145875cabbff54da69bb6f5d754d8544-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPDxS +[Phase 4] ZKdo3vLUqcb4sYreiCPE6e: coverImage [cloudinary.asset] → image ref image-db14f4e95601edd452400a3393ce1c505de3c6ab-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPE6e +[Phase 4] ZKdo3vLUqcb4sYreiCPEFq: coverImage [cloudinary.asset] → image ref image-bec875a4de431b4ea9108d3daa60d9550d92ffc5-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPEFq +[Phase 4] ZKdo3vLUqcb4sYreiCPEP2: coverImage [cloudinary.asset] → image ref image-331c48ec775125f986e205beed3351307f86252a-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPEP2 +[Phase 4] ZKdo3vLUqcb4sYreiCPEYE: coverImage [cloudinary.asset] → image ref image-0cb4c03f856688ccf3fca0a0762cbbbd222191a0-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPEYE +[Phase 4] ZKdo3vLUqcb4sYreiCPEhQ: coverImage [cloudinary.asset] → image ref image-31db41f81b50703c568a970a26ac96659a869f21-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPEhQ +[Phase 4] ZKdo3vLUqcb4sYreiCPEqc: coverImage [cloudinary.asset] → image ref image-fb0a8d732f16769bcb38ca17e1612838f05da5d6-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPEqc +[Phase 4] ZKdo3vLUqcb4sYreiCPEzo: coverImage [cloudinary.asset] → image ref image-e009ad4d1fa90568d5bf4527b29e6cef2b85775c-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPEzo +[Phase 4] ZKdo3vLUqcb4sYreiCPF90: coverImage [cloudinary.asset] → image ref image-8803bcf670af71e7028f79bb7e94d79c206f0dc0-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPF90 +[Phase 4] ZKdo3vLUqcb4sYreiCPFIC: coverImage [cloudinary.asset] → image ref image-c5a962d0fc2d9c0f7a19abd0b2ef8dfc1ae2f3bb-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPFIC +[Phase 4] ZKdo3vLUqcb4sYreiCPFRO: coverImage [cloudinary.asset] → image ref image-54febe02a16971901582bbe5a6ec7aa89968d7b0-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPFRO +[Phase 4] ZKdo3vLUqcb4sYreiCPFaa: coverImage [cloudinary.asset] → image ref image-becef36fbb5ef0cad7e91260cf8e4793ce9bc1bf-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPFaa +[Phase 4] ZKdo3vLUqcb4sYreiCPFjm: coverImage [cloudinary.asset] → image ref image-ecbf04952f2de4fd0d43cbd39f0905561e5fbe1a-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPFjm +[Phase 4] ZKdo3vLUqcb4sYreiCPFsy: coverImage [cloudinary.asset] → image ref image-e134911c7d058f8ea59f9d5ec94dddc6b67cae1d-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPFsy +[Phase 4] ZKdo3vLUqcb4sYreiCPG2A: coverImage [cloudinary.asset] → image ref image-f04a4adafe0cb46060ba54c2d9e6dca14b4f444f-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPG2A +[Phase 4] ZKdo3vLUqcb4sYreiCPGBM: coverImage [cloudinary.asset] → image ref image-d45999ee9a90910e5afacd684c768cfb4c39c078-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPGBM +[Phase 4] ZKdo3vLUqcb4sYreiCPGKY: coverImage [cloudinary.asset] → image ref image-fb003c696cc7fb99c221f70a866f170a555083ab-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPGKY +[Phase 4] ZKdo3vLUqcb4sYreiCPGTk: coverImage [cloudinary.asset] → image ref image-860d0129b198a107bbd77607d094bd2a452cb708-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPGTk +[Phase 4] ZKdo3vLUqcb4sYreiCPGcw: coverImage [cloudinary.asset] → image ref image-5b27f20305af2b493cb31d3884028d0f1bf53596-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPGcw +[Phase 4] ZKdo3vLUqcb4sYreiCPGm8: coverImage [cloudinary.asset] → image ref image-bfb0d93748d5f957b07bab518493922552e646ad-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPGm8 +[Phase 4] ZKdo3vLUqcb4sYreiCPGvK: coverImage [cloudinary.asset] → image ref image-f492b9734b22bee8e5aaa311c4858c3f1dc67c1f-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPGvK +[Phase 4] ZKdo3vLUqcb4sYreiCPH4W: coverImage [cloudinary.asset] → image ref image-60f889306bbd8a3128934cc139625359daf840c8-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPH4W +[Phase 4] ZKdo3vLUqcb4sYreiCPHDi: coverImage [cloudinary.asset] → image ref image-e9e4314f20b5cec30b40b39872a718596e0da2fb-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPHDi +[Phase 4] ZKdo3vLUqcb4sYreiCPHMu: coverImage [cloudinary.asset] → image ref image-ef9ce8c5d03b6a3d55767d161439cf9b75189662-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPHMu +[Phase 4] ZKdo3vLUqcb4sYreiCPHW6: coverImage [cloudinary.asset] → image ref image-24c1db5904aa2d8a5062a56ac6809c4c58ac172f-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPHW6 +[Phase 4] ZKdo3vLUqcb4sYreiCPHfI: coverImage [cloudinary.asset] → image ref image-5025b84c224a43867c61a0a83063d3bb04ce97af-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPHfI +[Phase 4] ZKdo3vLUqcb4sYreiCPHoU: coverImage [cloudinary.asset] → image ref image-d523017127a68b6588da474d611883d6d54de86d-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPHoU +[Phase 4] ZKdo3vLUqcb4sYreiCPHxg: coverImage [cloudinary.asset] → image ref image-24a8af8c65f71e0b31a87272063c6fdcdae6c33a-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPHxg +[Phase 4] ZKdo3vLUqcb4sYreiCPI6s: coverImage [cloudinary.asset] → image ref image-b467ce9ac4f514e944e3d2af04fb1285bbeb6908-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPI6s +[Phase 4] ZKdo3vLUqcb4sYreiCPIG4: coverImage [cloudinary.asset] → image ref image-e276ebaf83daea241b96b2f1d1f372934413b34d-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPIG4 +[Phase 4] ZKdo3vLUqcb4sYreiCPIPG: coverImage [cloudinary.asset] → image ref image-7f349536c704502001ced3a01b4b0ba952741c2c-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPIPG +[Phase 4] ZKdo3vLUqcb4sYreiCPIYS: coverImage [cloudinary.asset] → image ref image-aa33dbdbb99fcf18e007e339648e03e7b230a76b-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPIYS +[Phase 4] ZKdo3vLUqcb4sYreiCPIhe: coverImage [cloudinary.asset] → image ref image-96a4d093a6575354e750ebf8b78abaf038d201db-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPIhe +[Phase 4] ZKdo3vLUqcb4sYreiCPIqq: coverImage [cloudinary.asset] → image ref image-53e5334d4e6af6e712b7064739d639b9ab8368b1-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPIqq +[Phase 4] ZKdo3vLUqcb4sYreiCPJ6A: coverImage [cloudinary.asset] → image ref image-9dd03afc201190ce96f5f04df5d0913cbf45a354-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPJ6A +[Phase 4] ZKdo3vLUqcb4sYreiCPJFM: coverImage [cloudinary.asset] → image ref image-b44b74db403dee54dff9f36013aeb173c45cea15-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPJFM +[Phase 4] ZKdo3vLUqcb4sYreiCPJOY: coverImage [cloudinary.asset] → image ref image-29564005a2da9cd3321b446e5fcd6647d5fb05cc-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPJOY +[Phase 4] ZKdo3vLUqcb4sYreiCPJXk: coverImage [cloudinary.asset] → image ref image-7da652cea28baa561224ed09d5ee118604c2d5b2-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPJXk +[Phase 4] ZKdo3vLUqcb4sYreiCPJtC: coverImage [cloudinary.asset] → image ref image-843d74ba2b3be885c3b1d91f396f4b2e98ab50a4-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPJtC +[Phase 4] ZKdo3vLUqcb4sYreiCPK2O: coverImage [cloudinary.asset] → image ref image-1c30255287eb5e57cef6c748fc2e1c94d1fc77a3-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPK2O +[Phase 4] ZKdo3vLUqcb4sYreiCPKBa: coverImage [cloudinary.asset] → image ref image-fa73af5c311213b9903b2e758a357938687cc102-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPKBa +[Phase 4] ZKdo3vLUqcb4sYreiCPL7o: coverImage [cloudinary.asset] → image ref image-7c5142b342ec21390b0e6d2336f5e33d3f5567f6-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPL7o +[Phase 4] ZKdo3vLUqcb4sYreiCPLH0: coverImage [cloudinary.asset] → image ref image-9d453ae001323bf88b6e10676c5557fe00e08a0c-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPLH0 +[Phase 4] ZKdo3vLUqcb4sYreiCPLQC: coverImage [cloudinary.asset] → image ref image-8d610eb34585924330f3ecb5d7725e06bb1857e4-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPLQC +[Phase 4] ZKdo3vLUqcb4sYreiCPLZO: coverImage [cloudinary.asset] → image ref image-0df694be5526d884d0c2714620dd1e254d5e6e72-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPLZO +[Phase 4] ZKdo3vLUqcb4sYreiCPLia: coverImage [cloudinary.asset] → image ref image-736c82f42a377b098d891890cdeafdb57525d08c-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPLia +[Phase 4] ZKdo3vLUqcb4sYreiCPLrm: coverImage [cloudinary.asset] → image ref image-bc1237eb727c0f9e14f914e6ce24f88d45e2dc40-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPLrm +[Phase 4] ZKdo3vLUqcb4sYreiCPM0y: coverImage [cloudinary.asset] → image ref image-7746cb172298c612bd72301b08e9132630d03dd1-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPM0y +[Phase 4] ZKdo3vLUqcb4sYreiCPMAA: coverImage [cloudinary.asset] → image ref image-644976f04c36dffa908d02c69741a06409f7fb67-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPMAA +[Phase 4] ZKdo3vLUqcb4sYreiCPMJM: coverImage [cloudinary.asset] → image ref image-04c031345cbb62314a2442bdb36b4eb9d1e8eaf1-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPMJM +[Phase 4] ZKdo3vLUqcb4sYreiCPMSY: coverImage [cloudinary.asset] → image ref image-5262cf39a1c1504ac6eb21410273ee077ebcb378-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPMSY +[Phase 4] ZKdo3vLUqcb4sYreiCPMbk: coverImage [cloudinary.asset] → image ref image-03d4e36e859ec6092bc7269ba2e14ba3b8410928-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPMbk +[Phase 4] ZKdo3vLUqcb4sYreiCPMkw: coverImage [cloudinary.asset] → image ref image-1607e51c512268f326acf005317a3d9ff8ee5a3b-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPMkw +[Phase 4] ZKdo3vLUqcb4sYreiCPMu8: coverImage [cloudinary.asset] → image ref image-6144a13d22d63733782b520a7a360f0215de113a-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPMu8 +[Phase 4] ZKdo3vLUqcb4sYreiCPN3K: coverImage [cloudinary.asset] → image ref image-e2358e6ae438f8af356dd81b46cffbe07ec25f0f-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPN3K +[Phase 4] ZKdo3vLUqcb4sYreiCPNCW: coverImage [cloudinary.asset] → image ref image-8d60cc4b069e7b29e2c636321e3d6fb3503295e7-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPNCW +[Phase 4] ZKdo3vLUqcb4sYreiCPNLi: coverImage [cloudinary.asset] → image ref image-45451c7f5f930a090d6b0e76538ef138987174e1-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPNLi +[Phase 4] ZKdo3vLUqcb4sYreiCPNUu: coverImage [cloudinary.asset] → image ref image-19aff6d7dcef4db7ab33863ac98d3a40714df018-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPNUu +[Phase 4] ZKdo3vLUqcb4sYreiCPNe6: coverImage [cloudinary.asset] → image ref image-046b909d79f07624af525f7131af64525728cabe-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPNe6 +[Phase 4] ZKdo3vLUqcb4sYreiCPNnI: coverImage [cloudinary.asset] → image ref image-1748265af5fef5def7fea9aadb970507a30d1f8f-1920x1080-jpg +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPNnI +[Phase 4] ZKdo3vLUqcb4sYreiCPO5g: coverImage [cloudinary.asset] → image ref image-9057dcae2c3083d8ee4e89924e8a62ea910b8890-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPO5g +[Phase 4] ZKdo3vLUqcb4sYreiCPOEs: coverImage [cloudinary.asset] → image ref image-de947917268702f04e1e8eff044d0c028fa755c1-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPOEs +[Phase 4] ZKdo3vLUqcb4sYreiCPOO4: coverImage [cloudinary.asset] → image ref image-b2a1c016cb2c29fe14ccf9b63c57d6e9386aa145-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPOO4 +[Phase 4] ZKdo3vLUqcb4sYreiCPOXG: coverImage [cloudinary.asset] → image ref image-709cd31089b10f57e06abeff02b7266a61b19b0d-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPOXG +[Phase 4] ZKdo3vLUqcb4sYreiCPP4y: coverImage [cloudinary.asset] → image ref image-df6ab7a43c0040f24f552d5488f4e5a6fd07e149-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPP4y +[Phase 4] ZKdo3vLUqcb4sYreiCPPEA: coverImage [cloudinary.asset] → image ref image-10efec187206f0d39eaeb838b367ee691cc154a4-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPPEA +[Phase 4] ZKdo3vLUqcb4sYreiCPPNM: coverImage [cloudinary.asset] → image ref image-7cab5a5a4b0795f6ab935e9b41d8f0d31d093535-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPPNM +[Phase 4] ZKdo3vLUqcb4sYreiCPPZc: coverImage [cloudinary.asset] → image ref image-af7ce83a1559a39950c561e31062b0e81b7b925a-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPPZc +[Phase 4] ZKdo3vLUqcb4sYreiCPPio: coverImage [cloudinary.asset] → image ref image-009b655d07154e28e0a7736e0683f61a011390bd-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPPio +[Phase 4] ZKdo3vLUqcb4sYreiCPPs0: coverImage [cloudinary.asset] → image ref image-f4cd4cba0125a1a2887be39dfb024209e79cd439-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPPs0 +[Phase 4] ZKdo3vLUqcb4sYreiCPQby: coverImage [cloudinary.asset] → image ref image-610a59daeb7777e7723fd543004c4d5e83431101-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPQby +[Phase 4] ZKdo3vLUqcb4sYreiCPRkS: coverImage [cloudinary.asset] → image ref image-c5530576f46ed0587714af973066df8e0583b06d-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPRkS +[Phase 4] ZKdo3vLUqcb4sYreiCPRte: coverImage [cloudinary.asset] → image ref image-5febb50ae39284c22bdb43dc525ae5fdaedbaa9a-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPRte +[Phase 4] ZKdo3vLUqcb4sYreiCPS2q: coverImage [cloudinary.asset] → image ref image-c010f3b5b43aaa88556f5fb6123d61f2ffa01b01-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPS2q +[Phase 4] ZKdo3vLUqcb4sYreiCPSC2: coverImage [cloudinary.asset] → image ref image-2d68f449c62af8ea1ac6f369c8e9c5f99777f574-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPSC2 +[Phase 4] ZKdo3vLUqcb4sYreiCPSLE: coverImage [cloudinary.asset] → image ref image-7d3977f54bf16c87c46a1beef484cada62c16923-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPSLE +[Phase 4] ZKdo3vLUqcb4sYreiCPSUQ: coverImage [cloudinary.asset] → image ref image-2e3a02b625146e6099fde4c9dff438a1933e1b4d-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPSUQ +[Phase 4] ZKdo3vLUqcb4sYreiCPSdc: coverImage [cloudinary.asset] → image ref image-2fea4cd44d8d8fd09f3f3ef0af1a01072103f69d-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPSdc +[Phase 4] ZKdo3vLUqcb4sYreiCPSmo: coverImage [cloudinary.asset] → image ref image-9e5538fac8ed46b66e3e3e50d1347669c826a7d9-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPSmo +[Phase 4] ZKdo3vLUqcb4sYreiCPSw0: coverImage [cloudinary.asset] → image ref image-212028e3fef8f64e9471806d7d26d0999a93063e-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPSw0 +[Phase 4] ZKdo3vLUqcb4sYreiCPT5C: coverImage [cloudinary.asset] → image ref image-da2928acb25d72f7023b4d120c5c1daf6aca3e34-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPT5C +[Phase 4] ZKdo3vLUqcb4sYreiCPTEO: coverImage [cloudinary.asset] → image ref image-3afec45da64654865d96bd2673880ca6a2e16a74-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPTEO +[Phase 4] ZKdo3vLUqcb4sYreiCPTNa: coverImage [cloudinary.asset] → image ref image-97e9213b6bff5259b7f12fc69908f300fcee99c5-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPTNa +[Phase 4] ZKdo3vLUqcb4sYreiCPTWm: coverImage [cloudinary.asset] → image ref image-f3e1a12bddee913446b424e66fbbd75220fdb5a6-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPTWm +[Phase 4] ZKdo3vLUqcb4sYreiCPTfy: coverImage [cloudinary.asset] → image ref image-9be71c92a88bc5fb120fa8dcc3ef8f1b6581ecb7-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPTfy +[Phase 4] ZKdo3vLUqcb4sYreiCPTpA: coverImage [cloudinary.asset] → image ref image-eb3c49cfe38d7a95e1d8a47289387b571f697e02-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPTpA +[Phase 4] ZKdo3vLUqcb4sYreiCPTyM: coverImage [cloudinary.asset] → image ref image-ecc37088c7cd9ce37c1790a8fc6393c956271cf6-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPTyM +[Phase 4] ZKdo3vLUqcb4sYreiCPU7Y: coverImage [cloudinary.asset] → image ref image-0f0c46e79995ee69acf9a45fde9165e8319642d3-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPU7Y +[Phase 4] ZKdo3vLUqcb4sYreiCPUGk: coverImage [cloudinary.asset] → image ref image-ee6b49a7ff8b7966607a7e397a1b0bb91a3b742c-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPUGk +[Phase 4] ZKdo3vLUqcb4sYreiCPUPw: coverImage [cloudinary.asset] → image ref image-ba7df7dd2d5e5ad0a058f96583bcfa2664b21d44-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPUPw +[Phase 4] ZKdo3vLUqcb4sYreiCPUZ8: coverImage [cloudinary.asset] → image ref image-135fd1bd7295a7173ec25208317befeb9fda3ab6-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPUZ8 +[Phase 4] ZKdo3vLUqcb4sYreiCPUiK: coverImage [cloudinary.asset] → image ref image-3933e331d455eb073f0105de1faf0588ff478a3f-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPUiK +[Phase 4] ZKdo3vLUqcb4sYreiCPUrW: coverImage [cloudinary.asset] → image ref image-368eb23766409dbbf6025172f5e7178875833875-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPUrW +[Phase 4] ZKdo3vLUqcb4sYreiCPV0i: coverImage [cloudinary.asset] → image ref image-c0ceae6b08b279593092c23b3de9ca33c50cbcfc-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPV0i +[Phase 4] ZKdo3vLUqcb4sYreiCPV9u: coverImage [cloudinary.asset] → image ref image-628c0895869e7f3e49a9253a5c5f72f2b8ffecb9-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPV9u +[Phase 4] ZKdo3vLUqcb4sYreiCPVJ6: coverImage [cloudinary.asset] → image ref image-ca193facbbc516ab21b53ab5bf94d8a48c2d007d-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPVJ6 +[Phase 4] ZKdo3vLUqcb4sYreiCPVSI: coverImage [cloudinary.asset] → image ref image-53e9ff3d86e5cefe91650bcaaf1bdb04232aa619-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPVSI +[Phase 4] ZKdo3vLUqcb4sYreiCPVbU: coverImage [cloudinary.asset] → image ref image-a153842b3a5b54b7f72326ea0027885ec4a1493e-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPVbU +[Phase 4] ZKdo3vLUqcb4sYreiCPVkg: coverImage [cloudinary.asset] → image ref image-9723fa4063e2cdf122862c1bdbd95225bc7ce5cd-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPVkg +[Phase 4] ZKdo3vLUqcb4sYreiCPVts: coverImage [cloudinary.asset] → image ref image-995ecf3d8e7e286b1d3650c4ef34c8f1a87df7f7-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPVts +[Phase 4] ZKdo3vLUqcb4sYreiCPW34: coverImage [cloudinary.asset] → image ref image-933224e9d3b7fd471896c99dcdf340de2d6a42fc-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPW34 +[Phase 4] ZKdo3vLUqcb4sYreiCPWCG: coverImage [cloudinary.asset] → image ref image-2691a24b52e6a655f49338a9602998b9d2e2d9c1-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPWCG +[Phase 4] ZKdo3vLUqcb4sYreiCPWLS: coverImage [cloudinary.asset] → image ref image-149aa0d3ba55935a4bfb8c84913643403d424458-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPWLS +[Phase 4] ZKdo3vLUqcb4sYreiCPWXi: coverImage [cloudinary.asset] → image ref image-a2e8a72e363dcb5866adebe2b62d47cb09425321-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPWXi +[Phase 4] ZKdo3vLUqcb4sYreiCPWgu: coverImage [cloudinary.asset] → image ref image-f74d827f531e688c8ac022cdf695bdebb067ab34-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPWgu +[Phase 4] ZKdo3vLUqcb4sYreiCPWq6: coverImage [cloudinary.asset] → image ref image-05af6c518affbc8643ef1db5e67afaa65a4a0d61-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPWq6 +[Phase 4] ZKdo3vLUqcb4sYreiCPX8U: coverImage [cloudinary.asset] → image ref image-0753d61f0436a4d52a78c893aaabdee468aac58c-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPX8U +[Phase 4] ZKdo3vLUqcb4sYreiCPXNo: coverImage [cloudinary.asset] → image ref image-e43fbdceae2ad533c194fae83cbf21f99115cf2e-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPXNo +[Phase 4] ZKdo3vLUqcb4sYreiCPXd8: coverImage [cloudinary.asset] → image ref image-84e778dffb92b4758c340627b0e26ccfcb8379f1-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPXd8 +[Phase 4] ZKdo3vLUqcb4sYreiCPXmK: coverImage [cloudinary.asset] → image ref image-b45c0560f68a2b5a0f0d0b4050e09cef2d2912cc-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPXmK +[Phase 4] ZKdo3vLUqcb4sYreiCPY4i: coverImage [cloudinary.asset] → image ref image-664511ceed94cd13a7cc426a81171a8853b56f10-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPY4i +[Phase 4] ZKdo3vLUqcb4sYreiCPYDu: coverImage [cloudinary.asset] → image ref image-2d0950d46c238cf82ca35a3f85759beb1644e053-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPYDu +[Phase 4] ZKdo3vLUqcb4sYreiCPYN6: coverImage [cloudinary.asset] → image ref image-4e335d3f72b6b42fb7c7c67a7fed6b707ca126d9-1920x1080-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPYN6 +[Phase 4] ZKdo3vLUqcb4sYreiCPYfU: coverImage [cloudinary.asset] → image ref image-c3b7247927f88234a1f7e416f3034b37b6ee611f-1920x1081-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPYfU +[Phase 4] ZKdo3vLUqcb4sYreiCPYog: coverImage [cloudinary.asset] → image ref image-c3b7247927f88234a1f7e416f3034b37b6ee611f-1920x1081-png +[Phase 4] ✓ Committed changes for ZKdo3vLUqcb4sYreiCPYog +[Phase 4] b61bf55d-ab3d-4164-8a35-f5f7eb770536: coverImage [cloudinary.asset] → image ref image-3d5fa42c489cd7f673ce2c4aa96db4ef5a31aa72-1920x1080-png +[Phase 4] ✓ Committed changes for b61bf55d-ab3d-4164-8a35-f5f7eb770536 +[Phase 4] drafts.RIgC5r4TJ0hCGOzxIBATCk: coverImage [cloudinary.asset] → image ref image-02525d90e4d91151568532e41eabc52982d4c281-1921x1080-png +[Phase 4] ✓ Committed changes for drafts.RIgC5r4TJ0hCGOzxIBATCk +[Phase 4] drafts.RIgC5r4TJ0hCGOzxIBDguH: coverImage [cloudinary.asset] → image ref image-c89927147885230ea6b3b5802bd574097b4d36b4-1920x1080-png +[Phase 4] ✓ Committed changes for drafts.RIgC5r4TJ0hCGOzxIBDguH +[Phase 4] drafts.RIgC5r4TJ0hCGOzxIBDhOl: coverImage [cloudinary.asset] → image ref image-675755017d489fd695f454dd31421831c9ae4ff8-1920x1080-png +[Phase 4] ✓ Committed changes for drafts.RIgC5r4TJ0hCGOzxIBDhOl +[Phase 4] drafts.RIgC5r4TJ0hCGOzxIBDjHc: coverImage [cloudinary.asset] → image ref image-2a43eccd308f1d4f6abe10a2b1d13951fd4f4280-1920x1080-png +[Phase 4] ✓ Committed changes for drafts.RIgC5r4TJ0hCGOzxIBDjHc +[Phase 4] drafts.ZKdo3vLUqcb4sYreiCMbUO: coverImage [cloudinary.asset] → image ref image-7713255dd24259b737b575b89d1fe7508d28c0c6-1920x1080-png +[Phase 4] ✓ Committed changes for drafts.ZKdo3vLUqcb4sYreiCMbUO +[Phase 4] drafts.ZKdo3vLUqcb4sYreiCMcm4: coverImage [cloudinary.asset] → image ref image-01546d01f7b69c501bc27be3337d984479b3d12a-1920x1080-png +[Phase 4] ✓ Committed changes for drafts.ZKdo3vLUqcb4sYreiCMcm4 +[Phase 4] drafts.ZKdo3vLUqcb4sYreiCPCB8: coverImage [cloudinary.asset] → image ref image-cc425b6346b3a804a920d5f90b51f9584921748e-1920x1080-jpg +[Phase 4] ✓ Committed changes for drafts.ZKdo3vLUqcb4sYreiCPCB8 +[Phase 4] +Phase 4 complete: 451 documents, 456 fields updated, 3 skipped +[Phase 5] ── Migration Report ── + +══════════════════════════════════════════════════════════ + MIGRATION SUMMARY +══════════════════════════════════════════════════════════ + Mode: LIVE + Documents with refs: 451 + Total references found: 459 + cloudinary.asset objects: 0 + raw Cloudinary objects: 454 + URL string fields: 0 + Embedded URLs in text: 5 + Unique Cloudinary URLs: 436 + Assets uploaded to Sanity: 433 + Document fields updated: 456 + Errors: 0 +══════════════════════════════════════════════════════════ + +Detailed report saved to /home/daytona/codingcat.dev/scripts/migration/migration-report.json From 7b1f3f3c5befbf62110a54a0fcdef1e9adbdc1be Mon Sep 17 00:00:00 2001 From: Miriad Date: Wed, 4 Mar 2026 19:29:06 +0000 Subject: [PATCH 7/8] chore: add orphan asset cleanup script 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 --- scripts/migration/cleanup-orphans.mjs | 165 ++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 scripts/migration/cleanup-orphans.mjs diff --git a/scripts/migration/cleanup-orphans.mjs b/scripts/migration/cleanup-orphans.mjs new file mode 100644 index 00000000..f7350866 --- /dev/null +++ b/scripts/migration/cleanup-orphans.mjs @@ -0,0 +1,165 @@ +import { createClient } from '@sanity/client'; +import dotenv from 'dotenv'; +import fs from 'fs'; + +dotenv.config(); + +const client = createClient({ + projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID, + dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || 'dev', + apiVersion: '2024-01-01', + token: process.env.SANITY_API_WRITE_TOKEN, + useCdn: false, +}); + +const DRY_RUN = process.argv.includes('--dry-run'); + +async function main() { + console.log(`\n🧹 Orphan Asset Cleanup${DRY_RUN ? ' (DRY RUN)' : ''}\n`); + + // 1. Load the current mapping to get the set of GOOD asset IDs + const mapping = JSON.parse(fs.readFileSync('asset-mapping.json', 'utf-8')); + const activeAssetIds = new Set(mapping.map(m => m.sanityAssetId)); + console.log(`Active assets (keep): ${activeAssetIds.size}`); + + // 2. Query ALL image and file assets from Sanity + // Use pagination to handle large datasets + let allAssets = []; + let lastId = ''; + + while (true) { + const batch = await client.fetch( + `*[_type in ["sanity.imageAsset", "sanity.fileAsset"] && _id > $lastId] | order(_id) [0...1000] { _id, _type, originalFilename, size }`, + { lastId } + ); + if (batch.length === 0) break; + allAssets = allAssets.concat(batch); + lastId = batch[batch.length - 1]._id; + console.log(` Fetched ${allAssets.length} assets so far...`); + } + + console.log(`Total assets in Sanity: ${allAssets.length}`); + + // 3. Separate into mapped (active) and candidates for deletion + const candidates = []; + const keptByMapping = []; + + for (const asset of allAssets) { + if (activeAssetIds.has(asset._id)) { + keptByMapping.push(asset._id); + } else { + candidates.push(asset); + } + } + + console.log(`\nAssets in mapping (auto-keep): ${keptByMapping.length}`); + console.log(`Candidates to check for references: ${candidates.length}`); + + // 4. Batch-check references for candidates + // Query all non-asset documents that reference any asset, then build a set of referenced asset IDs + console.log(`\nChecking which candidates are referenced by documents...`); + + // Get all asset IDs that are referenced by at least one non-asset document + // We do this by querying documents (not assets) and extracting their asset references + const referencedAssetIds = new Set(); + + // Check in batches of 50 candidates at a time using parallel queries + const BATCH_SIZE = 50; + let checked = 0; + + for (let i = 0; i < candidates.length; i += BATCH_SIZE) { + const batch = candidates.slice(i, i + BATCH_SIZE); + const ids = batch.map(a => a._id); + + // For each batch, check which IDs have references + const results = await Promise.all( + ids.map(id => + client.fetch(`count(*[references($id)])`, { id }) + .then(count => ({ id, count })) + ) + ); + + for (const { id, count } of results) { + if (count > 0) { + referencedAssetIds.add(id); + } + } + + checked += batch.length; + if (checked % 200 === 0 || checked === candidates.length) { + console.log(` Checked ${checked}/${candidates.length} candidates (${referencedAssetIds.size} referenced so far)...`); + } + } + + // 5. Build final orphan list + const orphans = []; + const kept = [...keptByMapping]; + + for (const asset of candidates) { + if (referencedAssetIds.has(asset._id)) { + console.log(` ⚠️ Keeping ${asset._id} — referenced by document(s)`); + kept.push(asset._id); + } else { + orphans.push(asset); + } + } + + console.log(`\nOrphans to delete: ${orphans.length}`); + console.log(`Assets to keep: ${kept.length}`); + + // 6. Delete orphans in batches + if (orphans.length === 0) { + console.log('No orphans found! 🎉'); + return; + } + + if (DRY_RUN) { + console.log('\n🔍 DRY RUN — would delete these orphans:'); + // Just show first 20 + for (const orphan of orphans.slice(0, 20)) { + console.log(` ${orphan._id} (${(orphan.size / 1024).toFixed(1)} KB)`); + } + if (orphans.length > 20) { + console.log(` ... and ${orphans.length - 20} more`); + } + const totalSize = orphans.reduce((sum, o) => sum + (o.size || 0), 0); + console.log(`\nTotal space to reclaim: ${(totalSize / 1024 / 1024).toFixed(1)} MB`); + return; + } + + // Delete in batches of 100 using transactions + const DEL_BATCH_SIZE = 100; + let deleted = 0; + + for (let i = 0; i < orphans.length; i += DEL_BATCH_SIZE) { + const batch = orphans.slice(i, i + DEL_BATCH_SIZE); + const tx = client.transaction(); + + for (const orphan of batch) { + tx.delete(orphan._id); + } + + try { + await tx.commit(); + deleted += batch.length; + console.log(` Deleted ${deleted}/${orphans.length} orphans...`); + } catch (err) { + console.error(` Error deleting batch: ${err.message}`); + // Try one by one for this batch + for (const orphan of batch) { + try { + await client.delete(orphan._id); + deleted++; + } catch (e) { + console.error(` Failed to delete ${orphan._id}: ${e.message}`); + } + } + } + } + + console.log(`\n✅ Deleted ${deleted} orphan assets`); + const totalSize = orphans.reduce((sum, o) => sum + (o.size || 0), 0); + console.log(`Space reclaimed: ${(totalSize / 1024 / 1024).toFixed(1)} MB`); +} + +main().catch(console.error); From 06c1f8dd57ea6b00fdbc899907c0b357912772cd Mon Sep 17 00:00:00 2001 From: Alex Patterson Date: Wed, 4 Mar 2026 16:32:18 -0500 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20RSS=20feed=20improvements=20?= =?UTF-8?q?=E2=80=94=20podcast=20iTunes=20support,=20proper=20enclosures,?= =?UTF-8?q?=20content-type=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/(main)/(course)/courses/rss.xml/route.ts | 2 +- .../(podcast)/podcasts/rss.xml/route.ts | 11 +- app/(main)/(post)/blog/rss.xml/route.ts | 2 +- app/api/youtube/rss.xml/route.tsx | 5 +- lib/rss.ts | 216 ++++++++++++++++-- sanity/lib/queries.ts | 6 + 6 files changed, 215 insertions(+), 27 deletions(-) diff --git a/app/(main)/(course)/courses/rss.xml/route.ts b/app/(main)/(course)/courses/rss.xml/route.ts index 0cb7df89..e0f1a1dd 100644 --- a/app/(main)/(course)/courses/rss.xml/route.ts +++ b/app/(main)/(course)/courses/rss.xml/route.ts @@ -9,7 +9,7 @@ export async function GET() { }); return new Response(feed.rss2(), { headers: { - "content-type": "text/xml", + "content-type": "application/rss+xml; charset=utf-8", "cache-control": "max-age=0, s-maxage=3600", }, }); diff --git a/app/(main)/(podcast)/podcasts/rss.xml/route.ts b/app/(main)/(podcast)/podcasts/rss.xml/route.ts index 2b93d7a8..d28aca1d 100644 --- a/app/(main)/(podcast)/podcasts/rss.xml/route.ts +++ b/app/(main)/(podcast)/podcasts/rss.xml/route.ts @@ -1,15 +1,12 @@ export const dynamic = "force-dynamic"; // defaults to auto -import { buildFeed } from "@/lib/rss"; -import { ContentType } from "@/lib/types"; +import { buildPodcastFeed } from "@/lib/rss"; export async function GET() { - const feed = await buildFeed({ - type: ContentType.podcast, - }); - return new Response(feed.rss2(), { + const xml = await buildPodcastFeed({}); + return new Response(xml, { headers: { - "content-type": "text/xml", + "content-type": "application/rss+xml; charset=utf-8", "cache-control": "max-age=0, s-maxage=3600", }, }); diff --git a/app/(main)/(post)/blog/rss.xml/route.ts b/app/(main)/(post)/blog/rss.xml/route.ts index 4f76d3b3..76085776 100644 --- a/app/(main)/(post)/blog/rss.xml/route.ts +++ b/app/(main)/(post)/blog/rss.xml/route.ts @@ -9,7 +9,7 @@ export async function GET() { }); return new Response(feed.rss2(), { headers: { - "content-type": "text/xml", + "content-type": "application/rss+xml; charset=utf-8", "cache-control": "max-age=0, s-maxage=3600", }, }); diff --git a/app/api/youtube/rss.xml/route.tsx b/app/api/youtube/rss.xml/route.tsx index 6311262e..880d42b1 100644 --- a/app/api/youtube/rss.xml/route.tsx +++ b/app/api/youtube/rss.xml/route.tsx @@ -63,8 +63,7 @@ export async function GET() { updated: new Date(), generator: "Next.js using Feed for Node.js", feedLinks: { - json: `${process.env.NEXT_PUBLIC_BASE_URL}/api/podcast-feed`, - atom: `${process.env.NEXT_PUBLIC_BASE_URL}/api/podcast-feed?format=atom`, + rss2: `${process.env.NEXT_PUBLIC_BASE_URL || "https://codingcat.dev"}/api/youtube/rss.xml`, }, }); @@ -93,7 +92,7 @@ export async function GET() { return new Response(feed.rss2(), { headers: { - "content-type": "text/xml", + "content-type": "application/rss+xml; charset=utf-8", "cache-control": "max-age=0, s-maxage=3600", }, }); diff --git a/lib/rss.ts b/lib/rss.ts index fa85a29d..8797c03c 100644 --- a/lib/rss.ts +++ b/lib/rss.ts @@ -1,7 +1,7 @@ -import { Feed, type Author as FeedAuthor } from "feed"; +import { Feed, type Author as FeedAuthor, type Item } from "feed"; import { sanityFetch } from "@/sanity/lib/live"; import type { RssQueryResult } from "@/sanity/types"; -import { rssQuery } from "@/sanity/lib/queries"; +import { rssQuery, rssPodcastQuery } from "@/sanity/lib/queries"; import { toHTML } from "@portabletext/to-html"; import { urlForImage } from "@/sanity/lib/utils"; @@ -10,15 +10,32 @@ const site = productionDomain ? `https://${productionDomain}` : "https://codingcat.dev"; +/** Map Sanity _type to the URL path segment used on the site */ +function typePath(type: string): string { + switch (type) { + case "post": + return "blog"; + case "podcast": + return "podcasts"; + case "course": + return "courses"; + default: + return type + "s"; + } +} + export async function buildFeed(params: { type: string; skip?: string; limit?: number; offset?: number; }) { + const isPodcast = params.type === "podcast"; + const query = isPodcast ? rssPodcastQuery : rssQuery; + const data = ( await sanityFetch({ - query: rssQuery, + query, params: { type: params.type, skip: params.skip || "none", @@ -28,19 +45,22 @@ export async function buildFeed(params: { }) ).data as RssQueryResult; + const feedPath = typePath(params.type); + const currentYear = new Date().getFullYear(); + const feed = new Feed({ - title: `${site} - ${params.type} feed`, - description: `${site} - ${params.type} feed`, + title: `CodingCat.dev - ${params.type} feed`, + description: `CodingCat.dev - ${params.type} feed`, id: `${site}`, - link: `${site}`, - language: "en", // optional, used only in RSS 2.0, possible values: http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes - image: - "https://media.codingcat.dev/image/upload/f_png,c_thumb,g_face,w_1200,h_630/dev-codingcatdev-photo/v60h88eohd7ufghkspgo.png", + link: `${site}/${feedPath}`, + language: "en", + image: `${site}/icon.svg`, favicon: `${site}/favicon.ico`, - copyright: `All rights reserved 2021, ${site}`, + copyright: `All rights reserved ${currentYear}, CodingCat.dev`, updated: new Date(), feedLinks: { - rss2: `${site}/blog/rss.xml`, + rss2: `${site}/${feedPath}/rss.xml`, + json: `${site}/${feedPath}/rss.json`, }, author: { name: "Alex Patterson", @@ -50,13 +70,16 @@ export async function buildFeed(params: { }); for (const item of data) { - feed.addItem({ + const imageUrl = + urlForImage(item.coverImage)?.width(1200).height(630).url() || undefined; + + const feedItem: Item = { title: item.title || "", content: item.content && Array.isArray(item.content) ? toHTML(item.content) : "", link: `${site}/${item._type}/${item.slug}`, - description: `${item.excerpt}`, - image: urlForImage(item.coverImage)?.width(1200).height(630).url() || feed.items.at(0)?.image, + description: item.excerpt || "", + image: imageUrl, date: item.date ? new Date(item.date) : new Date(), id: item._id, author: item.author @@ -71,7 +94,170 @@ export async function buildFeed(params: { link: `${site}/author/alex-patterson`, }, ], - }); + }; + + // Add podcast enclosure from Spotify RSS data if available + if (isPodcast && "spotify" in item && (item as any).spotify) { + const spotify = (item as any).spotify; + const enclosures = spotify.enclosures; + if (Array.isArray(enclosures) && enclosures.length > 0) { + const enc = enclosures[0]; + if (enc.url) { + feedItem.enclosure = { + url: enc.url, + length: enc.length || 0, + type: enc.type || "audio/mpeg", + }; + } + } + // Add audio URL as fallback if no enclosure but link exists + if (!feedItem.enclosure && spotify.link) { + feedItem.audio = spotify.link; + } + } + + feed.addItem(feedItem); } + return feed; } + +/** + * Build a podcast-specific RSS feed with iTunes namespace tags. + * Returns raw XML string with proper iTunes/podcast namespace support. + */ +export async function buildPodcastFeed(params: { + skip?: string; + limit?: number; + offset?: number; +}): Promise { + const data = ( + await sanityFetch({ + query: rssPodcastQuery, + params: { + type: "podcast", + skip: params.skip || "none", + limit: params.limit || 10000, + offset: params.offset || 0, + }, + }) + ).data as RssQueryResult; + + const currentYear = new Date().getFullYear(); + const feedUrl = `${site}/podcasts/rss.xml`; + const feedImage = `${site}/icon.svg`; + + // Build RSS 2.0 XML with iTunes namespace manually for full podcast support + const items = data + .map((item) => { + const imageUrl = + urlForImage(item.coverImage)?.width(1400).height(1400).url() || feedImage; + const pubDate = item.date + ? new Date(item.date).toUTCString() + : new Date().toUTCString(); + const link = `${site}/${item._type}/${item.slug}`; + const description = escapeXml(item.excerpt || ""); + const title = escapeXml(item.title || ""); + + let enclosureXml = ""; + let itunesXml = ""; + let itunesDuration = ""; + let itunesSeason = ""; + let itunesEpisode = ""; + let itunesEpisodeType = "full"; + + // Extract podcast-specific fields + const podcastItem = item as any; + if (podcastItem.spotify) { + const spotify = podcastItem.spotify; + if ( + Array.isArray(spotify.enclosures) && + spotify.enclosures.length > 0 + ) { + const enc = spotify.enclosures[0]; + if (enc.url) { + enclosureXml = `\n `; + } + } + if (spotify.itunes) { + const it = spotify.itunes; + if (it.duration) itunesDuration = it.duration; + if (it.episodeType) itunesEpisodeType = it.episodeType; + if (it.explicit) + itunesXml += `\n ${escapeXml(it.explicit)}`; + if (it.summary) + itunesXml += `\n ${escapeXml(it.summary)}`; + if (it.image?.href) + itunesXml += `\n `; + } + } + + if (podcastItem.season) { + itunesSeason = `\n ${podcastItem.season}`; + } + if (podcastItem.episode) { + itunesEpisode = `\n ${podcastItem.episode}`; + } + + const authors = item.author + ? item.author.map((a) => a.title).join(", ") + : "Alex Patterson"; + + return ` + ${title} + ${link} + ${item._id} + ${pubDate} + + ${escapeXml(authors)} + ${enclosureXml}${title} + ${escapeXml(authors)} + ${itunesSeason}${itunesEpisode} + ${itunesEpisodeType}${itunesDuration ? `\n ${escapeXml(itunesDuration)}` : ""}${itunesXml} + `; + }) + .join("\n"); + + const lastBuildDate = new Date().toUTCString(); + + return ` + + + CodingCat.dev Podcast + ${site}/podcasts + The CodingCat.dev Podcast features conversations about web development, design, and technology with industry experts and community members. + en + ${lastBuildDate} + + All rights reserved ${currentYear}, CodingCat.dev + Alex Patterson + + Alex Patterson + alex@codingcat.dev + + + + false + episodic + + ${feedImage} + CodingCat.dev Podcast + ${site}/podcasts + +${items} + +`; +} + +function escapeXml(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/sanity/lib/queries.ts b/sanity/lib/queries.ts index 5ab48f1d..ac5537ca 100644 --- a/sanity/lib/queries.ts +++ b/sanity/lib/queries.ts @@ -315,6 +315,12 @@ export const rssQuery = groq`*[_type == $type && _id != $skip && defined(slug.cu ${contentFields}, }`; +export const rssPodcastQuery = groq`*[_type == "podcast" && _id != $skip && defined(slug.current)] | order(date desc) [$offset...$limit] { + ${baseFieldsNoContent}, + ${contentFields}, + ${podcastFields}, +}`; + // Sitemaps export const sitemapQuery = groq`*[_type in ["author", "course", "guest", "page", "podcast", "post", "sponsor"] && defined(slug.current)] | order(_type asc) | order(_updated desc) { _type,