<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aldin Kozica</title>
    <description>The latest articles on DEV Community by Aldin Kozica (@dinall).</description>
    <link>https://dev.to/dinall</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3880600%2Ff7e41ef8-6368-47bc-adf7-81c74cf0b1f5.png</url>
      <title>DEV Community: Aldin Kozica</title>
      <link>https://dev.to/dinall</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dinall"/>
    <language>en</language>
    <item>
      <title>I built a WordPress plugin that turns any post title into a featured image (one click, no Canva)</title>
      <dc:creator>Aldin Kozica</dc:creator>
      <pubDate>Tue, 26 May 2026 21:43:53 +0000</pubDate>
      <link>https://dev.to/dinall/i-built-a-wordpress-plugin-that-turns-any-post-title-into-a-featured-image-one-click-no-canva-3na1</link>
      <guid>https://dev.to/dinall/i-built-a-wordpress-plugin-that-turns-any-post-title-into-a-featured-image-one-click-no-canva-3na1</guid>
      <description>&lt;p&gt;Every WordPress site I've worked on has the same friction: you write the post, you stare at the empty Featured Image slot, you go to Canva, design something, export it, upload it, set it. Then publish.&lt;/p&gt;

&lt;p&gt;I got tired of that loop, so I built a plugin that does the whole "design + upload + set as featured" step with a single button.&lt;/p&gt;

&lt;p&gt;This post is how it works under the hood — the small Gutenberg sidebar, the REST endpoint that sideloads to the Media Library, the brand-styled button. If you just want to install it, the link is at the bottom.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;The plugin adds a panel to the block editor sidebar with Style, Category, and a Generate button. One click sends the post title to the &lt;a href="https://thumbapi.dev" rel="noopener noreferrer"&gt;ThumbAPI&lt;/a&gt; &lt;code&gt;/v1/generate&lt;/code&gt; endpoint, downloads the returned image, and sets it as the post's featured image — all without leaving the editor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx10ty4bzx52fn6ms1uc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsx10ty4bzx52fn6ms1uc.png" alt="ThumbAPI panel in the WordPress block editor sidebar" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The settings page is the same minimal pattern — paste an API key, pick a default style. Nothing else.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0d9rtw583yb4uf01cbpx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0d9rtw583yb4uf01cbpx.png" alt="ThumbAPI plugin settings page in WordPress admin" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The plugin is three pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;strong&gt;REST endpoint&lt;/strong&gt; registered under &lt;code&gt;thumbapi/v1/generate-featured-image&lt;/code&gt; that takes a &lt;code&gt;post_id&lt;/code&gt; and orchestrates the whole flow server-side.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Gutenberg sidebar panel&lt;/strong&gt; built with &lt;code&gt;wp.element.createElement&lt;/code&gt; (no JSX, no build step) that calls that endpoint.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;classic editor fallback&lt;/strong&gt; because plenty of WP installs still use it. Same flow, jQuery DOM injection below the Featured Image meta box.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The interesting part is the server side — translating an AI-generated image from a third-party API into a proper WordPress Media Library attachment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering the REST endpoint
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;register_rest_route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi/v1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/generate-featured-image'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'methods'&lt;/span&gt;             &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;WP_REST_Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CREATABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'callback'&lt;/span&gt;            &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'handle_generate'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'permission_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'permission_check'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;'args'&lt;/span&gt;                &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'post_id'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'required'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'integer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'absint'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'image_style'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_text_field'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'category'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_callback'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'sanitize_text_field'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;permission_callback&lt;/code&gt; is non-negotiable. The WP REST API will reject any unsanitized route at submission time to wordpress.org:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;permission_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kt"&gt;WP_REST_Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$post_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get_param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'post_id'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'edit_post'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi_forbidden'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'You cannot edit this post.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nf"&gt;current_user_can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'upload_files'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi_no_upload'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'You cannot upload files.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi'&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two capabilities, both needed: &lt;code&gt;edit_post&lt;/code&gt; for the specific post ID, and &lt;code&gt;upload_files&lt;/code&gt; because we're about to write to the Media Library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sideloading the generated image
&lt;/h2&gt;

&lt;p&gt;ThumbAPI returns a &lt;code&gt;data:image/webp;base64,...&lt;/code&gt; URI. The job is to decode it, write it to &lt;code&gt;wp-content/uploads/&lt;/code&gt;, register it as a proper attachment so WordPress can generate &lt;code&gt;thumbnail&lt;/code&gt;/&lt;code&gt;medium&lt;/code&gt;/&lt;code&gt;large&lt;/code&gt; sizes from it, and return the attachment ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;sideload_data_uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$data_uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;preg_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'#^data:image/([a-z0-9+.-]+);base64,(.+)$#i'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$data_uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$matches&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi_invalid_data_uri'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Invalid image.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$mime_subtype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$decoded&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;base64_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$matches&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$decoded&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi_decode_failed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Could not decode image.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="no"&gt;ABSPATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'wp-admin/includes/file.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="no"&gt;ABSPATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'wp-admin/includes/image.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;require_once&lt;/span&gt; &lt;span class="no"&gt;ABSPATH&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'wp-admin/includes/media.php'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nv"&gt;$uploads&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_upload_dir&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$ext&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'webp'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// simplified&lt;/span&gt;
    &lt;span class="nv"&gt;$unique&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_unique_filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$uploads&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'path'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;sanitize_file_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$ext&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$filepath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;trailingslashit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$uploads&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'path'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$unique&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$decoded&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$attachment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_insert_attachment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'guid'&lt;/span&gt;           &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;trailingslashit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$uploads&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'url'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$unique&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'post_mime_type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'image/'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$mime_subtype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'post_title'&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sanitize_file_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'post_status'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'inherit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;$filepath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wp_generate_attachment_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attachment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$filepath&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;wp_update_attachment_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attachment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$metadata&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$attachment_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last &lt;code&gt;wp_generate_attachment_metadata&lt;/code&gt; call is what makes the image a proper WordPress citizen — WP scans dimensions, generates intermediate sizes, populates &lt;code&gt;srcset&lt;/code&gt; data. Skip it and your &lt;code&gt;&amp;lt;img srcset&amp;gt;&lt;/code&gt; is empty.&lt;/p&gt;

&lt;p&gt;After that, one line to wire it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;set_post_thumbnail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$post_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$attachment_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Gutenberg sidebar (no JSX, no build step)
&lt;/h2&gt;

