{"id":39964,"date":"2026-03-26T07:00:00","date_gmt":"2026-03-26T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=39964"},"modified":"2026-03-30T14:28:42","modified_gmt":"2026-03-30T21:28:42","slug":"content-fatigue-next-js","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js","title":{"rendered":"Solving Long-Form Content Fatigue With Next.js and Cloudinary"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><ul>\n<li>\n<strong>Live Demo:<\/strong> <a href=\"https:\/\/ai-video-hub.vercel.app\">ai-video-hub.vercel.app<\/a>\n<\/li>\n<li>\n<strong>Source Code:<\/strong> <a href=\"https:\/\/github.com\/musebe\/ai-video-hub\">github.com\/musebe\/ai-video-hub<\/a>\n<\/li>\n<\/ul>\n<p>We\u2019ve all been there: You need one, specific clip from a 60-minute webinar, but scrubbing through the timeline feels like searching for a needle in a haystack. For developers and content teams, video is often a \u201cblack box\u201d rich with information but opaque to search engines and users unless you manually tag every minute.<\/p>\n<p>This hassle leads to content fatigue. Users bounce because they can\u2019t find what they need, and teams burn out trying to manually transcribe and chapter every recording.<\/p>\n<p>In this guide, you\u2019ll combine <strong>Next.js 16<\/strong> with <strong>Cloudinary\u2019s AI video intelligence<\/strong> to build an AI video knowledge hub that automatically:<\/p>\n<ol>\n<li>\n<strong>Transcribes<\/strong> speech to text.<\/li>\n<li>\n<strong>Chapters<\/strong> video based on topic shifts.<\/li>\n<li>\n<strong>Indexes<\/strong> content for deep-search capability.<\/li>\n<\/ol>\n<p>The result? A Netflix-style portal where users can search for a keyword (e.g., \u201cNext.js Server Actions\u201d) and jump instantly to the exact second it was spoken.<\/p>\n<h2>The Setup: Infrastructure and AI Configuration<\/h2>\n<p>Before writing any code, you\u2019ll need to lay the foundation. You\u2019ll build on <strong>Next.js 16<\/strong> (App Router) to handle server-side rendering and <strong>Cloudinary<\/strong> to offload the heavy AI processing.<\/p>\n<h3>Initializing the Next.js 16 Project<\/h3>\n<p>Start by scaffolding a modern Next.js 16 application with TypeScript and Tailwind CSS for rapid styling.<\/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> ai-video-hub --typescript --tailwind --eslint\ncd ai-video-hub\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>Once inside, you\u2019ll install the core dependencies for your media player and icons:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm install next-cloudinary lucide-react\n<\/code><\/span><\/pre>\n<h3>Configuring the Cloudinary Upload Preset<\/h3>\n<p>This is the most critical part of the entire build. You aren\u2019t just uploading a video; you\u2019re triggering an AI pipeline.<\/p>\n<ol>\n<li>Log in to your <a href=\"https:\/\/console.cloudinary.com\/\">Cloudinary Console<\/a>.<\/li>\n<li>Navigate to <strong>Settings<\/strong> &gt; <strong>Upload<\/strong> &gt; <strong>Upload Presets<\/strong>.<\/li>\n<li>Click <strong>Add Upload Preset<\/strong> and name it <code>ai_video_hub_preset<\/code>.<\/li>\n<li>Enabling AI transcription:\n<ul>\n<li>Go to the <strong>Add-ons<\/strong> tab.<\/li>\n<li>Find <strong>Google AI Video Transcription<\/strong> and check the box.<\/li>\n<li>This tells Cloudinary to automatically generate a <code>.transcript<\/code> (JSON) and <code>.vtt<\/code> (Subtitle) file the moment a video finishes uploading.<\/li>\n<\/ul>\n<\/li>\n<li>Setting up auto-tagging:\n<ul>\n<li>Still in the preset settings, enable <strong>Auto-Tagging<\/strong>.<\/li>\n<li>This ensures every uploaded video is automatically categorized (e.g., \u201ctechnology\u201d, \u201cwebinar\u201d), making it easier to filter and search for content later without manual data entry.<\/li>\n<\/ul>\n<\/li>\n<li>Unsigned Mode:<\/li>\n<\/ol>\n<ul>\n<li>Set the Signing Mode to <strong>Unsigned<\/strong>.<\/li>\n<li>This allows our frontend to upload directly to Cloudinary without exposing our API Secret.<\/li>\n<\/ul>\n<p>To securely connect your Next.js application to Cloudinary, you\u2019ll need to store your API credentials in an environment variable file.<\/p>\n<h3>1. Create the File<\/h3>\n<p>In the <strong>root directory<\/strong> of your project (same level as <code>package.json<\/code>), create a new file named <code>.env.local<\/code>.<\/p>\n<h3>2. Add Your Credentials<\/h3>\n<p>Paste the following keys into that file. You can find these values in your <a href=\"https:\/\/console.cloudinary.com\/\">Cloudinary Console Dashboard<\/a> under \u201cProduct Environment Credentials\u201d.<\/p>\n<p>Code snippet<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-comment\"># Exposed to the browser (Client-Side)<\/span>\nNEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=<span class=\"hljs-string\">\"your_cloud_name\"<\/span>\nNEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=<span class=\"hljs-string\">\"ai_video_hub_preset\"<\/span>\n\n<span class=\"hljs-comment\"># Server-Side Only (Admin API &amp; Signing)<\/span>\nCLOUDINARY_API_KEY=<span class=\"hljs-string\">\"your_api_key\"<\/span>\nCLOUDINARY_API_SECRET=<span class=\"hljs-string\">\"your_api_secret\"<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Now, every time a user uploads a video through our app, Cloudinary will silently process it in the background, generating the metadata we need for our \u201cKnowledge Hub.\u201d<\/p>\n<h2>Building the \u2018Zombie-Proof\u2019 Video Stage<\/h2>\n<p>Most tutorials use a simple <code>&lt;video&gt;<\/code> tag, but in a Next.js playlist, rapidly switching videos creates a <strong>\u201cZombie Player\u201d race condition<\/strong>. React might unmount the old player <em>after<\/em> the new one initializes, causing the library to attach to a dead DOM node and resulting in a black screen.<\/p>\n<p>{The Fix:}\nUnique Session IDs: Force React to completely destroy and rebuild the player by using a <code>playbackSession<\/code> timestamp as a unique key.\n{\/The Fix}<\/p>\n<p><strong><code>app\/page.tsx<\/code> (The Controller)<\/strong><\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> handleVideoSelect = <span class=\"hljs-function\">(<span class=\"hljs-params\">newId: string<\/span>) =&gt;<\/span> {\n  setPlaybackSession(<span class=\"hljs-built_in\">Date<\/span>.now()); <span class=\"hljs-comment\">\/\/ Force fresh mount<\/span>\n  setPublicId(newId);\n};\n\n<span class=\"hljs-comment\">\/\/ ... inside render ...<\/span>\n<span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">VideoStage<\/span>\n  <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{<\/span>`${<span class=\"hljs-attr\">publicId<\/span>}<span class=\"hljs-attr\">-<\/span>${<span class=\"hljs-attr\">playbackSession<\/span>}`} \/\/ <span class=\"hljs-attr\">The<\/span> <span class=\"hljs-attr\">magic<\/span> <span class=\"hljs-attr\">key<\/span> <span class=\"hljs-attr\">forces<\/span> <span class=\"hljs-attr\">a<\/span> <span class=\"hljs-attr\">hard<\/span> <span class=\"hljs-attr\">reset<\/span>\n  <span class=\"hljs-attr\">publicId<\/span>=<span class=\"hljs-string\">{publicId}<\/span>\n  \/\/ <span class=\"hljs-attr\">...<\/span>\n\/&gt;<\/span><\/span>;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p><strong><code>components\/hub\/video-stage.tsx<\/code>  (The Engine)<\/strong><\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">VideoStage<\/span>(<span class=\"hljs-params\">{\n  publicId,\n  onTimeUpdate,\n  playerRef,\n}: VideoStageProps<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"relative aspect-video bg-slate-900 rounded-2xl overflow-hidden\"<\/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\">{<\/span>`<span class=\"hljs-attr\">player-<\/span>${<span class=\"hljs-attr\">publicId<\/span>}`}\n        <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">\"1920\"<\/span>\n        <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">\"1080\"<\/span>\n        <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{publicId}<\/span>\n        <span class=\"hljs-attr\">autoplay<\/span>=<span class=\"hljs-string\">{true}<\/span>\n        <span class=\"hljs-attr\">onDataLoad<\/span>=<span class=\"hljs-string\">{({<\/span> <span class=\"hljs-attr\">player<\/span> }) =&gt;<\/span> {\n          playerRef.current = player;\n          player.on(\"timeupdate\", () =&gt; {\n            onTimeUpdate(player.currentTime());\n          });\n        }}\n      \/&gt;\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/span>\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<blockquote>\n<p><a href=\"https:\/\/github.com\/musebe\/ai-video-hub\/blob\/main\/components\/hub\/video-stage.tsx\">View the full file on GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>The Intelligence Layer: Fetching and Parsing Metadata<\/h2>\n<p>Now that you have a stable video player, you\u2019ll need to feed it the AI-generated data. Cloudinary automatically creates a <code>.transcript<\/code> (JSON) and a <code>.vtt<\/code> (Subtitle) file for us, but retrieving them isn\u2019t as simple as just fetching a URL.<\/p>\n<h3>The Problem: The \u2018Async Gap\u2019<\/h3>\n<p>Video processing and AI transcription happen at different speeds.<\/p>\n<ul>\n<li>Video ready: Version <code>v100<\/code> (Instant)<\/li>\n<li>Transcript ready: Version <code>v105<\/code> (5 seconds later)<\/li>\n<\/ul>\n<p>If our app requests <code>...\/v100\/my-video.transcript<\/code> immediately, Cloudinary returns a <strong>404 Not Found<\/strong> because the file was actually saved under <code>v105<\/code>.<\/p>\n<h3>The Solution: Fuzzy Versioning and VTT Fallback<\/h3>\n<p>You built a robust Server Action that refuses to give up.<\/p>\n<ol>\n<li>Fuzzy versioning: If the exact version match fails, check versions <code>v+1<\/code> up to <code>v+15<\/code>. This \u201cfuzziness\u201d bridges the async gap perfectly.<\/li>\n<li>VTT fallback: If the rich JSON transcript is missing entirely, you\u2019ll automatically fetch and parse the standard <code>.vtt<\/code> subtitle file as a backup.<\/li>\n<\/ol>\n<p>Here\u2019s the logic that powers your intelligence layer: <strong><code>app\/actions\/media-process.ts<\/code><\/strong><\/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-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">getTranscriptAction<\/span>(<span class=\"hljs-params\">\n  publicId: string,\n  videoVersion?: string\n<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> baseUrl = <span class=\"hljs-string\">`https:\/\/res.cloudinary.com\/<span class=\"hljs-subst\">${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}<\/span>\/raw\/upload`<\/span>;\n\n  <span class=\"hljs-comment\">\/\/ 1. GENERATE CANDIDATES (Fuzzy Versioning)<\/span>\n  <span class=\"hljs-comment\">\/\/ We check the current version AND the next 15 versions to catch async delays.<\/span>\n  <span class=\"hljs-keyword\">const<\/span> versionsToCheck = &#91;];\n  <span class=\"hljs-keyword\">if<\/span> (videoVersion) {\n    <span class=\"hljs-keyword\">const<\/span> v = <span class=\"hljs-built_in\">parseInt<\/span>(videoVersion);\n    <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">let<\/span> i = <span class=\"hljs-number\">0<\/span>; i &lt;= <span class=\"hljs-number\">15<\/span>; i++) versionsToCheck.push(v + i);\n  }\n\n  <span class=\"hljs-comment\">\/\/ 2. CHECK CANDIDATES<\/span>\n  <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> v <span class=\"hljs-keyword\">of<\/span> versionsToCheck) {\n    <span class=\"hljs-comment\">\/\/ Priority A: Rich JSON Transcript<\/span>\n    <span class=\"hljs-keyword\">const<\/span> jsonUrl = <span class=\"hljs-string\">`<span class=\"hljs-subst\">${baseUrl}<\/span>\/v<span class=\"hljs-subst\">${v}<\/span>\/<span class=\"hljs-subst\">${publicId}<\/span>.transcript`<\/span>;\n    <span class=\"hljs-keyword\">const<\/span> jsonRes = <span class=\"hljs-keyword\">await<\/span> fetch(jsonUrl);\n    <span class=\"hljs-keyword\">if<\/span> (jsonRes.ok) <span class=\"hljs-keyword\">return<\/span> parseTranscriptJSON(<span class=\"hljs-keyword\">await<\/span> jsonRes.json());\n\n    <span class=\"hljs-comment\">\/\/ Priority B: VTT Fallback (Standard Subtitles)<\/span>\n    <span class=\"hljs-keyword\">const<\/span> vttUrl = <span class=\"hljs-string\">`<span class=\"hljs-subst\">${baseUrl}<\/span>\/v<span class=\"hljs-subst\">${v}<\/span>\/<span class=\"hljs-subst\">${publicId}<\/span>.en-US.vtt`<\/span>;\n    <span class=\"hljs-keyword\">const<\/span> vttRes = <span class=\"hljs-keyword\">await<\/span> fetch(vttUrl);\n    <span class=\"hljs-keyword\">if<\/span> (vttRes.ok) <span class=\"hljs-keyword\">return<\/span> parseVTT(<span class=\"hljs-keyword\">await<\/span> vttRes.text());\n  }\n\n  <span class=\"hljs-keyword\">return<\/span> &#91;]; \n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This single function ensures our UI never breaks, even when the AI is still \u201cthinking\u201d or when one file format fails to generate.<\/p>\n<blockquote>\n<p><a href=\"https:\/\/github.com\/musebe\/ai-video-hub\/blob\/main\/app\/actions\/media-process.ts\">View the full file on GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>Syncing State: The Searchable Transcript Sidebar<\/h2>\n<p>With the transcript data in hand, you\u2019ll need to display it. A static list isn\u2019t enough; we want a dynamic experience that highlights the current sentence, scrolls automatically, and filters in real-time.<\/p>\n<h3>Instant Client-Side Search<\/h3>\n<p>You don\u2019t need to hit the server every time the user types. By using <code>useMemo<\/code>, we filter the entire transcript array in real-time. This keeps the UI snappy even with hour-long webinars containing thousands of lines.<\/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\">\/\/ 1. Filter Logic: Updates instantly as 'query' changes<\/span>\n<span class=\"hljs-keyword\">const<\/span> filtered = useMemo(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">if<\/span> (!query) <span class=\"hljs-keyword\">return<\/span> transcript;\n  <span class=\"hljs-keyword\">return<\/span> transcript.filter(<span class=\"hljs-function\">(<span class=\"hljs-params\">t<\/span>) =&gt;<\/span>\n    t.text.toLowerCase().includes(query.toLowerCase())\n  );\n}, &#91;transcript, query]);\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<h3>Step 2: Auto-Scrolling &amp; Deep Linking<\/h3>\n<p>To keep the user oriented, we need the sidebar to follow the video.<\/p>\n<ul>\n<li>\n<strong>Deep Linking:<\/strong> Clicking a segment calls <code>onSeek(item.startTime)<\/code>, jumping the video player to that exact second.<\/li>\n<li>\n<strong>Auto-Scroll:<\/strong> We use <code>useEffect<\/code> to watch the video\u2019s <code>currentTime<\/code>. When a new segment becomes \u201cactive,\u201d we automatically scroll it into the center of the view.<\/li>\n<\/ul>\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\">\/\/ 2. Auto-Scroll Logic: Keeps the active line in view<\/span>\n<span class=\"hljs-keyword\">const<\/span> activeSegmentRef = useRef &lt; HTMLDivElement &gt; <span class=\"hljs-literal\">null<\/span>;\n\nuseEffect(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-comment\">\/\/ Only auto-scroll if the user isn't actively searching<\/span>\n  <span class=\"hljs-keyword\">if<\/span> (!query &amp;&amp; activeSegmentRef.current) {\n    activeSegmentRef.current.scrollIntoView({\n      <span class=\"hljs-attr\">behavior<\/span>: <span class=\"hljs-string\">\"smooth\"<\/span>,\n      <span class=\"hljs-attr\">block<\/span>: <span class=\"hljs-string\">\"center\"<\/span>,\n    });\n  }\n}, &#91;currentTime, query]);\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><a href=\"https:\/\/github.com\/musebe\/ai-video-hub\/blob\/main\/components\/hub\/insights-sidebar.tsx\">View full file on GitHub**<\/a>.<\/p>\n<\/blockquote>\n<h2>Managing Global State and Playlists<\/h2>\n<p>The player and the sidebar need to talk to each other. Plus, you\u2019ll need a way to let users browse <em>other<\/em> videos without reloading the page.<\/p>\n<h3>Lifting State Up<\/h3>\n<p>Instead of keeping state inside the player or sidebar, lift it to your main page controller (<code>app\/page.tsx<\/code>). This allows you to orchestrate the entire experience:<\/p>\n<ul>\n<li>\n<strong>Player<\/strong> tells the Controller: <em>\u201cI\u2019m at 00:45.\u201d<\/em>\n<\/li>\n<li>\n<strong>Controller<\/strong> tells the Sidebar: <em>\u201cHighlight the segment at 00:45.\u201d<\/em>\n<\/li>\n<li>\n<strong>Sidebar<\/strong> tells the Controller: <em>\u201cUser clicked 02:30.\u201d<\/em>\n<\/li>\n<li>\n<strong>Controller<\/strong> tells the Player: <em>\u201cSeek to 02:30.\u201d<\/em>\n<\/li>\n<\/ul>\n<h3>The Auto-Updating Playlist<\/h3>\n<p>You shouldn\u2019t have to manually update a JSON file every time you upload a video. Instead, you can use Cloudinary\u2019s Admin API to fetch the latest uploads automatically.<\/p>\n<p><strong><code>app\/actions\/media-process.ts<\/code><\/strong><\/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-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">getPlaylistAction<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n  <span class=\"hljs-comment\">\/\/ Fetch all videos with the 'ai-knowledge-hub' tag<\/span>\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 tags=ai-knowledge-hub\"<\/span>)\n    .sort_by(<span class=\"hljs-string\">\"created_at\"<\/span>, <span class=\"hljs-string\">\"desc\"<\/span>)\n    .max_results(<span class=\"hljs-number\">10<\/span>)\n    .execute();\n\n  <span class=\"hljs-keyword\">return<\/span> results.resources;\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Now, the moment you upload a video and it gets auto-tagged, it instantly appears in the playlist for everyone to see.<\/p>\n<blockquote>\n<p><a href=\"https:\/\/github.com\/musebe\/ai-video-hub\/blob\/main\/app\/actions\/media-process.ts\">View the full file on GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>Handling Real-World Edge Cases<\/h2>\n<p>What happens if the user loads the page <em>immediately<\/em> after uploading? The video might play, but the AI transcript won\u2019t exist yet.<\/p>\n<h3>Step 1: \u2018Are We There Yet?\u2019<\/h3>\n<p>If you just fetch the transcript once and fail, the user will see an empty sidebar. You need to <strong>poll<\/strong> the endpoint until the data arrives.<\/p>\n<h3>Step 2: Exponential Backoff Strategy<\/h3>\n<p>Spamming the server every 100ms is bad practice. With <strong>exponential backoff<\/strong>, you wait 1 second, then 2, then 4, up to a maximum limit.<\/p>\n<p><strong><code>components\/hub\/insights-sidebar.tsx<\/code><\/strong><\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ Inside our data fetching effect<\/span>\n<span class=\"hljs-keyword\">const<\/span> pollForTranscript = <span class=\"hljs-keyword\">async<\/span> (attempt = <span class=\"hljs-number\">0<\/span>) =&gt; {\n  <span class=\"hljs-keyword\">try<\/span> {\n    <span class=\"hljs-keyword\">const<\/span> data = <span class=\"hljs-keyword\">await<\/span> getTranscriptAction(publicId, version);\n    <span class=\"hljs-keyword\">if<\/span> (data &amp;&amp; data.length &gt; <span class=\"hljs-number\">0<\/span>) {\n      setTranscript(data);\n      <span class=\"hljs-keyword\">return<\/span>; <span class=\"hljs-comment\">\/\/ Success! Stop polling.<\/span>\n    }\n\n    <span class=\"hljs-comment\">\/\/ If fail, wait longer each time (1s, 2s, 4s...)<\/span>\n    <span class=\"hljs-keyword\">const<\/span> delay = <span class=\"hljs-built_in\">Math<\/span>.min(<span class=\"hljs-number\">1000<\/span> * <span class=\"hljs-built_in\">Math<\/span>.pow(<span class=\"hljs-number\">2<\/span>, attempt), <span class=\"hljs-number\">10000<\/span>);\n\n    <span class=\"hljs-keyword\">if<\/span> (attempt &lt; <span class=\"hljs-number\">5<\/span>) {\n      <span class=\"hljs-comment\">\/\/ Give up after 5 tries<\/span>\n      setTimeout(<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> pollForTranscript(attempt + <span class=\"hljs-number\">1<\/span>), delay);\n    }\n  } <span class=\"hljs-keyword\">catch<\/span> (e) {\n    <span class=\"hljs-built_in\">console<\/span>.error(<span class=\"hljs-string\">\"Polling failed\"<\/span>, e);\n  }\n};\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This ensures that if the AI takes 10 seconds to finish processing, your UI patiently waits and then seamlessly pops in the data without the user ever needing to refresh.<\/p>\n<blockquote>\n<p><a href=\"https:\/\/github.com\/musebe\/ai-video-hub\/blob\/main\/components\/hub\/insights-sidebar.tsx\">View full file on GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>The Result: A Self-Healing Accessibility Pipeline<\/h2>\n<p>By integrating Cloudinary\u2019s AI at the upload stage, you eliminated accessibility debt that usually piles up with video content.<\/p>\n<h3>The \u2018Zero-Touch\u2019 Workflow<\/h3>\n<ol>\n<li>\n<strong>Upload.<\/strong> A content manager drops a raw <code>.mp4<\/code> into the upload widget.<\/li>\n<li>\n<strong>Analyze.<\/strong> Cloudinary generates captions (<code>.vtt<\/code>) and a transcript (<code>.json<\/code>) automatically.<\/li>\n<li>\n<strong>Deliver.<\/strong> The frontend detects these new assets and attaches them to the player.<\/li>\n<\/ol>\n<p>You omit the fourth step, which would generally be typing out captions manually. The system heals itself. If you upload a video today, it\u2019s accessible by default tomorrow.<\/p>\n<h3>The Netflix Experience<\/h3>\n<p>Users get a professional interface where they can easily search within the video like it\u2019s a document, read the captions synced with audio (great for non-native speakers), and navigate via chapters instead of scrubbing blindly.<\/p>\n<h2>Conclusion: Moving Beyond Static Video<\/h2>\n<p>Instead of opaque pixels, your video content is searchable, accessible, and interactive.<\/p>\n<p>To recap your improvements:<\/p>\n<ul>\n<li>You used <code>f_auto<\/code> and <code>q_auto<\/code> to ensure you\u2019ll never serve a 100MB file when a 10MB AV1 version will suffice.<\/li>\n<li>No more managing a complex backend! Next.js handles the UI, Cloudinary handles the AI.<\/li>\n<li>You turned 60 minutes of tedious, manual work into five minutes of actionable insights.<\/li>\n<\/ul>\n<p>Make sure to <a href=\"https:\/\/cloudinary.com\/users\/register_free\">sign up<\/a> for a free Cloudinary, and try this project out for yourself today.<\/p>\n<ul>\n<li>\n<strong>Live Demo:<\/strong> <a href=\"https:\/\/ai-video-hub.vercel.app\">ai-video-hub.vercel.app<\/a>\n<\/li>\n<li>\n<strong>Source Code:<\/strong> <a href=\"https:\/\/github.com\/musebe\/ai-video-hub\">github.com\/musebe\/ai-video-hub<\/a>\n<\/li>\n<\/ul>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":87,"featured_media":39965,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[337,336,212,303],"class_list":["post-39964","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-accessibility","tag-ai","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>Solving Long-Form Content Fatigue With Next.js and Cloudinary<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Solving Long-Form Content Fatigue With Next.js and Cloudinary\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-03-26T14:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-03-30T21:28:42+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1773786155\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7.png?_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\/png\" \/>\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\/content-fatigue-next-js#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"Solving Long-Form Content Fatigue With Next.js and Cloudinary\",\"datePublished\":\"2026-03-26T14:00:00+00:00\",\"dateModified\":\"2026-03-30T21:28:42+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js\"},\"wordCount\":9,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1773786155\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7.png?_i=AA\",\"keywords\":[\"Accessibility\",\"AI\",\"Next.js\",\"Video\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2026\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js\",\"url\":\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js\",\"name\":\"Solving Long-Form Content Fatigue With Next.js and Cloudinary\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1773786155\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7.png?_i=AA\",\"datePublished\":\"2026-03-26T14:00:00+00:00\",\"dateModified\":\"2026-03-30T21:28:42+00:00\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1773786155\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1773786155\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7.png?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Solving Long-Form Content Fatigue With Next.js 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":"Solving Long-Form Content Fatigue With Next.js and Cloudinary","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\/content-fatigue-next-js","og_locale":"en_US","og_type":"article","og_title":"Solving Long-Form Content Fatigue With Next.js and Cloudinary","og_url":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js","og_site_name":"Cloudinary Blog","article_published_time":"2026-03-26T14:00:00+00:00","article_modified_time":"2026-03-30T21:28:42+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1773786155\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7.png?_i=AA","type":"image\/png"}],"author":"melindapham","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"Solving Long-Form Content Fatigue With Next.js and Cloudinary","datePublished":"2026-03-26T14:00:00+00:00","dateModified":"2026-03-30T21:28:42+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js"},"wordCount":9,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1773786155\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7.png?_i=AA","keywords":["Accessibility","AI","Next.js","Video"],"inLanguage":"en-US","copyrightYear":"2026","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js","url":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js","name":"Solving Long-Form Content Fatigue With Next.js and Cloudinary","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1773786155\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7.png?_i=AA","datePublished":"2026-03-26T14:00:00+00:00","dateModified":"2026-03-30T21:28:42+00:00","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/content-fatigue-next-js"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1773786155\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1773786155\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7.png?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/content-fatigue-next-js#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Solving Long-Form Content Fatigue With Next.js 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\/v1773786155\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7\/BlogSupportingGraphics2026-AI_Video_Hub_Blog-pwBm7.png?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39964","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=39964"}],"version-history":[{"count":1,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39964\/revisions"}],"predecessor-version":[{"id":39966,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/39964\/revisions\/39966"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/39965"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=39964"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=39964"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=39964"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}