{"id":38142,"date":"2025-08-06T07:00:00","date_gmt":"2025-08-06T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=38142"},"modified":"2025-12-12T17:17:00","modified_gmt":"2025-12-13T01:17:00","slug":"smart-video-thumbnail-picker-next-js-cloudinary-prisma","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma","title":{"rendered":"How to Build a Smart Video Thumbnail Picker With Next.js, Cloudinary, and Prisma"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p><a href=\"https:\/\/smart-video-thumbnail-picker.vercel.app\/\"><strong>Explore the Live Demo<\/strong><\/a> | <a href=\"https:\/\/github.com\/musebe\/smart-video-thumbnail-picker\"><strong>View the Source Code on GitHub<\/strong><\/a><\/p>\n<h2>The Challenge of Video Thumbnails<\/h2>\n<p>A great thumbnail can make the difference between a click and a scroll, but manually creating thumbnails for every video is tedious. You have to find the right frame, export it, and upload it separately. What if you could build an application that makes this process seamless and interactive?<\/p>\n<p>This guide will walk you through building a full-stack Next.js application that does just that. We\u2019ll use Cloudinary\u2019s powerful video API to create a tool that allows users to scrub through a video to select the perfect frame for their thumbnail.<\/p>\n<p>By the end, you\u2019ll have a powerful, responsive application and a clear understanding of how to use three best-in-class technologies to solve a real-world problem:<\/p>\n<ul>\n<li>\n<p><strong>Cloudinary.<\/strong> To handle our entire video workflow, from robust, chunked uploads to powerful, real-time URL-based transformations.<\/p>\n<\/li>\n<li>\n<p><strong>Next.js and Prisma.<\/strong> To serve as the modern, high-performance framework and database toolkit that ties everything together.<\/p>\n<\/li>\n<li>\n<p><strong>Neon.<\/strong> To provide a simple, serverless Postgres database for storing our video metadata.<\/p>\n<\/li>\n<\/ul>\n<h2>Scaffold the Next.js Project<\/h2>\n<p>Before we can dive into the fun parts, we need a solid foundation. We\u2019ll start by scaffolding a new Next.js project and integrating our UI components.<\/p>\n<p>First, create a new Next.js application using the App Router, 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>  smart-thumbnail-picker\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>Next, we\u2019ll add <a href=\"https:\/\/ui.shadcn.com\/\">Shadcn\/UI<\/a>, a fantastic collection of accessible and reusable components built on top of Radix UI and Tailwind CSS. This will save us a huge amount of time on styling.<\/p>\n<p>Initialize Shadcn\/UI in your project:<\/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>Then, add the specific components we\u2019ll need for our interface. This single command will add all the necessary files to your project.<\/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  button  card  input  slider  select  sonner\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>With our project structure and UI components in place, we\u2019re ready to set up the data layer.<\/p>\n<h2>The Data Layer: Set Up a Serverless Postgres DB With Prisma and Neon<\/h2>\n<p>Every application needs a place to store its data. For this project, we\u2019ll use a powerful and modern combination: <a href=\"https:\/\/neon.tech\/\">Neon<\/a> for a hassle-free, serverless Postgres database and <a href=\"https:\/\/www.prisma.io\/\">Prisma<\/a> as our ORM (Object-Relational Mapper) to make database interactions safe and intuitive.<\/p>\n<p>First, sign up for a free Neon account and create a new project. Neon will provide you with a database connection string, we\u2019ll need that in a moment.<\/p>\n<p>Next, install the Prisma CLI and Client into our 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\">prisma<\/span>  <span class=\"hljs-selector-tag\">--save-dev<\/span>\n<span class=\"hljs-selector-tag\">npm<\/span>  <span class=\"hljs-selector-tag\">install<\/span>  <span class=\"hljs-keyword\">@prisma<\/span>\/client\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>Now, we\u2019ll define our data structure in the <code>prisma\/schema.prisma<\/code> file. This schema is the single source of truth for our database tables. The <code>duration<\/code> field is crucial, as it will power our frame-scrubbing slider.<\/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\">\/\/ prisma\/schema.prisma<\/span>\n\nmodel Video {\n  id           <span class=\"hljs-built_in\">String<\/span>   @id @<span class=\"hljs-keyword\">default<\/span>(cuid())\n  publicId     <span class=\"hljs-built_in\">String<\/span>   @unique\n  thumbnailUrl <span class=\"hljs-built_in\">String<\/span>\n  duration     Float?   <span class=\"hljs-comment\">\/\/ Stores the video's length in seconds<\/span>\n  createdAt    DateTime @<span class=\"hljs-keyword\">default<\/span>(now())\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<blockquote>\n<p>View the final <code>schema.prisma<\/code> file on <a href=\"https:\/\/github.com\/musebe\/smart-video-thumbnail-picker\/blob\/main\/prisma\/schema.prisma\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<p>Finally, after adding your Neon connection string to a <code>.env.local<\/code> file, we\u2019ll run one command to sync our schema with the live database, creating the <code>Video<\/code> table.<\/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\">npx  prisma  migrate  dev  --name  <span class=\"hljs-string\">\"init\"<\/span>\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>With our database configured, we can now focus on getting videos into our system.<\/p>\n<h2>The Automation Engine: Configure a \u2018Smart\u2019 Cloudinary Upload Preset<\/h2>\n<p>Instead of writing complex server-side code to manage our uploads, we can offload a lot of the work to Cloudinary. The key to this is the <strong>upload preset<\/strong>. A preset is a collection of settings that Cloudinary applies to every file uploaded with it. It\u2019s our automation engine.<\/p>\n<p>For this project, the most important setting is the <strong>Signing Mode<\/strong>. By creating an <strong>Unsigned<\/strong> preset, we\u2019re telling Cloudinary that it\u2019s safe to accept files directly from a user\u2019s browser without a secure signature from our backend. This is perfect for our use case and dramatically simplifies the upload process.<\/p>\n<h3><strong>Create the Preset<\/strong><\/h3>\n<ol>\n<li>\n<p>Navigate to your Cloudinary Dashboard and go to <strong>Settings<\/strong> &gt; <strong>Upload<\/strong>.<\/p>\n<\/li>\n<li>\n<p>Scroll down to the <strong>Upload presets<\/strong> section and click <strong>Add upload preset<\/strong>.<\/p>\n<\/li>\n<li>\n<p>Set the <strong>Signing mode<\/strong> to <strong>Unsigned<\/strong>.<\/p>\n<\/li>\n<li>\n<p>Give it a memorable name, like <code>smart-thumbnail-picker<\/code>.<\/p>\n<\/li>\n<li>\n<p>You can also specify a folder name (e.g., <code>smart-videos<\/code>) to automatically organize all your uploads.<\/p>\n<\/li>\n<li>\n<p>Save the preset.<\/p>\n<\/li>\n<\/ol>\n<p>This simple configuration is incredibly powerful. It prepares Cloudinary to handle our client-side uploads securely and efficiently.<\/p>\n<h2>The User\u2019s Gateway: A Robust Client-Side Upload Component<\/h2>\n<p>With our backend and Cloudinary preset ready, we can build the user-facing upload interface. To avoid hitting serverless function timeouts and to provide a better user experience, we\u2019ll upload files directly from the user\u2019s browser to Cloudinary.<\/p>\n<p>The <code>next-cloudinary<\/code> library provides the perfect tool for this: the <code>&lt;CldUploadWidget&gt;<\/code> component. It handles all the complexity of file selection, progress tracking, and communication with the Cloudinary API. Crucially, it also manages <strong>chunked uploads<\/strong> automatically. If a user selects a large video file, the widget will intelligently break it into smaller pieces, upload them sequentially, and reassemble them on Cloudinary\u2019s side, ensuring even large files can be uploaded reliably.<\/p>\n<p>Our <code>UploadZone.tsx<\/code> component is a client-side wrapper around this widget. We pass our preset name to it and define an <code>onSuccess<\/code> callback function. This function is the bridge between the client and our server; it\u2019s triggered only after the file is securely in Cloudinary, at which point it receives the video\u2019s metadata and passes it to our server action.<\/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-comment\">\/\/ src\/components\/upload-zone.tsx<\/span>\n\n<span class=\"hljs-string\">\"use client\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { CldUploadWidget } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"next-cloudinary\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { saveVideoToDatabase } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/app\/actions\"<\/span>;\n<span class=\"hljs-comment\">\/\/ ...<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">UploadZone<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> handleSuccess = <span class=\"hljs-function\">(<span class=\"hljs-params\">result<\/span>) =&gt;<\/span> {\n    <span class=\"hljs-keyword\">const<\/span> info = result.info;\n    <span class=\"hljs-keyword\">if<\/span> (info?.public_id &amp;&amp; info?.secure_url &amp;&amp; info?.duration) {\n      <span class=\"hljs-comment\">\/\/ Pass the metadata to our secure server action<\/span>\n      saveVideoToDatabase(info.public_id, info.secure_url, info.duration);\n    }\n  };\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CldUploadWidget<\/span>\n      <span class=\"hljs-attr\">uploadPreset<\/span>=<span class=\"hljs-string\">\"smart-thumbnail-picker\"<\/span>\n      <span class=\"hljs-attr\">onSuccess<\/span>=<span class=\"hljs-string\">{handleSuccess}<\/span>\n    &gt;<\/span>\n      {({ open }) =&gt; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">onClick<\/span>=<span class=\"hljs-string\">{()<\/span> =&gt;<\/span> open()}&gt;Upload Video<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>}\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">CldUploadWidget<\/span>&gt;<\/span><\/span>\n  );\n}\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 final <code>UploadZone.tsx<\/code> component on <a href=\"https:\/\/github.com\/musebe\/smart-video-thumbnail-picker\/blob\/main\/src\/components\/upload-zone.tsx\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>Save Uploads With a Server Action<\/h2>\n<p>Once the Cloudinary widget successfully uploads the video, we need to save its metadata to our database. For this, we\u2019ll use Next.js Server Actions. A server action is a function that you can write once and call from either server or client components, but it is guaranteed to only execute on the server. This makes it the perfect tool for secure database operations.<\/p>\n<p>Our <code>saveVideoToDatabase<\/code> action takes the <code>publicId<\/code>, <code>videoUrl<\/code>, and <code>duration<\/code> from the client, creates a default thumbnail URL, and then uses Prisma to create a new record in our <code>Video<\/code> table.<\/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\">\/\/ src\/app\/actions.ts<\/span>\n\n<span class=\"hljs-string\">\"use server\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> prisma <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@\/lib\/prisma\"<\/span>;\n<span class=\"hljs-comment\">\/\/ ...<\/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\">saveVideoToDatabase<\/span>(<span class=\"hljs-params\">\n  publicId: string,\n  videoUrl: string,\n  duration: number\n<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">let<\/span> newVideo;\n  <span class=\"hljs-keyword\">try<\/span> {\n    newVideo = <span class=\"hljs-keyword\">await<\/span> prisma.video.create({\n      <span class=\"hljs-attr\">data<\/span>: {\n        <span class=\"hljs-attr\">publicId<\/span>: publicId,\n        <span class=\"hljs-attr\">thumbnailUrl<\/span>: <span class=\"hljs-comment\">\/* ...generate default url... *\/<\/span>,\n        <span class=\"hljs-attr\">duration<\/span>: duration,\n      },\n    });\n  } <span class=\"hljs-keyword\">catch<\/span> (error) {\n    <span class=\"hljs-comment\">\/\/ ... handle database errors<\/span>\n  }\n\n  <span class=\"hljs-comment\">\/\/ ... revalidate and redirect<\/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<h3>Debugging Deep Dive: The <code>NEXT_REDIRECT<\/code> Error<\/h3>\n<p>An interesting behavior of Server Actions is that the <code>redirect()<\/code> function works by throwing a special, non-fatal error. Our initial implementation had the <code>redirect()<\/code> call inside the <code>try...catch<\/code> block, which caused our app to mistakenly report a save failure on every successful upload!<\/p>\n<p>The fix was to move the <code>redirect()<\/code> call outside and after the <code>try...catch<\/code> block. This ensures we only catch actual database errors, allowing the redirect to function as intended.<\/p>\n<blockquote>\n<p>View the final <code>actions.ts<\/code> file on <a href=\"https:\/\/github.com\/musebe\/smart-video-thumbnail-picker\/blob\/main\/src\/app\/actions.ts\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>Build the Editor: The Interactive UI With a Video Player and Slider<\/h2>\n<p>With our data flow established, we can build the main event: the editor interface. This is where the user interacts with their video. Our <code>ThumbnailEditor.tsx<\/code> component orchestrates this experience.<\/p>\n<p>The UI is split into two main columns for larger screens. On the left, we place the <code>&lt;CldVideoPlayer&gt;<\/code>, which gives us a fully-featured video player with minimal setup. Directly below it, we add the <code>&lt;Slider&gt;<\/code> component from Shadcn\/UI. This slider is the primary control for frame selection. Its maximum value is dynamically bound to the <code>duration<\/code> of the video we fetched from our database, ensuring the user can scrub through the entire timeline.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">\/\/ src\/components\/thumbnail-editor.tsx\n\n\/\/ ...\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"lg:col-span-2 space-y-6\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"aspect-video w-full ...\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CldVideoPlayer<\/span>\n      <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">{video.publicId}<\/span>\n      <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{video.publicId}<\/span>\n      \/\/ <span class=\"hljs-attr\">...<\/span>\n    \/&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"space-y-4 pt-4\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Slider<\/span>\n      <span class=\"hljs-attr\">max<\/span>=<span class=\"hljs-string\">{Math.floor(video.duration<\/span> ?? <span class=\"hljs-attr\">0<\/span>)}\n      <span class=\"hljs-attr\">onValueChange<\/span>=<span class=\"hljs-string\">{(value)<\/span> =&gt;<\/span> setActiveTimestamp(value&#91;0])}\n    \/&gt;\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>;\n\/\/ ...\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h3>Performance Deep Dive: Avoiding Rerenders With <code>useMemo<\/code><\/h3>\n<p>A critical performance optimization was needed here. Initially, every movement of the slider caused the entire editor component to rerender, which also recreated the <code>&lt;CldVideoPlayer&gt;<\/code> instance. This led to console warnings and degraded performance.<\/p>\n<p>We solved this by wrapping the video player in a <code>useMemo<\/code> hook. This tells React to \u201cmemoize\u201d the player, only re-creating it if its dependency (the <code>video.publicId<\/code>) changes. This simple change prevents dozens of unnecessary re-renders and keeps the UI smooth and responsive.<\/p>\n<blockquote>\n<p>View the final <code>ThumbnailEditor.tsx<\/code> component on <a href=\"https:\/\/github.com\/musebe\/smart-video-thumbnail-picker\/blob\/main\/src\/components\/thumbnail-editor.tsx\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h3>On-the-Fly Transformations: Real-Time Thumbnail Previews<\/h3>\n<p>This is where the core logic of our application comes to life, and it\u2019s a perfect demonstration of Cloudinary\u2019s power. Instead of generating and storing hundreds of static image files, we can create any thumbnail we need, at any time, simply by constructing a special URL. No new files are ever saved on the server.<\/p>\n<p>The key is Cloudinary\u2019s URL-based transformation API. For this project, we rely on the <strong>start offset (<code>so<\/code>)<\/strong> parameter. When added to a video URL, it tells Cloudinary to seek to a specific point in the video before performing the next action.<\/p>\n<p>By changing the file extension of a video URL to an image format like <code>.jpg<\/code>, we instruct Cloudinary to extract a single frame. Combining these two features gives us precise control. A URL like this:<\/p>\n<p><code>...\/upload\/so_123\/my_video.jpg<\/code><\/p>\n<p>\u2026 is a set of real-time instructions: \u201cGo to <code>my_video<\/code>, seek forward (<code>so<\/code>) to the <code>123<\/code>-second mark, and return that frame as a JPG.\u201d<\/p>\n<p>Our <code>ThumbnailEditor.tsx<\/code> component uses this principle to create a live preview. A React state variable, <code>timestamp<\/code>, is updated by the slider. This variable is then used to dynamically construct the preview image URL on every state change.<\/p>\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-comment\">\/\/ src\/components\/thumbnail-editor.tsx<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> &#91;timestamp, setTimestamp] = useState(<span class=\"hljs-number\">0<\/span>);\n\n<span class=\"hljs-comment\">\/\/ The preview URL is re-generated every time the timestamp changes<\/span>\n<span class=\"hljs-keyword\">const<\/span> previewThumbnailUrl = <span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/<span class=\"hljs-subst\">${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}<\/span>\/video\/upload\/so_<span class=\"hljs-subst\">${timestamp}<\/span>\/<span class=\"hljs-subst\">${video.publicId}<\/span>.jpg`<\/span>;\n\n<span class=\"hljs-comment\">\/\/ ...<\/span>\n<span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Image<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{previewThumbnailUrl}<\/span> <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"Thumbnail preview\"<\/span> \/&gt;<\/span><\/span>;\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>This approach is incredibly efficient, providing a seamless, interactive experience for the user while minimizing storage costs and complexity.<\/p>\n<h2>Save the New Thumbnail and Add Downloads<\/h2>\n<p>Once the user has selected the perfect frame, the final step is to save it. Our \u201cSave as New Thumbnail\u201d button triggers the <code>updateThumbnail<\/code> server action. This action takes the video\u2019s ID and the newly generated thumbnail URL and uses Prisma to update the record in our database.<\/p>\n<p>A key function here is <code>revalidatePath<\/code> from Next.js. After the database is updated, we call <code>revalidatePath(&quot;\/&quot;)<\/code> and <code>revalidatePath(&quot;\/video\/[id]&quot;)<\/code>. This tells Next.js to clear its server-side cache for those pages, ensuring that the next time a user visits them, they will see the newly updated thumbnail immediately.<\/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-comment\">\/\/ src\/app\/actions.ts<\/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\">updateThumbnail<\/span>(<span class=\"hljs-params\">\n  videoId: string,\n  newThumbnailUrl: string\n<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">try<\/span> {\n    <span class=\"hljs-keyword\">await<\/span> prisma.video.update({\n      <span class=\"hljs-attr\">where<\/span>: { <span class=\"hljs-attr\">id<\/span>: videoId },\n      <span class=\"hljs-attr\">data<\/span>: { <span class=\"hljs-attr\">thumbnailUrl<\/span>: newThumbnailUrl },\n    });\n\n    <span class=\"hljs-comment\">\/\/ Invalidate the cache to ensure the UI updates<\/span>\n    revalidatePath(<span class=\"hljs-string\">\"\/\"<\/span>);\n    revalidatePath(<span class=\"hljs-string\">`\/video\/<span class=\"hljs-subst\">${videoId}<\/span>`<\/span>);\n  } <span class=\"hljs-keyword\">catch<\/span> (error) {\n    <span class=\"hljs-comment\">\/\/ ... handle errors<\/span>\n  }\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>To complete the user experience, we also added a download feature. This again leverages Cloudinary\u2019s on-the-fly transformations. By simply changing the file extension at the end of our preview URL (<code>.jpg<\/code>, <code>.png<\/code>, <code>.webp<\/code>), we can give the user the power to download the thumbnail in any format they choose, without any extra processing on our end.<\/p>\n<blockquote>\n<p>View the final <code>actions.ts<\/code> file on <a href=\"https:\/\/github.com\/musebe\/smart-video-thumbnail-picker\/blob\/main\/src\/app\/actions.ts\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>Conclusion<\/h2>\n<p>We\u2019ve gone from a blank project to a fully featured, interactive visual media tool. What\u2019s most remarkable isn\u2019t the complexity of the code we wrote, but rather its simplicity: treating media transformations as simple instructions in a URL.<\/p>\n<p>We didn\u2019t need to build a complex image processing queue or manage a separate file storage system for thumbnails. By leveraging Cloudinary\u2019s on-the-fly capabilities, a change in a URL string became a powerful editing tool.<\/p>\n<p>Explore the code, deploy your own version, and see what other powerful transformations you can build. The entire project is open-source and available for you to experiment with.<\/p>\n<p><a href=\"https:\/\/cloudinary.com\/users\/register_free\">Sign up<\/a> for Cloudinary today to get started.<\/p>\n<p><a href=\"https:\/\/smart-video-thumbnail-picker.vercel.app\/\"><strong>Explore the Live Demo<\/strong><\/a> | <a href=\"https:\/\/github.com\/musebe\/smart-video-thumbnail-picker\"><strong>View the Source Code on GitHub<\/strong><\/a><\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":87,"featured_media":38143,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[212,303],"class_list":["post-38142","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-next-js","tag-video"],"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>Build a Video Thumbnail Picker With Next.js, Cloudinary and Prisma<\/title>\n<meta name=\"description\" content=\"Learn how to build a full-stack Next.js application that lets users scrub through a video to select the perfect thumbnail frame. The guide uses Cloudinary&#039;s real-time transformations and a serverless database.\" \/>\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\/smart-video-thumbnail-picker-next-js-cloudinary-prisma\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How to Build a Smart Video Thumbnail Picker With Next.js, Cloudinary, and Prisma\" \/>\n<meta property=\"og:description\" content=\"Learn how to build a full-stack Next.js application that lets users scrub through a video to select the perfect thumbnail frame. The guide uses Cloudinary&#039;s real-time transformations and a serverless database.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-08-06T14:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-12-13T01:17:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1754089412\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_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\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"How to Build a Smart Video Thumbnail Picker With Next.js, Cloudinary, and Prisma\",\"datePublished\":\"2025-08-06T14:00:00+00:00\",\"dateModified\":\"2025-12-13T01:17:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma\"},\"wordCount\":14,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1754089412\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js.jpg?_i=AA\",\"keywords\":[\"Next.js\",\"Video\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2025\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma\",\"url\":\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma\",\"name\":\"Build a Video Thumbnail Picker With Next.js, Cloudinary and Prisma\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1754089412\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js.jpg?_i=AA\",\"datePublished\":\"2025-08-06T14:00:00+00:00\",\"dateModified\":\"2025-12-13T01:17:00+00:00\",\"description\":\"Learn how to build a full-stack Next.js application that lets users scrub through a video to select the perfect thumbnail frame. The guide uses Cloudinary's real-time transformations and a serverless database.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1754089412\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1754089412\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How to Build a Smart Video Thumbnail Picker With Next.js, Cloudinary, and Prisma\"}]},{\"@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":"Build a Video Thumbnail Picker With Next.js, Cloudinary and Prisma","description":"Learn how to build a full-stack Next.js application that lets users scrub through a video to select the perfect thumbnail frame. The guide uses Cloudinary's real-time transformations and a serverless database.","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\/smart-video-thumbnail-picker-next-js-cloudinary-prisma","og_locale":"en_US","og_type":"article","og_title":"How to Build a Smart Video Thumbnail Picker With Next.js, Cloudinary, and Prisma","og_description":"Learn how to build a full-stack Next.js application that lets users scrub through a video to select the perfect thumbnail frame. The guide uses Cloudinary's real-time transformations and a serverless database.","og_url":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma","og_site_name":"Cloudinary Blog","article_published_time":"2025-08-06T14:00:00+00:00","article_modified_time":"2025-12-13T01:17:00+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1754089412\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_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\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"How to Build a Smart Video Thumbnail Picker With Next.js, Cloudinary, and Prisma","datePublished":"2025-08-06T14:00:00+00:00","dateModified":"2025-12-13T01:17:00+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma"},"wordCount":14,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1754089412\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js.jpg?_i=AA","keywords":["Next.js","Video"],"inLanguage":"en-US","copyrightYear":"2025","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma","url":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma","name":"Build a Video Thumbnail Picker With Next.js, Cloudinary and Prisma","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1754089412\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js.jpg?_i=AA","datePublished":"2025-08-06T14:00:00+00:00","dateModified":"2025-12-13T01:17:00+00:00","description":"Learn how to build a full-stack Next.js application that lets users scrub through a video to select the perfect thumbnail frame. The guide uses Cloudinary's real-time transformations and a serverless database.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1754089412\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1754089412\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/smart-video-thumbnail-picker-next-js-cloudinary-prisma#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"How to Build a Smart Video Thumbnail Picker With Next.js, Cloudinary, and Prisma"}]},{"@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\/v1754089412\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js\/Blog_Smart_Video_Thumbnail_Picker_with_Cloudinary_Next.js.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/38142","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=38142"}],"version-history":[{"count":3,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/38142\/revisions"}],"predecessor-version":[{"id":39626,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/38142\/revisions\/39626"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/38143"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=38142"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=38142"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=38142"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}