&lt;p&gt;I deliberately wrote the editor JS without JSX. WordPress plugins that ship a Webpack bundle are an immediate review red flag — wordpress.org reviewers want code they can read in 30 seconds without running a build. &lt;code&gt;wp.element.createElement&lt;/code&gt; is verbose but cheap to ship:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;registerPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registerPlugin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;PluginDocumentSettingPanel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;editPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PluginDocumentSettingPanel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;Dropdown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Dropdown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;apiFetch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiFetch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;i18n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThumbAPIPanel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;faceless&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;select&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;core&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;core/editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCurrentPostId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEditedPostAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;editorDispatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useDispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;core/editor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleGenerate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;](&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;apiFetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/thumbapi/v1/generate-featured-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;post_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;image_style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attachment_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nx"&gt;editorDispatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;editPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;featured_media&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attachment_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;](&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;el&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;PluginDocumentSettingPanel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thumbapi-panel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ThumbAPI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="c1"&gt;// …style + category dropdowns here&lt;/span&gt;
            &lt;span class="nf"&gt;el&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;handleGenerate&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Generating…&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Generate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;registerPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thumbapi-featured-image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ThumbAPIPanel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;)(&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wp&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;apiFetch&lt;/code&gt; automatically attaches the &lt;code&gt;X-WP-Nonce&lt;/code&gt; header, so REST auth Just Works as long as the user is logged in. &lt;code&gt;editorDispatch.editPost({ featured_media: attachmentId })&lt;/code&gt; is the canonical way to update the featured image from JS — the editor reacts immediately, no full reload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Media Library picker for the "with photo" style
&lt;/h2&gt;

&lt;p&gt;ThumbAPI supports a &lt;code&gt;personImage&lt;/code&gt; parameter (base64) for compositing a face or logo into the thumbnail. For WordPress users, the natural source for that is the &lt;strong&gt;Media Library&lt;/strong&gt; — they already have their face/logo there. So instead of forcing them back to a dashboard upload, the plugin uses &lt;code&gt;wp.blockEditor.MediaUpload&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;el&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;wp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockEditor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MediaUpload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;allowedTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;onSelect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;               &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;filesizeInBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filesizeInBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;el&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secondary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Choose image&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user generates, the server reads that attachment back, base64-encodes it, and ships it inline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;encode_attachment_base64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attachment_id&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_attached_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attachment_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;file_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi_attachment_missing_file'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'File missing.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;filesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WP_Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi_attachment_too_large'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'Over 2 MB.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'thumbapi'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$mime&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_post_mime_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$attachment_id&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;'data:'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$mime&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;';base64,'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;base64_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$bytes&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two megabyte cap is enforced both client- and server-side. Client-side gives the user a clear "Over 2 MB" notice before they click Generate; server-side stops anyone who skips the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I had to fight with
&lt;/h2&gt;

&lt;p&gt;A few WordPress.org review gotchas I hit along the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Plugin URI&lt;/code&gt; and &lt;code&gt;Author URI&lt;/code&gt; cannot be the same.&lt;/strong&gt; Set them to different values or omit one. I put &lt;code&gt;Plugin URI: https://thumbapi.dev&lt;/code&gt; and &lt;code&gt;Author URI: https://www.linkedin.com/in/...&lt;/code&gt; to separate them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Text Domain&lt;/code&gt; must match the plugin slug exactly.&lt;/strong&gt; If your plugin is &lt;code&gt;thumbapi-auto-featured-images&lt;/code&gt;, your text domain has to be the same string. Mismatch is a warning at submit time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Domain Path: /languages&lt;/code&gt; requires the folder to exist&lt;/strong&gt; — even if you don't ship translations yet. Easier to omit the header until you do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No bundled libraries.&lt;/strong&gt; jQuery, React, &lt;code&gt;wp.element&lt;/code&gt;, etc. — all from WordPress core. No Webpack output that re-bundles them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST endpoints need real &lt;code&gt;permission_callback&lt;/code&gt;&lt;/strong&gt; that calls &lt;code&gt;current_user_can()&lt;/code&gt;. Returning &lt;code&gt;true&lt;/code&gt; from &lt;code&gt;permission_callback&lt;/code&gt; is an instant rejection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Install it
&lt;/h2&gt;

&lt;p&gt;The plugin is currently in review on the WordPress.org Plugin Directory (slug: &lt;code&gt;thumbapi&lt;/code&gt;). Until it lists, you can install it manually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download: &lt;a href="https://thumbapi.dev/downloads/thumbapi-wordpress.zip" rel="noopener noreferrer"&gt;https://thumbapi.dev/downloads/thumbapi-wordpress.zip&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Full integration page: &lt;a href="https://thumbapi.dev/integrations/wordpress" rel="noopener noreferrer"&gt;https://thumbapi.dev/integrations/wordpress&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need a free ThumbAPI key (5 generations/month on the free tier). After install: Settings → ThumbAPI → paste key → open any post → click Generate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source
&lt;/h2&gt;

&lt;p&gt;The official plugin ships through WordPress.org and the link above. The single-file auto-on-publish reference example (different flow — fires on &lt;code&gt;transition_post_status&lt;/code&gt; instead of a button) lives in the examples repo: &lt;a href="https://github.com/dinalllll/thumbapi-examples/tree/main/wordpress" rel="noopener noreferrer"&gt;github.com/dinalllll/thumbapi-examples/tree/main/wordpress&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you build something on top of this — or hit a bug — I'd love to know.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://thumbapi.dev/integrations/wordpress" rel="noopener noreferrer"&gt;thumbapi.dev/integrations/wordpress&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>php</category>
    </item>
    <item>
      <title>How to Auto-Generate YouTube Thumbnails with Make.com (Step-by-Step)</title>
      <dc:creator>Aldin Kozica</dc:creator>
      <pubDate>Sun, 24 May 2026 20:04:06 +0000</pubDate>
      <link>https://dev.to/dinall/how-to-auto-generate-youtube-thumbnails-with-makecom-step-by-step-1ma7</link>
      <guid>https://dev.to/dinall/how-to-auto-generate-youtube-thumbnails-with-makecom-step-by-step-1ma7</guid>
      <description>&lt;p&gt;Thumbnail creation is the last manual step in most content pipelines. You write the video title, edit the footage, write the description — and then you open Canva or Photoshop and spend 20 minutes designing an image you could have generated in under 30 seconds.&lt;/p&gt;

&lt;p&gt;This guide closes that gap with &lt;a href="https://www.make.com/en/register?pc=thumbapimake" rel="noopener noreferrer"&gt;Make.com&lt;/a&gt; (the visual no-code automation platform, formerly Integromat). By the end you will have a scenario that takes any title and returns a production-ready thumbnail you can save to Google Drive, attach to a Slack message, or set as a WordPress featured image. The whole setup is under 10 minutes if you use the public scenario template.&lt;/p&gt;

&lt;p&gt;If you prefer code-first workflows in n8n, see the &lt;a href="https://thumbapi.dev/blog/auto-generate-youtube-thumbnails-n8n" rel="noopener noreferrer"&gt;n8n version of this guide&lt;/a&gt;. This post is the Make.com-specific deep dive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Are Building
&lt;/h2&gt;

&lt;p&gt;The core scenario is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Receive a trigger (RSS, YouTube watch videos, Google Sheets row, Webhook — anything that produces a title)&lt;/li&gt;
&lt;li&gt;Send the title to ThumbAPI via an HTTP module&lt;/li&gt;
&lt;li&gt;Decode the base64 image response into binary data&lt;/li&gt;
&lt;li&gt;Upload the thumbnail to Google Drive, post to Slack, or push to your CMS&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Make.com handles the entire flow in a visual canvas — no code required beyond a few small formulas in the variable transform step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;a href="https://www.make.com/en/register?pc=thumbapimake" rel="noopener noreferrer"&gt;Make.com account&lt;/a&gt; (free tier covers 1,000 ops/month — more than enough for this scenario)&lt;/li&gt;
&lt;li&gt;A &lt;a href="https://app.thumbapi.dev" rel="noopener noreferrer"&gt;ThumbAPI account&lt;/a&gt; with an API key (free tier: 5 generations/month, no credit card)&lt;/li&gt;
&lt;li&gt;A Google Drive account if you want to use the Drive destination shown in the examples&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Plug-and-Play: Clone the Public Scenario
&lt;/h2&gt;

&lt;p&gt;The fastest path is to clone the ready-made scenario in one click:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://eu1.make.com/public/shared-scenario/QQ4D5gXkxnn/integration-tools-http" rel="noopener noreferrer"&gt;Clone the Make.com scenario template →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The clone drops three modules into your Make account:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Set variable&lt;/strong&gt; — a placeholder trigger holding a sample title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP — Make a request&lt;/strong&gt; — POSTs to the ThumbAPI generate endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set multiple variables&lt;/strong&gt; — strips the base64 data URI prefix and computes a &lt;code&gt;fileName&lt;/code&gt; + &lt;code&gt;mimeType&lt;/code&gt; you can feed into any file module.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;The cloned scenario ships with the public test key &lt;code&gt;thumbapi_test&lt;/code&gt;. That key returns a static placeholder image so you can confirm the wiring works without spending credits. To generate real thumbnails, swap it for your own API key from &lt;a href="https://app.thumbapi.dev" rel="noopener noreferrer"&gt;app.thumbapi.dev&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to understand what each module does (or build it from scratch in your own scenario), the next sections walk through every step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Get Your ThumbAPI Key
&lt;/h2&gt;

&lt;p&gt;Sign up at &lt;a href="https://app.thumbapi.dev" rel="noopener noreferrer"&gt;app.thumbapi.dev&lt;/a&gt; and copy your API key from the dashboard. The free plan includes 5 generations per month — enough to test and build the scenario.&lt;/p&gt;

&lt;p&gt;You will paste this key into the HTTP module's headers in the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create a New Scenario and Add a Trigger
&lt;/h2&gt;

&lt;p&gt;In Make.com, click &lt;strong&gt;Create a new scenario&lt;/strong&gt; (top right of the dashboard). The trigger module determines what kicks off thumbnail generation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;YouTube — Watch Videos&lt;/strong&gt; — fires when you publish a new video.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RSS — Watch RSS Feed Items&lt;/strong&gt; — works for any blog or YouTube channel that exposes a public feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Sheets — Watch Rows&lt;/strong&gt; — for content calendars where each row is one title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook — Custom Webhook&lt;/strong&gt; — universal, your own app POSTs the title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools — Set variable&lt;/strong&gt; — a manual placeholder useful while testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Connect your account, then &lt;strong&gt;Run this module once&lt;/strong&gt; to load sample data. Make uses the sample to populate field mappings in downstream modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add the HTTP Module
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;+&lt;/strong&gt; after your trigger and search for &lt;strong&gt;HTTP&lt;/strong&gt;. Select &lt;strong&gt;Make a request&lt;/strong&gt;. This is Make's built-in module for calling external APIs.&lt;/p&gt;

&lt;p&gt;Configure it as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URL:                 https://api.thumbapi.dev/v1/generate
Method:              POST

Headers (Add item):
  Name:  x-api-key
  Value: YOUR_THUMBAPI_KEY
  Name:  Content-Type
  Value: application/json

Body type:           Raw
Content type:        JSON (application/json)
Request content:     (see below)
Parse response:      Yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;strong&gt;Request content&lt;/strong&gt; field, paste:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{1.title}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"youtube"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imageStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"faceless"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;{{1.title}}&lt;/code&gt; expression pulls the title from the trigger module (module 1 in your scenario). When you click into the field, Make's right-side panel shows available variables — click the &lt;code&gt;title&lt;/code&gt; variable to insert the reference automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Format and Style Options
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Options&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;youtube&lt;/code&gt;, &lt;code&gt;instagram&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;blogpost&lt;/code&gt;, &lt;code&gt;linkedin&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imageStyle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;faceless&lt;/code&gt;, &lt;code&gt;with-image&lt;/code&gt;, &lt;code&gt;with-logo&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;outputFormat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;webp&lt;/code&gt; (default), &lt;code&gt;png&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For YouTube, &lt;code&gt;faceless&lt;/code&gt; generates text-and-graphic designs optimized for click-through. Switch to &lt;code&gt;with-image&lt;/code&gt; if you have uploaded a profile photo in the ThumbAPI dashboard and want your face included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Test the HTTP Module
&lt;/h2&gt;

&lt;p&gt;Right-click the HTTP module and select &lt;strong&gt;Run this module only&lt;/strong&gt;. Make sends the request to ThumbAPI and displays the response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data:image/webp;base64,UklGR..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"youtube"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dimensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;image&lt;/code&gt; field is the full base64-encoded WebP. With &lt;strong&gt;Parse response: Yes&lt;/strong&gt;, Make automatically extracts &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;format&lt;/code&gt;, &lt;code&gt;outputFormat&lt;/code&gt;, and &lt;code&gt;dimensions&lt;/code&gt; as mappable variables for downstream modules.&lt;/p&gt;

&lt;p&gt;If the module fails, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The header name is exactly &lt;code&gt;x-api-key&lt;/code&gt; (not &lt;code&gt;Authorization&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The API key is correct and active in your ThumbAPI dashboard&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;title&lt;/code&gt; field in your request body is not empty&lt;/li&gt;
&lt;li&gt;Body content is valid JSON (Make is strict about quotes and commas)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Decode the Base64 Image
&lt;/h2&gt;

&lt;p&gt;Make's &lt;code&gt;toBinary()&lt;/code&gt; function converts a base64 string into binary data that file modules can consume directly. Add a &lt;strong&gt;Tools → Set multiple variables&lt;/strong&gt; module after the HTTP module with three variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Variable name:  imageData
Variable value: {{toBinary(replace(2.data.image; "data:image/webp;base64,"; ""))}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Variable name:  fileName
Variable value: thumbnail-{{formatDate(now; "X")}}.{{ifempty(2.data.outputFormat; "webp")}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Variable name:  mimeType
Variable value: image/{{ifempty(2.data.outputFormat; "webp")}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;2.data.image&lt;/code&gt; references the &lt;code&gt;image&lt;/code&gt; field from the HTTP response (module 2).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;replace(...; "data:image/webp;base64,"; "")&lt;/code&gt; strips the data URI prefix.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;toBinary(...)&lt;/code&gt; converts the cleaned base64 string into a binary buffer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;formatDate(now; "X")&lt;/code&gt; is a Unix timestamp — prevents filename collisions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ifempty(...)&lt;/code&gt; falls back to &lt;code&gt;webp&lt;/code&gt; if &lt;code&gt;outputFormat&lt;/code&gt; is missing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6: Upload the Thumbnail to Google Drive
&lt;/h2&gt;

&lt;p&gt;Add a &lt;strong&gt;Google Drive — Upload a file&lt;/strong&gt; module. Connect your Google account when prompted, then map the variables from the previous step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Folder:          (pick your destination folder)
File name:       {{3.fileName}}
File data:       {{3.imageData}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you prefer Dropbox, S3, or any other file destination, the pattern is identical — every file module accepts the binary buffer produced by &lt;code&gt;toBinary()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Add a Slack Notification (Optional)
&lt;/h2&gt;

&lt;p&gt;For team workflows, append a &lt;strong&gt;Slack — Send a Message&lt;/strong&gt; module after the upload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Channel:  #thumbnails
Text:     Thumbnail ready for: {{1.title}}
          Drive link: {{4.webViewLink}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your scenario now detects, generates, uploads, and notifies — fully hands-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Multiple Platforms at Once
&lt;/h2&gt;

&lt;p&gt;If you distribute the same content across YouTube, Instagram, and your blog, Make's &lt;strong&gt;Router&lt;/strong&gt; module lets you fan out from a single trigger into parallel paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Trigger]
   ↓
[Router]
   ├── Route 1: HTTP → format: youtube     → Google Drive: YouTube/
   ├── Route 2: HTTP → format: instagram   → Instagram: Create Post
   └── Route 3: HTTP → format: blogpost    → WordPress: Update Featured Image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each route runs independently. If one fails, the others still complete. Add &lt;strong&gt;filters&lt;/strong&gt; to routes to conditionally generate thumbnails based on category, post type, or any field from the trigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Handling for Production Scenarios
&lt;/h2&gt;

&lt;p&gt;Make.com makes error handling straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enable error handlers on the HTTP module.&lt;/strong&gt; Right-click the module and add an error route — typically a Slack alert plus a retry. Make retries the request automatically if you select the "Retry" handler type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set "Allow storing of incomplete executions"&lt;/strong&gt; in the scenario's advanced settings so failed runs don't disappear silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a Sleep module&lt;/strong&gt; between iterations in batch scenarios (1–2 seconds) to avoid hitting the ThumbAPI rate limit when processing many titles in a row.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Batch Processing From Google Sheets
&lt;/h2&gt;

&lt;p&gt;For weekly content calendars, this pattern processes all titles in one Monday morning run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Schedule Trigger]    — every Monday at 9 AM
  ↓
[Google Sheets]       — Search Rows: this week
  ↓
[HTTP]                — POST to ThumbAPI with row title
  ↓
[Set multi variables] — decode base64 → binary
  ↓
[Google Drive]        — Upload thumbnail to weekly folder
  ↓
[Google Sheets]       — Update Row: mark "Thumbnail: Generated"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each row can specify its own &lt;code&gt;format&lt;/code&gt; and &lt;code&gt;imageStyle&lt;/code&gt; columns, so YouTube, blog, and Instagram thumbnails all come from the same weekly run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Make.com for Thumbnail Automation
&lt;/h2&gt;

&lt;p&gt;Make's strengths show up in three places:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visual flow.&lt;/strong&gt; You see the entire pipeline as a connected diagram — easy to explain to non-developers on your team and easy to debug because each module shows its actual data on every run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Router and Iterator modules.&lt;/strong&gt; Fan out into parallel paths or batch-process hundreds of rows from a spreadsheet without writing a loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operations-based pricing.&lt;/strong&gt; Each module run is one operation. The free tier (1,000 ops/month) is enough for ~100 thumbnail generations including all the surrounding modules. Above that, plans start at $9/month for 10k ops.&lt;/p&gt;

&lt;p&gt;If you prefer the code-first model with a Code node for custom JavaScript transforms, the &lt;a href="https://thumbapi.dev/blog/auto-generate-youtube-thumbnails-n8n" rel="noopener noreferrer"&gt;n8n version of this guide&lt;/a&gt; covers the same flow with n8n's terminology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;The fastest path from zero to working Make.com thumbnail automation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://www.make.com/en/register?pc=thumbapimake" rel="noopener noreferrer"&gt;Sign up for Make.com&lt;/a&gt; — free tier, no credit card.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://app.thumbapi.dev" rel="noopener noreferrer"&gt;Sign up for ThumbAPI&lt;/a&gt; — free tier, 5 generations/month.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://eu1.make.com/public/shared-scenario/QQ4D5gXkxnn/integration-tools-http" rel="noopener noreferrer"&gt;Clone the public scenario template&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Swap &lt;code&gt;thumbapi_test&lt;/code&gt; for your real ThumbAPI key in the HTTP module headers.&lt;/li&gt;
&lt;li&gt;Replace the Set variable placeholder with your real trigger (YouTube, RSS, Sheets, Webhook).&lt;/li&gt;
&lt;li&gt;Add your destination module (Google Drive, Slack, WordPress, etc.) and map &lt;code&gt;imageData&lt;/code&gt;, &lt;code&gt;fileName&lt;/code&gt;, &lt;code&gt;mimeType&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total time: 10–15 minutes. Thumbnail generation itself runs in under 25 seconds per request.&lt;/p&gt;

&lt;p&gt;For the canonical Make.com setup with all configuration details, see the &lt;a href="https://thumbapi.dev/integrations/make" rel="noopener noreferrer"&gt;ThumbAPI + Make.com integration guide&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://thumbapi.dev/integrations/make" rel="noopener noreferrer"&gt;thumbapi.dev/integrations/make&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>api</category>
      <category>design</category>
    </item>
    <item>
      <title>How to Auto-Generate YouTube Thumbnails with n8n (Step-by-Step)</title>
      <dc:creator>Aldin Kozica</dc:creator>
      <pubDate>Thu, 21 May 2026 06:54:54 +0000</pubDate>
      <link>https://dev.to/dinall/how-to-auto-generate-youtube-thumbnails-with-n8n-step-by-step-4d8h</link>
      <guid>https://dev.to/dinall/how-to-auto-generate-youtube-thumbnails-with-n8n-step-by-step-4d8h</guid>
      <description>&lt;p&gt;Thumbnail creation is the last manual step in most content pipelines. You write the video title, edit the footage, write the description — and then you open Canva or Photoshop and spend 20 minutes designing an image you could have generated in under 30 seconds.&lt;/p&gt;

&lt;p&gt;This guide shows you how to close that gap entirely. By the end, you will have an n8n workflow that detects new YouTube uploads, sends the video title to ThumbAPI, and saves a production-ready thumbnail to Google Drive automatically. No design software, no manual steps.&lt;/p&gt;

&lt;p&gt;The guide is written for developers and technically comfortable creators. If you want a higher-level overview of content automation, the &lt;a href="https://thumbapi.dev/blog/automating-content-creation" rel="noopener noreferrer"&gt;developer's guide to automating content creation&lt;/a&gt; covers the broader picture. This post is the n8n-specific deep dive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Are Building
&lt;/h2&gt;

&lt;p&gt;The core workflow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect a new YouTube video (via RSS or webhook)&lt;/li&gt;
&lt;li&gt;Extract the video title&lt;/li&gt;
&lt;li&gt;Call ThumbAPI with the title and target format&lt;/li&gt;
&lt;li&gt;Receive a base64-encoded WebP thumbnail&lt;/li&gt;
&lt;li&gt;Upload the thumbnail to Google Drive (or your CMS)&lt;/li&gt;
&lt;li&gt;Optionally notify your team via Slack&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This workflow runs entirely inside n8n. You can self-host it on your own server or use n8n Cloud — the node configuration is identical either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An n8n instance (self-hosted or n8n Cloud)&lt;/li&gt;
&lt;li&gt;A ThumbAPI account with an API key — the free tier is enough to test&lt;/li&gt;
&lt;li&gt;A YouTube channel ID (find it in YouTube Studio under Settings → Channel → Advanced settings)&lt;/li&gt;
&lt;li&gt;A Google Drive account if you want to use the Drive destination in the examples&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Store Your ThumbAPI Key as a Credential
&lt;/h2&gt;

&lt;p&gt;Never put API keys directly into HTTP Request nodes. n8n's credential manager encrypts them and keeps them out of your workflow JSON exports.&lt;/p&gt;

&lt;p&gt;Go to &lt;strong&gt;Settings → Credentials → Add Credential&lt;/strong&gt; and choose &lt;strong&gt;Header Auth&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name:         ThumbAPI
Header Name:  x-api-key
Header Value: YOUR_THUMBAPI_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the credential. You will select it in every HTTP Request node that talks to ThumbAPI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why &lt;code&gt;x-api-key&lt;/code&gt;? ThumbAPI authenticates via the &lt;code&gt;x-api-key&lt;/code&gt; header, not a Bearer token. Make sure the header name matches exactly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2: Create the Workflow and Add a Trigger
&lt;/h2&gt;

&lt;p&gt;Open n8n and click &lt;strong&gt;New Workflow&lt;/strong&gt;. The trigger node determines what kicks off thumbnail generation. The two most useful options for YouTube content are:&lt;/p&gt;

&lt;h3&gt;
  
  
  Option A: RSS Feed Trigger (Recommended for YouTube)
&lt;/h3&gt;

&lt;p&gt;YouTube publishes an RSS feed for every channel. The &lt;strong&gt;RSS Feed Read&lt;/strong&gt; node polls it on a schedule and fires when a new video appears.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node:          RSS Feed Read
Feed URL:      https://www.youtube.com/feeds/videos.xml?channel_id=YOUR_CHANNEL_ID
Poll Every:    15 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;YOUR_CHANNEL_ID&lt;/code&gt; with your actual channel ID. n8n will pass the video title as &lt;code&gt;$json.title&lt;/code&gt; to downstream nodes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Webhook Trigger (For CMS or Custom Pipelines)
&lt;/h3&gt;

&lt;p&gt;If you publish video metadata to a CMS before uploading to YouTube, use a &lt;strong&gt;Webhook&lt;/strong&gt; trigger instead. Your CMS fires a POST request to the n8n webhook URL when content is ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node:     Webhook
Method:   POST
Path:     /new-video
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The webhook payload should include at minimum a &lt;code&gt;title&lt;/code&gt; field. Everything else is optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Add the ThumbAPI HTTP Request Node
&lt;/h2&gt;

&lt;p&gt;Click &lt;strong&gt;+&lt;/strong&gt; after your trigger and add an &lt;strong&gt;HTTP Request&lt;/strong&gt; node. This is the node that calls ThumbAPI to generate the thumbnail.&lt;/p&gt;

&lt;p&gt;Configure it as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Method:              POST
URL:                 https://api.thumbapi.dev/v1/generate
Authentication:      Generic Credential Type → Header Auth
Credential:          ThumbAPI (the one you created in Step 1)
Send Body:           true
Body Content Type:   JSON
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the JSON body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.title }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"youtube"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imageStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"faceless"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;{{ $json.title }}&lt;/code&gt; expression pulls the video title from the trigger output. If your trigger uses a different field name, adjust the expression accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Format and Style Options
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Options&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;youtube&lt;/code&gt;, &lt;code&gt;instagram&lt;/code&gt;, &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;blogpost&lt;/code&gt;, &lt;code&gt;linkedin&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;imageStyle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;faceless&lt;/code&gt;, &lt;code&gt;with-image&lt;/code&gt;, &lt;code&gt;with-logo&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;outputFormat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;webp&lt;/code&gt; (default), &lt;code&gt;png&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For YouTube, &lt;code&gt;faceless&lt;/code&gt; generates text-and-graphic designs optimized for click-through. If you have uploaded a profile photo to the ThumbAPI dashboard, switch to &lt;code&gt;with-image&lt;/code&gt; to include your face in every generated thumbnail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Test the HTTP Request Node
&lt;/h2&gt;

&lt;p&gt;Before building out the rest of the workflow, test this node in isolation. Click &lt;strong&gt;Execute Node&lt;/strong&gt;. If the request succeeds, you will see the response in n8n's output panel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data:image/webp;base64,UklGR..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"youtube"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"outputFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dimensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;720&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;image&lt;/code&gt; field is the full base64-encoded WebP. A typical YouTube thumbnail is 50–150 KB encoded, which n8n handles without issues.&lt;/p&gt;

&lt;p&gt;If the node fails, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The credential header name is exactly &lt;code&gt;x-api-key&lt;/code&gt; (not &lt;code&gt;Authorization&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The API key is correct and active in your ThumbAPI dashboard&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;title&lt;/code&gt; field in your request body is not empty&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5: Decode the Image and Upload to Google Drive
&lt;/h2&gt;

&lt;p&gt;The base64 string needs to be converted into n8n binary data before you can upload it. Add a &lt;strong&gt;Code&lt;/strong&gt; node between the HTTP Request node and your destination:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base64String&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base64Data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base64String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;base64String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base64String&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputFormat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outputFormat&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base64Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`thumbnail-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`image/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;outputFormat&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This blogpost image is generated by ThumbAPI&lt;/p&gt;

</description>
      <category>automation</category>
      <category>youtube</category>
      <category>api</category>
      <category>n8n</category>
    </item>
    <item>
      <title>Stop Hardcoding Templates: How I Feed a Live 3x2 Inspiration Grid into Gemini Flash</title>
      <dc:creator>Aldin Kozica</dc:creator>
      <pubDate>Fri, 15 May 2026 19:48:28 +0000</pubDate>
      <link>https://dev.to/dinall/stop-hardcoding-templates-how-i-feed-a-live-3x2-inspiration-grid-into-gemini-flash-4b5e</link>
      <guid>https://dev.to/dinall/stop-hardcoding-templates-how-i-feed-a-live-3x2-inspiration-grid-into-gemini-flash-4b5e</guid>
      <description>&lt;p&gt;Every developer building a tech blog, open-source documentation site, or SaaS product hits the same annoying roadblock: &lt;strong&gt;Open Graph (OG) images.&lt;/strong&gt; When you share your project on Twitter/X, LinkedIn, or dev.to, a generic background with text gets ignored. But spending 15 minutes in Canva for every single release or article is a massive productivity killer. &lt;/p&gt;

&lt;p&gt;I wanted to completely automate this, but static, hardcoded templates are boring. Instead, I built a backend pipeline that looks at &lt;strong&gt;what is currently trending live&lt;/strong&gt;, builds a single 3x2 visual inspiration grid from those trends, and feeds that image into &lt;strong&gt;Gemini Flash&lt;/strong&gt; to generate a brand new, context-aware OG asset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best part? It adapts to shifting design trends completely on autopilot, with ZERO room for AI hallucinations.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcba3fdpe2c2o7tmc3dtx.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcba3fdpe2c2o7tmc3dtx.webp" alt="A conceptual architecture diagram showing a 3x2 grid of scraped developer images acting as a visual guardrail for an AI pipeline" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Conceptual Architecture: Zero Room for Hallucinations 🛡️
&lt;/h2&gt;

&lt;p&gt;The biggest issue with using GenAI for visual production is predictability. If you give an LLM too much freedom, it will hallucinate weird layouts, bad fonts, or completely off-brand designs. &lt;/p&gt;

&lt;p&gt;To fix this, my pipeline doesn't let the AI "think" from scratch. It builds a strict visual and contextual cage around it. Here is how the execution flow looks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger:&lt;/strong&gt; New Post or Git Push detected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 1:&lt;/strong&gt; Scrape Live Trend Images using Node.js on a Hetzner VPS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 2:&lt;/strong&gt; Compile those images into a single 3x2 Grid Image (The Visual Guardrail).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step 3:&lt;/strong&gt; Send the compiled Grid + Strict Title to the Gemini Flash API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Result:&lt;/strong&gt; A deterministic, on-trend 1200x630 OG Image is generated.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. The Live Trend Fetch
&lt;/h3&gt;

&lt;p&gt;When a new post or release is detected, the backend quickly scrapes the top-performing visual assets under that specific tech niche. &lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Grid Compilation
&lt;/h3&gt;

&lt;p&gt;The system takes those top 6 live image results and programmatically compiles them into a single 3x2 image grid buffer. &lt;strong&gt;This grid acts as our visual guardrail.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Multimodal Constraint
&lt;/h3&gt;

&lt;p&gt;We send this single grid image directly to &lt;strong&gt;Gemini Flash&lt;/strong&gt; alongside the exact title of the new post. &lt;/p&gt;

&lt;p&gt;Because Gemini Flash receives a concrete visual sample (the grid) and a literal text string (the title), &lt;strong&gt;there is absolutely no room for it to invent custom nonsense or hallucinate.&lt;/strong&gt; It is forced to morph the existing design patterns it sees in the grid with the exact input parameters provided to the underlying generation engine—which I abstracted into a dedicated infrastructure tool called &lt;a href="https://thumbapi.dev/" rel="noopener noreferrer"&gt;ThumbAPI&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcg5roawkgwkmcs435tz2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcg5roawkgwkmcs435tz2.webp" alt="A high-level overview of multimodal prompt logic analyzing layout structures and contrast alignment from an image input" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Prompt Logic: Turning Inspiration into Assets 🧠
&lt;/h2&gt;

&lt;p&gt;Since the model is multimodal, you don't need to write complex image-processing algorithms. You just need to guide the AI's "designer eye" to extract patterns from the grid rather than creating something out of thin air:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual Pattern Extraction:&lt;/strong&gt; The model scans the 3x2 grid to isolate the dominant layout structures (e.g., whether the community is currently leaning toward minimalist code blocks, dark mode neon gradients, or abstract geometric shapes).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contrast Alignment:&lt;/strong&gt; It determines how to place your specific text inside that exact structure so it pops inside the current active feed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Title Integration:&lt;/strong&gt; It maps the new post title into the calculated visual framework and outputs the final deployment-ready 1200x630 WebP image.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why This Pipeline Wins
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autopilot Relevancy:&lt;/strong&gt; The images aren't random or stuck in the past. If the dev community suddenly shifts its aesthetic preferences, the scraper catches it, the grid changes, and the AI automatically matches the current vibe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure Efficiency:&lt;/strong&gt; By hosting the scraper and grid compiler on a low-cost Hetzner VPS and pairing it with Gemini Flash's speed, running this production pipeline costs next to nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Canva Required:&lt;/strong&gt; The pipeline finishes in seconds, updating the CMS or repository automatically right after a git push.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Let's Discuss: How do you handle your project assets? 🚀
&lt;/h2&gt;

&lt;p&gt;Moving the inspiration and rendering pipeline completely to a programmatic, image-to-image AI workflow has completely changed how I ship content. It bridges the gap between pure code and marketing design without relying on unpredictable prompt engineering.&lt;/p&gt;

&lt;p&gt;I’d love to get your thoughts in the comments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How are you currently generating OG images or headers for your side projects? Do you stick to static code-generated templates, or do you still build them manually?&lt;/li&gt;
&lt;li&gt;Have you experimented with using multimodal image inputs as strict guardrails to stop AI hallucinations?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drop a comment below if you want to know more about the n8n integration, or feel free to check out &lt;a href="https://thumbapi.dev/" rel="noopener noreferrer"&gt;ThumbAPI&lt;/a&gt; if you want to test the programmatic asset generation logic yourself!&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;P.S.&lt;/strong&gt; All visual materials and image grids shown in this post were generated programmatically using &lt;a href="https://thumbapi.dev/" rel="noopener noreferrer"&gt;ThumbAPI&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>automation</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I was spending 2 hours a week making thumbnails. Here's the n8n workflow that fixed it.</title>
      <dc:creator>Aldin Kozica</dc:creator>
      <pubDate>Wed, 13 May 2026 17:22:30 +0000</pubDate>
      <link>https://dev.to/dinall/i-was-spending-2-hours-a-week-making-thumbnails-heres-the-n8n-workflow-that-fixed-it-8ma</link>
      <guid>https://dev.to/dinall/i-was-spending-2-hours-a-week-making-thumbnails-heres-the-n8n-workflow-that-fixed-it-8ma</guid>
      <description>&lt;p&gt;I run a small content automation pipeline. RSS feeds, AI summaries, auto-publishing — the usual stuff.&lt;/p&gt;

&lt;p&gt;Everything was automated &lt;em&gt;except&lt;/em&gt; thumbnails.&lt;/p&gt;

&lt;p&gt;Every time a new blog post went live, I'd open Canva, drag some text around, pick a background, export it, upload it. 10–15 minutes per post. Multiply that by 8–10 posts a week and you get the picture.&lt;/p&gt;

&lt;p&gt;It wasn't hard work. It was &lt;em&gt;boring&lt;/em&gt; work. The worst kind.&lt;/p&gt;

&lt;p&gt;So I spent a weekend building a workflow that does it for me. Here's exactly how it works.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;My publishing pipeline already ran on n8n. When a new post goes live on my blog, a webhook fires and kicks off a chain: notify newsletter, post to Twitter, update Notion. &lt;/p&gt;

&lt;p&gt;Adding thumbnail generation was just one more node.&lt;/p&gt;

&lt;p&gt;The idea: &lt;strong&gt;when a post publishes → generate a thumbnail → upload it back to the CMS.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The HTTP node that does the work
&lt;/h2&gt;

&lt;p&gt;I used &lt;a href="https://thumbapi.dev" rel="noopener noreferrer"&gt;ThumbAPI&lt;/a&gt; — a REST API that takes a title and returns a ready-made thumbnail image. One POST request, one image back as base64.&lt;/p&gt;

&lt;p&gt;Here's the exact node config in n8n:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Method:&lt;/strong&gt; POST&lt;br&gt;&lt;br&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;code&gt;https://api.thumbapi.dev/v1/generate&lt;/code&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Authentication:&lt;/strong&gt; Header — &lt;code&gt;Authorization: Bearer YOUR_API_KEY&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Body (JSON):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{ $json.post_title }}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blogpost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"imageStyle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"faceless"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The &lt;code&gt;$json.post_title&lt;/code&gt; pulls the title from the previous node in the workflow — whatever just triggered the webhook.&lt;/p&gt;




&lt;h2&gt;
  
  
  What comes back
&lt;/h2&gt;

&lt;p&gt;The response looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data:image/webp;base64,/9j/4AAQSkZJRgAB..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blogpost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dimensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"height"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A base64 WebP image, correct dimensions for blog OG images (1200×630), ready to use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Connecting it to my CMS
&lt;/h2&gt;

&lt;p&gt;Next node: an HTTP request to my CMS API to upload the image and attach it to the post. The exact steps depend on your CMS — I'm on a custom setup — but if you're on WordPress, Ghost, or anything with a REST API, it's the same pattern: decode base64, POST to your media endpoint, get back an image ID, attach to the post.&lt;/p&gt;

&lt;p&gt;In n8n this is just two more nodes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Code node&lt;/strong&gt; — strip the &lt;code&gt;data:image/webp;base64,&lt;/code&gt; prefix and convert to binary&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP node&lt;/strong&gt; — POST to your CMS media endpoint&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The full workflow in plain English
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Blog post published (Webhook)
  → Generate thumbnail (HTTP POST to ThumbAPI)
  → Convert base64 to binary (Code node)
  → Upload to CMS (HTTP POST to media endpoint)
  → Attach thumbnail to post (HTTP PATCH)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five nodes. Runs in about 25 seconds. Zero Canva.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;faceless&lt;/code&gt; style is great for blogs but if you're doing YouTube thumbnails and want your face on them, ThumbAPI supports uploading a reference photo and reusing it across generations. I haven't set that up yet but it's next.&lt;/p&gt;

&lt;p&gt;Also: error handling. Right now if the API call fails, the workflow just stops. I need to add a fallback node that pings me on Slack so I can manually handle it. Classic "it works, ship it" problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Is it worth it?
&lt;/h2&gt;

&lt;p&gt;I've now run this for about 6 weeks. Zero manual thumbnails since. The images aren't as polished as what I'd make in Canva on a good day, but they're consistent, on-brand, and they exist — which is more than I can say for the weeks I'd skip thumbnails because I didn't have time.&lt;/p&gt;

&lt;p&gt;If you have any automation pipeline that produces content regularly, this is a low-effort, high-payoff addition.&lt;/p&gt;

&lt;p&gt;Happy to share the full n8n workflow JSON if anyone wants it — drop a comment.&lt;br&gt;
PS:This cover blogpost image is generated by &lt;a href="https://thumbapi.dev" rel="noopener noreferrer"&gt;ThumbAPI&lt;/a&gt;.&lt;/p&gt;




</description>
      <category>api</category>
      <category>automation</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Thumbapi.dev solution i made to automate content creation workflows + API</title>
      <dc:creator>Aldin Kozica</dc:creator>
      <pubDate>Tue, 12 May 2026 16:56:36 +0000</pubDate>
      <link>https://dev.to/dinall/thumbapidev-solution-i-made-to-automate-content-creation-workflows-api-5086</link>
      <guid>https://dev.to/dinall/thumbapidev-solution-i-made-to-automate-content-creation-workflows-api-5086</guid>
      <description>&lt;p&gt;I've been having a lot of problems wiht the social media and my marketing. Creating a social post,video or blogpost took me so much time and energy. &lt;br&gt;
I wanted to automate best i could of this proces so i started &lt;a href="https://thumbapi.dev" rel="noopener noreferrer"&gt;https://thumbapi.dev&lt;/a&gt; that actualy creates me visuals instantly for any kind of social posts.&lt;br&gt;
There is couple of features:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Faceless visuals&lt;/li&gt;
&lt;li&gt;With personal photo&lt;/li&gt;
&lt;li&gt;With branded logo&lt;/li&gt;
&lt;li&gt;With custom dataset
So i could basicaly automate all platform in one place without wasting alot of time with finding inspiration for covers/thumbnails.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;1) This image is example of faceless blogpost visual + branded logo + title: "Built a tool that social visuals from a title automatically"&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8bm46w4cef6ic8wddqef.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8bm46w4cef6ic8wddqef.webp" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;2) Another example is youtube thumbnail + personal image + title : "Built a tool that social visuals from a title automatically"&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwc0gu72zbh0y2y7mi6nb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwc0gu72zbh0y2y7mi6nb.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;3) Last example is faceless blogpost + no brand / no photo + title "Built a tool that social visuals from a title automatically"&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwbrpw8tlrtic78qs3op.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkwbrpw8tlrtic78qs3op.webp" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is also output image format WEBP or PNG.&lt;br&gt;
WEBP for blogpost and best load optimization and PNG for youtube better quality thumbnails.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>automaton</category>
      <category>api</category>
    </item>
    <item>
      <title>Dealing with the first user feedback: The good, the bad, and the Brave Browser</title>
      <dc:creator>Aldin Kozica</dc:creator>
      <pubDate>Wed, 22 Apr 2026 07:44:42 +0000</pubDate>
      <link>https://dev.to/dinall/dealing-with-the-first-user-feedback-the-good-the-bad-and-the-brave-browser-l4p</link>
      <guid>https://dev.to/dinall/dealing-with-the-first-user-feedback-the-good-the-bad-and-the-brave-browser-l4p</guid>
      <description>&lt;p&gt;After 3 months of diving into SaaS discovery, marketing, and content creation—things that often make me feel uncomfortable as a developer—I finally got a sign that gave me a huge push.&lt;/p&gt;

&lt;p&gt;There’s nothing quite like getting that first email from a stranger using your tool. I recently launched &lt;strong&gt;YouThumb Tester&lt;/strong&gt;, and the first piece of feedback just landed in my inbox.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3fzxo92350mvuyshhbm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fp3fzxo92350mvuyshhbm.png" alt="Screenshot of an email from a user providing feedback about YouThumb Tester, mentioning a bug on Brave Browser and a missing disable button, while expressing they love the extension." width="800" height="135"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Reality Check
&lt;/h3&gt;

&lt;p&gt;Building in a silo is one thing, but real-world usage always reveals the cracks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Bug:&lt;/strong&gt; It seems like there's a caching or injection issue on &lt;strong&gt;Brave Browser&lt;/strong&gt;. The thumbnail doesn't always show up instantly without multiple reloads. Brave's aggressive shields might be playing a role here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The UX Flaw:&lt;/strong&gt; I completely missed a "clear/disable" button. Once the user is done testing, the preview persists in their feed. That's a friction point I need to kill immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Extension Struggle: Waiting for Approval ⏳
&lt;/h3&gt;

&lt;p&gt;Here is the kicker for anyone who isn't a browser extension dev: &lt;strong&gt;The fix isn't instant.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike a standard web app where I can just push a hotfix to the server and it's live for everyone in seconds, the Chrome Web Store (and other stores) has a manual review process. &lt;/p&gt;

&lt;p&gt;Even if I fix this bug in 10 minutes, I have to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bundle the new version.&lt;/li&gt;
&lt;li&gt;Submit it for review.&lt;/li&gt;
&lt;li&gt;Wait anywhere from 24 hours to several days for it to be approved.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It’s a strange feeling knowing there is a bug out there and having the solution ready, but being forced to wait behind a "Pending Review" status.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Win 🚀
&lt;/h3&gt;

&lt;p&gt;Despite the technical hiccups, the user mentioned they &lt;strong&gt;"really love the extension"&lt;/strong&gt; and specifically the advice it provides during the test. This is the ultimate fuel for any solo dev. It validates that the core value proposition is there, even if the implementation needs polish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next Steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Brave Debugging:&lt;/strong&gt; Investigating DOM injection issues specifically for Chromium-based browsers.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The "Reset" Button:&lt;/strong&gt; Adding a clear "Reset to Original" button in the popup.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Better State Management:&lt;/strong&gt; Improving logic so the preview doesn't "ghost" after the user's session.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;Technical note:&lt;/strong&gt; I tried to move as much logic as possible to my Node.js backend to keep the extension "thin," but since the issue is with DOM injection and Brave's shields, the fix still has to go through the content-script. I'm currently looking into implementing a Remote Config strategy to update selectors and UI flags via API, so I don't have to wait for a store review every time a CSS class changes.&lt;/p&gt;

&lt;p&gt;Check out the project here: &lt;a href="https://youthumb.online/" rel="noopener noreferrer"&gt;YouThumb.online&lt;/a&gt;&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>webdev</category>
      <category>saas</category>
      <category>showdev</category>
    </item>
    <item>
      <title>From 0 to 50 Users in 2 Months: Building a Chrome Extension with React, Node.js, and Gemini Nano</title>
      <dc:creator>Aldin Kozica</dc:creator>
      <pubDate>Wed, 15 Apr 2026 18:24:40 +0000</pubDate>
      <link>https://dev.to/dinall/from-0-to-50-users-in-2-months-building-a-chrome-extension-with-react-nodejs-and-gemini-nano-4l8g</link>
      <guid>https://dev.to/dinall/from-0-to-50-users-in-2-months-building-a-chrome-extension-with-react-nodejs-and-gemini-nano-4l8g</guid>
      <description>&lt;p&gt;Building a browser extension sounds simple until you actually try to scale it. Two months ago, I launched YouThumb a tool that help creators with the thumbnails to optimize CTR with AI, and while 50 users might seem small to some, the technical and marketing journey to get there was a massive learning curve.&lt;/p&gt;

&lt;p&gt;Here is the exact stack I used and what I’ve learned about launching a dev-tool.&lt;/p&gt;

&lt;p&gt;The Architecture (The "Banana" Stack 🍌)&lt;br&gt;
I wanted a setup that was both scalable and cost-effective. Here’s how it looks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt; (Extension): Built with React. It handles the UI for thumbnail comparisons and the overlay logic on the YouTube DOM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend&lt;/strong&gt;: A Node.js API hosted on a Hetzner VPS. I chose Hetzner for the high performance-to-cost ratio, which is crucial when you're a solo founder keeping margins high.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database &amp;amp; Auth&lt;/strong&gt;: Firebase. It’s still the king for quick implementation of Google Auth and real-time data syncing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI Brain&lt;/strong&gt;: I’m using Gemini Nano for on-device analysis. It allows for fast, multimodal processing without the heavy latency (or cost) of constantly hitting a cloud API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Landing Page&lt;/strong&gt;: Built with Next.js for that sweet SEO and fast load times.&lt;/p&gt;

&lt;p&gt;Check out the landing page here: [&lt;a href="https://youthumb.online/" rel="noopener noreferrer"&gt;https://youthumb.online/&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;The 50-User Milestone: What Worked?&lt;br&gt;
Getting those first 50 users wasn't about "build it and they will come." It was about being where the creators are.&lt;/p&gt;

&lt;p&gt;Building in Public: Documenting the OAUTH struggles and the "Featured" badge win on social media.&lt;/p&gt;

&lt;p&gt;Dogfooding: Using the tool for my own YouTube channel and showing the results.&lt;/p&gt;

&lt;p&gt;The "Featured" Badge: This was a game-changer for credibility in the Chrome Web Store.&lt;/p&gt;

&lt;p&gt;The Biggest Challenge: The "Manifest.json" &amp;amp; OAUTH Trap&lt;br&gt;
If you’ve worked with Manifest V3, you know the pain. My biggest nightmare was aligning Firebase Auth with Chrome’s identity API while moving the backend to a dedicated VPS. It’s a delicate dance of redirect URIs and background service worker lifecycles.&lt;br&gt;
Also in addition a feature badge on chrome store to build trust and authority.&lt;/p&gt;

&lt;p&gt;I need your advice: How to scale from 50 to 500?&lt;br&gt;
Now that the technical foundation is solid and the first 50 users are providing feedback, I’m looking at the next phase.&lt;/p&gt;

&lt;p&gt;My question to the Dev.to community:&lt;br&gt;
How do you approach marketing for a browser extension? Should I focus on SEO for the landing page &lt;a href="https://youthumb.online/" rel="noopener noreferrer"&gt;YouThumb&lt;/a&gt;, or double down on niche communities like Reddit, Discord or Youtube? If you’ve launched an extension, what was your "inflection point" for growth?&lt;/p&gt;

&lt;p&gt;Let’s discuss in the comments!&lt;/p&gt;

&lt;h1&gt;
  
  
  webdev #showdev #javascript #ai #node #buildinpublic #chromextension #discuss
&lt;/h1&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
      <category>buildinpublic</category>
      <category>discuss</category>
    </item>
  </channel>
</rss>
