 {"id":519649,"date":"2025-07-03T13:48:43","date_gmt":"2025-07-03T20:48:43","guid":{"rendered":"https:\/\/jorgep.com\/blog\/?p=519649"},"modified":"2025-12-29T19:40:43","modified_gmt":"2025-12-30T02:40:43","slug":"build-blog-blog-image-generator-workflow","status":"publish","type":"post","link":"https:\/\/jorgep.com\/blog\/build-blog-blog-image-generator-workflow\/","title":{"rendered":"Build Blog: Blog Image Generator Workflow"},"content":{"rendered":"<style>.wp-block-kadence-advancedheading.kt-adv-heading519190_4a1b6f-84, .wp-block-kadence-advancedheading.kt-adv-heading519190_4a1b6f-84[data-kb-block=\"kb-adv-heading519190_4a1b6f-84\"]{font-size:var(--global-kb-font-size-sm, 0.9rem);font-style:normal;}.wp-block-kadence-advancedheading.kt-adv-heading519190_4a1b6f-84 mark.kt-highlight, .wp-block-kadence-advancedheading.kt-adv-heading519190_4a1b6f-84[data-kb-block=\"kb-adv-heading519190_4a1b6f-84\"] mark.kt-highlight{font-style:normal;color:#f76a0c;-webkit-box-decoration-break:clone;box-decoration-break:clone;padding-top:0px;padding-right:0px;padding-bottom:0px;padding-left:0px;}.wp-block-kadence-advancedheading.kt-adv-heading519190_4a1b6f-84 img.kb-inline-image, .wp-block-kadence-advancedheading.kt-adv-heading519190_4a1b6f-84[data-kb-block=\"kb-adv-heading519190_4a1b6f-84\"] img.kb-inline-image{width:150px;vertical-align:baseline;}<\/style>\n<p class=\"kt-adv-heading519190_4a1b6f-84 wp-block-kadence-advancedheading\" data-kb-block=\"kb-adv-heading519190_4a1b6f-84\">AI Disclaimer I love exploring new technology, and that includes using AI to help with research and editing! My digital &#8220;team&#8221; includes tools like Google Gemini, Notebook LM, Microsoft Copilot, Perplexity.ai, Claude.ai, and others as needed. They help me gather insights and polish content\u2014so you get the best, most up-to-date information possible.<\/p>\n\n\n\n<p><strong>Author:<\/strong> Jorge Pereira<\/p>\n\n\n\n<p><strong>Date:<\/strong> June 2025<\/p>\n\n\n\n<p><strong>Tech Stack:<\/strong> n8n (Docker), Ollama, Open WebUI, JavaScript\/HTML, Windows 11<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>The goal is simple: Create a web-based tool where you enter a blog post URL, and the system scrapes the title, generates an AI image, resizes it to a 1024&#215;512 pixels (2:1 rati)o, and displays it back to you instantly.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">1. The Full Architecture<\/h3>\n\n\n\n<p><\/p>\n\n\n\n<p>The process is a closed loop: It starts at the user&#8217;s browser, travels through n8n&#8217;s scraping and AI layers, and returns directly to the browser as a visual image.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p><strong>HTML Form:<\/strong> User inputs a URL and clicks &#8220;Generate.&#8221;<\/p>\n\n\n\n<p><strong>Webhook:<\/strong> n8n receives the URL and holds the connection open.<\/p>\n\n\n\n<p><strong>HTTP Request:<\/strong> n8n visits the URL to download the raw HTML code.<\/p>\n\n\n\n<p><strong>HTML Scraper:<\/strong> n8n extracts the <code>&lt;h1&gt;<\/code> title from that code.<\/p>\n\n\n\n<p><strong>AI Image Gen:<\/strong> A prompt is built using that title to create a relevant image.<\/p>\n\n\n\n<p><strong>Image Editor:<\/strong> The AI result is cropped and resized to exactly <strong>1024&#215;512<\/strong>.<\/p>\n\n\n\n<p><strong>Webhook Response:<\/strong> The final <code>.png<\/code> is sent back to the browser.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"233\" src=\"https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-147-1024x233.png\" alt=\"\" class=\"wp-image-519651\" srcset=\"https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-147-1024x233.png 1024w, https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-147-300x68.png 300w, https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-147-768x175.png 768w, https:\/\/jorgep.com\/blog\/wp-content\/uploads\/image-147.png 1179w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">2. The n8n Workflow Configuration<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Node 1: Webhook Trigger (The Gateway)<\/strong><\/h4>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>HTTP Method:<\/strong> <code>POST<\/code><\/li>\n\n\n\n<li><strong>Path:<\/strong> <code>GenerateBlogPostImage<\/code><\/li>\n\n\n\n<li><strong>Response Mode:<\/strong> Set to <strong>&#8220;Using &#8216;Respond to Webhook&#8217; Node&#8221;<\/strong>.<em>Crucial: Without this setting, the browser won&#8217;t wait for the AI to finish.<\/em><\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Node 2 &amp; 3: The Scraper (HTTP Request &amp; HTML Node)<\/strong><\/h4>\n\n\n\n<p>You can&#8217;t scrape a title without first &#8220;fetching&#8221; the site code.<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li><strong>HTTP Request:<\/strong> Use <code>GET<\/code> and set the URL to <code>{{ $json.body.url }}<\/code>. Set <strong>Response Format<\/strong> to <code>Text<\/code>.<\/li>\n\n\n\n<li><strong>HTML Node:<\/strong> Set <strong>Source Data<\/strong> to <code>JSON<\/code> and <strong>JSON Property<\/strong> to <code>data<\/code>. Use a CSS Selector like <code>h1<\/code> to grab the title.<\/li>\n<\/ol>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Node 4: Image Editing (The Sizer &amp; Namer)<\/strong><\/h4>\n\n\n\n<p>To ensure every image fits your blog perfectly, use the <strong>Image Editing<\/strong> node:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Action:<\/strong> <code>Resize<\/code><\/li>\n\n\n\n<li><strong>Width:<\/strong> <code>1024<\/code> \/ <strong>Height:<\/strong> <code>512<\/code> \/ <strong>Mode:<\/strong> <code>Cover<\/code><\/li>\n\n\n\n<li><strong>File Name (Expression):<\/strong> We use this JavaScript logic to turn a messy title into a clean, CamelCase filename:JavaScript<code>{{ \"FeatureImage-\" + $('HTML').item.json.title.replace(\/[^a-zA-Z0-9 ]\/g, \"\").toLowerCase().split(\" \").map(w =&gt; w.charAt(0).toUpperCase() + w.slice(1)).join(\"\") + \".png\" }}<\/code><\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\"><strong>Node 5: Respond to Webhook (The Delivery)<\/strong><\/h4>\n\n\n\n<p>This node sends the file back to your browser.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Respond With:<\/strong> <code>Binary File<\/code><\/li>\n\n\n\n<li><strong>Binary Property:<\/strong> <code>data<\/code><\/li>\n\n\n\n<li><strong>Headers (The CORS Fix):<\/strong> To prevent browser errors, add these headers:\n<ul class=\"wp-block-list\">\n<li><code>Access-Control-Allow-Origin<\/code>: <code>*<\/code><\/li>\n\n\n\n<li><code>Content-Type<\/code>: <code>image\/png<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">3. The Frontend (The Trigger Page)<\/h3>\n\n\n\n<p>You can trigger this workflow using a simple HTML page. This script sends the URL to n8n and converts the incoming binary data into an image you can see.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p>This code creates a clean user interface. It uses the <code>fetch<\/code> API to send data to n8n and converts the incoming <strong>Binary Blob<\/strong> into a viewable image on your screen.<\/p>\n\n\n\n<p>HTML<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;!DOCTYPE html>\n&lt;html lang=\"en\">\n&lt;head>\n    &lt;meta charset=\"UTF-8\">\n    &lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    &lt;title>AI Header Processor Pro&lt;\/title>\n    &lt;style>\n        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; align-items: center; padding: 40px; background: #f0f2f5; color: #333; }\n        .container { background: white; padding: 30px; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); width: 100%; max-width: 480px; text-align: center; }\n        h2 { margin-top: 0; color: #2c3e50; }\n        input { width: 100%; padding: 14px; margin: 15px 0; border: 2px solid #eee; border-radius: 8px; box-sizing: border-box; font-size: 14px; transition: border 0.3s; }\n        input:focus { border-color: #ff6d5a; outline: none; }\n        \n        button { width: 100%; padding: 14px; background: #ff6d5a; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; font-size: 16px; transition: transform 0.1s; }\n        button:active { transform: scale(0.98); }\n        button:disabled { background: #ccc; cursor: not-allowed; }\n        \n        #status { margin-top: 15px; font-weight: 500; color: #7f8c8d; }\n\n        \/* Results Area *\/\n        #resultArea { margin-top: 30px; display: none; width: 100%; max-width: 1024px; animation: fadeIn 0.5s; }\n        @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }\n        \n        canvas { border-radius: 12px; box-shadow: 0 8px 20px rgba(0,0,0,0.15); width: 100%; height: auto; background: #ddd; }\n        \n        .button-group { display: flex; gap: 10px; margin-top: 20px; justify-content: center; }\n        .dl-link { flex: 1; padding: 12px; border-radius: 8px; color: white; text-decoration: none; font-weight: bold; font-size: 14px; text-align: center; }\n        .bg-green { background: #27ae60; }\n        .bg-blue { background: #3498db; }\n        .dl-link:hover { opacity: 0.9; }\n    &lt;\/style>\n&lt;\/head>\n&lt;body>\n\n&lt;div class=\"container\">\n    &lt;h2>Header Processor&lt;\/h2>\n    &lt;p>Generate, resize, and pad in one click.&lt;\/p>\n    &lt;input type=\"text\" id=\"blogUrl\" placeholder=\"Paste blog post URL here...\">\n    &lt;button id=\"btn\" onclick=\"runWorkflow()\">Generate &amp; Process&lt;\/button>\n    &lt;div id=\"status\">Ready&lt;\/div>\n&lt;\/div>\n\n&lt;div id=\"resultArea\">\n    &lt;canvas id=\"outputCanvas\" width=\"1024\" height=\"512\">&lt;\/canvas>\n    \n    &lt;div class=\"button-group\">\n        &lt;a id=\"dlProcessed\" class=\"dl-link bg-green\" href=\"#\">Download Processed (1024x512)&lt;\/a>\n        \n        &lt;a id=\"dlOriginal\" class=\"dl-link bg-blue\" href=\"#\">Download Original Square&lt;\/a>\n    &lt;\/div>\n&lt;\/div>\n\n&lt;script>\n    async function runWorkflow() {\n        const urlInput = document.getElementById('blogUrl').value;\n        const btn = document.getElementById('btn');\n        const status = document.getElementById('status');\n        const resultArea = document.getElementById('resultArea');\n        const canvas = document.getElementById('outputCanvas');\n        const dlProcessed = document.getElementById('dlProcessed');\n        const dlOriginal = document.getElementById('dlOriginal');\n        const ctx = canvas.getContext('2d');\n\n        if (!urlInput) return alert(\"Please enter a URL\");\n\n        \/\/ UI Reset\n        btn.disabled = true;\n        status.innerText = \"Step 1: Fetching from n8n...\";\n        resultArea.style.display = \"none\";\n\n        try {\n            const response = await fetch('http:\/\/192.168.4.88:5678\/webhook\/GenerateBlogPostImage', {\n                method: 'POST',\n                headers: { 'Content-Type': 'application\/json' },\n                body: JSON.stringify({ url: urlInput })\n            });\n\n            if (!response.ok) throw new Error(\"n8n workflow error\");\n\n            \/\/ --- EXTRACT FILENAME FROM N8N HEADERS ---\n            const contentDisposition = response.headers.get('Content-Disposition');\n            let generatedName = \"\";\n\n            if (contentDisposition &amp;&amp; contentDisposition.includes('filename=')) {\n                \/\/ Extracts the name from the header and removes quotes\n                generatedName = contentDisposition.split('filename=')&#91;1].replaceAll('\"', '');\n            } else {\n                \/\/ Fallback: Generate a name from the URL if header is missing\n                try {\n                    const urlParts = new URL(urlInput).pathname.split('\/');\n                    generatedName = urlParts.filter(p => p).pop() || \"blog-header.png\";\n                } catch(e) { \n                    generatedName = \"blog-header.png\"; \n                }\n            }\n\n            \/\/ Remove the extension to add our own suffixes cleanly\n            const baseName = generatedName.replace(\/\\.&#91;^\/.]+$\/, \"\");\n\n            const blob = await response.blob();\n            const originalUrl = URL.createObjectURL(blob);\n            \n            const img = new Image();\n            img.src = originalUrl;\n\n            img.onload = function() {\n                status.innerText = \"Step 2: Processing 1024x512 canvas...\";\n\n                \/\/ 1. Process the Canvas (The Padding Step)\n                ctx.fillStyle = \"#0A5AA7\"; \/\/ Background color\n                ctx.fillRect(0, 0, canvas.width, canvas.height);\n\n                const targetH = 512;\n                const targetW = (img.width \/ img.height) * targetH;\n                const offsetX = (canvas.width - targetW) \/ 2;\n\n                ctx.drawImage(img, offsetX, 0, targetW, targetH);\n\n                \/\/ 2. Setup \"Download Original\" using n8n filename\n                dlOriginal.href = originalUrl;\n                dlOriginal.download = `${baseName}-original.png`;\n\n                \/\/ 3. Setup \"Download Processed\" using n8n filename\n                const processedDataUrl = canvas.toDataURL(\"image\/png\");\n                dlProcessed.href = processedDataUrl;\n                dlProcessed.download = `${baseName}-1024x512.png`;\n\n                \/\/ 4. Show Everything\n                resultArea.style.display = \"block\";\n                status.innerText = `Success! Retrieved filename: ${generatedName}`;\n            };\n\n        } catch (err) {\n            status.innerText = \"Error: \" + err.message;\n            console.error(err);\n        } finally {\n            btn.disabled = false;\n        }\n    }\n&lt;\/script>\n\n&lt;\/body>\n&lt;\/html>\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">4. Critical Troubleshooting Lessons<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>The &#8220;422&#8221; Error:<\/strong> If testing with <strong>CURL<\/strong> in Windows PowerShell, you must escape double quotes: <code>\\\"url\\\":\\\"https:\/\/...\\\"<\/code>.<\/li>\n\n\n\n<li><strong>CORS Block:<\/strong> If the webpage says &#8220;Error,&#8221; ensure your <strong>Respond to Webhook<\/strong> node has the <code>Access-Control-Allow-Origin: *<\/code> header.<\/li>\n\n\n\n<li><strong>Missing &#8220;data&#8221;:<\/strong> In n8n, the file is stored in a property called <code>data<\/code>. Ensure your final node references this exact name in the <strong>Binary Property<\/strong> field.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Key Node Settings Recap<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><td><strong>Node<\/strong><\/td><td><strong>Field<\/strong><\/td><td><strong>Setting<\/strong><\/td><\/tr><\/thead><tbody><tr><td><strong>Webhook<\/strong><\/td><td><strong>Response Mode<\/strong><\/td><td><code>Using 'Respond to Webhook' Node<\/code><\/td><\/tr><tr><td><strong>HTTP Request<\/strong><\/td><td><strong>Response Format<\/strong><\/td><td><code>Text<\/code><\/td><\/tr><tr><td><strong>HTML<\/strong><\/td><td><strong>JSON Property<\/strong><\/td><td><code>data<\/code><\/td><\/tr><tr><td><strong>Image Edit<\/strong><\/td><td><strong>Resize<\/strong><\/td><td><code>1024<\/code> x <code>512<\/code> (Cover)<\/td><\/tr><tr><td><strong>Respond Webhook<\/strong><\/td><td><strong>Respond With<\/strong><\/td><td><code>Binary File<\/code><\/td><\/tr><tr><td><strong>Respond Webhook<\/strong><\/td><td><strong>Binary Property<\/strong><\/td><td><code>data<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why This Works<\/h3>\n\n\n\n<p>The magic happens in the <strong>Respond to Webhook<\/strong> node. By setting the <code>Content-Type<\/code> header to <code>image\/png<\/code> and providing the <code>Access-Control-Allow-Origin: *<\/code> header, n8n stops acting like a simple data processor and starts acting like a specialized <strong>Image API<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Conclusion<\/h3>\n\n\n\n<p>By combining n8n\u2019s ability to handle binary data with AI image generation, you can build a powerful internal tool that saves hours of design work. Your blog headers will now be perfectly sized, correctly named, and contextually relevant to your content.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Author: Jorge Pereira Date: June 2025 Tech Stack: n8n (Docker), Ollama, Open WebUI, JavaScript\/HTML, Windows 11 The goal is simple: Create a web-based tool where you enter a blog post URL, and the system scrapes the title, generates an AI image, resizes it to a 1024&#215;512 pixels (2:1 rati)o, and displays it back to you&#8230;<\/p>\n","protected":false},"author":2,"featured_media":519660,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_kad_blocks_custom_css":"","_kad_blocks_head_custom_js":"","_kad_blocks_body_custom_js":"","_kad_blocks_footer_custom_js":"","ngg_post_thumbnail":0,"episode_type":"","audio_file":"","podmotor_file_id":"","podmotor_episode_id":"","cover_image":"","cover_image_id":"","duration":"","filesize":"","filesize_raw":"","date_recorded":"","explicit":"","block":"","itunes_episode_number":"","itunes_title":"","itunes_season_number":"","itunes_episode_type":"","_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"categories":[17],"tags":[],"class_list":["post-519649","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-journey"],"taxonomy_info":{"category":[{"value":17,"label":"Journey"}]},"featured_image_src_large":["https:\/\/jorgep.com\/blog\/wp-content\/uploads\/FeaturedImage-BuildBlogBlogImageGenerator-Mod01.png",1024,512,false],"author_info":{"display_name":"Jorge Pereira","author_link":"https:\/\/jorgep.com\/blog\/author\/jorge\/"},"comment_info":0,"category_info":[{"term_id":17,"name":"Journey","slug":"journey","term_group":0,"term_taxonomy_id":18,"taxonomy":"category","description":"","parent":0,"count":318,"filter":"raw","cat_ID":17,"category_count":318,"category_description":"","cat_name":"Journey","category_nicename":"journey","category_parent":0}],"tag_info":false,"_links":{"self":[{"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/posts\/519649","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/comments?post=519649"}],"version-history":[{"count":5,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/posts\/519649\/revisions"}],"predecessor-version":[{"id":519663,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/posts\/519649\/revisions\/519663"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/media\/519660"}],"wp:attachment":[{"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/media?parent=519649"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/categories?post=519649"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/jorgep.com\/blog\/wp-json\/wp\/v2\/tags?post=519649"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}