{
  "id": "ueYdxm9B8ARNObVc",
  "name": "Blog to Social Media Captions, Copy and Paste Generator",
  "tags": [],
  "nodes": [
    {
      "id": "91bf6697-52df-4852-a543-cbed58e2f466",
      "name": "Overview and Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -416,
        80
      ],
      "parameters": {
        "width": 532,
        "height": 1140,
        "content": "## Blog to Social Media Captions\n\nPaste a blog URL or blog text into a form and get ready to copy captions for LinkedIn, Twitter/X, Facebook, Instagram, Threads, Pinterest, YouTube Community, and Reddit, plus alt text for the featured image. Built for bloggers, marketers, and small teams who want to promote one post everywhere without rewriting it eight times by hand. Nothing gets posted automatically, you copy and paste each caption yourself.\n\n### How it works\n1. **Blog Input Form** collects a blog URL, or pasted blog text, plus an optional image URL.\n2. **Has Blog URL?** checks which field was filled in and picks the right path.\n3. **Fetch Blog Page** downloads the page and **Extract Article Text** strips it to clean text. **Use Pasted Content** handles the manual paste path instead.\n4. **Build Prompt** writes clear per platform instructions for the AI model.\n5. **Generate Captions** calls the AI model and returns one JSON object with every caption.\n6. **Build Copy-Paste Page** turns that JSON into a styled page with copy buttons, shown to you by **Show Captions**.\n\n### Setup steps\n1. Open node 8, OpenAI Chat Model, and connect your own OpenAI credential.\n2. Activate the workflow.\n3. Open the Production URL on node 1, Blog Input Form, and test it with a real blog post.\n\n### Customization\nSwap the OpenAI Chat Model node for Anthropic, Gemini, Groq, or Ollama, the AI Agent works with any of them. Edit the prompt inside Build Prompt to change tone, caption length, or hashtag count per platform."
      },
      "typeVersion": 1
    },
    {
      "id": "7783ff92-fbfe-4190-8504-bdd6b79dcda0",
      "name": "Section 1 - Input and Routing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 542,
        "height": 388,
        "content": "## 1. Input and routing\nBlog Input Form collects the blog URL, pasted text, and image URL. Has Blog URL? checks which one was filled in and routes to the right path."
      },
      "typeVersion": 1
    },
    {
      "id": "f7dea3c9-62a7-4b42-9ab5-171f9e0ef0de",
      "name": "Section 2 - Fetch and Extract Content",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        928,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 490,
        "height": 708,
        "content": "## 2. Fetch and extract content\nFetch Blog Page downloads the URL and Extract Article Text strips it to clean text. Use Pasted Content handles the manual paste path instead."
      },
      "typeVersion": 1
    },
    {
      "id": "a49fd559-a3d2-4f8c-9410-a8096216c418",
      "name": "Section 3 - AI Caption Generation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1632,
        320
      ],
      "parameters": {
        "color": 7,
        "width": 575,
        "height": 660,
        "content": "## 3. Generate captions with AI\nBuild Prompt writes platform by platform instructions. Generate Captions, powered by the OpenAI Chat Model, returns one JSON object with every caption."
      },
      "typeVersion": 1
    },
    {
      "id": "53bf74e7-0398-4767-87e7-c113c5dfe80a",
      "name": "Section 4 - Build and Deliver Output",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2400,
        400
      ],
      "parameters": {
        "color": 7,
        "width": 643,
        "height": 420,
        "content": "## 4. Build and deliver the page\nBuild Copy-Paste Page turns the AI output into a styled page with copy buttons. Show Captions displays it to the user."
      },
      "typeVersion": 1
    },
    {
      "id": "33e0c4a3-41ef-43c0-be50-ee0eed7f31d6",
      "name": "Blog Input Form",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        304,
        608
      ],
      "parameters": {
        "options": {},
        "formTitle": "Blog to Social Media Captions",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Blog URL",
              "placeholder": "https://yourblog.com/post-title"
            },
            {
              "fieldType": "textarea",
              "fieldLabel": "Blog Content",
              "placeholder": "Or paste the full blog text here if you do not have a URL"
            },
            {
              "fieldLabel": "Featured Image URL",
              "placeholder": "https://yourblog.com/image.jpg (optional)"
            }
          ]
        },
        "responseMode": "lastNode",
        "formDescription": "Paste your blog URL or blog content below. You will get ready-to-copy captions for every major social platform."
      },
      "typeVersion": 2.2
    },
    {
      "id": "1c1bd6be-e160-47fc-beb5-297e7813a3bf",
      "name": "Has Blog URL?",
      "type": "n8n-nodes-base.if",
      "position": [
        544,
        608
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-has-url",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json['Blog URL'] }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "6431563b-3f70-4013-8960-536b7a425b35",
      "name": "Fetch Blog Page",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        992,
        480
      ],
      "parameters": {
        "url": "={{ $json['Blog URL'] }}",
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "responseFormat": "text"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "bb496d98-efea-4db3-ac47-d4a5bd013b00",
      "name": "Extract Article Text",
      "type": "n8n-nodes-base.code",
      "position": [
        1216,
        480
      ],
      "parameters": {
        "jsCode": "const first = $input.first().json;\nconst src = String(first.data ?? first.body ?? '');\n\nconst titleMatch = src.match(/<title[^>]*>([\\s\\S]*?)<\\/title>/i);\nconst pageTitle = titleMatch ? titleMatch[1].trim() : '';\n\nconst text = src\n  .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n  .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n  .replace(/<nav[\\s\\S]*?<\\/nav>/gi, ' ')\n  .replace(/<footer[\\s\\S]*?<\\/footer>/gi, ' ')\n  .replace(/<[^>]+>/g, ' ')\n  .replace(/&nbsp;/g, ' ')\n  .replace(/&amp;/g, '&')\n  .replace(/&quot;/g, '\"')\n  .replace(/&#39;/g, \"'\")\n  .replace(/\\s+/g, ' ')\n  .trim();\n\nconst form = $('Blog Input Form').first().json;\n\nreturn [{\n  json: {\n    article_text: (pageTitle ? pageTitle + '\\n\\n' : '') + text.slice(0, 8000),\n    blog_url: form['Blog URL'] || '',\n    featured_image_url: form['Featured Image URL'] || ''\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "205778fa-d81c-4b2f-8dbd-4f859fd23d6b",
      "name": "Use Pasted Content",
      "type": "n8n-nodes-base.code",
      "position": [
        1088,
        752
      ],
      "parameters": {
        "jsCode": "const form = $('Blog Input Form').first().json;\n\nreturn [{\n  json: {\n    article_text: String(form['Blog Content'] || '').slice(0, 8000),\n    blog_url: form['Blog URL'] || '',\n    featured_image_url: form['Featured Image URL'] || ''\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "1877e575-c86e-44b6-b92d-57c3862b5016",
      "name": "Build Prompt",
      "type": "n8n-nodes-base.code",
      "position": [
        1680,
        608
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nconst article = String(item.article_text || '').trim();\nconst blogUrl = item.blog_url || '';\n\nif (!article) {\n  throw new Error('No blog content found. Fill in either the Blog URL field or the Blog Content field in the form.');\n}\n\nconst prompt = [\n  'Blog Content:',\n  article,\n  '',\n  'Blog URL (include this as the call to action link where it fits naturally): ' + (blogUrl || 'not provided'),\n  '',\n  'Write platform-specific social media captions promoting this blog. Return strictly in JSON:',\n  '{',\n  '  \"linkedin\": \"professional tone, under 200 words, strong first line hook, ends with a question, 3 hashtags\",',\n  '  \"twitter\": \"under 260 characters, hook-driven, 2 hashtags\",',\n  '  \"twitter_thread\": [array of 5 to 7 tweets, each under 270 characters, tweet 1 is the hook, last tweet has the blog link and a call to action],',\n  '  \"facebook\": \"conversational tone, under 150 words, no hashtags\",',\n  '  \"instagram\": \"casual tone, under 150 words, line breaks for readability, 8 relevant hashtags at the end\",',\n  '  \"threads\": \"casual and punchy, under 480 characters\",',\n  '  \"pinterest\": { \"title\": \"under 90 characters, keyword rich\", \"description\": \"under 480 characters, keyword rich, natural language\" },',\n  '  \"youtube_community\": \"under 120 words, friendly, ends with a question to drive comments\",',\n  '  \"reddit\": { \"title\": \"under 280 characters, no clickbait, states the value plainly\", \"body\": \"under 150 words, genuinely useful and non-promotional in tone, mentions the link once at the end\" },',\n  '  \"image_alt_text\": \"under 120 characters, descriptive alt text for the featured image\"',\n  '}',\n  '',\n  'Rules:',\n  '- Adapt tone per platform but keep the core message consistent.',\n  '- Every caption must give a reason to click through to the blog.',\n  '- No generic filler like \"check this out\".',\n  '- Do not use em dashes anywhere in the output.',\n  '- Return the JSON object only, no explanations and no markdown code fences.'\n].join('\\n');\n\nreturn [{\n  json: {\n    prompt,\n    blog_url: blogUrl,\n    featured_image_url: item.featured_image_url || ''\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "8676aa47-4f4b-4314-ae82-4d3361b1601b",
      "name": "Generate Captions",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1936,
        608
      ],
      "parameters": {
        "text": "={{ $json.prompt }}",
        "options": {
          "systemMessage": "You are a social media content strategist. You write platform-native captions that sound human, never templated.\n\nYou always respond with a single valid JSON object and nothing else. No markdown code fences, no preamble, no explanation before or after the JSON."
        },
        "promptType": "define"
      },
      "typeVersion": 2
    },
    {
      "id": "25479efc-9a12-4322-aa88-e27fda730dc6",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1936,
        832
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "gpt-4o-mini"
        },
        "options": {
          "temperature": 0.7,
          "responseFormat": "json_object"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "969b6714-a4f8-40cd-87e7-2008653ba075",
      "name": "Build Copy-Paste Page",
      "type": "n8n-nodes-base.code",
      "position": [
        2544,
        608
      ],
      "parameters": {
        "jsCode": "const res = $input.first().json;\n\nlet raw = res.output ?? res.text ?? res.content ?? '';\nif (typeof raw === 'object' && raw !== null) {\n  raw = JSON.stringify(raw);\n}\nraw = String(raw).replace(/```json/gi, '').replace(/```/g, '').trim();\n\nlet c;\ntry {\n  c = JSON.parse(raw);\n} catch (e) {\n  return [{ json: { html: '<html><body style=\"font-family:sans-serif;padding:32px\"><h2>Could not parse the AI response</h2><pre style=\"white-space:pre-wrap;background:#f4f5f7;padding:16px;border-radius:8px\">' + raw.replace(/</g, '&lt;') + '</pre></body></html>' } }];\n}\n\nconst form = $('Blog Input Form').first().json;\nconst imageUrl = form['Featured Image URL'] || '';\nconst blogUrl = form['Blog URL'] || '';\n\nconst esc = (s) => String(s == null ? '' : s)\n  .replace(/&/g, '&amp;')\n  .replace(/</g, '&lt;')\n  .replace(/>/g, '&gt;')\n  .replace(/\"/g, '&quot;');\n\nlet cardId = 0;\nconst card = (platform, body, note) => {\n  if (!body) return '';\n  cardId++;\n  const id = 'cap' + cardId;\n  return '<div class=\"card\">'\n    + '<div class=\"card-head\"><span class=\"platform\">' + esc(platform) + '</span>'\n    + '<button class=\"copy-btn\" data-target=\"' + id + '\">Copy</button></div>'\n    + (note ? '<div class=\"note\">' + esc(note) + '</div>' : '')\n    + '<pre id=\"' + id + '\" class=\"caption\">' + esc(body) + '</pre>'\n    + '</div>';\n};\n\nconst threadText = Array.isArray(c.twitter_thread)\n  ? c.twitter_thread.map((t, i) => (i + 1) + '/' + c.twitter_thread.length + ' ' + t).join('\\n\\n')\n  : '';\n\nconst attachNote = imageUrl ? 'Attach the featured image when posting' : '';\n\nlet cards = '';\ncards += card('LinkedIn', c.linkedin, attachNote);\ncards += card('Twitter / X (single post)', c.twitter, attachNote);\ncards += card('Twitter / X (thread)', threadText, 'Post each numbered block as a separate tweet in one thread');\ncards += card('Facebook', c.facebook, attachNote);\ncards += card('Instagram', c.instagram, imageUrl ? 'Image required. Use the featured image above.' : 'Instagram needs an image. Add one before posting.');\ncards += card('Threads', c.threads, attachNote);\nif (c.pinterest) {\n  cards += card('Pinterest title', c.pinterest.title, '');\n  cards += card('Pinterest description', c.pinterest.description, imageUrl ? 'Image required. Use the featured image above.' : 'Pinterest needs an image. Add one before posting.');\n}\ncards += card('YouTube Community post', c.youtube_community, '');\nif (c.reddit) {\n  cards += card('Reddit title', c.reddit.title, '');\n  cards += card('Reddit body', c.reddit.body, 'Check the subreddit rules on self-promotion before posting');\n}\ncards += card('Image alt text', c.image_alt_text, 'Paste this into the alt text field on any platform that supports it');\n\nconst imageBlock = imageUrl\n  ? '<div class=\"imgwrap\"><div class=\"imglabel\">Featured image</div><img src=\"' + esc(imageUrl) + '\" alt=\"' + esc(c.image_alt_text || '') + '\"><div class=\"imgurl\">' + esc(imageUrl) + '</div></div>'\n  : '<div class=\"imgwrap noimg\">No featured image was provided. Instagram and Pinterest both require an image before you can post.</div>';\n\nconst blogLine = blogUrl\n  ? '<div class=\"bloglink\">Source: <a href=\"' + esc(blogUrl) + '\" target=\"_blank\" rel=\"noopener\">' + esc(blogUrl) + '</a></div>'\n  : '';\n\nconst styles = 'body{font-family:-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif;background:#f4f5f7;color:#1f2328;margin:0;padding:32px 16px}'\n  + '*{box-sizing:border-box}'\n  + '.wrap{max-width:820px;margin:0 auto}'\n  + 'h1{font-size:24px;margin:0 0 6px}'\n  + '.sub{color:#57606a;font-size:14px;margin-bottom:24px}'\n  + '.bloglink{font-size:13px;margin-bottom:20px}.bloglink a{color:#0969da}'\n  + '.imgwrap{background:#fff;border:1px solid #d8dee4;border-radius:10px;padding:16px;margin-bottom:24px}'\n  + '.imgwrap.noimg{color:#9a6700;background:#fff8c5;border-color:#eac54f;font-size:14px}'\n  + '.imglabel{font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#57606a;margin-bottom:10px}'\n  + '.imgwrap img{max-width:100%;border-radius:8px;display:block}'\n  + '.imgurl{font-size:12px;color:#57606a;margin-top:8px;word-break:break-all}'\n  + '.card{background:#fff;border:1px solid #d8dee4;border-radius:10px;margin-bottom:16px;overflow:hidden}'\n  + '.card-head{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #eaeef2;background:#f6f8fa}'\n  + '.platform{font-weight:600;font-size:14px}'\n  + '.copy-btn{background:#1f2328;color:#fff;border:0;border-radius:6px;padding:6px 14px;font-size:13px;cursor:pointer}'\n  + '.copy-btn:hover{background:#444c56}.copy-btn.done{background:#1a7f37}'\n  + '.note{padding:8px 16px;font-size:12px;color:#9a6700;background:#fff8c5;border-bottom:1px solid #eaeef2}'\n  + '.caption{margin:0;padding:16px;white-space:pre-wrap;word-wrap:break-word;font-family:inherit;font-size:14px;line-height:1.6}';\n\nconst script = 'document.querySelectorAll(\".copy-btn\").forEach(function(btn){'\n  + 'btn.addEventListener(\"click\",function(){'\n  + 'var el=document.getElementById(btn.getAttribute(\"data-target\"));'\n  + 'var text=el.innerText;'\n  + 'var done=function(){var old=btn.innerText;btn.innerText=\"Copied\";btn.classList.add(\"done\");'\n  + 'setTimeout(function(){btn.innerText=old;btn.classList.remove(\"done\");},1600);};'\n  + 'if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(text).then(done);}'\n  + 'else{var ta=document.createElement(\"textarea\");ta.value=text;document.body.appendChild(ta);ta.select();'\n  + 'document.execCommand(\"copy\");document.body.removeChild(ta);done();}'\n  + '});});';\n\nconst html = '<!DOCTYPE html><html><head><meta charset=\"utf-8\">'\n  + '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">'\n  + '<title>Your Social Media Captions</title><style>' + styles + '</style></head><body><div class=\"wrap\">'\n  + '<h1>Your social media captions</h1>'\n  + '<div class=\"sub\">Copy any caption below and paste it on the platform. Nothing is posted automatically.</div>'\n  + blogLine + imageBlock + cards\n  + '</div><script>' + script + '<\\/script></body></html>';\n\nreturn [{ json: { html } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "c112c514-168a-42c7-a0dc-692f60138954",
      "name": "Show Captions",
      "type": "n8n-nodes-base.form",
      "position": [
        2800,
        608
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "showText",
        "responseText": "={{ $json.html }}"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "a02f81cd-8914-4eca-849e-7248bbb669fd",
  "nodeGroups": [],
  "connections": {
    "Build Prompt": {
      "main": [
        [
          {
            "node": "Generate Captions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Blog URL?": {
      "main": [
        [
          {
            "node": "Fetch Blog Page",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Use Pasted Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Blog Input Form": {
      "main": [
        [
          {
            "node": "Has Blog URL?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Blog Page": {
      "main": [
        [
          {
            "node": "Extract Article Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Captions": {
      "main": [
        [
          {
            "node": "Build Copy-Paste Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate Captions",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Use Pasted Content": {
      "main": [
        [
          {
            "node": "Build Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Article Text": {
      "main": [
        [
          {
            "node": "Build Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Copy-Paste Page": {
      "main": [
        [
          {
            "node": "Show Captions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}