Build Blog: Blog Image Generator Workflow
Note: Written with the help of my research and editorial team 🙂 including: (Google Gemini, Google Notebook LM, Microsoft Copilot, Perplexity.ai, Claude.ai and others as needed)
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×512 pixels (2:1 rati)o, and displays it back to you instantly.
1. The Full Architecture
The process is a closed loop: It starts at the user’s browser, travels through n8n’s scraping and AI layers, and returns directly to the browser as a visual image.
HTML Form: User inputs a URL and clicks “Generate.”
Webhook: n8n receives the URL and holds the connection open.
HTTP Request: n8n visits the URL to download the raw HTML code.
HTML Scraper: n8n extracts the <h1> title from that code.
AI Image Gen: A prompt is built using that title to create a relevant image.
Image Editor: The AI result is cropped and resized to exactly 1024×512.
Webhook Response: The final .png is sent back to the browser.

2. The n8n Workflow Configuration
Node 1: Webhook Trigger (The Gateway)
- HTTP Method:
POST - Path:
GenerateBlogPostImage - Response Mode: Set to “Using ‘Respond to Webhook’ Node”.Crucial: Without this setting, the browser won’t wait for the AI to finish.
Node 2 & 3: The Scraper (HTTP Request & HTML Node)
You can’t scrape a title without first “fetching” the site code.
- HTTP Request: Use
GETand set the URL to{{ $json.body.url }}. Set Response Format toText. - HTML Node: Set Source Data to
JSONand JSON Property todata. Use a CSS Selector likeh1to grab the title.
Node 4: Image Editing (The Sizer & Namer)
To ensure every image fits your blog perfectly, use the Image Editing node:
- Action:
Resize - Width:
1024/ Height:512/ Mode:Cover - File Name (Expression): We use this JavaScript logic to turn a messy title into a clean, CamelCase filename:JavaScript
{{ "FeatureImage-" + $('HTML').item.json.title.replace(/[^a-zA-Z0-9 ]/g, "").toLowerCase().split(" ").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("") + ".png" }}
Node 5: Respond to Webhook (The Delivery)
This node sends the file back to your browser.
- Respond With:
Binary File - Binary Property:
data - Headers (The CORS Fix): To prevent browser errors, add these headers:
Access-Control-Allow-Origin:*Content-Type:image/png
3. The Frontend (The Trigger Page)
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.
This code creates a clean user interface. It uses the fetch API to send data to n8n and converts the incoming Binary Blob into a viewable image on your screen.
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Header Processor Pro</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; display: flex; flex-direction: column; align-items: center; padding: 40px; background: #f0f2f5; color: #333; }
.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; }
h2 { margin-top: 0; color: #2c3e50; }
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; }
input:focus { border-color: #ff6d5a; outline: none; }
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; }
button:active { transform: scale(0.98); }
button:disabled { background: #ccc; cursor: not-allowed; }
#status { margin-top: 15px; font-weight: 500; color: #7f8c8d; }
/* Results Area */
#resultArea { margin-top: 30px; display: none; width: 100%; max-width: 1024px; animation: fadeIn 0.5s; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
canvas { border-radius: 12px; box-shadow: 0 8px 20px rgba(0,0,0,0.15); width: 100%; height: auto; background: #ddd; }
.button-group { display: flex; gap: 10px; margin-top: 20px; justify-content: center; }
.dl-link { flex: 1; padding: 12px; border-radius: 8px; color: white; text-decoration: none; font-weight: bold; font-size: 14px; text-align: center; }
.bg-green { background: #27ae60; }
.bg-blue { background: #3498db; }
.dl-link:hover { opacity: 0.9; }
</style>
</head>
<body>
<div class="container">
<h2>Header Processor</h2>
<p>Generate, resize, and pad in one click.</p>
<input type="text" id="blogUrl" placeholder="Paste blog post URL here...">
<button id="btn" onclick="runWorkflow()">Generate & Process</button>
<div id="status">Ready</div>
</div>
<div id="resultArea">
<canvas id="outputCanvas" width="1024" height="512"></canvas>
<div class="button-group">
<a id="dlProcessed" class="dl-link bg-green" href="#">Download Processed (1024x512)</a>
<a id="dlOriginal" class="dl-link bg-blue" href="#">Download Original Square</a>
</div>
</div>
<script>
async function runWorkflow() {
const urlInput = document.getElementById('blogUrl').value;
const btn = document.getElementById('btn');
const status = document.getElementById('status');
const resultArea = document.getElementById('resultArea');
const canvas = document.getElementById('outputCanvas');
const dlProcessed = document.getElementById('dlProcessed');
const dlOriginal = document.getElementById('dlOriginal');
const ctx = canvas.getContext('2d');
if (!urlInput) return alert("Please enter a URL");
// UI Reset
btn.disabled = true;
status.innerText = "Step 1: Fetching from n8n...";
resultArea.style.display = "none";
try {
const response = await fetch('http://192.168.4.88:5678/webhook/GenerateBlogPostImage', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: urlInput })
});
if (!response.ok) throw new Error("n8n workflow error");
// --- EXTRACT FILENAME FROM N8N HEADERS ---
const contentDisposition = response.headers.get('Content-Disposition');
let generatedName = "";
if (contentDisposition && contentDisposition.includes('filename=')) {
// Extracts the name from the header and removes quotes
generatedName = contentDisposition.split('filename=')[1].replaceAll('"', '');
} else {
// Fallback: Generate a name from the URL if header is missing
try {
const urlParts = new URL(urlInput).pathname.split('/');
generatedName = urlParts.filter(p => p).pop() || "blog-header.png";
} catch(e) {
generatedName = "blog-header.png";
}
}
// Remove the extension to add our own suffixes cleanly
const baseName = generatedName.replace(/\.[^/.]+$/, "");
const blob = await response.blob();
const originalUrl = URL.createObjectURL(blob);
const img = new Image();
img.src = originalUrl;
img.onload = function() {
status.innerText = "Step 2: Processing 1024x512 canvas...";
// 1. Process the Canvas (The Padding Step)
ctx.fillStyle = "#0A5AA7"; // Background color
ctx.fillRect(0, 0, canvas.width, canvas.height);
const targetH = 512;
const targetW = (img.width / img.height) * targetH;
const offsetX = (canvas.width - targetW) / 2;
ctx.drawImage(img, offsetX, 0, targetW, targetH);
// 2. Setup "Download Original" using n8n filename
dlOriginal.href = originalUrl;
dlOriginal.download = `${baseName}-original.png`;
// 3. Setup "Download Processed" using n8n filename
const processedDataUrl = canvas.toDataURL("image/png");
dlProcessed.href = processedDataUrl;
dlProcessed.download = `${baseName}-1024x512.png`;
// 4. Show Everything
resultArea.style.display = "block";
status.innerText = `Success! Retrieved filename: ${generatedName}`;
};
} catch (err) {
status.innerText = "Error: " + err.message;
console.error(err);
} finally {
btn.disabled = false;
}
}
</script>
</body>
</html>
4. Critical Troubleshooting Lessons
- The “422” Error: If testing with CURL in Windows PowerShell, you must escape double quotes:
\"url\":\"https://...\". - CORS Block: If the webpage says “Error,” ensure your Respond to Webhook node has the
Access-Control-Allow-Origin: *header. - Missing “data”: In n8n, the file is stored in a property called
data. Ensure your final node references this exact name in the Binary Property field.
Key Node Settings Recap
| Node | Field | Setting |
| Webhook | Response Mode | Using 'Respond to Webhook' Node |
| HTTP Request | Response Format | Text |
| HTML | JSON Property | data |
| Image Edit | Resize | 1024 x 512 (Cover) |
| Respond Webhook | Respond With | Binary File |
| Respond Webhook | Binary Property | data |
Why This Works
The magic happens in the Respond to Webhook node. By setting the Content-Type header to image/png and providing the Access-Control-Allow-Origin: * header, n8n stops acting like a simple data processor and starts acting like a specialized Image API.
Conclusion
By combining n8n’s 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.
