{"id":40359,"date":"2026-08-27T07:00:00","date_gmt":"2026-08-27T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=40359"},"modified":"2026-08-27T11:54:44","modified_gmt":"2026-08-27T18:54:44","slug":"ugc-image-moderation-cloudinary-powerflows-nextjs","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs","title":{"rendered":"No-Code Content Moderation for User-Generated Images"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>Every product that lets users upload images eventually meets the same unwelcome guest: the upload you really wish hadn\u2019t happened. A profile photo that\u2019s actually a screenshot of something graphic. A \u201cproduct shot\u201d that\u2019s spam. The na\u00efve answer, <em>just trust users and clean up later<\/em> doesn\u2019t scale past your first hundred uploads, and manual review queues are slow, expensive, and demoralizing.<\/p>\n<p>The better answer is to moderate <strong>the moment the image lands<\/strong>, automatically, and only loop in a human for the genuine edge cases. In this tutorial we\u2019ll build exactly that, mostly without writing queue code, using <strong>Cloudinary PowerFlows<\/strong> for the pipeline and <strong>Next.js<\/strong> for the frontend.<\/p>\n<p>Here\u2019s the finished product, a gallery where approved images render crisply and anything unapproved is blurred until it clears review:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1786558341\/blog-No-Code_Content_Moderation_for_User-Generated_Images-1.webp\" alt=\"Finished UGC moderation gallery\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1568\" height=\"737\"\/><\/p>\n<ul>\n<li>\n<strong>Live demo<\/strong>: <a href=\"https:\/\/ugc-moderation-pipeline.vercel.app\/\">https:\/\/ugc-moderation-pipeline.vercel.app\/<\/a>\n<\/li>\n<li>\n<strong>Source code<\/strong>: <a href=\"https:\/\/github.com\/musebe\/ugc-moderation-pipeline\">https:\/\/github.com\/musebe\/ugc-moderation-pipeline<\/a>\n<\/li>\n<\/ul>\n<h2>What We\u2019re Building<\/h2>\n<p>We\u2019ll build a content-moderation pipeline that triggers on every upload to a <code>ugc\/<\/code> folder, runs Cloudinary\u2019s AI Vision moderation, routes approved images to <code>published\/<\/code> and rejected ones to <code>quarantine\/<\/code>, fires a webhook, and shows the result in a Next.js gallery where unapproved images stay blurred.<\/p>\n<p>Let\u2019s start at the end. By the time you\u2019re done you\u2019ll have a deployed app with:<\/p>\n<figure class=\"table-wrapper\"><table>\n<thead>\n<tr>\n<th>Feature<\/th>\n<th>What It Does<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Drag-and-drop upload<\/strong><\/td>\n<td>Sends images straight into a <code>ugc\/<\/code> folder via an unsigned preset<\/td>\n<\/tr>\n<tr>\n<td><strong>Automatic AI moderation<\/strong><\/td>\n<td>Cloudinary AI Vision scores each image against plain-language rules<\/td>\n<\/tr>\n<tr>\n<td><strong>Auto-routing<\/strong><\/td>\n<td>Approved \u2192 <code>published\/<\/code>, rejected \u2192 <code>quarantine\/<\/code><\/td>\n<\/tr>\n<tr>\n<td><strong>Live status gallery<\/strong><\/td>\n<td>Filter by All \/ Approved \/ Rejected \/ Pending<\/td>\n<\/tr>\n<tr>\n<td><strong>Blur-on-unapproved<\/strong><\/td>\n<td>Only approved images render clearly; the rest stay obscured<\/td>\n<\/tr>\n<tr>\n<td><strong>Webhook callback<\/strong><\/td>\n<td>Your app is notified the instant a verdict is reached<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n<h2>Architecture at a Glance<\/h2>\n<p>The whole system is a relay race between Cloudinary and your Next.js app:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n upload \u2502   ugc\/      \u2502\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25b6\u2502  (Next.js)  \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n               \u2502  trigger\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n        \u2502  PowerFlow            \u2502\n        \u2502  \u2022 Add to Logs        \u2502\n        \u2502  \u2022 AI Vision Moderate \u2502\n        \u2502  \u2022 Condition (rejected?)\n        \u2514\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2518\n       approved          rejected\n            \u2502                \u2502\n   \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2510  \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n   \u2502 Update Tags  \u2502  \u2502 Move to Folder \u2502\n   \u2502 \u2192 published\/ \u2502  \u2502 \u2192 quarantine\/  \u2502\n   \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2518  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n            \u2502                \u2502\n            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n            Send HTTP Request\n                   \u2502\n        \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n        \u2502 \/api\/webhooks\/...     \u2502\n        \u2502 + <span class=\"hljs-regexp\">\/api\/g<\/span>allery (read) \u2502\n        \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Each stage maps to a specific Cloudinary capability:<\/p>\n<figure class=\"table-wrapper\"><table>\n<thead>\n<tr>\n<th>Pipeline stage<\/th>\n<th>Powered by<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Trigger on new upload<\/td>\n<td>PowerFlow \u201cCloudinary Upload\u201d trigger<\/td>\n<\/tr>\n<tr>\n<td>Audit trail<\/td>\n<td>\u201cAdd to Logs\u201d block<\/td>\n<\/tr>\n<tr>\n<td>Content scoring<\/td>\n<td><strong>AI Vision Moderate by Prompts<\/strong> add-on<\/td>\n<\/tr>\n<tr>\n<td>Branching<\/td>\n<td>\u201cCondition\u201d block on <code>any_rejected<\/code><\/td>\n<\/tr>\n<tr>\n<td>Routing<\/td>\n<td>\u201cMove to Folder\u201d + \u201cUpdate Tags\u201d<\/td>\n<\/tr>\n<tr>\n<td>Notify app<\/td>\n<td>\u201cSend HTTP Request\u201d block<\/td>\n<\/tr>\n<tr>\n<td>Read status back<\/td>\n<td>Admin <strong>Search API<\/strong> (<code>folder=&quot;\u2026&quot;<\/code>)<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n<h2>Why PowerFlows Instead of EasyFlows?<\/h2>\n<p>Cloudinary offers two automation tools:<\/p>\n<p><strong>EasyFlows<\/strong> are great for linear \u201cdo X then Y\u201d chains, but moderation is fundamentally a <em>fork in the road<\/em>: approved and rejected images need to go to different places.<\/p>\n<p><strong>PowerFlows<\/strong> gives you the option to branch. You start with a visual canvas containing a <code>Condition<\/code> block, which you can express: \u201cif any rule rejects this image, quarantine it; otherwise publish it\u201d without hand-writing a queue worker. That\u2019s the entire reason this tutorial is built on PowerFlows.<\/p>\n<blockquote>\n<p><strong>Note:<\/strong> If your automation has an <em>if\/else<\/em>, go with PowerFlows.<\/p>\n<\/blockquote>\n<h2>Prerequisites<\/h2>\n<p>Before building, make sure you have:<\/p>\n<ul>\n<li>\n<p>A <strong>Cloudinary account<\/strong> with the <strong>AI Content Analysis (AI Vision)<\/strong> add-on enabled.<\/p>\n<\/li>\n<li>\n<p><strong>Node.js 18+<\/strong> and npm.<\/p>\n<\/li>\n<li>\n<p>An <strong>unsigned upload preset<\/strong> that drops files into <code>ugc\/<\/code>. Create it under <strong>Settings \u2192 Upload \u2192 Add upload preset<\/strong>:<\/p>\n<figure class=\"table-wrapper\"><table>\n<thead>\n<tr>\n<th>Field<\/th>\n<th>Value<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Preset name<\/td>\n<td><code>ugc_unsigned<\/code><\/td>\n<\/tr>\n<tr>\n<td>Signing Mode<\/td>\n<td><strong>Unsigned<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Folder<\/td>\n<td><code>ugc<\/code><\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n<\/li>\n<\/ul>\n<blockquote>\n<p><strong>Note:<\/strong> Don\u2019t add a moderation add-on to the preset. Moderation runs <em>inside<\/em> the PowerFlow. Doing both moderates every image twice and burns double the credits.<\/p>\n<\/blockquote>\n<h2>Part 1: Building the Moderation Pipeline<\/h2>\n<p>Open <strong>MediaFlows &gt; PowerFlows &gt; Create flow<\/strong>. Here\u2019s the full canvas we\u2019re aiming for:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1786558341\/blog-No-Code_Content_Moderation_for_User-Generated_Images-2.webp\" alt=\"Full PowerFlow canvas overview\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1568\" height=\"737\"\/><\/p>\n<blockquote>\n<p>Prefer clicking through it yourself? The deployed app has a <a href=\"https:\/\/ugc-moderation-pipeline.vercel.app\/setup\">step-by-step setup page<\/a> that walks each block with these same screenshots.<\/p>\n<\/blockquote>\n<h3>Step 1. The Trigger<\/h3>\n<p>Add a <strong>Cloudinary Upload<\/strong> trigger and scope it to images landing in the <code>ugc\/<\/code> folder. This is what makes the pipeline fire automatically on every new upload.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1786558341\/blog-No-Code_Content_Moderation_for_User-Generated_Images-3.webp\" alt=\"Cloudinary Upload trigger configured for the ugc folder\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1568\" height=\"737\"\/><\/p>\n<h3>Step 2. AI Vision Moderation<\/h3>\n<p>Add the <strong>AI Vision Moderate by Prompts<\/strong> block. Instead of training a model, you describe what to reject in plain language (e.g., \u201ccontains nudity\u201d, \u201ccontains violence\u201d). Each rule returns a pass\/fail.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1786558463\/blog-No-Code_Content_Moderation_for_User-Generated_Images-4.webp\" alt=\"AI Vision Moderate by Prompts block with rules\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1568\" height=\"737\"\/><\/p>\n<h3>Step 3. The Branch<\/h3>\n<p>Add a <strong>Condition<\/strong> block testing <code>any_rejected == true<\/code>. This is the fork that EasyFlows couldn\u2019t give us.<\/p>\n<p>![Condition block testing any_rejected](<a href=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1786558716\/blog-No-Code_Content_Moderation_for_User-Generated_Images-5.webp\">https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1786558716\/blog-No-Code_Content_Moderation_for_User-Generated_Images-5.webp<\/a><\/p>\n<h3>Step 4. Route the Losers<\/h3>\n<p>On the <strong>rejected<\/strong> branch, add <strong>Move to Folder \u2192 <code>quarantine<\/code><\/strong>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1786558716\/blog-No-Code_Content_Moderation_for_User-Generated_Images-6.webp\" alt=\"Move to Folder quarantine block\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1568\" height=\"737\"\/><\/p>\n<h3>Step 5. Bless the Winners<\/h3>\n<p>On the <strong>approved<\/strong> branch, add <strong>Update Tags<\/strong> to tag the asset <code>published<\/code> and route it to <code>published\/<\/code>.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1786558717\/blog-No-Code_Content_Moderation_for_User-Generated_Images-7.webp\" alt=\"Update Tags approved block\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1568\" height=\"737\"\/><\/p>\n<h3>Step 6. Tell Your App<\/h3>\n<p>Finally, add a <strong>Send HTTP Request<\/strong> block pointing at your webhook. Use a JSON body that includes the verdict and the asset\u2019s public ID:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JSON \/ JSON with Comments\" data-shcb-language-slug=\"json\"><span><code class=\"hljs language-json shcb-wrap-lines\">{\n  <span class=\"hljs-attr\">\"status\"<\/span>: <span class=\"hljs-string\">\"approved\"<\/span>,\n  <span class=\"hljs-attr\">\"folder\"<\/span>: <span class=\"hljs-string\">\"published\"<\/span>,\n  <span class=\"hljs-attr\">\"moderation\"<\/span>: <span class=\"hljs-string\">\"ai_vision\"<\/span>,\n  <span class=\"hljs-attr\">\"public_id\"<\/span>: <span class=\"hljs-string\">\"{{$.Cloudinary_Upload.result.public_id}}\"<\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JSON \/ JSON with Comments<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">json<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/demo-article-projects\/image\/upload\/f_auto,q_auto\/tutorials\/ugc-moderation-pipeline\/07-send-http-request-webhook\" alt=\"Send HTTP Request webhook block\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1568\" height=\"737\"\/><\/p>\n<blockquote>\n<p><strong>Do this on both branches.<\/strong> The rejected branch sends the same body with <code>&quot;status&quot;: &quot;rejected&quot;<\/code> and <code>&quot;folder&quot;: &quot;quarantine&quot;<\/code>. Otherwise your app only ever hears about approvals.<\/p>\n<\/blockquote>\n<p>Hit <strong>Test<\/strong>, then open <strong>Live Logs<\/strong>. Every block should glow green:<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/cloudinary-marketing-res.cloudinary.com\/image\/upload\/v1786558341\/blog-No-Code_Content_Moderation_for_User-Generated_Images-1.webp\" alt=\"All-green Live Logs test run\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"1568\" height=\"737\"\/><\/p>\n<h2>Part 2: Scaffolding the Next.js and <code>shadcn<\/code> Frontend<\/h2>\n<p>Spin up 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\">create-next-app<\/span><span class=\"hljs-keyword\">@latest<\/span> ugc-moderation-pipeline\n# TypeScript \u00b7 Tailwind \u00b7 ESLint \u00b7 App Router \u00b7 src\/ dir \u00b7 alias @\/*\n\nnpx shadcn@latest init -d\nnpx shadcn@latest add button card badge separator alert tabs\n\nnpm i cloudinary next-cloudinary lucide-react\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Create <code>.env.local<\/code>:<\/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\">NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your-cloud-name\nNEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=ugc_unsigned\nCLOUDINARY_API_KEY=your_api_key      <span class=\"hljs-comment\"># server-only<\/span>\nCLOUDINARY_API_SECRET=your_api_secret <span class=\"hljs-comment\"># server-only \u2014 never NEXT_PUBLIC<\/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<p>The project structure:<\/p>\n<figure class=\"table-wrapper\"><table>\n<thead>\n<tr>\n<th>Path<\/th>\n<th>Role<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><a href=\"https:\/\/github.com\/musebe\/ugc-moderation-pipeline\/blob\/main\/src\/app\/page.tsx\"><code>src\/app\/page.tsx<\/code><\/a><\/td>\n<td>Home \u2014 hero + upload + gallery<\/td>\n<\/tr>\n<tr>\n<td><a href=\"https:\/\/github.com\/musebe\/ugc-moderation-pipeline\/blob\/main\/src\/components\/upload-gallery.tsx\"><code>src\/components\/upload-gallery.tsx<\/code><\/a><\/td>\n<td>Upload widget + filterable gallery<\/td>\n<\/tr>\n<tr>\n<td><a href=\"https:\/\/github.com\/musebe\/ugc-moderation-pipeline\/blob\/main\/src\/app\/api\/gallery\/route.ts\"><code>src\/app\/api\/gallery\/route.ts<\/code><\/a><\/td>\n<td>Lists assets + derives status<\/td>\n<\/tr>\n<tr>\n<td><a href=\"https:\/\/github.com\/musebe\/ugc-moderation-pipeline\/blob\/main\/src\/app\/api\/webhooks\/cloudinary-moderation\/route.ts\"><code>src\/app\/api\/webhooks\/cloudinary-moderation\/route.ts<\/code><\/a><\/td>\n<td>Receives verdicts<\/td>\n<\/tr>\n<tr>\n<td><a href=\"https:\/\/github.com\/musebe\/ugc-moderation-pipeline\/blob\/main\/src\/lib\"><code>src\/lib\/cloudinary.ts<\/code><\/a><\/td>\n<td>Server SDK config<\/td>\n<\/tr>\n<tr>\n<td><a href=\"https:\/\/github.com\/musebe\/ugc-moderation-pipeline\/blob\/main\/src\/app\/setup\/page.tsx\"><code>src\/app\/setup\/page.tsx<\/code><\/a><\/td>\n<td>Visual PowerFlow walkthrough<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n<h2>Part 3: The Upload and Gallery Experience<\/h2>\n<p>The home page does two jobs: It lets users upload, and shows everything that\u2019s been uploaded with a moderation badge. Uploads use <code>CldUploadWidget<\/code> (unsigned, targeting <code>ugc\/<\/code>); the gallery refetches after each upload and polls briefly so a freshly-uploaded <em>pending<\/em> tile flips to its verdict once the flow finishes.<\/p>\n<p>Here\u2019s the <strong>engine<\/strong> \u2014 the status mapping and the blur rule.<\/p>\n<blockquote>\n<p>View the full component: <a href=\"https:\/\/github.com\/musebe\/ugc-moderation-pipeline\/blob\/main\/src\/components\/upload-gallery.tsx\"><code>upload-gallery.tsx<\/code><\/a><\/p>\n<\/blockquote>\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\">\/\/ A tile is only sharp when approved \u2014 everything else stays blurred.<\/span>\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">GalleryTile<\/span>(<span class=\"hljs-params\">{ item }: { item: GalleryItem }<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> blurred = item.status !== <span class=\"hljs-string\">\"approved\"<\/span>;\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Card<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"overflow-hidden\"<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"relative aspect-square bg-muted\"<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CldImage<\/span>\n          <span class=\"hljs-attr\">width<\/span>=<span class=\"hljs-string\">{400}<\/span>\n          <span class=\"hljs-attr\">height<\/span>=<span class=\"hljs-string\">{400}<\/span>\n          <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{item.publicId}<\/span>\n          <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">{item.status}<\/span>\n          <span class=\"hljs-attr\">crop<\/span>=<span class=\"hljs-string\">\"fill\"<\/span>\n          <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">{<\/span>`<span class=\"hljs-attr\">h-full<\/span> <span class=\"hljs-attr\">w-full<\/span> <span class=\"hljs-attr\">object-cover<\/span> <span class=\"hljs-attr\">transition<\/span> ${\n            <span class=\"hljs-attr\">blurred<\/span> ? \"<span class=\"hljs-attr\">blur-xl<\/span> <span class=\"hljs-attr\">scale-110<\/span>\" <span class=\"hljs-attr\">:<\/span> \"\"\n          }`}\n        \/&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"absolute left-2 top-2\"<\/span>&gt;<\/span>\n          <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">StatusBadge<\/span> <span class=\"hljs-attr\">status<\/span>=<span class=\"hljs-string\">{item.status}<\/span> \/&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Card<\/span>&gt;<\/span><\/span>\n  );\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>The status-to-UI contract, in one table:<\/p>\n<figure class=\"table-wrapper\"><table>\n<thead>\n<tr>\n<th>Status<\/th>\n<th>Folder<\/th>\n<th>Badge<\/th>\n<th>Image Treatment<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>\u2705 Approved<\/td>\n<td><code>published\/<\/code><\/td>\n<td>green<\/td>\n<td>rendered clearly<\/td>\n<\/tr>\n<tr>\n<td>\u26d4 Rejected<\/td>\n<td><code>quarantine\/<\/code><\/td>\n<td>red<\/td>\n<td>blurred + \u201chidden\u201d overlay<\/td>\n<\/tr>\n<tr>\n<td>\u23f3 Pending<\/td>\n<td><code>ugc\/<\/code><\/td>\n<td>grey<\/td>\n<td>blurred + \u201creviewing\u2026\u201d<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n<h2>Part 4: Reading Moderation Status From Cloudinary<\/h2>\n<p>Here\u2019s the clever bit: we don\u2019t need a database to know each image\u2019s status. <strong>The folder <em>is<\/em> the status.<\/strong> An image in <code>published\/<\/code> is approved; in <code>quarantine\/<\/code> it\u2019s rejected; still in <code>ugc\/<\/code> it\u2019s pending. We just ask Cloudinary what\u2019s in each folder.<\/p>\n<p>The <strong>engine<\/strong> of <a href=\"https:\/\/github.com\/musebe\/ugc-moderation-pipeline\/blob\/main\/src\/app\/api\/gallery\/route.ts\"><code>src\/app\/api\/gallery\/route.ts<\/code><\/a>:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">async function listFolder(folder: string): Promise<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Raw&#91;]<\/span>&gt;<\/span> {\n  const res = await cloudinary.search\n    .expression(`folder=\"${folder}\"`)\n    .sort_by(\"created_at\", \"desc\")\n    .max_results(100)\n    .execute();\n  return (res.resources ?? &#91;]) as Raw&#91;];\n}\n\n\/\/ GET \/api\/gallery \u2192 merge ugc\/ (pending) + published\/ (approved) + quarantine\/ (rejected)\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><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<blockquote>\n<p><strong>Gotcha we hit (so you don\u2019t):<\/strong> our first version used <code>cloudinary.api.resources_by_asset_folder(...)<\/code>, which returned <strong><code>404<\/code><\/strong> for every folder. That endpoint only exists on accounts in <em>dynamic-folder<\/em> mode. The <strong>Search API<\/strong> (<code>folder=&quot;\u2026&quot;<\/code>) works on both fixed and dynamic folders \u2014 switch to it and the gallery lights up instantly. Surfacing the real error message instead of a generic \u201cfailed\u201d string is what made this a 30-second fix instead of an afternoon.<\/p>\n<\/blockquote>\n<h2>Part 5: The Webhook Receiver<\/h2>\n<p>The webhook endpoint stores each verdict so the app can read it back. For a demo it keeps an in-memory map; for production it auto-detects a Vercel KV store. The <strong>engine<\/strong> of <a href=\"https:\/\/github.com\/musebe\/ugc-moderation-pipeline\/blob\/main\/src\/app\/api\/webhooks\/cloudinary-moderation\/route.ts\"><code>route.ts<\/code><\/a>:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">POST<\/span>(<span class=\"hljs-params\">req: NextRequest<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> secret = process.env.WEBHOOK_SHARED_SECRET;\n  <span class=\"hljs-keyword\">if<\/span> (secret &amp;&amp; secret !== <span class=\"hljs-string\">\"change-me\"<\/span>) {\n    <span class=\"hljs-keyword\">if<\/span> (req.headers.get(<span class=\"hljs-string\">\"x-webhook-secret\"<\/span>) !== secret) {\n      <span class=\"hljs-keyword\">return<\/span> NextResponse.json({ <span class=\"hljs-attr\">error<\/span>: <span class=\"hljs-string\">\"unauthorized\"<\/span> }, { <span class=\"hljs-attr\">status<\/span>: <span class=\"hljs-number\">401<\/span> });\n    }\n  }\n  <span class=\"hljs-keyword\">const<\/span> { status, folder, moderation, public_id } = <span class=\"hljs-keyword\">await<\/span> req.json();\n  <span class=\"hljs-keyword\">if<\/span> (public_id) <span class=\"hljs-keyword\">await<\/span> setVerdict(public_id, { status, folder, moderation, <span class=\"hljs-attr\">at<\/span>: <span class=\"hljs-built_in\">Date<\/span>.now() });\n  <span class=\"hljs-keyword\">return<\/span> NextResponse.json({ <span class=\"hljs-attr\">ok<\/span>: <span class=\"hljs-literal\">true<\/span> });\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>The Classic Express Equivalent<\/h3>\n<p>If you\u2019re not on Next.js, here\u2019s the same receiver in ~20 lines of Express:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> express = <span class=\"hljs-keyword\">require<\/span>(<span class=\"hljs-string\">\"express\"<\/span>);\n<span class=\"hljs-keyword\">const<\/span> app = express();\napp.<span class=\"hljs-keyword\">use<\/span>(<span class=\"hljs-title\">express<\/span>.<span class=\"hljs-title\">json<\/span>());\n\n<span class=\"hljs-keyword\">const<\/span> verdicts = {};\n\napp.post(<span class=\"hljs-string\">\"\/webhooks\/cloudinary-moderation\"<\/span>, (req, res) =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> { status, folder, moderation, public_id } = req.body || {};\n  <span class=\"hljs-keyword\">if<\/span> (public_id) verdicts&#91;public_id] = { status, folder, moderation, at: Date.now() };\n  console.log(<span class=\"hljs-string\">\"&#91;moderation]\"<\/span>, req.body);\n  res.json({ ok: <span class=\"hljs-keyword\">true<\/span> });\n});\n\napp.get(<span class=\"hljs-string\">\"\/webhooks\/cloudinary-moderation\"<\/span>, (req, res) =&gt; res.json({ verdicts }));\n\napp.listen(<span class=\"hljs-number\">3000<\/span>, () =&gt; console.log(<span class=\"hljs-string\">\"Webhook receiver on :3000\"<\/span>));\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><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<h2>Extending It<\/h2>\n<p>Where to take this next:<\/p>\n<figure class=\"table-wrapper\"><table>\n<thead>\n<tr>\n<th>Extension<\/th>\n<th>How<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Slack alerts<\/strong><\/td>\n<td>Add a second HTTP block on the rejected branch posting to a Slack <a href=\"https:\/\/api.slack.com\/messaging\/webhooks\">Incoming Webhook<\/a> with the asset URL<\/td>\n<\/tr>\n<tr>\n<td><strong>Durable storage<\/strong><\/td>\n<td>Swap the in-memory map for <a href=\"https:\/\/vercel.com\/docs\/storage\/vercel-kv\">Vercel KV<\/a> \u2014 the route already auto-detects it<\/td>\n<\/tr>\n<tr>\n<td><strong>Human-in-the-loop<\/strong><\/td>\n<td>Route low-confidence scores to a third \u201creview\u201d folder instead of auto-rejecting<\/td>\n<\/tr>\n<tr>\n<td><strong>Custom rules<\/strong><\/td>\n<td>Edit the AI Vision prompts \u2014 they\u2019re plain English, no retraining<\/td>\n<\/tr>\n<\/tbody>\n<\/table><\/figure>\n<h2>Deploying to Vercel<\/h2>\n<ol>\n<li>Push to GitHub and <strong>import the repo<\/strong> into Vercel.<\/li>\n<li>Add all four <code>env vars<\/code> in <strong>Project \u2192 Settings \u2192 Environment Variables<\/strong>, including the server-only <code>CLOUDINARY_API_KEY<\/code> \/ <code>CLOUDINARY_API_SECRET<\/code>.<\/li>\n<li>Deploy, then copy your production URL.<\/li>\n<li>Back in the PowerFlow, set both <strong>Send HTTP Request<\/strong> blocks\u2019 Target URL to:\n<code>https:\/\/&lt;your-app&gt;.vercel.app\/api\/webhooks\/cloudinary-moderation<\/code>\n<\/li>\n<li>Rerun <strong>Test<\/strong> \u2014 all green.<\/li>\n<\/ol>\n<blockquote>\n<p><strong>Note:<\/strong> <code>Env var<\/code> changes don\u2019t apply to existing deployments. <strong>Redeploy<\/strong> after editing them.<\/p>\n<\/blockquote>\n<h2>Conclusion<\/h2>\n<p>You\u2019ve built a production-shaped moderation pipeline where scoring, branching, routing, and notifying lives in a visual PowerFlow, and your Next.js app simply reads the result back from Cloudinary\u2019s folders. No queue workers, no model training, and the two bugs most people hit (the <code>by_asset_folder<\/code> 404 and the webhook 401) already defused.<\/p>\n<ul>\n<li>\n<strong>Live demo:<\/strong> <a href=\"https:\/\/ugc-moderation-pipeline.vercel.app\/\">ugc-moderation-pipeline.vercel.app<\/a>\n<\/li>\n<li>\n<strong>Full source:<\/strong> <a href=\"https:\/\/github.com\/musebe\/ugc-moderation-pipeline\">github.com\/musebe\/ugc-moderation-pipeline<\/a>\n<\/li>\n<li>\n<strong>Docs:<\/strong> <a href=\"https:\/\/cloudinary.com\/documentation\/mediaflows_powerflows\">Cloudinary PowerFlows<\/a> \u00b7 <a href=\"https:\/\/cloudinary.com\/documentation\/cloudinary_ai_vision_addon\">AI Vision moderation<\/a> \u00b7 <a href=\"https:\/\/next.cloudinary.dev\/\">next-cloudinary<\/a>\n<\/li>\n<\/ul>\n<p>Ready to level up your visual media workflow? <a href=\"https:\/\/cloudinary.com\/users\/register_free\">Start using Cloudinary for free<\/a> and build better visual experiences today.<\/p>\n<\/div>\n\n\n<style>\n.faqs {\n    padding: 30px 60px;\n    margin-top: 40px;\n    background: var(--color-background-offset);\n    border-radius: 20px;\n}\n\n#frequently_asked_questions {\n    margin-bottom: 20px;\n}\n\n.question {\n    margin-bottom: 12px;\n    border-bottom: 1px solid rgba(0, 0, 0, 0.12);\n}\n\n.question summary {\n    position: relative;\n    padding: 18px 40px 18px 0;\n    cursor: pointer;\n    font-weight: 700;\n    list-style: none;\n}\n\n.question summary::-webkit-details-marker {\n    display: none;\n}\n\n.question summary::after {\n    content: \"+\";\n    position: absolute;\n    top: 50%;\n    right: 0;\n    transform: translateY(-50%);\n    font-size: 24px;\n    font-weight: 400;\n    line-height: 1;\n}\n\n.question[open] summary::after {\n    content: \"\u2212\";\n}\n\n.answer {\n    padding: 0 40px 20px 0;\n}\n<\/style>\n\n<div class=\"faqs\">\n\n<h2 id=\"frequently_asked_questions\">Frequently Asked Questions<\/h2>\n\n<details class=\"question\">\n    <summary>Why is manual image moderation unsustainable for UGC platforms, and how does automated routing solve it?<\/summary>\n    <div class=\"answer\">\n        Manual review is too slow. It fails to scale past early traffic and exposes workers to potentially graphic uploads. Automated routing intercepts uploads instantly and makes a moderation decision within seconds. Only edge cases require human review. This helps keep platforms safe, protects the user experience, and reduces staffing overhead.\n    <\/div>\n<\/details>\n\n<details class=\"question\">\n    <summary>What is the technical difference between Cloudinary EasyFlows and PowerFlows, and why is the latter required for moderation?<\/summary>\n    <div class=\"answer\">\n        EasyFlows are designed for simple, linear pipelines that perform step A and then step B. Content moderation is not always linear because approved and rejected images may need to be routed to different destinations.\n        <br><br>\n        PowerFlows provide a visual canvas with condition blocks that can evaluate rules and route assets to different folders based on the moderation verdict. This makes PowerFlows better suited to branching moderation workflows.\n    <\/div>\n<\/details>\n\n<details class=\"question\">\n    <summary>How does the plain-language prompt scoring system in AI Vision Moderation work without training custom models?<\/summary>\n    <div class=\"answer\">\n        Training custom computer vision models can be expensive and may require large labeled datasets. Cloudinary AI Vision uses natural-language prompts instead. Developers can define rules in plain English, such as \u201ccontains explicit graphics\u201d or \u201ccontains promotional text.\u201d\n        <br><br>\n        The model evaluates the image against those prompts and returns a result for each rule. This enables teams to set up moderation logic without building or training their own machine learning models.\n    <\/div>\n<\/details>\n\n<details class=\"question\">\n    <summary>How can developers determine the moderation status of an image without using a database?<\/summary>\n    <div class=\"answer\">\n        Instead of tracking moderation state in a separate database, an application can use asset folders as state. Images in a raw upload folder can represent pending assets, images moved to a published folder can represent approved assets, and images routed to a quarantine folder can represent rejected assets.\n        <br><br>\n        The gallery can then query the contents of those folders directly. This reduces database synchronization requirements and keeps the asset workflow lightweight.\n    <\/div>\n<\/details>\n\n<details class=\"question\">\n    <summary>Why does the standard <code>resources_by_asset_folder<\/code> endpoint sometimes throw a 404 error, and how do you resolve it?<\/summary>\n    <div class=\"answer\">\n        The <code>resources_by_asset_folder<\/code> endpoint has account configuration requirements and may not work as expected in environments that don&#8217;t support the required folder behavior. In those cases, the request can return a 404 error.\n        <br><br>\n        A more flexible approach is to use the <a href=\"https:\/\/cloudinary.com\/documentation\/search_method\">Cloudinary Search API<\/a> with an expression that queries assets by folder. This allows developers to retrieve folder assets through search expressions and can simplify troubleshooting across different account configurations.\n    <\/div>\n<\/details>\n\n<details class=\"question\">\n    <summary>How does the frontend handle image blurring for unapproved or pending assets?<\/summary>\n    <div class=\"answer\">\n        The gallery can check an asset&#8217;s moderation or folder status before deciding how it should be displayed. If an asset has not been approved, the interface can apply a CSS blur filter and display a loading or blocked-state indicator instead of presenting the image normally.\n        <br><br>\n        Once the application receives confirmation that the asset has been approved, the UI can update and transition the image into sharp focus.\n    <\/div>\n<\/details>\n\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Frequently Asked Questions Why is manual image moderation unsustainable for UGC platforms, and how does automated routing solve it? Manual review is too slow. It fails to scale past early traffic and exposes workers to potentially graphic uploads. Automated routing intercepts uploads instantly and makes a moderation decision within seconds. Only edge cases require human [&hellip;]<\/p>\n","protected":false},"author":87,"featured_media":40393,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1],"tags":[370,363,203,300],"class_list":["post-40359","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-image","tag-media-flows","tag-moderation","tag-user-generated-content"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v28.3 (Yoast SEO v28.3) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>No-Code UGC Image Moderation: Cloudinary PowerFlows &amp; Next.js<\/title>\n<meta name=\"description\" content=\"Build an automated AI image moderation pipeline. Use Cloudinary PowerFlows to auto-route approved images and quarantine rejected files with no queue code.\" \/>\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\/ugc-image-moderation-cloudinary-powerflows-nextjs\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"No-Code Content Moderation for User-Generated Images\" \/>\n<meta property=\"og:description\" content=\"Build an automated AI image moderation pipeline. Use Cloudinary PowerFlows to auto-route approved images and quarantine rejected files with no queue code.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-08-27T14:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-08-27T18:54:44+00:00\" \/>\n<meta name=\"author\" content=\"melindapham\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1787856761\/Web_Assets\/blog\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#\\\/schema\\\/person\\\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"No-Code Content Moderation for User-Generated Images\",\"datePublished\":\"2026-08-27T14:00:00+00:00\",\"dateModified\":\"2026-08-27T18:54:44+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs\"},\"wordCount\":491,\"publisher\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/res.cloudinary.com\\\/cloudinary-marketing\\\/images\\\/f_auto,q_auto\\\/v1787856761\\\/Web_Assets\\\/blog\\\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\\\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA\",\"keywords\":[\"Image\",\"MediaFlows\",\"Moderation\",\"User-Generated Content\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2026\",\"copyrightHolder\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs\",\"url\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs\",\"name\":\"No-Code UGC Image Moderation: Cloudinary PowerFlows & Next.js\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/res.cloudinary.com\\\/cloudinary-marketing\\\/images\\\/f_auto,q_auto\\\/v1787856761\\\/Web_Assets\\\/blog\\\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\\\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA\",\"datePublished\":\"2026-08-27T14:00:00+00:00\",\"dateModified\":\"2026-08-27T18:54:44+00:00\",\"description\":\"Build an automated AI image moderation pipeline. Use Cloudinary PowerFlows to auto-route approved images and quarantine rejected files with no queue code.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs#primaryimage\",\"url\":\"https:\\\/\\\/res.cloudinary.com\\\/cloudinary-marketing\\\/images\\\/f_auto,q_auto\\\/v1787856761\\\/Web_Assets\\\/blog\\\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\\\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA\",\"contentUrl\":\"https:\\\/\\\/res.cloudinary.com\\\/cloudinary-marketing\\\/images\\\/f_auto,q_auto\\\/v1787856761\\\/Web_Assets\\\/blog\\\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\\\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA\",\"width\":4000,\"height\":2200},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/ugc-image-moderation-cloudinary-powerflows-nextjs#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/cloudinary.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"No-Code Content Moderation for User-Generated Images\"}]},{\"@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:\\\/\\\/secure.gravatar.com\\\/avatar\\\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"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":"No-Code UGC Image Moderation: Cloudinary PowerFlows & Next.js","description":"Build an automated AI image moderation pipeline. Use Cloudinary PowerFlows to auto-route approved images and quarantine rejected files with no queue code.","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\/ugc-image-moderation-cloudinary-powerflows-nextjs","og_locale":"en_US","og_type":"article","og_title":"No-Code Content Moderation for User-Generated Images","og_description":"Build an automated AI image moderation pipeline. Use Cloudinary PowerFlows to auto-route approved images and quarantine rejected files with no queue code.","og_url":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs","og_site_name":"Cloudinary Blog","article_published_time":"2026-08-27T14:00:00+00:00","article_modified_time":"2026-08-27T18:54:44+00:00","author":"melindapham","twitter_card":"summary_large_image","twitter_image":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1787856761\/Web_Assets\/blog\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"No-Code Content Moderation for User-Generated Images","datePublished":"2026-08-27T14:00:00+00:00","dateModified":"2026-08-27T18:54:44+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs"},"wordCount":491,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1787856761\/Web_Assets\/blog\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA","keywords":["Image","MediaFlows","Moderation","User-Generated Content"],"inLanguage":"en-US","copyrightYear":"2026","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs","url":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs","name":"No-Code UGC Image Moderation: Cloudinary PowerFlows & Next.js","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1787856761\/Web_Assets\/blog\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA","datePublished":"2026-08-27T14:00:00+00:00","dateModified":"2026-08-27T18:54:44+00:00","description":"Build an automated AI image moderation pipeline. Use Cloudinary PowerFlows to auto-route approved images and quarantine rejected files with no queue code.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1787856761\/Web_Assets\/blog\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1787856761\/Web_Assets\/blog\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA","width":4000,"height":2200},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"No-Code Content Moderation for User-Generated Images"}]},{"@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:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","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"}}]}},"parsely":{"version":"1.1.0","canonical_url":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs","smart_links":{"inbound":0,"outbound":0},"traffic_boost_suggestions_count":0,"meta":{"@context":"https:\/\/schema.org","@type":"NewsArticle","headline":"No-Code Content Moderation for User-Generated Images","url":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs","mainEntityOfPage":{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1787856761\/Web_Assets\/blog\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA&w=150&h=150&crop=1","image":{"@type":"ImageObject","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1787856761\/Web_Assets\/blog\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA"},"articleSection":"Uncategorized","author":[{"@type":"Person","name":"melindapham"}],"creator":["melindapham"],"publisher":{"@type":"Organization","name":"Cloudinary Blog","logo":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA"},"keywords":["image","mediaflows","moderation","user-generated content"],"dateCreated":"2026-08-27T14:00:00Z","datePublished":"2026-08-27T14:00:00Z","dateModified":"2026-08-27T18:54:44Z"},"rendered":"<meta name=\"parsely-title\" content=\"No-Code Content Moderation for User-Generated Images\" \/>\n<meta name=\"parsely-link\" content=\"https:\/\/cloudinary.com\/blog\/ugc-image-moderation-cloudinary-powerflows-nextjs\" \/>\n<meta name=\"parsely-type\" content=\"post\" \/>\n<meta name=\"parsely-image-url\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1787856761\/Web_Assets\/blog\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA&w=150&amp;h=150&amp;crop=1\" \/>\n<meta name=\"parsely-pub-date\" content=\"2026-08-27T14:00:00Z\" \/>\n<meta name=\"parsely-section\" content=\"Uncategorized\" \/>\n<meta name=\"parsely-tags\" content=\"image,mediaflows,moderation,user-generated content\" \/>\n<meta name=\"parsely-author\" content=\"melindapham\" \/>","tracker_url":"https:\/\/cdn.parsely.com\/keys\/cloudinary.com\/p.js"},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1787856761\/Web_Assets\/blog\/Blog_No-Code_Content_Moderation_for_User-Generated_Images\/Blog_No-Code_Content_Moderation_for_User-Generated_Images.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/40359","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=40359"}],"version-history":[{"count":3,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/40359\/revisions"}],"predecessor-version":[{"id":40396,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/40359\/revisions\/40396"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/40393"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=40359"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=40359"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=40359"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}