Bug Description
It’s already instelled, and the issue is not the code itself
To Reproduce
The workflow concerned part:
{
“nodes”: [
{
“parameters”: {
“html”: “Productivity Tips*{margin:0;padding:0;box-sizing:border-box}body{font-family:Inter,sans-serif;background:linear-gradient(150deg,#1e3a8a 0%,#2563eb 25%,#3b82f6 50%,#60a5fa@keyframes100%);min-height:100vh}@keyframes spin{to{transform:rotate(360deg)}}.container{width:100%;min-height:100vh}.header{background:linear-gradient(135deg,#2563eb,#60a5fa);color:#fff;padding:50px 35px;text-align:center}.header h1{font-size:36px;font-weight:800}.hook,.bridge{padding:36px;font-size:18px;line-height:1.85;color:#dbeafe}.hook{background:linear-gradient(135deg,rgba(30,58,138,.95),rgba(37,99,235,.95));border-left:7px solid* #3b82f6*}.bridge{background:linear-gradient(135deg,rgba(37,99,235,.93),rgba(59,130,246,.93));border-left:7px solid* #60a5fa*}.tips-section{padding:50px 35px}.tips-title{font-size:30px;color:#dbeafe;margin-bottom:35px;font-weight:800;text-align:center}.tip{background:linear-gradient(135deg,rgba(30,58,138,.75),rgba(37,99,235,.75));padding:32px;margin-bottom:24px;border-radius:16px;border-left:7px solid* #3b82f6*;transition:all .4s}.tip:hover{transform:translateX(12px) scale(1.03)}.tip-content{font-size:18px;line-height:1.8;color:#dbeafe}.engagement{background:linear-gradient(135deg,#2563eb,#93c5fd);color:#fff;padding:42px 36px;text-align:center;margin:0 35px 50px;border-radius:18px}.engagement-question{font-size:21px;font-weight:700}
Automation Strategies{{$(‘Code’).item.json.tip_1}}{{$(‘Code’).item.json.tip_2}}{{$(‘Code’).item.json.tip_3}}{{$(‘Code’).item.json.engagement_question}}"
},
“type”: “n8n-nodes-base.html”,
“typeVersion”: 1.2,
“position”: [
2864,
-272
],
“id”: “98e30da6-b8ff-4104-8e13-4f3f4db1610f”,
“name”: “HTML1”
},
{
“parameters”: {
“jsCode”: "// Paste this into an n8n “Code” node (JavaScript)\n// It expects items to be provided by the incoming node(s).\n// Output: items with binary.image containing PNG (base64).\n\nconst DEFAULT_HTML = <!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Lab Tips</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n background: white;\n min-height: 100vh;\n display: flex;\n }\n \n .container {\n width: 100%;\n background: white;\n overflow: hidden;\n }\n \n .header {\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n padding: 30px;\n text-align: center;\n }\n \n .header h1 {\n font-size: 28px;\n margin-bottom: 8px;\n font-weight: 700;\n }\n \n .header .icon {\n font-size: 48px;\n margin-bottom: 10px;\n }\n \n .hook {\n background: #f8f9ff;\n padding: 25px 30px;\n border-left: 4px solid #667eea;\n margin: 0;\n font-size: 18px;\n line-height: 1.6;\n color: #2d3748;\n font-weight: 500;\n }\n \n .bridge {\n padding: 25px 30px;\n background: #fff9e6;\n border-left: 4px solid #f59e0b;\n font-size: 16px;\n line-height: 1.7;\n color: #4a5568;\n }\n \n .tips-section {\n padding: 30px;\n }\n \n .tips-title {\n font-size: 22px;\n color: #1a202c;\n margin-bottom: 20px;\n font-weight: 700;\n text-align: center;\n }\n \n .tip {\n background: #f7fafc;\n padding: 20px;\n margin-bottom: 15px;\n border-radius: 10px;\n border-left: 4px solid #667eea;\n transition: transform 0.2s, box-shadow 0.2s;\n }\n \n .tip:hover {\n transform: translateX(5px);\n box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);\n }\n \n .tip-content {\n font-size: 16px;\n line-height: 1.6;\n color: #2d3748;\n }\n \n .engagement {\n background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);\n color: white;\n padding: 25px 30px;\n text-align: center;\n margin: 0 30px 30px 30px;\n border-radius: 10px;\n box-shadow: 0 4px 12px rgba(245, 87, 108, 0.3);\n }\n \n .engagement-question {\n font-size: 18px;\n font-weight: 600;\n line-height: 1.5;\n }\n \n .hashtags {\n background: #1a202c;\n color: #a0aec0;\n padding: 20px 30px;\n text-align: center;\n font-size: 14px;\n line-height: 1.8;\n }\n \n .hashtags span {\n display: inline-block;\n margin: 0 5px;\n color: #667eea;\n font-weight: 500;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <div class=\"icon\">🔬</div>\n <h1>Quick Lab Tips</h1>\n </div>\n \n <div class=\"hook\">\n {{ $json.hook }}\n </div>\n \n <div class=\"bridge\">\n {{ $json.bridge }}\n </div>\n \n <div class=\"tips-section\">\n <h2 class=\"tips-title\">💡 Pro Tips</h2>\n \n <div class=\"tip\">\n <div class=\"tip-content\">\n {{ $json.tip_1 }}\n </div>\n </div>\n \n <div class=\"tip\">\n <div class=\"tip-content\">\n {{ $json.tip_2 }}\n </div>\n </div>\n \n <div class=\"tip\">\n <div class=\"tip-content\">\n {{ $json.tip_3 }}\n </div>\n </div>\n </div>\n \n <div class=\"engagement\">\n <div class=\"engagement-question\">\n {{ $json.engagement_question }}\n </div>\n </div>\n\n </div>\n</body>\n</html>;\n\n// Screenshot options — tweak as needed\nconst VIEWPORT = { width: 1250, height: 850, deviceScaleFactor: 3 };\nconst FULL_PAGE = true; // true to capture entire scrollable page\nconst IMAGE_TYPE = ‘png’; // ‘png’ or ‘jpeg’\nconst JPEG_QUALITY = 95; // used only if type === ‘jpeg’\n\nasync function getPuppeteer() {\n // Try to require puppeteer or puppeteer-core\n try {\n return require(‘puppeteer’); // prefer puppeteer if available (bundles chromium)\n } catch (e1) {\n try {\n return require(‘puppeteer-core’);\n } catch (e2) {\n throw new Error(\n 'Neither “puppeteer” nor “puppeteer-core” is installed in the n8n environment. ’ +\n ‘Install one (e.g. npm install puppeteer) or use puppeteer-core + set PUPPETEER_EXECUTABLE_PATH to a Chrome/Chromium binary.’\n );\n }\n }\n}\n\nfunction replaceTemplateVariables(html, data) {\n let result = html;\n // Replace {{ $json.key }} with actual values from data object\n result = result.replace(/\{\{\s*\$json\.(\w+)\s*\}\}/g, (match, key) => {\n return data[key] !== undefined ? String(data[key]) : match;\n });\n return result;\n}\n\nasync function renderHtmlToPngBuffer(html, options = {}) {\n const puppeteer = await getPuppeteer();\n\n // Launch options: allow overriding executablePath via env var\n const launchOpts = {\n args: [‘–no-sandbox’, ‘–disable-setuid-sandbox’],\n };\n\n // If developer provided an explicit executable for Chrome/Chromium:\n const execPath = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || process.env.PUPPETEER_EXECUTABLE;\n if (execPath) {\n launchOpts.executablePath = execPath;\n }\n\n const browser = await puppeteer.launch(launchOpts);\n try {\n const page = await browser.newPage();\n\n // set viewport (or let the HTML/CSS control width via style)\n await page.setViewport(options.viewport || VIEWPORT);\n\n // Set content and wait for network idle (good for injected images/fonts)\n await page.setContent(html, { waitUntil: ‘networkidle0’, timeout: 30000 });\n\n // Wait a tiny bit if the page has animations you want rendered (optional)\n await new Promise(resolve => setTimeout(resolve, 200));\n\n const screenshotOptions = {\n type: options.type || IMAGE_TYPE,\n fullPage: typeof options.fullPage === ‘boolean’ ? options.fullPage : FULL_PAGE,\n };\n if (screenshotOptions.type === ‘jpeg’) screenshotOptions.quality = options.quality || JPEG_QUALITY;\n\n const buffer = await page.screenshot(screenshotOptions);\n\n await page.close();\n return buffer;\n } finally {\n await browser.close();\n }\n}\n\n// Main execution: process all incoming items\nconst output = ;\n\nfor (let i = 0; i < items.length; i++) {\n const item = items[i];\n\n // User can supply html in item.json.html OR item.json.body (common names), else fallback\n const htmlTemplate =\n (item && item.json && (item.json.html || item.json.body || item.json.content || item.json.template)) ||\n DEFAULT_HTML;\n\n // Replace template variables with actual data from item.json\n const htmlInput = replaceTemplateVariables(htmlTemplate, item.json || {});\n\n // Optionally allow per-item overrides in json (viewport/fullPage/type)\n const perItemOptions = {\n viewport: item?.json?.viewport || VIEWPORT,\n fullPage: typeof item?.json?.fullPage !== ‘undefined’ ? item.json.fullPage : FULL_PAGE,\n type: item?.json?.imageType || IMAGE_TYPE,\n quality: item?.json?.jpegQuality || JPEG_QUALITY,\n };\n\n // Render to buffer (await)\n // Note: in n8n Code node the top-level supports async/await\n // We call the async renderer and convert buffer → base64 for binary output\n // Keep errors captured per item to avoid entire workflow crash\n try {\n const buffer = await renderHtmlToPngBuffer(String(htmlInput), perItemOptions);\n\n const base64 = buffer.toString(‘base64’);\n\n // Build output item: keep original json and attach binary.image\n const newItem = {\n json: item.json || {},\n binary: {\n image: {\n data: base64,\n fileName: item.json?.fileName || render-${i}.${perItemOptions.type === 'jpeg' ? 'jpg' : 'png'},\n mimeType: perItemOptions.type === ‘jpeg’ ? ‘image/jpeg’ : ‘image/png’,\n },\n },\n };\n\n output.push(newItem);\n } catch (err) {\n // Return an item containing the error (so you can inspect it downstream)\n output.push({\n json: {\n error: true,\n message: err.message,\n stack: err.stack ? String(err.stack).split(‘\n’).slice(0, 6).join(‘\n’) : undefined,\n },\n });\n }\n}\n\nreturn output;”
},
“type”: “n8n-nodes-base.code”,
“typeVersion”: 2,
“position”: [
3008,
-272
],
“id”: “05ff60ef-da2f-49fe-ba92-faff0831b359”,
“name”: “Code3”
}
],
“connections”: {
“HTML1”: {
“main”: [
[
{
“node”: “Code3”,
“type”: “main”,
“index”: 0
}
]
]
}
},
“pinData”: {},
“meta”: {
“templateCredsSetupCompleted”: true,
“instanceId”: “a56a2089db42feeef20e145aa7658cf5b966d1acc052fc08bd6a11314daa674e”
}
}
Debug info
core
-
n8nVersion: 2.4.5
-
platform: npm
-
nodeJsVersion: 22.17.0
-
nodeEnv: undefined
-
database: sqlite
-
executionMode: regular
-
concurrency: -1
-
license: enterprise (production)
-
consumerId: debcc182-c27f-4112-8abd-7e8e577797c8
storage
-
success: all
-
error: all
-
progress: false
-
manual: true
-
binaryMode: filesystem
pruning
-
enabled: true
-
maxAge: 336 hours
-
maxCount: 10000 executions
client
-
userAgent: mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) chrome/144.0.0.0 safari/537.36 edg/144.0.0.0
-
isTouchDevice: false
Generated at: 2026-01-22T20:28:42.420Z
Hosting
self hosted
},
“type”: “n8n-nodes-base.html”,
“typeVersion”: 1.2,
“position”: [
2864,
-272
],
“id”: “98e30da6-b8ff-4104-8e13-4f3f4db1610f”,
“name”: “HTML1”
},
{
“parameters”: {
“jsCode”: "// Paste this into an n8n “Code” node (JavaScript)\n// It expects
items to be provided by the incoming node(s).\n// Output: items with binary.image containing PNG (base64).\n\nconst DEFAULT_HTML = <!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Lab Tips</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;\n background: white;\n min-height: 100vh;\n display: flex;\n }\n \n .container {\n width: 100%;\n background: white;\n overflow: hidden;\n }\n \n .header {\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: white;\n padding: 30px;\n text-align: center;\n }\n \n .header h1 {\n font-size: 28px;\n margin-bottom: 8px;\n font-weight: 700;\n }\n \n .header .icon {\n font-size: 48px;\n margin-bottom: 10px;\n }\n \n .hook {\n background: #f8f9ff;\n padding: 25px 30px;\n border-left: 4px solid #667eea;\n margin: 0;\n font-size: 18px;\n line-height: 1.6;\n color: #2d3748;\n font-weight: 500;\n }\n \n .bridge {\n padding: 25px 30px;\n background: #fff9e6;\n border-left: 4px solid #f59e0b;\n font-size: 16px;\n line-height: 1.7;\n color: #4a5568;\n }\n \n .tips-section {\n padding: 30px;\n }\n \n .tips-title {\n font-size: 22px;\n color: #1a202c;\n margin-bottom: 20px;\n font-weight: 700;\n text-align: center;\n }\n \n .tip {\n background: #f7fafc;\n padding: 20px;\n margin-bottom: 15px;\n border-radius: 10px;\n border-left: 4px solid #667eea;\n transition: transform 0.2s, box-shadow 0.2s;\n }\n \n .tip:hover {\n transform: translateX(5px);\n box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);\n }\n \n .tip-content {\n font-size: 16px;\n line-height: 1.6;\n color: #2d3748;\n }\n \n .engagement {\n background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);\n color: white;\n padding: 25px 30px;\n text-align: center;\n margin: 0 30px 30px 30px;\n border-radius: 10px;\n box-shadow: 0 4px 12px rgba(245, 87, 108, 0.3);\n }\n \n .engagement-question {\n font-size: 18px;\n font-weight: 600;\n line-height: 1.5;\n }\n \n .hashtags {\n background: #1a202c;\n color: #a0aec0;\n padding: 20px 30px;\n text-align: center;\n font-size: 14px;\n line-height: 1.8;\n }\n \n .hashtags span {\n display: inline-block;\n margin: 0 5px;\n color: #667eea;\n font-weight: 500;\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <div class=\"icon\">🔬</div>\n <h1>Quick Lab Tips</h1>\n </div>\n \n <div class=\"hook\">\n {{ $json.hook }}\n </div>\n \n <div class=\"bridge\">\n {{ $json.bridge }}\n </div>\n \n <div class=\"tips-section\">\n <h2 class=\"tips-title\">💡 Pro Tips</h2>\n \n <div class=\"tip\">\n <div class=\"tip-content\">\n {{ $json.tip_1 }}\n </div>\n </div>\n \n <div class=\"tip\">\n <div class=\"tip-content\">\n {{ $json.tip_2 }}\n </div>\n </div>\n \n <div class=\"tip\">\n <div class=\"tip-content\">\n {{ $json.tip_3 }}\n </div>\n </div>\n </div>\n \n <div class=\"engagement\">\n <div class=\"engagement-question\">\n {{ $json.engagement_question }}\n </div>\n </div>\n\n </div>\n</body>\n</html>;\n\n// Screenshot options — tweak as needed\nconst VIEWPORT = { width: 1250, height: 850, deviceScaleFactor: 3 };\nconst FULL_PAGE = true; // true to capture entire scrollable page\nconst IMAGE_TYPE = ‘png’; // ‘png’ or ‘jpeg’\nconst JPEG_QUALITY = 95; // used only if type === ‘jpeg’\n\nasync function getPuppeteer() {\n // Try to require puppeteer or puppeteer-core\n try {\n return require(‘puppeteer’); // prefer puppeteer if available (bundles chromium)\n } catch (e1) {\n try {\n return require(‘puppeteer-core’);\n } catch (e2) {\n throw new Error(\n 'Neither “puppeteer” nor “puppeteer-core” is installed in the n8n environment. ’ +\n ‘Install one (e.g. npm install puppeteer) or use puppeteer-core + set PUPPETEER_EXECUTABLE_PATH to a Chrome/Chromium binary.’\n );\n }\n }\n}\n\nfunction replaceTemplateVariables(html, data) {\n let result = html;\n // Replace {{ $json.key }} with actual values from data object\n result = result.replace(/\{\{\s*\$json\.(\w+)\s*\}\}/g, (match, key) => {\n return data[key] !== undefined ? String(data[key]) : match;\n });\n return result;\n}\n\nasync function renderHtmlToPngBuffer(html, options = {}) {\n const puppeteer = await getPuppeteer();\n\n // Launch options: allow overriding executablePath via env var\n const launchOpts = {\n args: [‘–no-sandbox’, ‘–disable-setuid-sandbox’],\n };\n\n // If developer provided an explicit executable for Chrome/Chromium:\n const execPath = process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || process.env.PUPPETEER_EXECUTABLE;\n if (execPath) {\n launchOpts.executablePath = execPath;\n }\n\n const browser = await puppeteer.launch(launchOpts);\n try {\n const page = await browser.newPage();\n\n // set viewport (or let the HTML/CSS control width via style)\n await page.setViewport(options.viewport || VIEWPORT);\n\n // Set content and wait for network idle (good for injected images/fonts)\n await page.setContent(html, { waitUntil: ‘networkidle0’, timeout: 30000 });\n\n // Wait a tiny bit if the page has animations you want rendered (optional)\n await new Promise(resolve => setTimeout(resolve, 200));\n\n const screenshotOptions = {\n type: options.type || IMAGE_TYPE,\n fullPage: typeof options.fullPage === ‘boolean’ ? options.fullPage : FULL_PAGE,\n };\n if (screenshotOptions.type === ‘jpeg’) screenshotOptions.quality = options.quality || JPEG_QUALITY;\n\n const buffer = await page.screenshot(screenshotOptions);\n\n await page.close();\n return buffer;\n } finally {\n await browser.close();\n }\n}\n\n// Main execution: process all incoming items\nconst output = ;\n\nfor (let i = 0; i < items.length; i++) {\n const item = items[i];\n\n // User can supply html in item.json.html OR item.json.body (common names), else fallback\n const htmlTemplate =\n (item && item.json && (item.json.html || item.json.body || item.json.content || item.json.template)) ||\n DEFAULT_HTML;\n\n // Replace template variables with actual data from item.json\n const htmlInput = replaceTemplateVariables(htmlTemplate, item.json || {});\n\n // Optionally allow per-item overrides in json (viewport/fullPage/type)\n const perItemOptions = {\n viewport: item?.json?.viewport || VIEWPORT,\n fullPage: typeof item?.json?.fullPage !== ‘undefined’ ? item.json.fullPage : FULL_PAGE,\n type: item?.json?.imageType || IMAGE_TYPE,\n quality: item?.json?.jpegQuality || JPEG_QUALITY,\n };\n\n // Render to buffer (await)\n // Note: in n8n Code node the top-level supports async/await\n // We call the async renderer and convert buffer → base64 for binary output\n // Keep errors captured per item to avoid entire workflow crash\n try {\n const buffer = await renderHtmlToPngBuffer(String(htmlInput), perItemOptions);\n\n const base64 = buffer.toString(‘base64’);\n\n // Build output item: keep original json and attach binary.image\n const newItem = {\n json: item.json || {},\n binary: {\n image: {\n data: base64,\n fileName: item.json?.fileName || render-${i}.${perItemOptions.type === 'jpeg' ? 'jpg' : 'png'},\n mimeType: perItemOptions.type === ‘jpeg’ ? ‘image/jpeg’ : ‘image/png’,\n },\n },\n };\n\n output.push(newItem);\n } catch (err) {\n // Return an item containing the error (so you can inspect it downstream)\n output.push({\n json: {\n error: true,\n message: err.message,\n stack: err.stack ? String(err.stack).split(‘\n’).slice(0, 6).join(‘\n’) : undefined,\n },\n });\n }\n}\n\nreturn output;”},
“type”: “n8n-nodes-base.code”,
“typeVersion”: 2,
“position”: [
3008,
-272
],
“id”: “05ff60ef-da2f-49fe-ba92-faff0831b359”,
“name”: “Code3”
}
],
“connections”: {
“HTML1”: {
“main”: [
[
{
“node”: “Code3”,
“type”: “main”,
“index”: 0
}
]
]
}
},
“pinData”: {},
“meta”: {
“templateCredsSetupCompleted”: true,
“instanceId”: “a56a2089db42feeef20e145aa7658cf5b966d1acc052fc08bd6a11314daa674e”
}
}
Debug info
core
-
n8nVersion: 2.4.5
-
platform: npm
-
nodeJsVersion: 22.17.0
-
nodeEnv: undefined
-
database: sqlite
-
executionMode: regular
-
concurrency: -1
-
license: enterprise (production)
-
consumerId: debcc182-c27f-4112-8abd-7e8e577797c8
storage
-
success: all
-
error: all
-
progress: false
-
manual: true
-
binaryMode: filesystem
pruning
-
enabled: true
-
maxAge: 336 hours
-
maxCount: 10000 executions
client
-
userAgent: mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) chrome/144.0.0.0 safari/537.36 edg/144.0.0.0
-
isTouchDevice: false
Generated at: 2026-01-22T20:28:42.420Z
Hosting
self hosted
