{"id":39086,"date":"2025-11-13T07:00:00","date_gmt":"2025-11-13T15:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=39086"},"modified":"2025-12-12T15:30:19","modified_gmt":"2025-12-12T23:30:19","slug":"a-b-test-video-thumbnails-posthog-next-js","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js","title":{"rendered":"How to A\/B Test Video Thumbnails Programmatically With Cloudinary, PostHog, and Next.js"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>An overlooked factor that can determine whether or not your video gets watched is\u2026 the thumbnail. It\u2019s the digital handshake, movie poster, and book cover all in one. A compelling thumbnail can make a video go viral, but a poor one can get glossed over. But what style is more engaging? The one with the bright text overlay, or the simple, clean frame?<\/p>\n<p>Guessing is a losing game, and manual testing is slow and inefficient. You\u2019re leaving engagement and views on the table.<\/p>\n<p>In this guide, you\u2019ll build a powerful, automated A\/B testing pipeline using <strong>Next.js<\/strong>, <strong>Cloudinary<\/strong> for dynamic media generation, and <strong>PostHog<\/strong> for robust analytics. You\u2019ll learn how to programmatically serve different thumbnail variants to users, track every interaction, and build a custom dashboard to confidently declare a winning thumbnail that can boost click-through rate (CTR).<\/p>\n<ul>\n<li>\n<strong>Live demo:<\/strong> <a href=\"https:\/\/cloudinary-video-thumbnails-ab-test.vercel.app\/\">cloudinary-video-thumbnails-ab-test.vercel.app<\/a>\n<\/li>\n<li>\n<strong>GitHub repo:<\/strong> <a href=\"https:\/\/github.com\/musebe\/cloudinary-video_thumbnails_ab-testing\">github.com\/musebe\/cloudinary-video_thumbnails_ab-testing<\/a>\n<\/li>\n<\/ul>\n<h2>What You\u2019ll Need and How It Works<\/h2>\n<p>Before we start building, make sure you have:<\/p>\n<ul>\n<li>\n<strong>Node.js v18+<\/strong> to build the thumbnail component, create secure API routes that talk to PostHog, and render everything efficiently.<\/li>\n<li>A free <strong>Cloudinary account<\/strong> to manage our video and generate A\/B test thumbnail variants on the fly.<\/li>\n<li>A free <strong>PostHog account<\/strong> for event tracking and analytics, e.g., impressions, clicks, and plays. You\u2019ll query this data with its HogQL API to see which variant performs better.<\/li>\n<li>Basic knowledge of <strong>Next.js<\/strong>, including the App Router.<\/li>\n<\/ul>\n<p>With these tools in place, we\u2019re ready to build a system that connects them into a fully functional A\/B test.<\/p>\n<h2>Cloudinary and PostHog<\/h2>\n<p><a href=\"https:\/\/cloudinary.com\/documentation\/video_analytics\">Cloudinary Video Analytics<\/a> tracks powerful metrics (e.g., views, engagement, playback quality) <em>after<\/em> you hit the play button. These insights are invaluable for understanding video delivery, performance, and audience retention. However, because our A\/B test is focused on what happens <em>before<\/em> the click, we\u2019ll use PostHog to track the specific user journey leading up to the play event:<\/p>\n<ul>\n<li>Capture <strong>custom events<\/strong> like <code>thumbnail_impression<\/code> and <code>thumbnail_clicked<\/code>.<\/li>\n<li>Measure the <strong>click-through rate (CTR)<\/strong> between our A and B variants.<\/li>\n<li>Use <strong>HogQL<\/strong> (PostHog\u2019s SQL-style query tool) to build a custom analytics dashboard.<\/li>\n<li>Create <strong>funnels<\/strong> to visualize how many impressions convert into clicks, and then into plays.<\/li>\n<\/ul>\n<p>In this project, Cloudinary and PostHog work together as a perfect team. <strong>Cloudinary<\/strong> handles the video delivery and dynamic thumbnail generation, while <strong>PostHog<\/strong> tracks the user behavior that tells us <em>why<\/em> they chose to play the video in the first place.<\/p>\n<h2>Step 1: Setting Up the Next.js Project<\/h2>\n<p>First, let\u2019s lay the groundwork for our application. You\u2019ll start with a new <strong>Next.js<\/strong> project and use <strong>shadcn\/ui<\/strong> to build a clean interface quickly. You\u2019ll also bring in libraries like <strong>Tailwind CSS<\/strong>, <strong>Lucide Icons<\/strong>, and <strong>Cloudinary\u2019s SDKs<\/strong> as seen in the codebase.<\/p>\n<h3>1. Create the Next.js App<\/h3>\n<p>Open your terminal and run this command to create the app with TypeScript and Tailwind CSS:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" 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> cloudinary-ab-test\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><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>Accept the defaults when prompted. This gives you the standard project structure with everything you need to start coding.<\/p>\n<h3>2. Initialize <code>shadcn\/ui<\/code><\/h3>\n<p>Now, let\u2019s set up <code>shadcn\/ui<\/code>, a lightweight component system built on top of Tailwind. It helps you avoid the bulk of traditional UI libraries while keeping full design control.<\/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\">shadcn<\/span><span class=\"hljs-keyword\">@latest<\/span> init\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>Accept the default options. You can add UI components like <code>Card<\/code>, <code>Dialog<\/code>, <code>Table<\/code>, <code>Alert<\/code>, and <code>Badge<\/code> whenever you need them using this command:<\/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\">npx<\/span> <span class=\"hljs-selector-tag\">shadcn<\/span><span class=\"hljs-keyword\">@latest<\/span> add card dialog table alert badge\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>These are the same components used in the project files such as <code>app\/page.tsx<\/code>, <code>app\/analytics\/page.tsx<\/code>, and the custom <code>ABTestThumbnail<\/code> component.<\/p>\n<h3>3. Install Extra Dependencies<\/h3>\n<p>You\u2019ll also need a few more packages used throughout the project:<\/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\">npm<\/span> <span class=\"hljs-selector-tag\">install<\/span> <span class=\"hljs-selector-tag\">posthog-js<\/span> <span class=\"hljs-keyword\">@cloudinary<\/span>\/react @cloudinary\/url-gen\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>These include:<\/p>\n<ul>\n<li>\n<strong>posthog-js<\/strong> for event tracking.<\/li>\n<li>\n<strong>@cloudinary\/react<\/strong> and <strong>@cloudinary\/url-gen<\/strong> for video and thumbnail generation.<\/li>\n<li>\n<strong>lucide-react<\/strong> for icons in the UI (like the play button and chart icons).<\/li>\n<\/ul>\n<h3>4. Configure Your Environment Variables<\/h3>\n<p>This step connects your app to <strong>Cloudinary<\/strong> and <strong>PostHog<\/strong>.<\/p>\n<p>Create a new file named <code>.env.local<\/code> in the root of your project and add the following keys:<\/p>\n<pre class=\"js-syntax-highlighted\"><code># .env.local\n\n# Cloudinary (Find these in your Cloudinary dashboard)\nNEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=YOUR_CLOUD_NAME\n\n# PostHog (Public key for sending client events)\nNEXT_PUBLIC_POSTHOG_KEY=YOUR_PUBLIC_CLIENT_KEY\nNEXT_PUBLIC_POSTHOG_HOST=https:\/\/us.posthog.com # Or your regional host\n\n# PostHog (Secret key for server analytics queries)\nPOSTHOG_API_KEY=YOUR_PERSONAL_API_KEY\n<\/code><\/pre>\n<p>This setup keeps your API keys private and prepares your app for backend analytics.<\/p>\n<p>With the environment and base app ready, we can now move on to generating <strong>dynamic thumbnail variants<\/strong> with Cloudinary.<\/p>\n<h2>Step 2: Generating Thumbnails With Cloudinary<\/h2>\n<p>Now that your project is set up, let\u2019s build the heart of the A\/B test, dynamic video thumbnails using <strong>Cloudinary<\/strong>.<\/p>\n<p>Cloudinary lets you transform media on the fly using <strong>URL-based transformations<\/strong>. This means you can generate multiple thumbnail styles from the same video without uploading extra files.<\/p>\n<h3>1. Configure the Cloudinary SDK<\/h3>\n<p>Create a helper file to manage your Cloudinary setup:<\/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\"><span class=\"hljs-comment\">\/\/ lib\/cloudinary.ts<\/span>\n<span class=\"hljs-keyword\">import<\/span> { Cloudinary } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@cloudinary\/url-gen\"<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> cld = <span class=\"hljs-keyword\">new<\/span> Cloudinary({\n  <span class=\"hljs-attr\">cloud<\/span>: {\n    <span class=\"hljs-attr\">cloudName<\/span>: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,\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<p>This gives you a reusable instance of Cloudinary\u2019s client that can create image and video transformations anywhere in your app.<\/p>\n<h3>2. Create Two Thumbnail Variants<\/h3>\n<p>In the component <code>ABTestThumbnail.tsx<\/code>, two different thumbnails are generated from the same video source:<\/p>\n<ul>\n<li>Variant A: A clean, unedited thumbnail.<\/li>\n<li>Variant B: A version with a \u201cNew Episode!\u201d overlay and dark gradient.<\/li>\n<\/ul>\n<p>Here\u2019s a simplified version of how that\u2019s built:<\/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\">const<\/span> thumbnailA = cld\n  .video(videoPublicId)\n  .resize(fill().width(<span class=\"hljs-number\">1280<\/span>).height(<span class=\"hljs-number\">720<\/span>))\n  .roundCorners(byRadius(<span class=\"hljs-number\">15<\/span>))\n  .addTransformation(<span class=\"hljs-string\">\"so_2,f_jpg,q_auto\"<\/span>);\n\n<span class=\"hljs-keyword\">const<\/span> thumbnailB = cld\n  .video(videoPublicId)\n  .resize(fill().width(<span class=\"hljs-number\">1280<\/span>).height(<span class=\"hljs-number\">720<\/span>))\n  .roundCorners(byRadius(<span class=\"hljs-number\">15<\/span>))\n  .addTransformation(\n    <span class=\"hljs-string\">\"l_text:Arial_60_bold:New%20Episode!,co_rgb:FFFFFF,b_rgb:00000090\/fl_layer_apply,g_south_east,x_20,y_20\/so_8,f_jpg,q_auto\"<\/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<p>Each variant uses Cloudinary\u2019s <strong>URL transformation API<\/strong> to apply effects, cropping, rounding, overlays, and quality adjustments.<\/p>\n<h3>3. Display and Track the Thumbnails<\/h3>\n<p>The app randomly assigns users to one of the variants using a simple randomizer and logs an impression event to PostHog:<\/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\">const<\/span> assigned = <span class=\"hljs-built_in\">Math<\/span>.random() &lt; <span class=\"hljs-number\">0.5<\/span> ? <span class=\"hljs-string\">\"A\"<\/span> : <span class=\"hljs-string\">\"B\"<\/span>;\nposthog.capture(<span class=\"hljs-string\">\"thumbnail_impression\"<\/span>, { video_id, <span class=\"hljs-attr\">variant<\/span>: assigned });\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<p>When the user clicks the thumbnail, a second event is logged: <code>thumbnail_clicked<\/code>.<\/p>\n<p>This lightweight setup allows Cloudinary to handle visuals while PostHog captures real engagement data.<\/p>\n<blockquote>\n<p>You can view the full component code here: <a href=\"https:\/\/github.com\/musebe\/cloudinary-video_thumbnails_ab-testing\/blob\/main\/components\/ABTestThumbnail.tsx\"><strong>ABTestThumbnail Component<\/strong><\/a><\/p>\n<\/blockquote>\n<h2>Step 3: Tracking User Events With PostHog<\/h2>\n<p>Now that your thumbnails are ready, let\u2019s connect <strong>PostHog<\/strong> to track how users interact with them. This allows you to measure which variant attracts more clicks and views.<\/p>\n<h3>1. Initialize PostHog<\/h3>\n<p>You\u2019ll use a provider file to load PostHog on the client side. This keeps analytics lightweight and avoids server-side errors.<\/p>\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-comment\">\/\/ app\/providers.tsx<\/span>\n<span class=\"hljs-string\">'use client'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> posthog <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'posthog-js'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { PostHogProvider } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'posthog-js\/react'<\/span>;\n\n<span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-keyword\">typeof<\/span> <span class=\"hljs-built_in\">window<\/span> !== <span class=\"hljs-string\">'undefined'<\/span>) {\n  posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {\n    <span class=\"hljs-attr\">api_host<\/span>: <span class=\"hljs-string\">'\/ingest'<\/span>,\n    <span class=\"hljs-attr\">ui_host<\/span>: process.env.NEXT_PUBLIC_POSTHOG_UI_HOST,\n    <span class=\"hljs-attr\">person_profiles<\/span>: <span class=\"hljs-string\">'always'<\/span>,\n  });\n}\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">CSPostHogProvider<\/span>(<span class=\"hljs-params\">{ children }: { children: React.ReactNode }<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">PostHogProvider<\/span> <span class=\"hljs-attr\">client<\/span>=<span class=\"hljs-string\">{posthog}<\/span>&gt;<\/span>{children}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">PostHogProvider<\/span>&gt;<\/span><\/span>;\n}\n\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>This file ensures PostHog only runs in the browser. The <code>api_host<\/code> points to a custom proxy route that you\u2019ll create next.<\/p>\n<blockquote>\n<p>Full file:  <a href=\"https:\/\/github.com\/musebe\/cloudinary-video_thumbnails_ab-testing\/blob\/main\/app\/providers.tsx\"><strong>app\/providers.tsx on GitHub<\/strong><\/a><\/p>\n<\/blockquote>\n<h3>2. Proxy PostHog Requests<\/h3>\n<p>Instead of sending data directly to PostHog\u2019s public API, you\u2019ll use your own Next.js route as a secure proxy.<\/p>\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-comment\">\/\/ app\/ingest\/&#91;&#91;...path]]\/route.ts<\/span>\n<span class=\"hljs-keyword\">const<\/span> POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST;\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\">POST<\/span>(<span class=\"hljs-params\">request: Request<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> payload = <span class=\"hljs-keyword\">await<\/span> request.text();\n  <span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> fetch(POSTHOG_HOST!, {\n    <span class=\"hljs-attr\">method<\/span>: <span class=\"hljs-string\">'POST'<\/span>,\n    <span class=\"hljs-attr\">headers<\/span>: request.headers,\n    <span class=\"hljs-attr\">body<\/span>: payload,\n  });\n  <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> Response(<span class=\"hljs-keyword\">await<\/span> response.text(), { <span class=\"hljs-attr\">status<\/span>: response.status });\n}\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>This keeps your host URL private and helps avoid browser CORS issues.<\/p>\n<blockquote>\n<p>Full file: <a href=\"https:\/\/github.com\/musebe\/cloudinary-video_thumbnails_ab-testing\/blob\/main\/app\/ingest\/%5B%5B...path%5D%5D\/route.ts\"><strong>app\/ingest\/[[\u2026path]]\/route.ts on GitHub<\/strong><\/a><\/p>\n<\/blockquote>\n<h3>3. Track Key Events<\/h3>\n<p>Once PostHog is set up, start tracking the main actions:<\/p>\n<ul>\n<li>\n<code>thumbnail_impression<\/code> when a thumbnail appears.<\/li>\n<li>\n<code>thumbnail_clicked<\/code> when a user selects it.<\/li>\n<li>\n<code>video_played<\/code> when the video starts.<\/li>\n<\/ul>\n<p>Example snippet:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">posthog<\/span><span class=\"hljs-selector-class\">.capture<\/span>(\"<span class=\"hljs-selector-tag\">thumbnail_clicked<\/span>\", {\n  <span class=\"hljs-attribute\">video_id<\/span>: videoPublicId,\n  variant,\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\">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>These events will appear in your PostHog dashboard immediately. You can then analyze click-through rates, funnels, and trends for each thumbnail variant.<\/p>\n<h2>Step 4: Building the A\/B Logic<\/h2>\n<p>Now that PostHog is active, it\u2019s time to wire up the logic that powers the actual A\/B test. This is where your app randomly assigns a user to <strong>Variant A<\/strong> or <strong>Variant B<\/strong> and tracks their interactions.<\/p>\n<h3>1. Create the A\/B Test Component<\/h3>\n<p>The A\/B logic lives inside the <a href=\"https:\/\/github.com\/musebe\/cloudinary-video_thumbnails_ab-testing\/blob\/main\/components\/ABTestThumbnail.tsx\"><code>ABTestThumbnail<\/code><\/a> component.<\/p>\n<p>It will randomly assign users a thumbnail variant, display the correct image using Cloudinary transformations, and then capture impressions and click events with PostHog.<\/p>\n<p>Here\u2019s the key part of the logic:<\/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> &#91;variant, setVariant] = (useState &lt; string) | (<span class=\"hljs-literal\">null<\/span> &gt; <span class=\"hljs-literal\">null<\/span>);\n\nuseEffect(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> assigned = <span class=\"hljs-built_in\">Math<\/span>.random() &lt; <span class=\"hljs-number\">0.5<\/span> ? <span class=\"hljs-string\">\"A\"<\/span> : <span class=\"hljs-string\">\"B\"<\/span>;\n  setVariant(assigned);\n  posthog.capture(<span class=\"hljs-string\">\"thumbnail_impression\"<\/span>, {\n    <span class=\"hljs-attr\">video_id<\/span>: videoPublicId,\n    <span class=\"hljs-attr\">variant<\/span>: assigned,\n  });\n}, &#91;]);\n\n<span class=\"hljs-keyword\">const<\/span> handleClick = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">if<\/span> (!variant) <span class=\"hljs-keyword\">return<\/span>;\n  posthog.capture(<span class=\"hljs-string\">\"thumbnail_clicked\"<\/span>, {\n    <span class=\"hljs-attr\">video_id<\/span>: videoPublicId,\n    variant,\n  });\n  <span class=\"hljs-keyword\">const<\/span> thumbnailUrl = variants&#91;variant].toURL();\n  onThumbnailClick(thumbnailUrl);\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\">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 code randomly assigns the user to a variant, logs the first view, and sends a click event when they engage.<\/p>\n<h3>2. Generate Visual Variants<\/h3>\n<p>The component uses <strong>Cloudinary\u2019s transformation API<\/strong> to create two thumbnail styles dynamically:<\/p>\n<ul>\n<li>Variant A: Plain version.<\/li>\n<li>Variant B: Version with a text overlay (\u201cNew Episode!\u201d).<\/li>\n<\/ul>\n<p>Example:<\/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\"><span class=\"hljs-keyword\">const<\/span> thumbnailB = cld\n  .video(videoPublicId)\n  .resize(fill().width(<span class=\"hljs-number\">1280<\/span>).height(<span class=\"hljs-number\">720<\/span>))\n  .roundCorners(byRadius(<span class=\"hljs-number\">15<\/span>))\n  .addTransformation(\n    <span class=\"hljs-string\">\"l_text:Arial_60_bold:New%20Episode!,co_rgb:FFFFFF,b_rgb:00000090\/fl_layer_apply,g_south_east,x_20,y_20\/so_8,f_jpg,q_auto\"<\/span>\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>Each user sees only one variant per session, giving you clean, unbiased data.<\/p>\n<h3>3. Display the Variant<\/h3>\n<p>The thumbnail is displayed inside a styled card built with <strong>shadcn\/ui<\/strong> components. It uses the <code>AdvancedImage<\/code> component from <code>@cloudinary\/react<\/code> for optimized image rendering and lazy loading.<\/p>\n<p>You can explore the complete code here:  <a href=\"https:\/\/github.com\/musebe\/cloudinary-video_thumbnails_ab-testing\/blob\/main\/components\/ABTestThumbnail.tsx\"><strong>ABTestThumbnail Component on GitHub<\/strong><\/a><\/p>\n<p>With A\/B assignment and tracking complete, we can now measure how each variant performs using analytics.<\/p>\n<h2>Step 5: Creating the Analytics Dashboard<\/h2>\n<p>Once your app collects events from PostHog, you need a way to view results. The analytics dashboard does exactly that. It fetches event data from PostHog\u2019s API and shows how each thumbnail variant performs.<\/p>\n<h3>1. Build the Analytics API Route<\/h3>\n<p>The logic for fetching A\/B test data lives inside <a href=\"https:\/\/github.com\/musebe\/cloudinary-video_thumbnails_ab-testing\/blob\/main\/app\/api\/analytics\/route.ts\"><code>app\/api\/analytics\/route.ts<\/code><\/a>.<\/p>\n<p>It uses PostHog\u2019s <strong>HogQL API<\/strong> to query recent events:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> hogqlQuery = `\n  SELECT\n    event,\n    properties.variant <span class=\"hljs-keyword\">as<\/span> variant,\n    count() <span class=\"hljs-keyword\">as<\/span> total\n  FROM events\n  WHERE\n    event IN (<span class=\"hljs-string\">'thumbnail_impression'<\/span>, <span class=\"hljs-string\">'thumbnail_clicked'<\/span>) <span class=\"hljs-keyword\">AND<\/span>\n    timestamp &gt;= now() - INTERVAL <span class=\"hljs-number\">7<\/span> DAY\n  GROUP BY event, variant\n`;\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\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This query counts impressions and clicks for both variants over the past seven days.<\/p>\n<p>The API route then structures the data for the frontend:<\/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> results = {\n  <span class=\"hljs-attr\">A<\/span>: { <span class=\"hljs-attr\">impressions<\/span>: <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-attr\">clicks<\/span>: <span class=\"hljs-number\">0<\/span> },\n  <span class=\"hljs-attr\">B<\/span>: { <span class=\"hljs-attr\">impressions<\/span>: <span class=\"hljs-number\">0<\/span>, <span class=\"hljs-attr\">clicks<\/span>: <span class=\"hljs-number\">0<\/span> },\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>The endpoint returns this JSON format to the analytics page.<\/p>\n<h3>2. Create the Analytics Page<\/h3>\n<p>The results are displayed on <a href=\"https:\/\/github.com\/musebe\/cloudinary-video_thumbnails_ab-testing\/blob\/main\/app\/analytics\/page.tsx\"><code>app\/analytics\/page.tsx<\/code><\/a>.<\/p>\n<p>It fetches data from the API and renders a clean results table using <code>shadcn\/ui<\/code> components like <code>Card<\/code>, <code>Table<\/code>, and <code>Alert<\/code>.<\/p>\n<p>Here\u2019s the key rendering logic:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-15\" 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\">TableBody<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableRow<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>Variant A<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>{results?.A.impressions ?? 0}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>{results?.A.clicks ?? 0}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>\n      {calculateCTR(results?.A.clicks ?? 0, results?.A.impressions ?? 0)}\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableRow<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableRow<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>Variant B<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>{results?.B.impressions ?? 0}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>{results?.B.clicks ?? 0}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>\n      {calculateCTR(results?.B.clicks ?? 0, results?.B.impressions ?? 0)}\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableRow<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableBody<\/span>&gt;<\/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\">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>Each variant\u2019s CTR is calculated and displayed clearly.<\/p>\n<h3>3. View and Compare Results<\/h3>\n<p>When you open <code>\/analytics<\/code>, the dashboard loads data automatically. You\u2019ll see impressions and clicks for each variant as well as the calculated CTR to show which one performs better.<\/p>\n<p>This gives you real, measurable results to guide your creative decisions.<\/p>\n<p>You can view the full files here:<\/p>\n<ul>\n<li>\n<a href=\"https:\/\/github.com\/musebe\/cloudinary-video_thumbnails_ab-testing\/blob\/main\/app\/api\/analytics\/route.ts\"><strong>Analytics API Route<\/strong><\/a>\n<\/li>\n<li>\n<a href=\"https:\/\/github.com\/musebe\/cloudinary-video_thumbnails_ab-testing\/blob\/main\/app\/analytics\/page.tsx\"><strong>Analytics Page<\/strong><\/a>\n<\/li>\n<\/ul>\n<h2>Step 6: Running the Experiment<\/h2>\n<p>Your app is ready. The thumbnails load, events track, and analytics display in real time. Now it\u2019s time to run your experiment and interpret the data.<\/p>\n<h3>1. Deploy the App<\/h3>\n<p>Deploy to <a href=\"http:\/\/vercel.com\"><strong>Vercel<\/strong><\/a> for the best performance and easy environment management.<\/p>\n<p>Add your environment variables in the Vercel dashboard under <strong>Settings \u2192 Environment Variables<\/strong>.<\/p>\n<p>Use the same keys from your <code>.env.local<\/code> file.<\/p>\n<p>Once deployed, visit your site and confirm that the thumbnails load correctly, PostHog events appear in your PostHog dashboard under <strong>Events<\/strong>, and the <code>\/analytics<\/code> page fetches and displays data.<\/p>\n<blockquote>\n<p>Live demo:  <a href=\"https:\/\/cloudinary-video-thumbnails-ab-test.vercel.app\/\"><strong>cloudinary-video-thumbnails-ab-test.vercel.app<\/strong><\/a><\/p>\n<\/blockquote>\n<h3>2. Test User Flow<\/h3>\n<p>Open your site in a few browsers or incognito windows to simulate different users.<\/p>\n<p>Each session randomly receives Variant A or Variant B.<\/p>\n<p>Try the following actions:<\/p>\n<ul>\n<li>Refresh the page to generate new thumbnail impressions.<\/li>\n<li>Click each thumbnail a few times.<\/li>\n<li>Watch the video briefly.<\/li>\n<\/ul>\n<p>Then, check your PostHog dashboard. You should see events like <code>thumbnail_impression<\/code>, <code>thumbnail_clicked<\/code>, and <code>video_played<\/code>.<\/p>\n<h3>3. Analyze CTR<\/h3>\n<p>Visit your <code>\/analytics<\/code> page to see how both variants perform.<\/p>\n<p>Example result:<\/p>\n<figure class=\"table-wrapper\"><table>\n<thead>\n<tr>\n<th>Variant<\/th>\n<th>Impressions<\/th>\n<th>Clicks<\/th>\n<th>CTR<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>A<\/td>\n<td>42<\/td>\n<td>10<\/td>\n<td>23.8%<\/td>\n<\/tr>\n<tr>\n<td>B<\/td>\n<td>36<\/td>\n<td>15<\/td>\n<td>41.6%<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n<p>In this example, <strong>Variant B<\/strong> performs better, so you\u2019d keep that design for future videos.<\/p>\n<h3>4. Scale the Test<\/h3>\n<p>Once your pipeline works, you can add more variants using additional Cloudinary transformations. Be sure to extend event tracking to include engagement depth (watch time) and connect results to your internal dashboards using PostHog\u2019s API.<\/p>\n<p>This workflow can scale easily across hundreds of thumbnails and campaigns.<\/p>\n<h2>Conclusion<\/h2>\n<p>You\u2019ve built a full A\/B testing system that helps you make informed creative decisions. With Cloudinary and PostHog, you can now see both sides of viewer behavior, i.e., how your digital media looks and how it\u2019s received.<\/p>\n<p>Test, measure, and improve your video thumbnails at scale. <a href=\"https:\/\/cloudinary.com\/users\/register_free\">Sign up for a free Cloudinary account<\/a> today to get started.<\/p>\n<ul>\n<li>\n<p><strong>Full code:<\/strong>  <a href=\"https:\/\/github.com\/musebe\/cloudinary-video_thumbnails_ab-testing\">github.com\/musebe\/cloudinary-video_thumbnails_ab-testing<\/a><\/p>\n<\/li>\n<li>\n<p><strong>Live demo:<\/strong> <a href=\"https:\/\/cloudinary-video-thumbnails-ab-test.vercel.app\/\">cloudinary-video-thumbnails-ab-test.vercel.app<\/a><\/p>\n<\/li>\n<\/ul>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":87,"featured_media":39087,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[212,383,303,304],"class_list":["post-39086","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-next-js","tag-nodejs","tag-video","tag-video-transformation"],"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>How to A\/B Test Video Thumbnails Programmatically With Cloudinary, PostHog, and Next.js<\/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\/a-b-test-video-thumbnails-posthog-next-js\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How to A\/B Test Video Thumbnails Programmatically With Cloudinary, PostHog, and Next.js\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-11-13T15:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-12-12T23:30:19+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1761940989\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js.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\/a-b-test-video-thumbnails-posthog-next-js#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"How to A\/B Test Video Thumbnails Programmatically With Cloudinary, PostHog, and Next.js\",\"datePublished\":\"2025-11-13T15:00:00+00:00\",\"dateModified\":\"2025-12-12T23:30:19+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js\"},\"wordCount\":14,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1761940989\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js.jpg?_i=AA\",\"keywords\":[\"Next.js\",\"Node(JS)\",\"Video\",\"Video Transformation\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2025\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js\",\"url\":\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js\",\"name\":\"How to A\/B Test Video Thumbnails Programmatically With Cloudinary, PostHog, and Next.js\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1761940989\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js.jpg?_i=AA\",\"datePublished\":\"2025-11-13T15:00:00+00:00\",\"dateModified\":\"2025-12-12T23:30:19+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1761940989\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1761940989\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How to A\/B Test Video Thumbnails Programmatically With Cloudinary, PostHog, and Next.js\"}]},{\"@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":"How to A\/B Test Video Thumbnails Programmatically With Cloudinary, PostHog, and Next.js","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\/a-b-test-video-thumbnails-posthog-next-js","og_locale":"en_US","og_type":"article","og_title":"How to A\/B Test Video Thumbnails Programmatically With Cloudinary, PostHog, and Next.js","og_url":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js","og_site_name":"Cloudinary Blog","article_published_time":"2025-11-13T15:00:00+00:00","article_modified_time":"2025-12-12T23:30:19+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1761940989\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js.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\/a-b-test-video-thumbnails-posthog-next-js#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"How to A\/B Test Video Thumbnails Programmatically With Cloudinary, PostHog, and Next.js","datePublished":"2025-11-13T15:00:00+00:00","dateModified":"2025-12-12T23:30:19+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js"},"wordCount":14,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1761940989\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js.jpg?_i=AA","keywords":["Next.js","Node(JS)","Video","Video Transformation"],"inLanguage":"en-US","copyrightYear":"2025","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js","url":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js","name":"How to A\/B Test Video Thumbnails Programmatically With Cloudinary, PostHog, and Next.js","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1761940989\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js.jpg?_i=AA","datePublished":"2025-11-13T15:00:00+00:00","dateModified":"2025-12-12T23:30:19+00:00","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1761940989\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1761940989\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/a-b-test-video-thumbnails-posthog-next-js#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"How to A\/B Test Video Thumbnails Programmatically With Cloudinary, PostHog, and Next.js"}]},{"@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\/v1761940989\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js\/B_Test_Video_Thumbnails_Programmatically_with_Cloudinary_PostHog_and_Next.js.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39086","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=39086"}],"version-history":[{"count":1,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39086\/revisions"}],"predecessor-version":[{"id":39088,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39086\/revisions\/39088"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/39087"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=39086"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=39086"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=39086"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}