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/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/(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/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/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/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 51b508a5..f803a6c1 100644 --- a/components/cover-image.tsx +++ b/components/cover-image.tsx @@ -1,64 +1,46 @@ -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}`; - }; + // 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 - let image: JSX.Element | undefined; - if (source?.public_id) { - image = ( - - ); - } else { - image =
; - } + if (!imageUrl) { + return ( +
+
+
+ ); + } - 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..b294fa50 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 ? ( - - ) : ( -
- ); + // 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('-'); + 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/lib/rss.ts b/lib/rss.ts index 37a83472..8797c03c 100644 --- a/lib/rss.ts +++ b/lib/rss.ts @@ -1,23 +1,41 @@ -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"; const productionDomain = process.env.VERCEL_PROJECT_PRODUCTION_URL; 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", @@ -27,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", @@ -49,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: item.coverImage?.secure_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 @@ -70,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/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.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/lib/queries.ts b/sanity/lib/queries.ts index c7b7663b..ac5537ca 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 @@ -317,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, 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, }; 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.", }), ], 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/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); 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..28d0cbf9 --- /dev/null +++ b/scripts/migration/migrate.mjs @@ -0,0 +1,1019 @@ +#!/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', + 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) + // 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; + + 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 rawCloudinaryCount = 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 === 'raw-cloudinary-object') rawCloudinaryCount++; + else if (r.type === 'url') urlCount++; + else if (r.type === 'embedded') embeddedCount++; + } + } + + 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) { + await writeJsonFile(DISCOVERY_FILE, docsWithRefs); + log(1, `Saved discovery to ${DISCOVERY_FILE}`); + } + + 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 +// ═══════════════════════════════════════════════════════════════════════════════ + +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) { + if (!ref.url) continue; + + // 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(originalUrl); + if (!entry.sourceDocIds.includes(doc._id)) { + entry.sourceDocIds.push(doc._id); + } + } else { + urlMap.set(originalUrl, { + cloudinaryUrl: originalUrl, + cloudinaryPublicId: ref.publicId || extractPublicIdFromUrl(originalUrl), + resourceType: ref.resourceType || guessResourceType(originalUrl), + 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, 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) { + 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, ref.publicId); + + 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' || 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 + ? { + _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 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 + ); + 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, + rawCloudinaryObjects: rawCloudinaryRefs, + 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(` 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}`); + 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/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/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" + } + } + } +} 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 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