{"id":38489,"date":"2025-09-11T07:11:00","date_gmt":"2025-09-11T14:11:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=38489"},"modified":"2025-12-15T23:40:38","modified_gmt":"2025-12-16T07:40:38","slug":"smart-video-embed-service-next-js-hono","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono","title":{"rendered":"Build a Smart Video Embed Service With Next.js, Hono, and Cloudinary"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p><a href=\"https:\/\/github.com\/musebe\/smart-video-embed-kit\"><strong>GitHub Repository<\/strong><\/a> | <a href=\"https:\/\/smart-video-embed-kit.vercel.app\/\"><strong>Live Demo<\/strong><\/a><\/p>\n<p>Imagine giving your users or clients a polished dashboard where they can simply upload a video and instantly get a powerful, customizable player to embed on any website, no coding required. This is the promise of a \u201csmart\u201d video embed as a service. Instead of manually wrestling with video tags and complex player configurations, you provide a seamless, self-service experience.<\/p>\n<p>This platform provides a drop-in <code>&lt;iframe&gt;<\/code> or <code>&lt;script&gt;<\/code> widget that renders Cloudinary\u2019s high-performance, adaptive video player. It\u2019s all powered by a modern, full-stack setup: a beautiful <strong>Next.js<\/strong> dashboard, a lightning-fast <strong>Hono<\/strong> edge API, and <strong>Cloudinary<\/strong> for all the heavy lifting of video storage, optimization, and delivery.<\/p>\n<p>In this tutorial, you\u2019ll build a complete video platform that:<\/p>\n<ol>\n<li>\n<p><strong>Provides a polished Next.js dashboard<\/strong> with Shadcn\/UI for uploading and managing a video library.<\/p>\n<\/li>\n<li>\n<p><strong>Uses a fast, server-side Hono API<\/strong> to securely fetch the list of video assets from your Cloudinary account.<\/p>\n<\/li>\n<li>\n<p><strong>Features a customization page<\/strong> with a real-time <code>&lt;iframe&gt;<\/code> preview where users can adjust player dimensions and accent colors.<\/p>\n<\/li>\n<li>\n<p><strong>Generates multiple, production-ready embed codes<\/strong> (URL, iFrame, and Javascript) for maximum flexibility.<\/p>\n<\/li>\n<\/ol>\n<p>Let\u2019s dive in!<\/p>\n<h2>Project Setup and Cloudinary Configuration<\/h2>\n<p>Before we write any application code, we need to lay the foundation. This involves scaffolding our Next.js + Hono project, installing our core dependencies, and configuring our Cloudinary account to allow for secure, client-side uploads.<\/p>\n<h3>1. Scaffold the Next.js + Hono App<\/h3>\n<p>We\u2019ll start with the official Vercel template for a seamless Next.js and Hono integration.<\/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>  my-video-service  --example  <span class=\"hljs-attribute\">https:<\/span>\/\/github.com\/honojs\/hono-nextjs-template\n\ncd  my-video-service\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<h3>2. Install Dependencies and Initialize UI<\/h3>\n<p>Next, install the Cloudinary SDK and the libraries for our UI: <code>next-themes<\/code> for dark mode and <code>lucide-react<\/code> for icons.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm  install  cloudinary  next-themes  lucide-react\n<\/code><\/span><\/pre>\n<p>Now, let\u2019s set up <strong>Shadcn\/UI<\/strong>, which will provide our beautiful, accessible components.<\/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>Follow the command-line prompts, the default options are fine. After initialization, add the components we\u2019ll use throughout the 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  table  dropdown-menu  dialog  textarea  card  button  sheet  skeleton  tabs  label  input  separator\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<h3>3. Create a Cloudinary Unsigned Upload Preset<\/h3>\n<p>For our app to allow users to upload videos directly from their browser, we need an <strong>Unsigned Upload Preset<\/strong>. This is a special rule in Cloudinary that defines how to handle incoming files without requiring a secure signature from our server for every single upload.<\/p>\n<ol>\n<li>\n<p>Log in to your <a href=\"https:\/\/cloudinary.com\/console\">Cloudinary account<\/a>.<\/p>\n<\/li>\n<li>\n<p>Navigate to <strong>Settings<\/strong> &gt; <strong>Upload<\/strong> tab.<\/p>\n<\/li>\n<li>\n<p>Scroll down to <strong>Upload presets<\/strong> and click <strong>Add upload preset<\/strong>.<\/p>\n<\/li>\n<li>\n<p>Change the <strong>Signing Mode<\/strong> from <code>Signed<\/code> to <strong><code>Unsigned<\/code><\/strong>. This is the most important step.<\/p>\n<\/li>\n<li>\n<p>Give it a memorable name (e.g., <code>smart-video-uploads<\/code>).<\/p>\n<\/li>\n<li>\n<p>Click <strong>Save<\/strong> and copy the preset\u2019s name.<\/p>\n<\/li>\n<\/ol>\n<h3>4. Configure Environment Variables<\/h3>\n<p>Create a file named <code>.env.local<\/code> in the root of your project and add your Cloudinary credentials.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-comment\"># Get these from your Cloudinary Dashboard homepage<\/span>\n\nNEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=<span class=\"hljs-string\">\"your_cloud_name\"<\/span>\nCLOUDINARY_API_KEY=<span class=\"hljs-string\">\"your_api_key\"<\/span>\nCLOUDINARY_API_SECRET=<span class=\"hljs-string\">\"your_api_secret\"<\/span>\n\n<span class=\"hljs-comment\"># The name of the unsigned upload preset you just created<\/span>\nNEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=<span class=\"hljs-string\">\"smart-video-uploads\"<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><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<blockquote>\n<p><strong>Important<\/strong>: Never commit your .env.local file to version control, as it contains your secret keys.<\/p>\n<\/blockquote>\n<h2>Building the API Layer With Hono<\/h2>\n<p>To display a list of existing videos, our dashboard needs to securely ask Cloudinary for the data. We can\u2019t do this from the user\u2019s browser, as it would expose our secret API key. Instead, we\u2019ll create a server-side API endpoint using <strong>Hono<\/strong> to act as a secure proxy.<\/p>\n<h3>1. Create a Secure, Server-Side Cloudinary Client<\/h3>\n<p>First, we need a helper file that configures the Cloudinary Node.js SDK using our private <code>API_KEY<\/code> and <code>API_SECRET<\/code>. This keeps our configuration in one place and separates it from our application logic.<\/p>\n<p>Create a new file at <code>lib\/cloudinary\/server.ts<\/code>:<\/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\/server.ts<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { v2 <span class=\"hljs-keyword\">as<\/span> cloudinary } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"cloudinary\"<\/span>;\n\n<span class=\"hljs-comment\">\/\/ Configure the SDK with your server-side credentials<\/span>\ncloudinary.config({\n  <span class=\"hljs-attr\">cloud_name<\/span>: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,\n  <span class=\"hljs-attr\">api_key<\/span>: process.env.CLOUDINARY_API_KEY,\n  <span class=\"hljs-attr\">api_secret<\/span>: process.env.CLOUDINARY_API_SECRET,\n});\n\n<span class=\"hljs-keyword\">export<\/span> { cloudinary };\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 configured <code>cloudinary<\/code> instance has the power to use the Admin API for tasks like searching, deleting, or tagging assets.<\/p>\n<h3>2. Create the <code>\/videos<\/code> Endpoint<\/h3>\n<p>Now, let\u2019s edit our Hono API file at <code>app\/api\/[[...route]]\/route.ts<\/code> to create the <code>GET \/videos<\/code> route. This endpoint uses our secure client to perform a search and return the results as JSON.<\/p>\n<p>The core of this endpoint is the Cloudinary Search API call:<\/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-comment\">\/\/ app\/api\/&#91;&#91;...route]]\/route.ts (snippet)<\/span>\n\napp.get(<span class=\"hljs-string\">\"\/videos\"<\/span>, <span class=\"hljs-keyword\">async<\/span> (c) =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> results = <span class=\"hljs-keyword\">await<\/span> cloudinary.search\n    .expression(<span class=\"hljs-string\">\"resource_type:video AND folder=smart-video-uploads\"<\/span>)\n    .sort_by(<span class=\"hljs-string\">\"created_at\"<\/span>, <span class=\"hljs-string\">\"desc\"<\/span>)\n    .max_results(<span class=\"hljs-number\">50<\/span>)\n    .execute();\n\n  <span class=\"hljs-keyword\">return<\/span> c.json(results.resources);\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>Let\u2019s break down the <strong>Cloudinary Search<\/strong> part in more detail:<\/p>\n<ul>\n<li>\n<p><code>.expression('...')<\/code>: This is the powerful filter for your search.<\/p>\n<\/li>\n<li>\n<p><code>resource_type:video<\/code> tells Cloudinary to only return video assets, ignoring images or other files.<\/p>\n<\/li>\n<li>\n<p><code>folder=smart-video-uploads<\/code> scopes the search to the specific folder our upload widget uses.<\/p>\n<\/li>\n<li>\n<p><code>.sort_by('created_at', 'desc')<\/code>: This organizes the results to show the most recently uploaded videos first, which is exactly what a user expects to see on a dashboard.<\/p>\n<\/li>\n<li>\n<p><code>.max_results(50)<\/code>: A crucial performance optimization. It limits the response to a maximum of 50 videos, preventing the API from getting bogged down if you have thousands of assets.<\/p>\n<\/li>\n<li>\n<p><code>.execute()<\/code>: This final command runs the search query with all the specified parameters and returns the results.<\/p>\n<\/li>\n<\/ul>\n<blockquote>\n<p>Important Note: Do Not Use the Edge Runtime!<\/p>\n<\/blockquote>\n<p><em>The official Cloudinary Node.js library uses native Node modules (like http) to function. Because of this, we must run this API route in the standard Node.js serverless environment, not Vercel\u2019s Edge Runtime (which does not support these modules). Ensure your route file does not contain export const runtime = \u2018edge\u2019;.<\/em><\/p>\n<p>This approach provides a fast, secure, and scalable way to fetch your video library.<\/p>\n<blockquote>\n<p>See the full API route on GitHub: <a href=\"https:\/\/github.com\/musebe\/smart-video-embed-kit\/blob\/main\/app\/api\/%5B...route%5D\/route.ts\">app\/api\/[[\u2026route]]\/route.ts<\/a><\/p>\n<\/blockquote>\n<h2>Implementing Seamless Uploads<\/h2>\n<p>With our API ready to fetch videos, we now need a way to get them into Cloudinary in the first place. Instead of building a complex, multi-part form uploader from scratch, we\u2019ll leverage the <strong>Cloudinary Upload Widget<\/strong>. This powerful, prebuilt component handles the entire upload UI, including drag-and-drop, progress bars, and multiple sources, and sends files directly to Cloudinary, which is faster and more scalable than routing them through our own server.<\/p>\n<h3>1. Create a Reusable <code>UploadButton<\/code> Component<\/h3>\n<p>To keep our code clean and organized, we\u2019ll wrap the widget\u2019s logic in its own client component.<\/p>\n<p>Create a new file at <code>app\/components\/upload-button.tsx<\/code>. Inside, we\u2019ll write the logic to load the widget\u2019s script and configure its behavior.<\/p>\n<p>The core of this component is the function that creates and opens the widget:<\/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\">\/\/ app\/components\/upload-button.tsx (snippet)<\/span>\n\n<span class=\"hljs-string\">\"use client\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { useEffect } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react'<\/span>;\n\n<span class=\"hljs-comment\">\/\/ ... component setup ...<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> handleUpload = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> myWidget = <span class=\"hljs-built_in\">window<\/span>.cloudinary.createUploadWidget({\n    <span class=\"hljs-attr\">cloudName<\/span>: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,\n    <span class=\"hljs-attr\">uploadPreset<\/span>: process.env.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET,\n    <span class=\"hljs-attr\">sources<\/span>: &#91;<span class=\"hljs-string\">'local'<\/span>, <span class=\"hljs-string\">'url'<\/span>, <span class=\"hljs-string\">'google_drive'<\/span>, <span class=\"hljs-string\">'dropbox'<\/span>],\n    <span class=\"hljs-attr\">folder<\/span>: <span class=\"hljs-string\">'smart-video-uploads'<\/span>,\n    <span class=\"hljs-attr\">clientAllowedFormats<\/span>: &#91;<span class=\"hljs-string\">'video'<\/span>],\n  }, (error, result) =&gt; {\n    <span class=\"hljs-keyword\">if<\/span> (!error &amp;&amp; result &amp;&amp; result.event === <span class=\"hljs-string\">\"success\"<\/span>) {\n      onUploadSuccess(result.info);\n    }\n  });\n\n  myWidget.open();\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<h3>2. Understanding the Cloudinary Widget Configuration<\/h3>\n<p>Let\u2019s break down the key parameters we\u2019re passing to <code>createUploadWidget<\/code>:<\/p>\n<ul>\n<li>\n<p><strong><code>cloudName<\/code>.<\/strong> This is your specific Cloudinary account identifier, pulled from our environment variables.<\/p>\n<\/li>\n<li>\n<p><strong><code>uploadPreset<\/code>.<\/strong> This is the most important part. It tells the widget to use the <strong>unsigned preset<\/strong> we created in Section 2, which grants it permission to upload files without a secure server-side signature.<\/p>\n<\/li>\n<li>\n<p><strong><code>sources<\/code>.<\/strong> This array controls which upload options the user sees. We\u2019re enabling local files, URLs, and popular cloud storage providers.<\/p>\n<\/li>\n<li>\n<p><strong><code>folder<\/code>.<\/strong> We specify a folder (<code>smart-video-uploads<\/code>) to keep all our uploaded videos organized within the Cloudinary Media Library. This also makes them easy to find with our Hono API.<\/p>\n<\/li>\n<li>\n<p><strong><code>clientAllowedFormats<\/code><\/strong> A helpful validation rule that restricts uploads to only video file types.<\/p>\n<\/li>\n<li>\n<p><strong>The Callback Function.<\/strong> The final argument is a function that fires on different upload events. We specifically check for <code>result.event === &quot;success&quot;<\/code> and, when it occurs, we call <code>onUploadSuccess(result.info)<\/code>. This passes the data for the newly uploaded video (like its <code>public_id<\/code>) back up to our main dashboard page so we can update the UI in real-time.<\/p>\n<\/li>\n<\/ul>\n<p>This component now provides a complete, production-ready upload experience that can be dropped anywhere in our application.<\/p>\n<blockquote>\n<p>See the full component on GitHub: <a href=\"https:\/\/github.com\/musebe\/smart-video-embed-kit\/blob\/main\/app\/components\/upload-button.tsx\">app\/components\/upload-button.tsx<\/a><\/p>\n<\/blockquote>\n<h2>Designing a Professional Dashboard<\/h2>\n<p>With our API and upload component ready, it\u2019s time to build the heart of our application: the main dashboard. To create a professional and scalable UI, we\u2019ll move away from a simple grid of cards to a data-rich <strong>Table View<\/strong> using Shadcn\u2019s components. This layout is much better for displaying metadata and providing quick actions for each video.<\/p>\n<h3>1. Fetching Data and Managing State<\/h3>\n<p>The first step on our <code>page.tsx<\/code> is to fetch the video library from the Hono API we just built. We use a standard React <code>useEffect<\/code> hook to call the <code>\/api\/videos<\/code> endpoint when the page loads. This hook also manages our loading and error states to ensure a smooth user experience.<\/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\/page.tsx (snippet)<\/span>\n\n<span class=\"hljs-string\">\"use client\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { useState, useEffect } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react'<\/span>;\n\n<span class=\"hljs-comment\">\/\/ ...<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">DashboardPage<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> &#91;videos, setVideos] = useState&lt;CloudinaryResource&#91;]&gt;(&#91;]);\n  <span class=\"hljs-keyword\">const<\/span> &#91;isLoading, setIsLoading] = useState(<span class=\"hljs-literal\">true<\/span>);\n\n  useEffect(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n    <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">fetchVideos<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n      setIsLoading(<span class=\"hljs-literal\">true<\/span>);\n      <span class=\"hljs-keyword\">try<\/span> {\n        <span class=\"hljs-keyword\">const<\/span> res = <span class=\"hljs-keyword\">await<\/span> fetch(<span class=\"hljs-string\">'\/api\/videos'<\/span>);\n        <span class=\"hljs-keyword\">if<\/span> (!res.ok) <span class=\"hljs-keyword\">throw<\/span> <span class=\"hljs-keyword\">new<\/span> <span class=\"hljs-built_in\">Error<\/span>(<span class=\"hljs-string\">`API call failed: <span class=\"hljs-subst\">${res.status}<\/span>`<\/span>);\n        <span class=\"hljs-keyword\">const<\/span> data = <span class=\"hljs-keyword\">await<\/span> res.json();\n        setVideos(<span class=\"hljs-built_in\">Array<\/span>.isArray(data) ? data : &#91;]);\n      } <span class=\"hljs-keyword\">catch<\/span> (error) {\n        <span class=\"hljs-comment\">\/\/ Handle errors gracefully<\/span>\n      } <span class=\"hljs-keyword\">finally<\/span> {\n        setIsLoading(<span class=\"hljs-literal\">false<\/span>);\n      }\n    }\n    fetchVideos();\n  }, &#91;]);\n\n  <span class=\"hljs-comment\">\/\/ ... rest of the component<\/span>\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>2. Displaying Videos in a Table With Dynamic Thumbnails<\/h3>\n<p>Inside our <code>renderVideoContent<\/code> function, we map over the <code>videos<\/code> state to create a <code>&lt;TableRow&gt;<\/code> for each asset. The most interesting part here is how we generate the video thumbnails on the fly.<\/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\">\n\/\/ app\/page.tsx (snippet)\n\n\/\/ ... inside the videos.map function ...\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableRow<\/span> <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{video.asset_id}<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">TableCell<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"hidden sm:table-cell\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span>\n      <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"Video thumbnail\"<\/span>\n      <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"aspect-video rounded-md object-cover\"<\/span>\n      <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{<\/span>`<span class=\"hljs-attr\">https:<\/span>\/\/<span class=\"hljs-attr\">res.cloudinary.com<\/span>\/<span class=\"hljs-attr\">...<\/span>\/<span class=\"hljs-attr\">video<\/span>\/<span class=\"hljs-attr\">upload<\/span>\/<span class=\"hljs-attr\">w_100<\/span>,<span class=\"hljs-attr\">h_64<\/span>,<span class=\"hljs-attr\">c_fill<\/span>,<span class=\"hljs-attr\">q_auto<\/span>,<span class=\"hljs-attr\">f_auto<\/span>\/${<span class=\"hljs-attr\">video.public_id<\/span>}<span class=\"hljs-attr\">.jpg<\/span>`}\n    \/&gt;<\/span>\n  <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> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"font-medium\"<\/span>&gt;<\/span>\n    {video.original_filename}\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableCell<\/span>&gt;<\/span>\n  {\/* ... other cells ... *\/}\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">TableRow<\/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<p>Let\u2019s break down the <strong>Cloudinary URL transformation<\/strong> used for the <code>src<\/code> attribute. This is where Cloudinary\u2019s power shines:<\/p>\n<ul>\n<li>\n<p><code>...\/video\/upload\/...<\/code>: This tells Cloudinary we are transforming a video asset.<\/p>\n<\/li>\n<li>\n<p><strong><code>w_100,h_64,c_fill<\/code>.<\/strong> These are chained transformations. We\u2019re requesting a <strong>w<\/strong>idth of 100px, a <strong>h<\/strong>eight of 64px, and telling Cloudinary to intelligently <strong>c<\/strong>rop the video to <strong>fill<\/strong> those exact dimensions without distortion.<\/p>\n<\/li>\n<li>\n<p><strong><code>q_auto,f_auto<\/code>.<\/strong> These are performance optimizations. <code>q_auto<\/code> tells Cloudinary to choose the optimal compression <strong>q<\/strong>uality, while <code>f_auto<\/code> tells it to deliver the image in the most efficient <strong>f<\/strong>ormat for the user\u2019s browser (like WebP or AVIF).<\/p>\n<\/li>\n<li>\n<p><strong><code>\/${video.public_id}.jpg<\/code>.<\/strong> This is the magic trick. By appending <code>.jpg<\/code> to the video\u2019s public ID, we are asking Cloudinary to extract the <strong>first frame<\/strong> of the video and deliver it as a static JPEG image.<\/p>\n<\/li>\n<\/ul>\n<p>This entire process happens in real-time on Cloudinary\u2019s servers, giving us perfectly sized thumbnails without any extra work.<\/p>\n<blockquote>\n<p>See the full dashboard component on <a href=\"https:\/\/github.com\/musebe\/smart-video-embed-kit\/blob\/main\/app\/page.tsx\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>The Heart of the App: The Customization Page<\/h2>\n<p>While a quick embed code is useful, the real power of our service lies in customization. This is where we build the dedicated settings page, a dynamic route that allows users to fine-tune the player\u2019s appearance and dimensions and see their changes reflected in real-time.<\/p>\n<h3>1. Creating the Dynamic Route<\/h3>\n<p>First, we create a dynamic route in Next.js at <code>app\/video\/[publicId]\/page.tsx<\/code>. This structure tells Next.js that any URL matching the pattern <code>\/video\/...<\/code> should render this page, passing the part after <code>\/video\/<\/code> as the <code>publicId<\/code> parameter. This <code>publicId<\/code> is crucial, as it tells us which video to load.<\/p>\n<h3>2. Managing Player State<\/h3>\n<p>On this page, we\u2019ll use React\u2019s <code>useState<\/code> hooks to manage every customizable setting. This is the \u201cbrain\u201d of our customization engine. Any change to these state variables will automatically trigger a re-render of the component and update our preview.<\/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\">\/\/ app\/video\/&#91;publicId]\/page.tsx (snippet)<\/span>\n\n<span class=\"hljs-string\">\"use client\"<\/span>;\n<span class=\"hljs-keyword\">import<\/span> { useState, useEffect } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react'<\/span>;\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">VideoSettingsPage<\/span>(<span class=\"hljs-params\">{ params }: { params: { publicId: string } }<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> decodedPublicId = <span class=\"hljs-built_in\">decodeURIComponent<\/span>(params.publicId);\n\n  <span class=\"hljs-comment\">\/\/ State for all our customizable settings<\/span>\n  <span class=\"hljs-keyword\">const<\/span> &#91;accentColor, setAccentColor] = useState(<span class=\"hljs-string\">\"#4f46e5\"<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> &#91;width, setWidth] = useState(<span class=\"hljs-number\">640<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> &#91;height, setHeight] = useState(<span class=\"hljs-number\">360<\/span>);\n\n  <span class=\"hljs-comment\">\/\/ ... rest of the component<\/span>\n}\n\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h3>3. Binding State to UI Controls<\/h3>\n<p>Each piece of state is then linked to a UI input. For example, the <code>accentColor<\/code> state is bound to an <code>&lt;Input type=&quot;color&quot;&gt;<\/code>. When the user picks a new color, the input\u2019s <code>onChange<\/code> event fires, calling <code>setAccentColor<\/code> and updating the state with the new value.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">\/\/ app\/video\/&#91;publicId]\/page.tsx (snippet)\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"grid gap-3\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">htmlFor<\/span>=<span class=\"hljs-string\">\"color\"<\/span>&gt;<\/span>Accent Color \/ Border<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Input<\/span>\n    <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"color\"<\/span>\n    <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"color\"<\/span>\n    <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{accentColor}<\/span>\n    <span class=\"hljs-attr\">onChange<\/span>=<span class=\"hljs-string\">{(e)<\/span> =&gt;<\/span> setAccentColor(e.target.value)}\n    className=\"w-16 h-10 p-1\"\n  \/&gt;\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The same principle applies to the width and height inputs, providing an interactive and responsive user experience. The page is designed with a spacious two-column layout: settings are on the left, and a large, sticky preview area stays visible on the right as you scroll, giving immediate visual feedback.<\/p>\n<blockquote>\n<p>Explore the complete settings page component on <a href=\"https:\/\/github.com\/musebe\/smart-video-embed-kit\/blob\/main\/app\/video\/%5BpublicId%5D\/page.tsx\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>Generating Native Cloudinary Embeds<\/h2>\n<p>The core of this project is not writing a custom video player. Instead, we construct parameters for <strong>Cloudinary\u2019s native player<\/strong>. This is faster, more reliable, and unlocks advanced features automatically.<\/p>\n<h3>The Engine: A <code>useEffect<\/code> Hook<\/h3>\n<p>All logic lives inside a <code>useEffect<\/code> hook on the video settings page. When the user changes options like <code>accentColor<\/code>, <code>width<\/code>, or <code>height<\/code>, the hook regenerates three things:<\/p>\n<ol>\n<li>\n<p>A <strong>base embed URL<\/strong>.<\/p>\n<\/li>\n<li>\n<p>An <strong>iFrame code snippet<\/strong>.<\/p>\n<\/li>\n<li>\n<p>A <strong>JavaScript player snippet<\/strong>.<\/p>\n<\/li>\n<\/ol>\n<h3>Part 1: Setup and Base URL<\/h3>\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\">useEffect(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;\n\n  <span class=\"hljs-keyword\">const<\/span> url = <span class=\"hljs-keyword\">new<\/span> URL(<span class=\"hljs-string\">'https:\/\/player.cloudinary.com\/embed\/'<\/span>);\n  url.searchParams.set(<span class=\"hljs-string\">'cloud_name'<\/span>, cloudName || <span class=\"hljs-string\">''<\/span>);\n  url.searchParams.set(<span class=\"hljs-string\">'public_id'<\/span>, decodedPublicId);\n  url.searchParams.set(<span class=\"hljs-string\">'player&#91;colors]&#91;accent]'<\/span>, accentColor);\n\n  setEmbedUrl(url.toString());\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<ul>\n<li>\n<p>Start by reading your Cloudinary cloud name.<\/p>\n<\/li>\n<li>\n<p>Build the base embed URL.<\/p>\n<\/li>\n<li>\n<p>Add account name, video ID, and accent color.<\/p>\n<\/li>\n<\/ul>\n<h3>Part 2: Generate the iFrame Code<\/h3>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">setIframeCode(\n  `<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">iframe<\/span>\n      <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"${url.toString()}\"<\/span>\n      <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">\"${width}\"<\/span>\n      <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">\"${height}\"<\/span>\n      <span class=\"hljs-attr\">allow<\/span>=<span class=\"hljs-string\">\"autoplay; fullscreen; encrypted-media\"<\/span>\n      <span class=\"hljs-attr\">frameborder<\/span>=<span class=\"hljs-string\">\"0\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">iframe<\/span>&gt;<\/span>`\n);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<ul>\n<li>\n<p>Wrap the embed URL in an iFrame.<\/p>\n<\/li>\n<li>\n<p>Sets width, height, and permissions.<\/p>\n<\/li>\n<li>\n<p>Copy-paste ready for any HTML page.<\/p>\n<\/li>\n<\/ul>\n<h3>Part 3: Generate the JavaScript Code<\/h3>\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\">  setJsCode(\n    <span class=\"hljs-string\">`const player = cloudinary.player('player', {\n      cloudName: '<span class=\"hljs-subst\">${cloudName}<\/span>',\n      publicId: '<span class=\"hljs-subst\">${decodedPublicId}<\/span>',\n      colors: { accent: '<span class=\"hljs-subst\">${accentColor}<\/span>' },\n      width: <span class=\"hljs-subst\">${width}<\/span>,\n      height: <span class=\"hljs-subst\">${height}<\/span>\n    });`<\/span>\n  );\n}, &#91;decodedPublicId, accentColor, width, height]);\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<ul>\n<li>\n<p>Builds a snippet for Cloudinary\u2019s JS Player API.<\/p>\n<\/li>\n<li>\n<p>Updates when video ID, color, or size changes.<\/p>\n<\/li>\n<li>\n<p>Lets developers embed the player programmatically.<\/p>\n<\/li>\n<\/ul>\n<h3>Understanding the Cloudinary Player URL<\/h3>\n<p>The system revolves around the <strong>embed URL<\/strong>:<\/p>\n<ul>\n<li>\n<p><strong><code>https:\/\/player.cloudinary.com\/embed\/<\/code>.<\/strong> Base endpoint.<\/p>\n<\/li>\n<li>\n<p><strong><code>?cloud_name=...<\/code>.<\/strong> Which Cloudinary account to use.<\/p>\n<\/li>\n<li>\n<p><strong><code>&amp;public_id=...<\/code>.<\/strong> Which video asset to load.<\/p>\n<\/li>\n<li>\n<p><strong><code>&amp;player[colors][accent]=#4f46e5<\/code>.<\/strong> Customize UI styling.<\/p>\n<\/li>\n<\/ul>\n<p>With this foundation, generating the iFrame and JavaScript snippets becomes straightforward. The URL is the single source of truth.<\/p>\n<blockquote>\n<p>View the full code on <a href=\"https:\/\/github.com\/musebe\/smart-video-embed-kit\/blob\/main\/app\/video\/%5BpublicId%5D\/page.tsx\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>Creating a True Live Preview<\/h2>\n<p>A settings page is only useful if the user can see the immediate impact of their changes. A simulated preview can often be misleading, so our application provides a <strong>100% accurate, real-time preview<\/strong> by rendering the actual embeddable player inside an <code>&lt;iframe&gt;<\/code>. What you see is exactly what you get.<\/p>\n<h3>The Implementation: A Reactive <code>&lt;iframe&gt;<\/code><\/h3>\n<p>The magic lies in a simple but powerful React pattern. The <code>&lt;iframe&gt;<\/code> on our settings page doesn\u2019t have a static source; its <code>src<\/code> attribute is directly bound to the <code>embedUrl<\/code> state variable we generated in the previous step.<\/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\">\/\/ app\/video\/&#91;publicId]\/page.tsx (snippet)\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>\n  <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">aspectRatio:<\/span> `${<span class=\"hljs-attr\">width<\/span>} \/ ${<span class=\"hljs-attr\">height<\/span>}` }}\n  <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"rounded-lg bg-slate-900 overflow-hidden\"<\/span>\n&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">iframe<\/span>\n    <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{embedUrl}<\/span>\n    <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{embedUrl}<\/span>\n    <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"w-full h-full\"<\/span>\n    <span class=\"hljs-attr\">frameBorder<\/span>=<span class=\"hljs-string\">\"0\"<\/span>\n    <span class=\"hljs-attr\">allow<\/span>=<span class=\"hljs-string\">\"autoplay; fullscreen; encrypted-media; picture-in-picture\"<\/span>\n    <span class=\"hljs-attr\">allowFullScreen<\/span>\n  &gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">iframe<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/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<h3>How It Works<\/h3>\n<ol>\n<li>\n<p><strong>Dynamic source.<\/strong> When a user changes the accent color, the <code>useEffect<\/code> hook from the previous step runs, creating a new <code>embedUrl<\/code> with the updated color parameter (e.g., <code>...&amp;player[colors][accent]=#ff0000<\/code>).<\/p>\n<\/li>\n<li>\n<p><strong>The <code>key<\/code> prop is crucial.<\/strong> We\u2019ve set <code>key={embedUrl}<\/code> on the <code>&lt;iframe&gt;<\/code>. In React, when a component\u2019s <code>key<\/code> changes, React unmounts the old component and mounts a completely new one. This forces the browser to discard the old <code>&lt;iframe&gt;<\/code> and load a fresh one with the new <code>src<\/code>, guaranteeing that the player re-initializes with the latest settings.<\/p>\n<\/li>\n<li>\n<p><strong>Granting permissions.<\/strong> The <code>allow=&quot;autoplay; ...&quot;<\/code> attribute is a critical security feature. It\u2019s our way of telling the browser, \u201cI trust the content at this source, and I give it permission to autoplay (muted) inside this frame.\u201d This is what prevents the preview from showing a black screen or getting blocked by browser security policies.<\/p>\n<\/li>\n<\/ol>\n<p>This technique ensures that our live preview is not a guess, it\u2019s a perfect, real-world representation of the final embedded player.<\/p>\n<blockquote>\n<p>Check out the full implementation on <a href=\"https:\/\/github.com\/musebe\/smart-video-embed-kit\/blob\/main\/app\/video\/%5BpublicId%5D\/page.tsx\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>Conclusion and Next Steps<\/h2>\n<p>By combining <strong>Next.js<\/strong> frontend, a fast <strong>Hono<\/strong> API, and <strong>Cloudinary\u2019s<\/strong> robust media infrastructure, you\u2019ve created a professional tool for smart video embedding.<\/p>\n<p>This architecture isn\u2019t just a demo; it\u2019s a solid foundation for a real-world SaaS product. You now have a powerful system that you can extend and build upon.<\/p>\n<h3>Further Reading and Future Enhancements<\/h3>\n<p>Here are some ideas for taking this project to the next level:<\/p>\n<ol>\n<li>\n<strong>Expand player customizations.<\/strong> The Cloudinary Player supports dozens of options. You could easily add more controls to your settings page for features like <code>autoplay<\/code>, <code>loop<\/code>, <code>showLogo<\/code>, or even adding your own watermark.<\/li>\n<\/ol>\n<ul>\n<li>Resource: <a href=\"https:\/\/cloudinary.com\/documentation\/video_player_api_reference\">Cloudinary Video Player API Reference<\/a>\n<\/li>\n<\/ul>\n<ol start=\"2\">\n<li>\n<strong>Integrate AI features.<\/strong> Make the player even \u201csmarter\u201d by leveraging Cloudinary\u2019s AI add-ons. You could add a button to your settings page that triggers an API call to automatically generate video captions using the <strong>Google AI Video Transcription<\/strong> add-on.<\/li>\n<\/ol>\n<ul>\n<li>Resource: <a href=\"https:\/\/cloudinary.com\/documentation\/google_ai_video_transcription_addon\">Google AI Video Transcription Add-on<\/a>\n<\/li>\n<\/ul>\n<ol start=\"3\">\n<li>\n<strong>Add user accounts and a database.<\/strong> To make this a true multi-tenant service, integrate an authentication provider (like NextAuth.js or Clerk) and a database (like Vercel Postgres or Supabase) to save user-specific settings and video metadata.<\/li>\n<\/ol>\n<h3>Project Links<\/h3>\n<ul>\n<li>\n<p>View the final code on <a href=\"https:\/\/github.com\/musebe\/smart-video-embed-kit\">GitHub<\/a>.<\/p>\n<\/li>\n<li>\n<p>See the <a href=\"https:\/\/smart-video-embed-kit.vercel.app\/\">live application<\/a>.<\/p>\n<\/li>\n<\/ul>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":87,"featured_media":38490,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[303,305,310],"class_list":["post-38489","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-video","tag-video-api","tag-video-player"],"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 Smart Video Embed Service With Next.js, Hono, and Cloudinary<\/title>\n<meta name=\"description\" content=\"Learn how to create a professional video embed platform using Next.js, Hono, and Cloudinary. This step-by-step guide shows you how to build a customizable dashboard, secure API, seamless uploads, and production-ready embed codes.\" \/>\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-embed-service-next-js-hono\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Build a Smart Video Embed Service With Next.js, Hono, and Cloudinary\" \/>\n<meta property=\"og:description\" content=\"Learn how to create a professional video embed platform using Next.js, Hono, and Cloudinary. This step-by-step guide shows you how to build a customizable dashboard, secure API, seamless uploads, and production-ready embed codes.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-09-11T14:11:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-12-16T07:40:38+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1757024458\/Blog_Smart_Video_Embed-as-a-Service\/Blog_Smart_Video_Embed-as-a-Service.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-embed-service-next-js-hono#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"Build a Smart Video Embed Service With Next.js, Hono, and Cloudinary\",\"datePublished\":\"2025-09-11T14:11:00+00:00\",\"dateModified\":\"2025-12-16T07:40:38+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono\"},\"wordCount\":12,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1757024458\/Blog_Smart_Video_Embed-as-a-Service\/Blog_Smart_Video_Embed-as-a-Service.jpg?_i=AA\",\"keywords\":[\"Video\",\"Video API\",\"Video Player\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2025\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono\",\"url\":\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono\",\"name\":\"Build a Smart Video Embed Service With Next.js, Hono, and Cloudinary\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1757024458\/Blog_Smart_Video_Embed-as-a-Service\/Blog_Smart_Video_Embed-as-a-Service.jpg?_i=AA\",\"datePublished\":\"2025-09-11T14:11:00+00:00\",\"dateModified\":\"2025-12-16T07:40:38+00:00\",\"description\":\"Learn how to create a professional video embed platform using Next.js, Hono, and Cloudinary. This step-by-step guide shows you how to build a customizable dashboard, secure API, seamless uploads, and production-ready embed codes.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1757024458\/Blog_Smart_Video_Embed-as-a-Service\/Blog_Smart_Video_Embed-as-a-Service.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1757024458\/Blog_Smart_Video_Embed-as-a-Service\/Blog_Smart_Video_Embed-as-a-Service.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Build a Smart Video Embed Service With Next.js, Hono, and Cloudinary\"}]},{\"@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 Smart Video Embed Service With Next.js, Hono, and Cloudinary","description":"Learn how to create a professional video embed platform using Next.js, Hono, and Cloudinary. This step-by-step guide shows you how to build a customizable dashboard, secure API, seamless uploads, and production-ready embed codes.","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-embed-service-next-js-hono","og_locale":"en_US","og_type":"article","og_title":"Build a Smart Video Embed Service With Next.js, Hono, and Cloudinary","og_description":"Learn how to create a professional video embed platform using Next.js, Hono, and Cloudinary. This step-by-step guide shows you how to build a customizable dashboard, secure API, seamless uploads, and production-ready embed codes.","og_url":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono","og_site_name":"Cloudinary Blog","article_published_time":"2025-09-11T14:11:00+00:00","article_modified_time":"2025-12-16T07:40:38+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1757024458\/Blog_Smart_Video_Embed-as-a-Service\/Blog_Smart_Video_Embed-as-a-Service.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-embed-service-next-js-hono#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"Build a Smart Video Embed Service With Next.js, Hono, and Cloudinary","datePublished":"2025-09-11T14:11:00+00:00","dateModified":"2025-12-16T07:40:38+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono"},"wordCount":12,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1757024458\/Blog_Smart_Video_Embed-as-a-Service\/Blog_Smart_Video_Embed-as-a-Service.jpg?_i=AA","keywords":["Video","Video API","Video Player"],"inLanguage":"en-US","copyrightYear":"2025","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono","url":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono","name":"Build a Smart Video Embed Service With Next.js, Hono, and Cloudinary","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1757024458\/Blog_Smart_Video_Embed-as-a-Service\/Blog_Smart_Video_Embed-as-a-Service.jpg?_i=AA","datePublished":"2025-09-11T14:11:00+00:00","dateModified":"2025-12-16T07:40:38+00:00","description":"Learn how to create a professional video embed platform using Next.js, Hono, and Cloudinary. This step-by-step guide shows you how to build a customizable dashboard, secure API, seamless uploads, and production-ready embed codes.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1757024458\/Blog_Smart_Video_Embed-as-a-Service\/Blog_Smart_Video_Embed-as-a-Service.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1757024458\/Blog_Smart_Video_Embed-as-a-Service\/Blog_Smart_Video_Embed-as-a-Service.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/smart-video-embed-service-next-js-hono#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Build a Smart Video Embed Service With Next.js, Hono, and Cloudinary"}]},{"@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\/v1757024458\/Blog_Smart_Video_Embed-as-a-Service\/Blog_Smart_Video_Embed-as-a-Service.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/38489","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=38489"}],"version-history":[{"count":3,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/38489\/revisions"}],"predecessor-version":[{"id":38493,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/38489\/revisions\/38493"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/38490"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=38489"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=38489"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=38489"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}