{"id":37798,"date":"2025-06-18T07:00:00","date_gmt":"2025-06-18T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=37798"},"modified":"2025-06-20T17:00:05","modified_gmt":"2025-06-21T00:00:05","slug":"personalize-e-commerce-marketing-next-js-generative-replace-overlays","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays","title":{"rendered":"Personalize E-commerce and Marketing Visuals in Next.js With Generative Replace and Overlays"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p><a href=\"https:\/\/github.com\/musebe\/cloudinary-personalized-visuals\"><strong>GitHub Repository<\/strong><\/a> | <a href=\"https:\/\/cloudinary-personalized-visuals.vercel.app\/\"><strong>Live Demo<\/strong><\/a><\/p>\n<p>E-commerce thrives on visual storytelling: a quick, personalized banner or badge can turn a casual browser into a buyer. With Cloudinary\u2019s AI-driven Generative Replace and Overlays, you can dynamically swap out product elements (e.g., \u201c50% OFF\u201d \u2192 \u201c75% OFF\u201d) or superimpose seasonal callouts (\u201c\ud83c\udf89 Holiday Sale!\u201d) all without manual Photoshop work.<\/p>\n<p>In this tutorial, we\u2019ll take you from zero to a working Next.js 15 app that:<\/p>\n<ul>\n<li>\n<strong>Generates AI-powered image edits<\/strong> (<code>e_gen_replace<\/code>).<\/li>\n<li>\n<strong>Applies text or image overlays<\/strong> with custom fonts, colors, and positioning.<\/li>\n<li>\n<strong>Persists<\/strong> your exact preview via a cache-busted query string (<code>?v=<\/code>).<\/li>\n<li>\n<strong>Displays<\/strong> a drag-and-drop uploader, live preview, and recent transform gallery.<\/li>\n<\/ul>\n<h2>Setup<\/h2>\n<h3>Clone the Starter Template<\/h3>\n<p>Our <a href=\"https:\/\/github.com\/musebe\/cloudinary-personalized-visuals\/tree\/starter-template\">starter-template branch<\/a> already includes:<\/p>\n<ul>\n<li>Next.js 15 App Router scaffold.<\/li>\n<li>Tailwind CSS, shadcn\/ui.<\/li>\n<li>Motion.dev for animations.<\/li>\n<li>Placeholder components for Cloudinary integration.<\/li>\n<\/ul>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\">git  <span class=\"hljs-keyword\">clone<\/span>  https:<span class=\"hljs-comment\">\/\/github.com\/musebe\/cloudinary-personalized-visuals.git  <\/span>\n\ncd  cloudinary-personalized-visuals \n\ngit  checkout  starter-template\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h3>Install Dependencies<\/h3>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm  install\n<\/code><\/span><\/pre>\n<p>The key packages in <code>package.json<\/code> include:<\/p>\n<ul>\n<li>\n<code>next@15.x<\/code>, <code>react@19.x<\/code>, <code>next-cloudinary<\/code>\n<\/li>\n<li>\n<code>cloudinary<\/code> (Node SDK)<\/li>\n<li>\n<code>lucide-react<\/code>, <code>motion<\/code>, <code>sonner<\/code>\n<\/li>\n<li>\n<code>tailwindcss<\/code>, <code>shadcn\/ui<\/code>, <code>clsx<\/code>\n<\/li>\n<\/ul>\n<h3>Configure Environment Variables<\/h3>\n<p>Create a <code>.env.local<\/code> at your project root:<\/p>\n<pre class=\"js-syntax-highlighted\"><code>NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name\n\nNEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=your-upload-preset\n\nNEXT_PUBLIC_CLOUDINARY_API_KEY=your-api-key\n\nCLOUDINARY_API_SECRET=your-api-secret\n<\/code><\/pre>\n<blockquote>\n<p>Tip: Cloudinary\u2019s Upload Widget reads the <code>upload_preset<\/code> and <code>cloud_name<\/code> client-side, while your server action uses the secret key to generate secure URLs.<\/p>\n<\/blockquote>\n<h2>Next.js Configuration<\/h2>\n<p>Before we can optimize and serve Cloudinary images (and use the Upload Widget), we need to tell Next.js to trust those external domains and load the widget script early.<\/p>\n<h3>Trust Cloudinary in <code>next.config.js<\/code><\/h3>\n<p>Add your Cloudinary cloud to the <code>images.remotePatterns<\/code> array so Next.js can fetch and optimize those assets:<\/p>\n<pre class=\"js-syntax-highlighted\"><code>\/\/ next.config.js\nexport default {\n  images: {\n    remotePatterns: [\n      {\n        protocol: &quot;https&quot;,\n        hostname: &quot;res.cloudinary.com&quot;,\n        pathname: `\/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}\/**`,\n      },\n    ],\n  },\n};\n\n<\/code><\/pre>\n<blockquote>\n<p>See Next.js docs on configuring external images: <a href=\"https:\/\/nextjs.org\/docs\/app\/api-reference\/config\/next-config-js\/images\">https:\/\/nextjs.org\/docs\/app\/api-reference\/config\/next-config-js\/images<\/a><\/p>\n<\/blockquote>\n<h3>Load the Cloudinary Upload Widget<\/h3>\n<p>Make sure the Upload Widget\u2019s global script is loaded <strong>before<\/strong> any client-side code runs:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ app\/head.tsx<\/span>\n<span class=\"hljs-keyword\">import<\/span> Script <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next\/script\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">Head<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Script<\/span>\n      <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"https:\/\/upload-widget.cloudinary.com\/global\/all.js\"<\/span>\n      <span class=\"hljs-attr\">strategy<\/span>=<span class=\"hljs-string\">\"beforeInteractive\"<\/span>\n    \/&gt;<\/span><\/span>\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<blockquote>\n<p>Learn more in the Cloudinary Upload Widget docs: <a href=\"https:\/\/cloudinary.com\/documentation\/upload_widget_reference\">https:\/\/cloudinary.com\/documentation\/upload_widget_reference<\/a><\/p>\n<\/blockquote>\n<h2>Core Libraries and Helpers<\/h2>\n<p>These three minimal modules under <code>src\/lib<\/code> power uploads, persistence, and URL segment building:<\/p>\n<ol>\n<li>\n<strong><code>cloudinary.ts<\/code>.<\/strong> Server-side Cloudinary SDK init.<\/li>\n<li>\n<strong><code>db.ts<\/code>.<\/strong> Simple JSON store for transform records.<\/li>\n<li>\n<strong><code>transform.ts<\/code>.<\/strong> Assemble your Generative Replace + Overlay path segment.<\/li>\n<\/ol>\n<h3>Cloudinary SDK Initialization<\/h3>\n<p>Configure the Cloudinary Node SDK with your account credentials (server-side only).<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ src\/lib\/cloudinary.ts<\/span>\n<span class=\"hljs-keyword\">import<\/span> { v2 <span class=\"hljs-keyword\">as<\/span> cloudinary } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"cloudinary\"<\/span>;\n\ncloudinary.config({\n  <span class=\"hljs-attr\">cloud_name<\/span>:  process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME!,\n  <span class=\"hljs-attr\">api_key<\/span>:     process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY!,\n  <span class=\"hljs-attr\">api_secret<\/span>:  process.env.CLOUDINARY_API_SECRET!,\n  <span class=\"hljs-attr\">secure<\/span>:      <span class=\"hljs-literal\">true<\/span>,\n});\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> cloudinary;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h3>JSON \u201cDatabase\u201d for Transforms<\/h3>\n<p>Manages a local <code>transforms.json<\/code> file, letting us read all saved transform records (newest first) and prepend new ones.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ src\/lib\/db.ts<\/span>\n<span class=\"hljs-keyword\">import<\/span> fs <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"fs\/promises\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> path <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"path\"<\/span>;\n\n<span class=\"hljs-keyword\">const<\/span> DB_PATH = path.join(process.cwd(), <span class=\"hljs-string\">\"transforms.json\"<\/span>);\n\n<span class=\"hljs-comment\">\/\/ Read all saved records, returning an empty array if the file doesn't exist<\/span>\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">readAll<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">try<\/span> {\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-built_in\">JSON<\/span>.parse(<span class=\"hljs-keyword\">await<\/span> fs.readFile(DB_PATH, <span class=\"hljs-string\">\"utf-8\"<\/span>));\n  } <span class=\"hljs-keyword\">catch<\/span> {\n    <span class=\"hljs-keyword\">return<\/span> &#91;];\n  }\n}\n\n<span class=\"hljs-comment\">\/\/ Prepend a new record to the array and write back to disk<\/span>\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">write<\/span>(<span class=\"hljs-params\">record: any<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> data = <span class=\"hljs-keyword\">await<\/span> readAll();\n  data.unshift(record);\n  <span class=\"hljs-keyword\">await<\/span> fs.writeFile(DB_PATH, <span class=\"hljs-built_in\">JSON<\/span>.stringify(data, <span class=\"hljs-literal\">null<\/span>, <span class=\"hljs-number\">2<\/span>));\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<blockquote>\n<p>Full source: <a href=\"https:\/\/github.com\/musebe\/cloudinary-personalized-visuals\/blob\/main\/src\/lib\/db.ts\">db.ts<\/a><\/p>\n<\/blockquote>\n<h3>Build the Cloudinary Transform Segment<\/h3>\n<p>Turn your options (generative replace + text\/image overlay) into a Cloudinary URL path segment:<\/p>\n<ol>\n<li>\n<strong>Generative Replace<\/strong>\n<\/li>\n<\/ol>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">from<\/span> &amp;&amp; to) {\n  chain.push(\n    <span class=\"hljs-string\">`e_gen_replace:from_<span class=\"hljs-subst\">${<span class=\"hljs-built_in\">encodeURIComponent<\/span>(<span class=\"hljs-keyword\">from<\/span>)}<\/span>;to_<span class=\"hljs-subst\">${<span class=\"hljs-built_in\">encodeURIComponent<\/span>(to)}<\/span>`<\/span>\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ol start=\"2\">\n<li>\n<strong>Text Overlay<\/strong>\n<\/li>\n<\/ol>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">chain.push(\n  &#91;\n    <span class=\"hljs-string\">`l_text:<span class=\"hljs-subst\">${<span class=\"hljs-built_in\">encodeURIComponent<\/span>(<span class=\"hljs-string\">`<span class=\"hljs-subst\">${fontFamily}<\/span>_<span class=\"hljs-subst\">${fontSize}<\/span>_<span class=\"hljs-subst\">${fontWeight}<\/span>`<\/span>)}<\/span>:<span class=\"hljs-subst\">${<span class=\"hljs-built_in\">encodeURIComponent<\/span>(overlay)}<\/span>`<\/span>,\n    <span class=\"hljs-string\">`co_rgb:<span class=\"hljs-subst\">${textColor}<\/span>`<\/span>,\n    bgColor ? <span class=\"hljs-string\">`b_rgb:<span class=\"hljs-subst\">${bgColor}<\/span>`<\/span> : <span class=\"hljs-string\">\"\"<\/span>,\n    <span class=\"hljs-string\">`g_<span class=\"hljs-subst\">${gravity}<\/span>`<\/span>, <span class=\"hljs-string\">`x_<span class=\"hljs-subst\">${x}<\/span>`<\/span>, <span class=\"hljs-string\">`y_<span class=\"hljs-subst\">${y}<\/span>`<\/span>,\n  ]\n    .filter(<span class=\"hljs-built_in\">Boolean<\/span>)\n    .join(<span class=\"hljs-string\">\",\"<\/span>)\n);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ol start=\"3\">\n<li>\n<strong>Image Overlay<\/strong>\n<\/li>\n<\/ol>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">chain.push(\n  &#91;<span class=\"hljs-string\">`l_<span class=\"hljs-subst\">${overlay}<\/span>`<\/span>, <span class=\"hljs-string\">`g_<span class=\"hljs-subst\">${gravity}<\/span>`<\/span>, <span class=\"hljs-string\">`x_<span class=\"hljs-subst\">${x}<\/span>`<\/span>, <span class=\"hljs-string\">`y_<span class=\"hljs-subst\">${y}<\/span>`<\/span>].join(<span class=\"hljs-string\">\",\"<\/span>)\n);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ol start=\"4\">\n<li>\n<strong>Final Assembly<\/strong>\n<\/li>\n<\/ol>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">return<\/span>  chain.length  ?  <span class=\"hljs-string\">`<span class=\"hljs-subst\">${chain.join(<span class=\"hljs-string\">\"\/\"<\/span>)}<\/span>\/`<\/span>  :  <span class=\"hljs-string\">\"\"<\/span>;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<blockquote>\n<p>Full source: <a href=\"https:\/\/github.com\/musebe\/cloudinary-personalized-visuals\/blob\/main\/src\/lib\/transform.ts\">transform.ts<\/a><\/p>\n<\/blockquote>\n<p>With these helpers ready, we can now build a <strong>LivePreview<\/strong> component that debounces inputs, shows a loader during AI replace, and mirrors exactly what will be saved.<\/p>\n<h2>Live Preview Component<\/h2>\n<p>We\u2019ll break down how the preview \u201cstage\u201d is set up, how overlays get their \u201cstage directions\u201d (gravity + offsets), and finally how we render text badges vs. image badges\u2014step by step.<\/p>\n<h3>Stage and Backdrop<\/h3>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"relative w-full h-&#91;400px] rounded-lg border bg-gray-50 overflow-hidden\"<\/span>&gt;<\/span>\n  {baseUrl &amp;&amp; (\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Image<\/span>\n      <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{baseUrl}<\/span>\n      <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"preview\"<\/span>\n      <span class=\"hljs-attr\">fill<\/span>\n      <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"object-contain\"<\/span>\n      <span class=\"hljs-attr\">unoptimized<\/span>\n    \/&gt;<\/span>\n  )}\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ol>\n<li>\n<strong>Stage<\/strong> (<code>relative<\/code>, <code>overflow-hidden<\/code>):<\/li>\n<\/ol>\n<ul>\n<li>Think of this as a theater stage with borders. Anything that moves beyond its edges gets clipped, so overlays never \u201cbleed\u201d outside.<\/li>\n<\/ul>\n<ol start=\"2\">\n<li>\n<strong>Backdrop<\/strong> (<code>Image<\/code> with <code>object-contain<\/code>):<\/li>\n<\/ol>\n<ul>\n<li>The product image or AI\u2010replaced render is the backdrop. <code>object-contain<\/code> ensures the full image fits within the stage, like shrink\u2010to\u2010fit scenery.<\/li>\n<\/ul>\n<h3>Stage Directions: Gravity Mapping<\/h3>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> GRAVITY_TO_CLASS: Record&lt;Gravity, string&gt; = {\n  <span class=\"hljs-attr\">north_west<\/span>: <span class=\"hljs-string\">\"items-start justify-start\"<\/span>,\n  <span class=\"hljs-attr\">north<\/span>:      <span class=\"hljs-string\">\"items-start justify-center\"<\/span>,\n  <span class=\"hljs-attr\">north_east<\/span>: <span class=\"hljs-string\">\"items-start justify-end\"<\/span>,\n  <span class=\"hljs-attr\">west<\/span>:       <span class=\"hljs-string\">\"items-center justify-start\"<\/span>,\n  <span class=\"hljs-attr\">center<\/span>:     <span class=\"hljs-string\">\"items-center justify-center\"<\/span>,\n  <span class=\"hljs-attr\">east<\/span>:       <span class=\"hljs-string\">\"items-center justify-end\"<\/span>,\n  <span class=\"hljs-attr\">south_west<\/span>: <span class=\"hljs-string\">\"items-end justify-start\"<\/span>,\n  <span class=\"hljs-attr\">south<\/span>:      <span class=\"hljs-string\">\"items-end justify-center\"<\/span>,\n  <span class=\"hljs-attr\">south_east<\/span>: <span class=\"hljs-string\">\"items-end justify-end\"<\/span>,\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<strong>Analogy:<\/strong> Gravity is like telling an actor where to stand on stage: front\u2010left, center, back\u2010right, etc.<\/li>\n<li>We map each of the 9 positions to Flexbox alignments (<code>items-\u2026<\/code> + <code>justify-\u2026<\/code>).<\/li>\n<\/ul>\n<h3>Place the Overlay: Wrapper + Offsets<\/h3>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">{overlay &amp;&amp; (\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>\n    <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">{<\/span>`<span class=\"hljs-attr\">absolute<\/span> <span class=\"hljs-attr\">inset-0<\/span> <span class=\"hljs-attr\">flex<\/span> ${\n      <span class=\"hljs-attr\">GRAVITY_TO_CLASS<\/span>&#91;<span class=\"hljs-attr\">overlay.gravity<\/span>]\n    }`}\n    <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">pointerEvents:<\/span> \"<span class=\"hljs-attr\">none<\/span>\" }}\n  &gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">transform:<\/span> `<span class=\"hljs-attr\">translate<\/span>(${<span class=\"hljs-attr\">overlay.x<\/span>}<span class=\"hljs-attr\">px<\/span>, ${<span class=\"hljs-attr\">overlay.y<\/span>}<span class=\"hljs-attr\">px<\/span>)` }}&gt;<\/span>\n      {\/* overlay badge goes here *\/}\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n)}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ol>\n<li>\n<strong>Overlay Wrapper<\/strong> (<code>absolute inset-0 flex \u2026<\/code>):<\/li>\n<\/ol>\n<ul>\n<li>Covers the whole stage, then uses our gravity mapping to anchor the overlay container.<\/li>\n<\/ul>\n<ol start=\"2\">\n<li>\n<strong>Offset Nudge<\/strong> (<code>translate(x, y)<\/code>):<\/li>\n<\/ol>\n<ul>\n<li>After anchoring, we nudge the overlay by a few pixels horizontally\/vertically for fine\u2010tuning, like stage marks on the floor.<\/li>\n<\/ul>\n<h3>Overlay \u201cActors\u201d: Text Badge vs. Image Badge<\/h3>\n<h3>Text Badge<\/h3>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">motion.span<\/span>\n  <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"inline-block rounded-lg shadow-md\"<\/span>\n  <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span>\n    <span class=\"hljs-attr\">color:<\/span>           `#${<span class=\"hljs-attr\">overlay.textColor<\/span>}`,\n    <span class=\"hljs-attr\">backgroundColor:<\/span> `#${<span class=\"hljs-attr\">overlay.bgColor<\/span>}`,\n    <span class=\"hljs-attr\">fontFamily:<\/span>      <span class=\"hljs-attr\">overlay.fontFamily<\/span>,\n    <span class=\"hljs-attr\">fontSize:<\/span>        <span class=\"hljs-attr\">overlay.fontSize<\/span>,\n    <span class=\"hljs-attr\">fontWeight:<\/span>      <span class=\"hljs-attr\">overlay.fontWeight<\/span>,\n    <span class=\"hljs-attr\">padding:<\/span>         \"<span class=\"hljs-attr\">4px<\/span> <span class=\"hljs-attr\">12px<\/span>\",\n  }}\n  <span class=\"hljs-attr\">initial<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">opacity:<\/span> <span class=\"hljs-attr\">0<\/span>, <span class=\"hljs-attr\">scale:<\/span> <span class=\"hljs-attr\">0.9<\/span> }}\n  <span class=\"hljs-attr\">animate<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">opacity:<\/span> <span class=\"hljs-attr\">1<\/span>, <span class=\"hljs-attr\">scale:<\/span> <span class=\"hljs-attr\">1<\/span> }}\n  <span class=\"hljs-attr\">transition<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">duration:<\/span> <span class=\"hljs-attr\">0.15<\/span> }}\n&gt;<\/span>\n  {overlay.text}\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">motion.span<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<strong>Entrance cue:<\/strong> We fade+scale the text into view (0.9 \u2192 1.0) for a subtle \u201cactor entrance.\u201d<\/li>\n<\/ul>\n<p><strong>Costume and props:<\/strong><\/p>\n<ul>\n<li>Rounded corners + drop\u2010shadow act like stage lighting and set design.<\/li>\n<li>Font, colours, padding style, the badge.<\/li>\n<\/ul>\n<h3>Image Badge<\/h3>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">motion.div<\/span>\n  <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"relative w-32 h-32 overflow-hidden rounded-lg shadow-md\"<\/span>\n  <span class=\"hljs-attr\">initial<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">opacity:<\/span> <span class=\"hljs-attr\">0<\/span>, <span class=\"hljs-attr\">scale:<\/span> <span class=\"hljs-attr\">0.9<\/span> }}\n  <span class=\"hljs-attr\">animate<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">opacity:<\/span> <span class=\"hljs-attr\">1<\/span>, <span class=\"hljs-attr\">scale:<\/span> <span class=\"hljs-attr\">1<\/span> }}\n  <span class=\"hljs-attr\">transition<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">duration:<\/span> <span class=\"hljs-attr\">0.15<\/span> }}\n&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Image<\/span>\n    <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{<\/span>`<span class=\"hljs-attr\">https:<\/span>\/\/<span class=\"hljs-attr\">res.cloudinary.com<\/span>\/${<span class=\"hljs-attr\">process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME<\/span>}\/<span class=\"hljs-attr\">image<\/span>\/<span class=\"hljs-attr\">upload<\/span>\/${<span class=\"hljs-attr\">overlay.imageId<\/span>}<span class=\"hljs-attr\">.png<\/span>`}\n    <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"overlay\"<\/span>\n    <span class=\"hljs-attr\">fill<\/span>\n    <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"object-contain\"<\/span>\n    <span class=\"hljs-attr\">unoptimized<\/span>\n  \/&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">motion.div<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p><strong>Actor is an image:<\/strong><\/p>\n<ul>\n<li>Container keeps the aspect ratio, rounds the edges, and adds a drop\u2010shadow \u201cfootlight.\u201d<\/li>\n<li>Animates in just like the text badge.<\/li>\n<\/ul>\n<p>With the stage, stage directions, and actor entrances defined, <strong>LivePreview<\/strong> mirrors exactly what the final Cloudinary\u2010transformed URL will show\u2014right down to gravity, offsets, colours, and sizing.<\/p>\n<blockquote>\n<p>Full component for reference: <a href=\"https:\/\/github.com\/musebe\/cloudinary-personalized-visuals\/blob\/main\/src\/components\/LivePreview.tsx\">LivePreview.tsx<\/a><\/p>\n<\/blockquote>\n<h2>Uploader Component \u2013 Core Logic<\/h2>\n<p>Building on <strong>LivePreview<\/strong> (which simply renders any <code>baseUrl<\/code> + overlays), <strong>Uploader<\/strong> is where we:<\/p>\n<ol>\n<li>Turn your \u201cfrom \u2192 to\u201d and overlay options into a Cloudinary URL.<\/li>\n<li>Debounce typing so we don\u2019t flood the UI with reloads.<\/li>\n<li>Show a \u201cGenerating\u2026\u201d spinner while the AI does its work.<\/li>\n<li>Let you \u201cTry a new variation\u201d by bumping a cache\u2010buster.<\/li>\n<li>Save the exact preview you saw into our gallery.<\/li>\n<\/ol>\n<h3>Debounce and URL Construction<\/h3>\n<p><em>Inputs + debounce \u2192 transform segment \u2192 <code>baseUrl<\/code><\/em><\/p>\n<p>Think of your raw inputs <code>repFrom<\/code> and <code>repTo<\/code> as paint colors. Before we mix them, we wait (debounce) for the painter to finish each stroke:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ 350 ms after you stop typing, lock in the values<\/span>\n<span class=\"hljs-keyword\">const<\/span> fromDeb = useDebounced(repFrom);\n<span class=\"hljs-keyword\">const<\/span> toDeb   = useDebounced(repTo);\n\n<span class=\"hljs-comment\">\/\/ Build the Cloudinary path just once both words are set<\/span>\n<span class=\"hljs-keyword\">const<\/span> segment = buildTransform({\n  <span class=\"hljs-attr\">from<\/span>: repEnabled &amp;&amp; fromDeb &amp;&amp; toDeb ? fromDeb : <span class=\"hljs-literal\">undefined<\/span>,\n  <span class=\"hljs-attr\">to<\/span>:   repEnabled &amp;&amp; fromDeb &amp;&amp; toDeb ?   toDeb   : <span class=\"hljs-literal\">undefined<\/span>,\n  <span class=\"hljs-comment\">\/\/ overlay props omitted\u2026<\/span>\n});\n\n<span class=\"hljs-comment\">\/\/ Append ?v=version so each regenerate yields a fresh AI-render<\/span>\n<span class=\"hljs-keyword\">const<\/span> baseUrl = publicId\n  ? <span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/<span class=\"hljs-subst\">${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}<\/span>`<\/span> +\n    <span class=\"hljs-string\">`\/image\/upload\/<span class=\"hljs-subst\">${segment}<\/span><span class=\"hljs-subst\">${publicId}<\/span>.png?v=<span class=\"hljs-subst\">${version}<\/span>`<\/span>\n  : <span class=\"hljs-string\">\"\"<\/span>;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<blockquote>\n<p>Flow: typing stops \u2192 fromDeb\/toDeb update \u2192 segment rebuilds \u2192 baseUrl changes \u2192 LivePreview rerenders.<\/p>\n<\/blockquote>\n<h3>Loading Spinner<\/h3>\n<p><em>New URL \u2192 preload \u2192 hide spinner<\/em><\/p>\n<p>Just like dimming the stage lights while scenery changes, we hide the preview until it\u2019s fully ready:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">useEffect(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">if<\/span> (!publicId) <span class=\"hljs-keyword\">return<\/span>;\n  setLoading(<span class=\"hljs-literal\">true<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> img = <span class=\"hljs-keyword\">new<\/span> Image();\n  img.src = baseUrl;       <span class=\"hljs-comment\">\/\/ start loading AI\u2010rendered image<\/span>\n  img.onload = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> setLoading(<span class=\"hljs-literal\">false<\/span>);\n  img.onerror = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> setLoading(<span class=\"hljs-literal\">false<\/span>);\n}, &#91;baseUrl, publicId]);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<strong>Before<\/strong>: <code>loading = true<\/code> \u2192 spinner overlays the LivePreview<\/li>\n<li>\n<strong>After<\/strong>: image loads \u2192 <code>loading = false<\/code> \u2192 spinner disappears \u2192 new image appears<\/li>\n<\/ul>\n<h3>Regenerate AI Replace<\/h3>\n<p><em>Version bump \u2192 fresh AI output<\/em><\/p>\n<p>If you\u2019re not happy with the first take and ask the director for another shot:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> regenerate = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">if<\/span> (!repEnabled || !publicId || !fromDeb || !toDeb) <span class=\"hljs-keyword\">return<\/span>;\n  setVersion(<span class=\"hljs-function\"><span class=\"hljs-params\">v<\/span> =&gt;<\/span> v + <span class=\"hljs-number\">1<\/span>);\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>Each time <code>version<\/code> increments, <code>?v=<\/code> changes \u2192 browser treats it as a new request \u2192 Cloudinary runs AI\u2010replace again.<\/li>\n<\/ul>\n<h3>Persist the Exact Preview<\/h3>\n<p><em>Preview freeze\u2010frame \u2192 gallery entry<\/em><\/p>\n<p>Once you\u2019ve got your perfect shot, you \u201cSave\u201d it:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> handleSave = <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">if<\/span> (!publicId) <span class=\"hljs-keyword\">return<\/span>;\n  <span class=\"hljs-keyword\">const<\/span> form = <span class=\"hljs-keyword\">new<\/span> FormData();\n  form.set(<span class=\"hljs-string\">\"publicId\"<\/span>, publicId);\n  form.set(<span class=\"hljs-string\">\"version\"<\/span>, <span class=\"hljs-built_in\">String<\/span>(version));    <span class=\"hljs-comment\">\/\/ lock in this exact ?v=<\/span>\n  form.set(<span class=\"hljs-string\">\"from\"<\/span>,    repEnabled ? repFrom : <span class=\"hljs-string\">\"\"<\/span>);\n  form.set(<span class=\"hljs-string\">\"to\"<\/span>,      repEnabled ? repTo   : <span class=\"hljs-string\">\"\"<\/span>);\n  <span class=\"hljs-comment\">\/\/ \u2026overlay, colors, gravity, x\/y\u2026<\/span>\n  <span class=\"hljs-keyword\">await<\/span> addTransform(<span class=\"hljs-literal\">null<\/span>, form);\n  router.refresh(); <span class=\"hljs-comment\">\/\/ show your saved transform in the gallery<\/span>\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>The server reads <code>version<\/code> and appends <code>?v=<\/code> to the saved URL.<\/li>\n<li>\n<strong>TransformGallery<\/strong> loads that same cache\u2010busted URL, so what you previewed is exactly what\u2019s persisted.<\/li>\n<\/ul>\n<p>With <strong>Uploader<\/strong> In place, you now have a seamless in-browser workflow: type your replace prompts, fine-tune overlays, watch a live AI-powered preview with graceful loading, and save exactly what you see. This sets the stage for our final piece\u2014the gallery that showcases every transformation you\u2019ve created.<\/p>\n<blockquote>\n<p>Full component for reference:\n<a href=\"https:\/\/github.com\/musebe\/cloudinary-personalized-visuals\/blob\/main\/src\/components\/Uploader.tsx\">Uploader.tsx \u203a GitHub<\/a><\/p>\n<\/blockquote>\n<h2>Server Action and Persistence \u2013 Core Logic<\/h2>\n<p>Continuing from <strong>Uploader<\/strong> (where we captured your perfect AI render with <code>?v=<\/code>), our server action in <code>src\/app\/actions\/transforms.ts<\/code> faithfully recreates \u2014 and freezes \u2014 that exact preview before adding it to our gallery. Here\u2019s how it all fits together:<\/p>\n<ol>\n<li>\n<strong>Unpack the preview.<\/strong> The client \u201cSave\u201d step sends every parameter\u2014especially the <code>version<\/code> that is locked in your cache-buster.<\/li>\n<li>\n<strong>Rebuild the transform.<\/strong> We call the exact same <code>buildTransform<\/code> helper you used in the browser, ensuring no drift between what you saw and what we persist.<\/li>\n<li>\n<strong>Append <code>?v=<\/code><\/strong>. By tacking on the same query string, the saved URL hits Cloudinary\u2019s CDN with that unique AI-generated asset.<\/li>\n<li>\n<strong>Write to disk<\/strong>. We prepend the new record into <code>transforms.json<\/code>, so your gallery always shows the newest edits first.<\/li>\n<\/ol>\n<h3>Unpack Client Inputs<\/h3>\n<p><em>Like a film director calling \u201caction,\u201d the server grabs every detail you define in the UI.<\/em><\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> publicId = data.get(<span class=\"hljs-string\">\"publicId\"<\/span>) <span class=\"hljs-keyword\">as<\/span> string;\n<span class=\"hljs-keyword\">const<\/span> version  = <span class=\"hljs-built_in\">Number<\/span>(data.get(<span class=\"hljs-string\">\"version\"<\/span>) ?? <span class=\"hljs-number\">0<\/span>);\n\n<span class=\"hljs-comment\">\/\/ The \u201cfrom\u201d &amp; \u201cto\u201d prompts for AI replace:<\/span>\n<span class=\"hljs-keyword\">const<\/span> <span class=\"hljs-keyword\">from<\/span> = (data.get(<span class=\"hljs-string\">\"from\"<\/span>) <span class=\"hljs-keyword\">as<\/span> string) || <span class=\"hljs-literal\">undefined<\/span>;\n<span class=\"hljs-keyword\">const<\/span> to   = (data.get(<span class=\"hljs-string\">\"to\"<\/span>)   <span class=\"hljs-keyword\">as<\/span> string) || <span class=\"hljs-literal\">undefined<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Overlay parameters for text or image badges:<\/span>\n<span class=\"hljs-keyword\">const<\/span> overlay     = (data.get(<span class=\"hljs-string\">\"overlay\"<\/span>)     <span class=\"hljs-keyword\">as<\/span> string) || <span class=\"hljs-literal\">undefined<\/span>;\n<span class=\"hljs-keyword\">const<\/span> overlayMode = (data.get(<span class=\"hljs-string\">\"overlayMode\"<\/span>) <span class=\"hljs-keyword\">as<\/span> string) === <span class=\"hljs-string\">\"image\"<\/span> ? <span class=\"hljs-string\">\"image\"<\/span> : <span class=\"hljs-string\">\"text\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Placement &amp; styling:<\/span>\n<span class=\"hljs-keyword\">const<\/span> gravity    = (data.get(<span class=\"hljs-string\">\"gravity\"<\/span>)    <span class=\"hljs-keyword\">as<\/span> string) ?? <span class=\"hljs-string\">\"north_west\"<\/span>;\n<span class=\"hljs-keyword\">const<\/span> x          = <span class=\"hljs-built_in\">Number<\/span>(data.get(<span class=\"hljs-string\">\"x\"<\/span>)    ?? <span class=\"hljs-number\">0<\/span>);\n<span class=\"hljs-keyword\">const<\/span> y          = <span class=\"hljs-built_in\">Number<\/span>(data.get(<span class=\"hljs-string\">\"y\"<\/span>)    ?? <span class=\"hljs-number\">0<\/span>);\n<span class=\"hljs-keyword\">const<\/span> textColor  = (data.get(<span class=\"hljs-string\">\"overlayColor\"<\/span>) <span class=\"hljs-keyword\">as<\/span> string) ?? <span class=\"hljs-string\">\"000000\"<\/span>;\n<span class=\"hljs-keyword\">const<\/span> bgColor    = (data.get(<span class=\"hljs-string\">\"overlayBg\"<\/span>)    <span class=\"hljs-keyword\">as<\/span> string) || <span class=\"hljs-literal\">undefined<\/span>;\n<span class=\"hljs-keyword\">const<\/span> fontFamily = (data.get(<span class=\"hljs-string\">\"fontFamily\"<\/span>)  <span class=\"hljs-keyword\">as<\/span> string) ?? <span class=\"hljs-string\">\"Arial\"<\/span>;\n<span class=\"hljs-keyword\">const<\/span> fontSize   = <span class=\"hljs-built_in\">Number<\/span>(data.get(<span class=\"hljs-string\">\"fontSize\"<\/span>) ?? <span class=\"hljs-number\">40<\/span>);\n<span class=\"hljs-keyword\">const<\/span> fontWeight = (data.get(<span class=\"hljs-string\">\"fontWeight\"<\/span>) <span class=\"hljs-keyword\">as<\/span> string) === <span class=\"hljs-string\">\"normal\"<\/span> ? <span class=\"hljs-string\">\"normal\"<\/span> : <span class=\"hljs-string\">\"bold\"<\/span>;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<blockquote>\n<p>Analogy: Think of these as your call sheet: every actor (parameter) must be exactly where you directed.<\/p>\n<\/blockquote>\n<h3>Reconstruct the Transform Path<\/h3>\n<p>We\u2019ll reuse your client\u2019s recipe, so the saved URL matches the live preview exactly.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> segment = buildTransform({\n  <span class=\"hljs-keyword\">from<\/span>, to,\n  overlay, overlayMode,\n  gravity, x, y,\n  textColor, bgColor,\n  fontFamily, fontSize, fontWeight,\n});\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<strong>Why reuse?<\/strong> It guarantees parity: what you mixed in the browser is what we\u2019ll serve from the server.<\/li>\n<li>\n<strong>Result<\/strong>: A string like<\/li>\n<\/ul>\n<p><code>e_gen_replace:from_shoe;to_boot\/l_text:Arial_40_bold:SALE,co_rgb:FF0000,...\/<\/code><\/p>\n<h3>Craft the Cache-Busted URL<\/h3>\n<p><em>Attach the <code>?v=<\/code> so Cloudinary returns that unique AI-rendered asset.<\/em><\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> transformedUrl =\n  <span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/<span class=\"hljs-subst\">${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}<\/span>`<\/span> +\n  <span class=\"hljs-string\">`\/image\/upload\/<span class=\"hljs-subst\">${segment}<\/span><span class=\"hljs-subst\">${publicId}<\/span>.png`<\/span> +\n  (version ? <span class=\"hljs-string\">`?v=<span class=\"hljs-subst\">${version}<\/span>`<\/span> : <span class=\"hljs-string\">\"\"<\/span>);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>If <code>version = 3<\/code>, your URL ends in <code>...\/image.png?v=3<\/code>.<\/li>\n<li>This prevents any caching mismatch: your gallery image is the identical frame you approved in <strong>LivePreview<\/strong>.<\/li>\n<\/ul>\n<h3>Persist the Record<\/h3>\n<p>We\u2019ll timestamp and store every transform, newest-first, for our gallery to consume.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> record = {\n  <span class=\"hljs-attr\">id<\/span>:             crypto.randomUUID(),\n  publicId,\n  transformedUrl,   <span class=\"hljs-comment\">\/\/ includes ?v=<\/span>\n  <span class=\"hljs-keyword\">from<\/span>, to,\n  overlay, overlayMode,\n  <span class=\"hljs-attr\">pos<\/span>: { x, y },\n  <span class=\"hljs-attr\">createdAt<\/span>:      <span class=\"hljs-built_in\">Date<\/span>.now(),\n};\n\n<span class=\"hljs-keyword\">await<\/span> write(record);  <span class=\"hljs-comment\">\/\/ prepends to transforms.json<\/span>\n<span class=\"hljs-keyword\">return<\/span> record;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<strong><code>createAt<\/code><\/strong> lets us sort transforms chronologically.<\/li>\n<li>\n<strong><code>write(record)<\/code><\/strong> uses our <code>db.ts<\/code> helper to insert it at the top of <code>transforms.json<\/code>.<\/li>\n<\/ul>\n<blockquote>\n<p>Flow Recap:\nUploader sends FormData (with version) \u2192 server parses fields \u2192 rebuilds the same transform segment \u2192 crafts cache-busted URL \u2192 writes record \u2192 client refreshes gallery.<\/p>\n<\/blockquote>\n<p>With this robust persistence layer, your <strong>TransformGallery<\/strong> will always display the exact AI-powered image you previewed and saved no surprises, just seamless consistency.<\/p>\n<blockquote>\n<p>Full action for reference:\n<a href=\"https:\/\/github.com\/musebe\/cloudinary-personalized-visuals\/blob\/main\/src\/app\/actions\/transforms.ts\">transforms.ts \u203a GitHub<\/a><\/p>\n<\/blockquote>\n<h2>Transform Gallery \u2013 Core Logic<\/h2>\n<p>Now that our server action has frozen in your exact AI-powered preview URLs, <strong>TransformGallery<\/strong> brings everything together by:<\/p>\n<ol>\n<li>\n<strong>Out-painting<\/strong> each original and transformed image to a consistent 800\u00d7400 \u201cstage\u201d using AI fill.<\/li>\n<li>\n<strong>Injecting<\/strong> that same out-paint segment into your saved transform URLs\u2014so the <code>?v=\u2026<\/code> you approved remains intact.<\/li>\n<li>\n<strong>Pairing<\/strong> the padded original with its matching padded transform.<\/li>\n<li>\n<strong>Sliding<\/strong> between them in a responsive, polished grid.<\/li>\n<\/ol>\n<h3>AI Out-Paint and Uniform Canvas<\/h3>\n<p>Turn any image into a full-bleed 800\u00d7400 card, no white gutters.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> BASE    = <span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/<span class=\"hljs-subst\">${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}<\/span>\/image\/upload`<\/span>;\n<span class=\"hljs-keyword\">const<\/span> genFill = <span class=\"hljs-string\">'c_pad,b_gen_fill,w_800,h_400'<\/span>;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<strong><code>c_pad<\/code><\/strong> adds transparent padding around the original.<\/li>\n<li>\n<strong><code>b_gen_fill<\/code><\/strong> asks Cloudinary\u2019s AI to \u201cpaint\u201d those padded areas \u2014 seamlessly out-painting left\/right <strong>and<\/strong> top\/bottom.<\/li>\n<li>Every card now shares a uniform 800\u00d7400 aspect, edge-to-edge.<\/li>\n<\/ul>\n<h3>Inject the Cache-Buster<\/h3>\n<p><em>Reuse the exact preview version you saved, then apply out-paint.<\/em><\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-23\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">initial.slice(<span class=\"hljs-number\">0<\/span>, <span class=\"hljs-number\">3<\/span>).map(<span class=\"hljs-function\">(<span class=\"hljs-params\">t<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-comment\">\/\/ BEFORE: pad the raw asset<\/span>\n  <span class=\"hljs-keyword\">const<\/span> beforeUrl = <span class=\"hljs-string\">`<span class=\"hljs-subst\">${BASE}<\/span>\/<span class=\"hljs-subst\">${genFill}<\/span>\/<span class=\"hljs-subst\">${t.publicId}<\/span>.png`<\/span>;\n\n  <span class=\"hljs-comment\">\/\/ AFTER: inject genFill into the saved transform path,<\/span>\n  <span class=\"hljs-comment\">\/\/ keeping your original ?v= version intact<\/span>\n  <span class=\"hljs-keyword\">const<\/span> &#91;path, qs] = t.transformedUrl.split(<span class=\"hljs-string\">\"?\"<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> injected   = path.replace(<span class=\"hljs-string\">\"\/upload\/\"<\/span>, <span class=\"hljs-string\">`\/upload\/<span class=\"hljs-subst\">${genFill}<\/span>\/`<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> afterUrl   = qs ? <span class=\"hljs-string\">`<span class=\"hljs-subst\">${injected}<\/span>?<span class=\"hljs-subst\">${qs}<\/span>`<\/span> : injected;\n\n  <span class=\"hljs-keyword\">return<\/span> { beforeUrl, afterUrl };\n});\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<strong>Split on <code>?<\/code><\/strong> so we preserve the exact <code>?v=\u2026<\/code> query that matches your LivePreview.<\/li>\n<li>\n<strong><code>replace('\/upload\/', '\/upload\/'+genFill+'\/')<\/code><\/strong> layers AI out-paint before any generative replace or overlays.<\/li>\n<\/ul>\n<h3>Render the Comparison Grid<\/h3>\n<p>Show your before\/after side by side with a sleek slider UI.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-24\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">section<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\"<\/span>&gt;<\/span>\n  {pairs.map(({ beforeUrl, afterUrl }, i) =&gt; (\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{i}<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"rounded-lg shadow-md overflow-hidden bg-white\"<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Comparison<\/span> <span class=\"hljs-attr\">before<\/span>=<span class=\"hljs-string\">{beforeUrl}<\/span> <span class=\"hljs-attr\">after<\/span>=<span class=\"hljs-string\">{afterUrl}<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  ))}\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">section<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<strong>Responsive layout<\/strong>: 1 column on mobile, up to 3 on desktop.<\/li>\n<li>\n<strong>Visual polish<\/strong>: cards with rounded corners, drop-shadow, and no overflow bleed\u2014just like a finished gallery.<\/li>\n<\/ul>\n<p>With <strong>TransformGallery<\/strong>, your end-to-end flow is complete:<\/p>\n<ul>\n<li>\n<strong>Upload<\/strong> \u2192 <strong>LivePreview<\/strong> AI replace and overlay.<\/li>\n<li>\n<strong>Save<\/strong> exact <code>?v=<\/code> URL \u2192 <strong>Server Action<\/strong>.<\/li>\n<li>\n<strong>Showcase<\/strong> before\/after out-painted cards in a slidable grid.<\/li>\n<\/ul>\n<blockquote>\n<p>Full component for reference:\n<a href=\"https:\/\/github.com\/musebe\/cloudinary-personalized-visuals\/blob\/main\/src\/components\/TransformGallery.tsx\">TransformGallery.tsx \u203a GitHub<\/a><\/p>\n<\/blockquote>\n<h2>What We\u2019ve Achieved<\/h2>\n<p>You\u2019ve built a fully integrated, AI-powered imaging pipeline in Next.js:<\/p>\n<ul>\n<li>\n<strong>Dynamic Generative Replace.<\/strong> Swap out any element (e.g., \u201cCup\u201d \u2192 \u201cPlate\u201d) on the fly with Cloudinary\u2019s <code>e_gen_replace<\/code>.<\/li>\n<li>\n<strong>Flexible overlays.<\/strong> Add styled text or image badges, complete with font, colour, background, gravity and pixel-perfect offsets.<\/li>\n<li>\n<strong>Live in-browser preview.<\/strong> Debounce inputs, cache-busted URLs, and a graceful loading spinner ensure you see exactly what Cloudinary will deliver.<\/li>\n<li>\n<strong>Exact persistence.<\/strong> Lock in the preview you love via <code>?v=<\/code> versioning, so saved gallery items match your live preview 1:1.<\/li>\n<li>\n<strong>Polished transform gallery.<\/strong> Uniform 800\u00d7400 AI out-painted canvases, before\/after sliders, and a responsive grid to showcase your creations.<\/li>\n<\/ul>\n<p>With these building blocks, you can now personalize e-commerce and marketing visuals at scale dynamically, programmatically, and beautifully.<\/p>\n<h3>Cloudinary Docs<\/h3>\n<ul>\n<li>Generative Replace and Fill: <a href=\"https:\/\/cloudinary.com\/documentation\/generative_ai_transformations\">https:\/\/cloudinary.com\/documentation\/generative_ai_transformations<\/a>\n<\/li>\n<li>Layers and Overlays: <a href=\"https:\/\/cloudinary.com\/documentation\/layers\">https:\/\/cloudinary.com\/documentation\/layers<\/a>\n<\/li>\n<\/ul>\n<h3>Full Source Code<\/h3>\n<ul>\n<li>GitHub: <a href=\"https:\/\/github.com\/musebe\/cloudinary-personalized-visuals\">https:\/\/github.com\/musebe\/cloudinary-personalized-visuals<\/a>\n<\/li>\n<li>Live Demo: <a href=\"https:\/\/cloudinary-personalized-visuals.vercel.app\/\">https:\/\/cloudinary-personalized-visuals.vercel.app\/<\/a>\n<\/li>\n<\/ul>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":87,"featured_media":37801,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[98,409,366],"class_list":["post-37798","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-e-commerce","tag-generative-ai","tag-personalization"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Personalize E-commerce and Marketing Visuals in Next.js With Generative Replace and Overlays<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Personalize E-commerce and Marketing Visuals in Next.js With Generative Replace and Overlays\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-06-18T14:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-06-21T00:00:05+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1749681834\/Personalization_NextJS-blog\/Personalization_NextJS-blog-jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"2000\" \/>\n\t<meta property=\"og:image:height\" content=\"1100\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"melindapham\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"Personalize E-commerce and Marketing Visuals in Next.js With Generative Replace and Overlays\",\"datePublished\":\"2025-06-18T14:00:00+00:00\",\"dateModified\":\"2025-06-21T00:00:05+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays\"},\"wordCount\":13,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1749681834\/Personalization_NextJS-blog\/Personalization_NextJS-blog.jpg?_i=AA\",\"keywords\":[\"E-commerce\",\"Generative AI\",\"Personalization\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2025\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays\",\"url\":\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays\",\"name\":\"Personalize E-commerce and Marketing Visuals in Next.js With Generative Replace and Overlays\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1749681834\/Personalization_NextJS-blog\/Personalization_NextJS-blog.jpg?_i=AA\",\"datePublished\":\"2025-06-18T14:00:00+00:00\",\"dateModified\":\"2025-06-21T00:00:05+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1749681834\/Personalization_NextJS-blog\/Personalization_NextJS-blog.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1749681834\/Personalization_NextJS-blog\/Personalization_NextJS-blog.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Personalize E-commerce and Marketing Visuals in Next.js With Generative Replace and Overlays\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\",\"name\":\"melindapham\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"caption\":\"melindapham\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Personalize E-commerce and Marketing Visuals in Next.js With Generative Replace and Overlays","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays","og_locale":"en_US","og_type":"article","og_title":"Personalize E-commerce and Marketing Visuals in Next.js With Generative Replace and Overlays","og_url":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays","og_site_name":"Cloudinary Blog","article_published_time":"2025-06-18T14:00:00+00:00","article_modified_time":"2025-06-21T00:00:05+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1749681834\/Personalization_NextJS-blog\/Personalization_NextJS-blog-jpg?_i=AA","type":"image\/jpeg"}],"author":"melindapham","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"Personalize E-commerce and Marketing Visuals in Next.js With Generative Replace and Overlays","datePublished":"2025-06-18T14:00:00+00:00","dateModified":"2025-06-21T00:00:05+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays"},"wordCount":13,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1749681834\/Personalization_NextJS-blog\/Personalization_NextJS-blog.jpg?_i=AA","keywords":["E-commerce","Generative AI","Personalization"],"inLanguage":"en-US","copyrightYear":"2025","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays","url":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays","name":"Personalize E-commerce and Marketing Visuals in Next.js With Generative Replace and Overlays","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1749681834\/Personalization_NextJS-blog\/Personalization_NextJS-blog.jpg?_i=AA","datePublished":"2025-06-18T14:00:00+00:00","dateModified":"2025-06-21T00:00:05+00:00","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1749681834\/Personalization_NextJS-blog\/Personalization_NextJS-blog.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1749681834\/Personalization_NextJS-blog\/Personalization_NextJS-blog.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/personalize-e-commerce-marketing-next-js-generative-replace-overlays#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Personalize E-commerce and Marketing Visuals in Next.js With Generative Replace and Overlays"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9","name":"melindapham","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","caption":"melindapham"}}]}},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1749681834\/Personalization_NextJS-blog\/Personalization_NextJS-blog.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37798","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/87"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=37798"}],"version-history":[{"count":2,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37798\/revisions"}],"predecessor-version":[{"id":37809,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37798\/revisions\/37809"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/37801"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=37798"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=37798"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=37798"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}