{"id":37664,"date":"2025-05-27T07:00:00","date_gmt":"2025-05-27T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=37664"},"modified":"2025-05-23T15:54:53","modified_gmt":"2025-05-23T22:54:53","slug":"dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill","title":{"rendered":"Transform Static Visuals Into Dynamic Banners in Next.js Using Ken Burns-Style Zoompan and Generative Fill"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>Static images are everywhere but in a fast, responsive web, they often get overlooked. Motion and intelligent layout adaptation are now expected, especially in marketing and product design.<\/p>\n<p>With <strong>Cloudinary\u2019s Zoompan and Generative Fill<\/strong>, you can:<\/p>\n<ul>\n<li>Add smooth, cinematic motion with the <strong>Ken Burns\u2013style Zoompan<\/strong> effect.<\/li>\n<li>Extend images automatically using <strong>AI-powered Generative Fill<\/strong>.<\/li>\n<li>Deliver optimized, responsive banners without manual editing.<\/li>\n<\/ul>\n<p>In this guide, you\u2019ll build a dynamic banner system into a <strong>Next.js 15<\/strong> app using:<\/p>\n<ul>\n<li>\n<strong>Cloudinary<\/strong> for media transformations.<\/li>\n<li>\n<strong>Tailwind CSS<\/strong> and <strong>shadcn\/ui<\/strong> for styling.<\/li>\n<li>\n<strong>Motion.dev<\/strong> for smooth animations.<\/li>\n<li>\n<strong>Redis<\/strong> for storing and displaying recent uploads.<\/li>\n<\/ul>\n<p><strong>Live Demo<\/strong>: <a href=\"https:\/\/zoompan-banners-oygp.vercel.app\/\">zoompan-banners.vercel.app<\/a><\/p>\n<p><strong>GitHub Repo<\/strong>: <a href=\"https:\/\/github.com\/musebe\/zoompan-banners\">github.com\/musebe\/zoompan-banners<\/a><\/p>\n<h2>Project Setup<\/h2>\n<p>We\u2019ll build this project from the ground up using <strong>Next.js 15<\/strong> with the App Router, plus Tailwind CSS, Cloudinary, Redis, and Motion.dev.<\/p>\n<h3>Prerequisites<\/h3>\n<p>Before starting, make sure you have:<\/p>\n<ul>\n<li>\n<strong>Node.js 18 or newer<\/strong>\nWe recommend using <code>nvm<\/code> to manage versions:<\/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\">nvm install <span class=\"hljs-number\">20<\/span>\nnvm <span class=\"hljs-keyword\">use<\/span> 20\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<ul>\n<li>A free <a href=\"https:\/\/cloudinary.com\/\">Cloudinary account<\/a>\n<\/li>\n<li>A free <a href=\"https:\/\/redis.com\/redis-enterprise-cloud\/overview\/\">Redis Cloud account<\/a>\n<\/li>\n<\/ul>\n<h3>Step 1: Create a New Next.js App<\/h3>\n<p>Create a new app with the App Router enabled and TypeScript support:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">npx<\/span>  <span class=\"hljs-selector-tag\">create-next-app<\/span><span class=\"hljs-keyword\">@latest<\/span>  zoompan-banners  --app  --typescript  --tailwind\ncd  zoompan-banners\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This sets up the base project with:<\/p>\n<ul>\n<li>\n<strong>Next.js 15<\/strong> (App Router)<\/li>\n<li>\n<strong>Tailwind CSS<\/strong>\n<\/li>\n<li>\n<strong>TypeScript<\/strong>\n<\/li>\n<\/ul>\n<h3>Step 2: Install Required Packages<\/h3>\n<p>Install the dependencies for image transformation, animation, Redis, and Cloudinary:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">npm<\/span>  <span class=\"hljs-selector-tag\">install<\/span>  <span class=\"hljs-keyword\">@cloudinary<\/span>\/url-gen  redis  motion  sonner  clsx  lucide-react\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Also install UI components using <a href=\"https:\/\/ui.shadcn.com\/docs\/installation\">shadcn\/ui<\/a>:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">npx<\/span>  <span class=\"hljs-selector-tag\">shadcn-ui<\/span><span class=\"hljs-keyword\">@latest<\/span>  init\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>When prompted, you can choose:<\/p>\n<ul>\n<li>\n<strong>Tailwind CSS<\/strong> as your styling system.<\/li>\n<li>\n<strong>App Router<\/strong>.<\/li>\n<li>Your preferred component path (default is fine).<\/li>\n<\/ul>\n<p>Then install components you\u2019ll need:<\/p>\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\">npx  shadcn-ui@latest  add  button  <span class=\"hljs-keyword\">switch<\/span>  label  card  dialog\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<h3>Step 3: Configure Cloudinary and Redis<\/h3>\n<p>Create a <code>.env.local<\/code> file in the project root and add your credentials:<\/p>\n<pre class=\"js-syntax-highlighted\"><code>NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name\n\nNEXT_PUBLIC_CLOUDINARY_FOLDER=marketing-banners\n\nCLOUDINARY_API_KEY=your-api-key\n\nCLOUDINARY_API_SECRET=your-api-secret\n\nREDIS_URL=your-redis-url\n<\/code><\/pre>\n<p>At this point, your project is ready for development.\nNext, we\u2019ll set up image uploads with Cloudinary.<\/p>\n<h2>Upload Images to Cloudinary<\/h2>\n<p>To apply any transformation whether it\u2019s a zoom animation or AI-powered fill, we\u2019ll first need an image hosted on Cloudinary.<\/p>\n<p>Rather than uploading from your server, we\u2019ll upload directly from the browser. This is faster, scales better, and keeps your backend light but it does require a <strong>secure signature<\/strong> from your backend for every upload.<\/p>\n<p>In this section, we\u2019ll build:<\/p>\n<ul>\n<li>A helper to <strong>generate a secure upload signature<\/strong>.<\/li>\n<li>An API route that exposes it to the client.<\/li>\n<li>A simple uploader UI that POSTs directly to Cloudinary.<\/li>\n<\/ul>\n<h3>Create the Signature Helper (lib)<\/h3>\n<p>Cloudinary requires a signed request before accepting direct uploads. We\u2019ll generate that signature server-side using their <code>api_sign_request<\/code> utility, and optionally cache it with Redis.<\/p>\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\"><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\">getUploadSignature<\/span>(<span class=\"hljs-params\">params<\/span>) <\/span>{\n  <span class=\"hljs-comment\">\/\/ 1. Sign params using your Cloudinary API secret<\/span>\n  <span class=\"hljs-comment\">\/\/ 2. Return { signature, timestamp }<\/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<blockquote>\n<p>View full helper \u2192 <a href=\"https:\/\/github.com\/musebe\/zoompan-banners\/blob\/main\/src\/lib\/cloudinary-cache.ts\">src\/lib\/cloudinary-cache.ts<\/a><\/p>\n<\/blockquote>\n<h3>Create the Signature API Route<\/h3>\n<p>We expose a route at <code>\/api\/upload-signature<\/code> that returns the signed upload config when called from the client.<\/p>\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\"><span class=\"hljs-keyword\">import<\/span> { getUploadSignature } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@\/lib\/cloudinary-cache'<\/span>;\n\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\">GET<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> params = {\n    <span class=\"hljs-attr\">folder<\/span>: process.env.NEXT_PUBLIC_CLOUDINARY_FOLDER,\n    <span class=\"hljs-attr\">use_filename<\/span>: <span class=\"hljs-literal\">true<\/span>,\n    <span class=\"hljs-attr\">unique_filename<\/span>: <span class=\"hljs-literal\">false<\/span>,\n  };\n  <span class=\"hljs-keyword\">const<\/span> { signature, timestamp } = <span class=\"hljs-built_in\">JSON<\/span>.parse(<span class=\"hljs-keyword\">await<\/span> getUploadSignature(params));\n  <span class=\"hljs-keyword\">return<\/span> Response.json({ signature, timestamp, ...params });\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<blockquote>\n<p>View the full route here \u2192 <a href=\"https:\/\/github.com\/musebe\/zoompan-banners\/blob\/main\/src\/app\/api\/upload-signature\/route.ts\">src\/app\/api\/upload-signature\/route.ts<\/a><\/p>\n<\/blockquote>\n<h3>Build the Upload UI (client)<\/h3>\n<p>Now the browser can:<\/p>\n<ol>\n<li>Fetch the signature.<\/li>\n<li>Append it to a <code>FormData<\/code> payload.<\/li>\n<li>POST directly to Cloudinary\u2019s image upload API.<\/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\">const<\/span> sig = <span class=\"hljs-keyword\">await<\/span> fetch(<span class=\"hljs-string\">'\/api\/upload-signature'<\/span>).then(<span class=\"hljs-function\"><span class=\"hljs-params\">r<\/span> =&gt;<\/span> r.json());\n\n<span class=\"hljs-keyword\">const<\/span> form = <span class=\"hljs-keyword\">new<\/span> FormData();\nform.append(<span class=\"hljs-string\">'file'<\/span>, file);\nform.append(<span class=\"hljs-string\">'api_key'<\/span>, process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY!);\nform.append(<span class=\"hljs-string\">'timestamp'<\/span>, <span class=\"hljs-built_in\">String<\/span>(sig.timestamp));\nform.append(<span class=\"hljs-string\">'signature'<\/span>, sig.signature);\n<span class=\"hljs-built_in\">Object<\/span>.entries(sig).forEach(<span class=\"hljs-function\">(<span class=\"hljs-params\">&#91;k, v]<\/span>) =&gt;<\/span> form.append(k, <span class=\"hljs-built_in\">String<\/span>(v)));\n\n<span class=\"hljs-keyword\">const<\/span> uploadUrl = <span class=\"hljs-string\">`https:\/\/api.cloudinary.com\/v1_1\/<span class=\"hljs-subst\">${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}<\/span>\/image\/upload`<\/span>;\n<span class=\"hljs-keyword\">const<\/span> { public_id } = <span class=\"hljs-keyword\">await<\/span> fetch(uploadUrl, { <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">'POST'<\/span>, <span class=\"hljs-attr\">body<\/span>: form }).then(<span class=\"hljs-function\"><span class=\"hljs-params\">r<\/span> =&gt;<\/span> r.json());\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<p>Once uploaded, the <code>public_id<\/code> returned by Cloudinary is all you need to apply Zoompan and Generative Fill.<\/p>\n<blockquote>\n<p>View full uploader \u2192 <a href=\"https:\/\/github.com\/musebe\/zoompan-banners\/blob\/main\/src\/components\/ImageUploader.tsx\">src\/components\/ImageUploader.tsx<\/a><\/p>\n<\/blockquote>\n<h2>Apply Generative Fill and Zoompan<\/h2>\n<p>Once your image is uploaded, it\u2019s just sitting there in Cloudinary. Now it\u2019s time to make it <em>do<\/em> something expand, animate, and adapt based on where it\u2019s shown.<\/p>\n<p>This section explains how we use Cloudinary\u2019s transformation URL API to turn a single static image into:<\/p>\n<ul>\n<li>A <strong>responsive banner<\/strong> using AI-based <strong>Generative Fill<\/strong>.<\/li>\n<li>A <strong>cinematic motion graphic<\/strong> using <strong>Zoompan<\/strong>.<\/li>\n<\/ul>\n<p>Let\u2019s break it down.<\/p>\n<h3>Why Use Cloudinary Transformations?<\/h3>\n<p>Imagine you\u2019ve got a marketing banner that\u2019s 800\u00d7800 looks great on Instagram, but now you need it:<\/p>\n<ul>\n<li>Wider for your homepage hero (e.g., 1600\u00d7900).<\/li>\n<li>Animated for a product landing page.<\/li>\n<li>Light enough for fast mobile loads.<\/li>\n<\/ul>\n<p>Instead of opening Photoshop every time, you let Cloudinary do it:<\/p>\n<ul>\n<li>Extend the image with <strong>AI<\/strong>.<\/li>\n<li>Animate it with <strong>Zoompan<\/strong>.<\/li>\n<li>Compress and format it automatically.<\/li>\n<\/ul>\n<p>That\u2019s what these helpers do.<\/p>\n<h3>Generative Fill: Extend With AI<\/h3>\n<p>Cloudinary\u2019s <strong>Generative Fill<\/strong> uses AI to expand your image while preserving its style. Think of it like saying:<\/p>\n<blockquote>\n<p>\u201cTake this square image and make it 16:9 but don\u2019t stretch it. Fill the extra space with something that looks like it belongs.<\/p>\n<\/blockquote>\n<h3>The Code<\/h3>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">createGenerativeFillURL<\/span>(<span class=\"hljs-params\">file: string, w: number, h: number<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> cldClient\n    .image(toId(file))\n    .resize(pad().width(w).height(h).background(generativeFill()))\n    .delivery(quality(<span class=\"hljs-string\">'auto'<\/span>))\n    .delivery(format(<span class=\"hljs-string\">'auto'<\/span>))\n    .toURL();\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><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<p>Instead of showing a centered image with whitespace, you get a full-width visual that <em>feels<\/em> complete, even if it started small.<\/p>\n<p>It\u2019s useful for:<\/p>\n<ul>\n<li>Making banners responsive.<\/li>\n<li>Adapting to wider screens.<\/li>\n<li>Removing manual cropping.<\/li>\n<\/ul>\n<blockquote>\n<p><strong>View on GitHub<\/strong> \u2192 <a href=\"https:\/\/github.com\/musebe\/zoompan-banners\/blob\/main\/src\/lib\/cloudinary-client-utils.ts\">src\/lib\/cloudinary-client-utils.ts<\/a><\/p>\n<\/blockquote>\n<h3>Zoompan: Animate With Ken Burns Motion<\/h3>\n<p><strong>Zoompan<\/strong> creates a smooth zooming and panning animation, like what you see in Apple keynotes or documentary intros.<\/p>\n<p>Think:<\/p>\n<blockquote>\n<p>\u201cTake this image and slowly zoom in, then reset \u2014 on a loop.\u201d<\/p>\n<\/blockquote>\n<p>No video editor. No rendering. Just a URL.<\/p>\n<h3>How It\u2019s Defined<\/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\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">createZoompanGifURL<\/span>(<span class=\"hljs-params\">file: string, opts = {}<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> {\n    duration = <span class=\"hljs-number\">6<\/span>,\n    loop = <span class=\"hljs-literal\">true<\/span>,\n    fps = <span class=\"hljs-number\">15<\/span>,\n    maxZoom = <span class=\"hljs-number\">1.05<\/span>,\n    format = <span class=\"hljs-string\">'gif'<\/span>,\n  } = opts;\n\n  <span class=\"hljs-keyword\">const<\/span> zoompanParams = &#91;\n    <span class=\"hljs-string\">`du_<span class=\"hljs-subst\">${duration}<\/span>`<\/span>,\n    <span class=\"hljs-string\">`maxzoom_<span class=\"hljs-subst\">${maxZoom}<\/span>`<\/span>,\n    <span class=\"hljs-string\">`fps_<span class=\"hljs-subst\">${fps}<\/span>`<\/span>,\n  ].join(<span class=\"hljs-string\">';'<\/span>);\n\n  <span class=\"hljs-keyword\">const<\/span> parts = &#91;\n    <span class=\"hljs-string\">`e_zoompan:<span class=\"hljs-subst\">${zoompanParams}<\/span>`<\/span>,\n    loop ? <span class=\"hljs-string\">'e_loop'<\/span> : <span class=\"hljs-literal\">null<\/span>,\n    <span class=\"hljs-string\">'e_sharpen'<\/span>,\n    <span class=\"hljs-string\">'fl_animated'<\/span>,\n    <span class=\"hljs-string\">'q_auto'<\/span>,\n  ].filter(<span class=\"hljs-built_in\">Boolean<\/span>);\n\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/<span class=\"hljs-subst\">${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}<\/span>\/image\/upload\/<span class=\"hljs-subst\">${parts.join(<span class=\"hljs-string\">'\/'<\/span>)}<\/span>\/<span class=\"hljs-subst\">${<span class=\"hljs-built_in\">encodeURIComponent<\/span>(toId(file))}<\/span>.<span class=\"hljs-subst\">${format}<\/span>`<\/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<p>It\u2019s useful for:<\/p>\n<ul>\n<li>Bringing static content to life.<\/li>\n<li>Adding motion without video.<\/li>\n<li>Looping banner animations.<\/li>\n<\/ul>\n<p>You can output it as <code>.gif<\/code>, <code>.webm<\/code>, or <code>.mp4<\/code>, depending on browser needs.<\/p>\n<blockquote>\n<p><strong>View on GitHub<\/strong> \u2192 <a href=\"https:\/\/github.com\/musebe\/zoompan-banners\/blob\/main\/src\/lib\/cloudinary-client-utils.ts\">src\/lib\/cloudinary-client-utils.ts<\/a><\/p>\n<\/blockquote>\n<h3>How These Helpers Fit in<\/h3>\n<p>Once you have the <code>public_id<\/code> of an image, you now have <strong>three versions<\/strong> of it instantly available:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-11\" 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> staticUrl = createOptimisedURL(publicId);\n<span class=\"hljs-keyword\">const<\/span> fillUrl = createGenerativeFillURL(publicId, <span class=\"hljs-number\">1600<\/span>, <span class=\"hljs-number\">900<\/span>);\n<span class=\"hljs-keyword\">const<\/span> zoompanUrl = createZoompanGifURL(publicId);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><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<p>Depending on the toggle state, you use the right one:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">\n<span class=\"hljs-keyword\">const<\/span> transformed = zoompan\n  ? zoompanUrl\n  : generativeFill\n  ? fillUrl\n  : staticUrl;\n  \n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><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<p>This logic powers:<\/p>\n<ul>\n<li>\n<code>&lt;Banner \/&gt;<\/code>: to show one enhanced visual.<\/li>\n<li>\n<code>&lt;SplitView \/&gt;<\/code>: to compare static vs transformed.<\/li>\n<li>\n<code>&lt;UploadGallery \/&gt;<\/code>: to preview recent uploads.<\/li>\n<\/ul>\n<p>By combining these helpers with just a <code>public_id<\/code>, you create a flexible, dynamic banner system all powered by Cloudinary\u2019s media engine.<\/p>\n<h2>Render Dynamic Banners<\/h2>\n<p>Once you\u2019ve transformed the image with Generative Fill or Zoompan, you\u2019ll need a flexible way to display it.<\/p>\n<p>The goal here is simple:<\/p>\n<blockquote>\n<p>Given an uploaded image, decide what to show static, filled, or animated and render it the right way.<\/p>\n<\/blockquote>\n<p>This logic lives in a reusable component, where the <strong>Cloudinary transformation<\/strong> and <strong>render method<\/strong> are chosen based on just two booleans: <code>generativeFill<\/code> and <code>zoompan<\/code>.<\/p>\n<h3>Step 1: Generate the Correct URLs<\/h3>\n<p>We\u2019ll use three utility functions, each returning a version of the image:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">\n<span class=\"hljs-keyword\">import<\/span> {\n  createOptimisedURL,\n  createGenerativeFillURL,\n  createZoompanGifURL,\n} <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@\/lib\/cloudinary-client-utils'<\/span>;\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><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<p>Inside the component, we determine the image versions like this:<\/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-keyword\">const<\/span> { staticUrl, gifUrl } = useMemo(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> staticUrl = generativeFill\n    ? createGenerativeFillURL(id, size.w, size.h)\n    : createOptimisedURL(id);\n\n  <span class=\"hljs-keyword\">const<\/span> gifUrl = zoompan ? createZoompanGifURL(id) : <span class=\"hljs-literal\">undefined<\/span>;\n\n  <span class=\"hljs-keyword\">return<\/span> { staticUrl, gifUrl };\n}, &#91;id, generativeFill, zoompan, size]);\n\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<p>What\u2019s happening here:<\/p>\n<ul>\n<li>If <code>generativeFill<\/code> is true, we\u2019ll generate a version of the image that\u2019s been extended to fit a given width and height using AI.<\/li>\n<li>If <code>zoompan<\/code> is true, we\u2019ll also generate a separate URL for an animated version with the Ken Burns effect.<\/li>\n<li>If both are false, we\u2019ll fall back to a clean, optimized version of the original image.<\/li>\n<\/ul>\n<p>This logic ensures <strong>you\u2019ll never fetch more than you need<\/strong>.<\/p>\n<h3>Step 2: Choose the Right Display<\/h3>\n<p>Once we have our URLs, we\u2019ll check if this should be a motion graphic or just an image.<\/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\"><span class=\"hljs-keyword\">if<\/span> (zoompan &amp;&amp; gifUrl) {\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">motion.img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{gifUrl}<\/span> <span class=\"hljs-attr\">...<\/span> \/&gt;<\/span><\/span>;\n}\n<span class=\"hljs-keyword\">return<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Image<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{staticUrl}<\/span> <span class=\"hljs-attr\">...<\/span> \/&gt;<\/span><\/span>;\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>If <code>zoompan<\/code> is true, we\u2019ll render a raw <code>&lt;img&gt;<\/code> tag enhanced with Motion\u2019s animation presets, ensuring the GIF or video plays correctly.<\/li>\n<li>If not, we\u2019ll use the framework\u2019s <code>&lt;Image \/&gt;<\/code> component for lazy loading, size hints, and better performance.<\/li>\n<\/ul>\n<p>That\u2019s it, all dynamic rendering handled in a few lines.<\/p>\n<blockquote>\n<p><strong>View on GitHub<\/strong> \u2192 <a href=\"https:\/\/github.com\/musebe\/zoompan-banners\/blob\/main\/src\/components\/Banner.tsx\">src\/components\/Banner.tsx<\/a><\/p>\n<\/blockquote>\n<p>This makes your banner logic fully declarative: You toggle features, and the correct transformation and rendering happens automatically.<\/p>\n<h2>Split View: Compare Original vs. Transformed<\/h2>\n<p>Once you apply Generative Fill or Zoompan, it\u2019s helpful to show how the image has changed.<\/p>\n<p>This section adds a <strong>split comparison view<\/strong> original on the left, transformed on the right using a draggable slider.<\/p>\n<h3>Core Logic<\/h3>\n<p>To render this comparison, we\u2019ll generate two URLs:<\/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> staticUrl = createOptimisedURL(publicId);\n<span class=\"hljs-keyword\">const<\/span> dynamicUrl = zoompan\n  ? createZoompanGifURL(publicId)\n  : createGenerativeFillURL(publicId, <span class=\"hljs-number\">1600<\/span>, <span class=\"hljs-number\">900<\/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<p>This ensures:<\/p>\n<ul>\n<li>You always have the untouched original<\/li>\n<li>You conditionally get the enhanced version<\/li>\n<\/ul>\n<h3>UI Component<\/h3>\n<p>The UI uses a horizontal slider with two overlaid images. When the user drags the handle, they can visually compare the changes side by side.<\/p>\n<blockquote>\n<p><strong>View on GitHub<\/strong> \u2192 <a href=\"https:\/\/github.com\/musebe\/zoompan-banners\/blob\/main\/src\/components\/SplitView.tsx\">src\/components\/SplitView.tsx<\/a><\/p>\n<\/blockquote>\n<p>This makes your enhancements feel real, not just processed in the background, but visibly improved in context.<\/p>\n<h2>Gallery: Display Recent Uploads<\/h2>\n<p>Every time a user uploads a new image, we store its <code>publicId<\/code> in Redis so others can see the results live. The gallery page pulls that list and displays each image using the same transformation logic.<\/p>\n<p>This adds a community showcase feel and is instant.<\/p>\n<h3>How It Works<\/h3>\n<p>Each time an upload completes:<\/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\">await<\/span> fetch(<span class=\"hljs-string\">'\/api\/uploads'<\/span>, {\n  <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">'POST'<\/span>,\n  <span class=\"hljs-attr\">headers<\/span>: { <span class=\"hljs-string\">'Content-Type'<\/span>: <span class=\"hljs-string\">'application\/json'<\/span> },\n  <span class=\"hljs-attr\">body<\/span>: <span class=\"hljs-built_in\">JSON<\/span>.stringify({ publicId }),\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<p>This writes the <code>publicId<\/code> to Redis.<\/p>\n<p>On the gallery page, we\u2019ll pull from Redis using offset + limit (pagination):<\/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> res = <span class=\"hljs-keyword\">await<\/span> fetch(<span class=\"hljs-string\">`\/api\/uploads?limit=3&amp;offset=<span class=\"hljs-subst\">${offset}<\/span>`<\/span>);\n<span class=\"hljs-keyword\">const<\/span> { uploads } = <span class=\"hljs-keyword\">await<\/span> res.json();\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<p>This gives us the next batch of uploaded images, which we render like so:<\/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> thumbUrl = zoompan\n  ? createZoompanGifURL(pid)\n  : generativeFill\n  ? createGenerativeFillURL(pid, <span class=\"hljs-number\">800<\/span>, <span class=\"hljs-number\">450<\/span>)\n  : createOptimisedURL(pid);\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<p>Each card reuses the same transformation helpers, but now for thumbnails.<\/p>\n<blockquote>\n<p><strong>View on GitHub<\/strong> \u2192 <a href=\"https:\/\/github.com\/musebe\/zoompan-banners\/blob\/main\/src\/components\/UploadGallery.tsx\">src\/components\/UploadGallery.tsx<\/a><\/p>\n<\/blockquote>\n<h2>Smooth Animations With Motion.dev<\/h2>\n<p>To polish the experience, we\u2019ll add gentle animations using <a href=\"https:\/\/motion.dev\/\">motion.dev<\/a>. It\u2019s lightweight, composable, and integrates naturally into your component logic.<\/p>\n<p>For example, in the gallery:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">motion.div<\/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\">y:<\/span> <span class=\"hljs-attr\">20<\/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\">y:<\/span> <span class=\"hljs-attr\">0<\/span> }}\n  <span class=\"hljs-attr\">transition<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">duration:<\/span> <span class=\"hljs-attr\">0.35<\/span> }}\n&gt;<\/span>\n  {\/* image card here *\/}\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">motion.div<\/span>&gt;<\/span>\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><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>And for looping zoom animation:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-21\" 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.img<\/span>\n  <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{zoompanUrl}<\/span>\n  <span class=\"hljs-attr\">variants<\/span>=<span class=\"hljs-string\">{zoomInOut}<\/span>\n  <span class=\"hljs-attr\">initial<\/span>=<span class=\"hljs-string\">\"rest\"<\/span>\n  <span class=\"hljs-attr\">animate<\/span>=<span class=\"hljs-string\">\"zoom\"<\/span>\n\/&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><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>Motion effects help your AI enhancements feel intentional. They don\u2019t just appear, they arrive.<\/p>\n<blockquote>\n<p>View animation presets \u2192 <a href=\"https:\/\/github.com\/musebe\/zoompan-banners\/blob\/main\/src\/lib\/motionPresets.ts\">src\/lib\/motionPresets.ts<\/a><\/p>\n<\/blockquote>\n<h2>Conclusion<\/h2>\n<p>What began as a static image upload evolved into a fully interactive, AI-powered banner engine by using a few Cloudinary transformations and carefully structured components.<\/p>\n<p>By combining:<\/p>\n<ul>\n<li>\n<strong>Generative Fill<\/strong> for intelligent layout extension,<\/li>\n<li>\n<strong>Zoompan<\/strong> for subtle motion and cinematic feel,<\/li>\n<li>\n<strong>Cloudinary<\/strong> for real-time delivery and optimization,<\/li>\n<li>and <strong>Motion.dev<\/strong>, <strong>Tailwind CSS<\/strong>, and <strong>Redis<\/strong> for UI and state,<\/li>\n<\/ul>\n<p>you\u2019ve built a modern, responsive banner system that adapts to devices, captures attention, and requires zero manual post-processing.<\/p>\n<p>This isn\u2019t just a demo, it\u2019s a foundation for more dynamic media workflows. You can now plug this system into product pages, landing campaigns, or CMS tools where visuals need to be smart, fast, and engaging.<\/p>\n<p>Explore the full codebase, remix it, or deploy your own:<\/p>\n<p><strong>Live Demo<\/strong> \u2192 <a href=\"https:\/\/zoompan-banners-oygp.vercel.app\/\">zoompan-banners.vercel.app<\/a><br \/>\n<strong>GitHub Repo<\/strong> \u2192 <a href=\"https:\/\/github.com\/musebe\/zoompan-banners\">github.com\/musebe\/zoompan-banners<\/a><\/p>\n<p>Want to take it further? Try adding:<\/p>\n<ul>\n<li>Support for other formats like <code>webm<\/code> or <code>mp4<\/code>.<\/li>\n<li>Custom zoom targets using <code>g_face<\/code> or <code>g_auto<\/code>.<\/li>\n<li>Caption overlays or branded templates.<\/li>\n<\/ul>\n<p>And <a href=\"https:\/\/cloudinary.com\/users\/register_free\">sign up for a free Cloudinary account<\/a> today to get started.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":87,"featured_media":37665,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[409,165,212],"class_list":["post-37664","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-generative-ai","tag-image-transformation","tag-next-js"],"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>Transform Static Visuals Into Dynamic Banners in Next.js Using Ken Burns-Style Zoompan and Generative Fill<\/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\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Transform Static Visuals Into Dynamic Banners in Next.js Using Ken Burns-Style Zoompan and Generative Fill\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-05-27T14:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1747760102\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill.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\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"Transform Static Visuals Into Dynamic Banners in Next.js Using Ken Burns-Style Zoompan and Generative Fill\",\"datePublished\":\"2025-05-27T14:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill\"},\"wordCount\":16,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1747760102\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill.jpg?_i=AA\",\"keywords\":[\"Generative AI\",\"Image Transformation\",\"Next.js\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2025\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill\",\"url\":\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill\",\"name\":\"Transform Static Visuals Into Dynamic Banners in Next.js Using Ken Burns-Style Zoompan and Generative Fill\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1747760102\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill.jpg?_i=AA\",\"datePublished\":\"2025-05-27T14:00:00+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1747760102\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1747760102\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Transform Static Visuals Into Dynamic Banners in Next.js Using Ken Burns-Style Zoompan and Generative Fill\"}]},{\"@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":"Transform Static Visuals Into Dynamic Banners in Next.js Using Ken Burns-Style Zoompan and Generative Fill","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\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill","og_locale":"en_US","og_type":"article","og_title":"Transform Static Visuals Into Dynamic Banners in Next.js Using Ken Burns-Style Zoompan and Generative Fill","og_url":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill","og_site_name":"Cloudinary Blog","article_published_time":"2025-05-27T14:00:00+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1747760102\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill.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\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"Transform Static Visuals Into Dynamic Banners in Next.js Using Ken Burns-Style Zoompan and Generative Fill","datePublished":"2025-05-27T14:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill"},"wordCount":16,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1747760102\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill.jpg?_i=AA","keywords":["Generative AI","Image Transformation","Next.js"],"inLanguage":"en-US","copyrightYear":"2025","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill","url":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill","name":"Transform Static Visuals Into Dynamic Banners in Next.js Using Ken Burns-Style Zoompan and Generative Fill","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1747760102\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill.jpg?_i=AA","datePublished":"2025-05-27T14:00:00+00:00","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1747760102\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1747760102\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/dynamic-banners-next-js-ken-burns-style-zoompan-generative-fill#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Transform Static Visuals Into Dynamic Banners in Next.js Using Ken Burns-Style Zoompan and Generative Fill"}]},{"@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\/v1747760102\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill\/Blog_Transform_Static_Visuals_into_Dynamic_Banners_in_Next.js_Using_Zoompan_and_Generative_Fill.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37664","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=37664"}],"version-history":[{"count":2,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37664\/revisions"}],"predecessor-version":[{"id":37667,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/37664\/revisions\/37667"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/37665"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=37664"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=37664"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=37664"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}