{
  "name": "Markdown \u2192 Confluence Pages (Create or Update, Multi-Page)",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "markdown-to-confluence",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "k1000000-0000-0000-0000-000000000001",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "function mdToConfluenceHtml(md) {\n  const lines = md.replace(/\\r/g, '').split('\\n');\n  let html = '', inCodeBlock = false, codeContent = '', codeLang = 'text', inList = false;\n  // Escape XML special chars in text BEFORE applying inline markdown, otherwise a bare\n  // & / < / > in prose (e.g. \"Andrew Hunt & David Thomas\") produces invalid storage XML,\n  // which Confluence rejects with the misleading \"Content contains unsupported extensions\".\n  function fmt(t) { t = t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); return t.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>').replace(/\\*(.+?)\\*/g, '<em>$1</em>').replace(/`([^`]+)`/g, '<code>$1</code>').replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\">$1</a>'); }\n  for (let i = 0; i < lines.length; i++) {\n    const line = lines[i];\n    if (line.startsWith('```') && !inCodeBlock) { if (inList) { html += '</ul>'; inList = false; } inCodeBlock = true; codeLang = line.slice(3).trim() || 'text'; codeContent = ''; continue; }\n    if (line.startsWith('```') && inCodeBlock) { inCodeBlock = false; var __mm = (typeof $env !== 'undefined' && $env.CONFLUENCE_MERMAID_MACRO) ? $env.CONFLUENCE_MERMAID_MACRO : 'mermaid-cloud'; if (codeLang.toLowerCase() === 'mermaid') { html += '<ac:structured-macro ac:name=\"' + __mm + '\"><ac:plain-text-body><![CDATA[' + codeContent.trim() + ']]></ac:plain-text-body></ac:structured-macro>'; } else { html += '<ac:structured-macro ac:name=\"code\"><ac:parameter ac:name=\"language\">' + codeLang + '</ac:parameter><ac:plain-text-body><![CDATA[' + codeContent.trim() + ']]></ac:plain-text-body></ac:structured-macro>'; } continue; }\n    if (inCodeBlock) { codeContent += line + '\\n'; continue; }\n    if (line.match(/^\\|.+\\|$/) && i + 1 < lines.length && lines[i+1].match(/^\\|[-| :]+\\|$/)) {\n      if (inList) { html += '</ul>'; inList = false; }\n      const headers = line.split('|').filter(c => c.trim()).map(c => '<th>' + fmt(c.trim()) + '</th>').join('');\n      html += '<table><thead><tr>' + headers + '</tr></thead><tbody>'; i++;\n      while (i + 1 < lines.length && lines[i+1].match(/^\\|.+\\|$/)) { i++; html += '<tr>' + lines[i].split('|').filter(c => c.trim()).map(c => '<td>' + fmt(c.trim()) + '</td>').join('') + '</tr>'; }\n      html += '</tbody></table>'; continue;\n    }\n    const hm = line.match(/^(#{1,6}) (.+)$/);\n    if (hm) { if (inList) { html += '</ul>'; inList = false; } html += '<h' + hm[1].length + '>' + fmt(hm[2]) + '</h' + hm[1].length + '>'; continue; }\n    if (line.match(/^---+$/)) { if (inList) { html += '</ul>'; inList = false; } html += '<hr/>'; continue; }\n    if (line.startsWith('> ')) { if (inList) { html += '</ul>'; inList = false; } html += '<blockquote><p>' + fmt(line.slice(2)) + '</p></blockquote>'; continue; }\n    if (line.match(/^- \\[([ xX])\\] /)) {\n      if (inList) { html += '</ul>'; inList = false; }\n      html += '<ac:task-list>';\n      while (i < lines.length && lines[i].match(/^- \\[([ xX])\\] /)) { const cm = lines[i].match(/^- \\[([ xX])\\] (.+)/); html += '<ac:task><ac:task-status>' + ((cm[1]==='x'||cm[1]==='X') ? 'complete' : 'incomplete') + '</ac:task-status><ac:task-body>' + fmt(cm[2]) + '</ac:task-body></ac:task>'; i++; }\n      html += '</ac:task-list>'; continue;\n    }\n    if (line.match(/^[-*] /)) { if (!inList) { html += '<ul>'; inList = true; } html += '<li>' + fmt(line.replace(/^[-*] /, '')) + '</li>'; continue; }\n    if (line.trim() === '') { if (inList) { html += '</ul>'; inList = false; } continue; }\n    if (inList) { html += '</ul>'; inList = false; }\n    html += '<p>' + fmt(line) + '</p>';\n  }\n  if (inList) html += '</ul>';\n  return html;\n}\n\nfunction extractPageId(val) { if (!val) return ''; val = val.trim(); const m = val.match(/\\/pages\\/(\\d+)/); if (m) return m[1]; return val.match(/^\\d+$/) ? val : val; }\n\nconst body = $input.first().json.body;\nconst pages = body.pages || [];\nconst labelsStr = body.labels || '';\nlet parentPageId = extractPageId(body.parentPageId || '') || ($env.CONFLUENCE_PARENT_PAGE_ID || '');\nconst date = new Date().toISOString().split('T')[0];\nconst labels = ['n8n-pipeline-generated'];\nif (labelsStr) labels.push(...labelsStr.split(',').map(l => l.trim()).filter(Boolean));\nconst labelsJson = labels.map(l => ({ prefix: 'global', name: l }));\n\nif (pages.length === 0 && body.pageMarkdown) {\n  pages.push({ markdown: body.pageMarkdown, pageId: body.pageId || '', title: body.title || '' });\n  const sms = body.sectionMarkdowns || []; for (const s of sms) { if (s.trim()) pages[0].markdown += '\\n\\n---\\n\\n' + s; }\n}\nif (pages.length === 0) return [{ json: { error: 'No pages provided' } }];\n\n// Process ALL pages sequentially in this single Code node.\n// For creates: build POST body. For updates: fetch version via API then build PUT body.\nconst confBase = $env.CONFLUENCE_BASE_URL;\nconst spaceKey = $env.CONFLUENCE_SPACE_KEY;\nconst results = [];\n\nfor (let idx = 0; idx < pages.length; idx++) {\n  const pg = pages[idx];\n  const md = pg.markdown || '';\n  const pageId = extractPageId(pg.pageId || '');\n  let title = pg.title || '';\n  if (!title) { const tm = md.replace(/\\r/g, '').match(/^# (.+)$/m); title = tm ? tm[1] : 'Page ' + (idx + 1); }\n  let html = mdToConfluenceHtml(md);\n  html += '<hr/><p><em>Published from markdown by <a href=\"https://github.com/openmindednewby/ai-confluence-pipeline\">ai-confluence-pipeline</a> on ' + date + '</em></p>';\n\n  if (pageId) {\n    // Update: output GET request first, then PUT\n    results.push({ json: { step: 'get-version', pageId, title, html, pageIndex: idx, method: 'GET', url: confBase + '/wiki/rest/api/content/' + pageId + '?expand=version', apiBody: null, isUpdate: true } });\n  } else {\n    // Create: output POST body\n    const confBody = { type: 'page', title, space: { key: spaceKey }, body: { storage: { value: html, representation: 'storage' } }, metadata: { labels: labelsJson } };\n    if (parentPageId) confBody.ancestors = [{ id: parentPageId }];\n    results.push({ json: { step: 'create', pageId: '', title, html, pageIndex: idx, method: 'POST', url: confBase + '/wiki/rest/api/content', apiBody: confBody, isUpdate: false } });\n  }\n}\n\nreturn results;"
      },
      "id": "k1000000-0000-0000-0000-000000000002",
      "name": "Prepare Pages",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "is-get",
              "leftValue": "={{ $json.step }}",
              "rightValue": "get-version",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "k1000000-0000-0000-0000-000000000031",
      "name": "Needs Version?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "options": {
          "timeout": 15000
        }
      },
      "id": "k1000000-0000-0000-0000-000000000011",
      "name": "Get Page Version",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        900,
        150
      ],
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// For each update item: get the version from HTTP response, get our data from the matching Prepare Pages item\nconst versionResponses = $input.all();\nconst prepItems = $('Needs Version?').all();\n\nreturn versionResponses.map((vr, i) => {\n  const currentVersion = vr.json.version?.number || 1;\n  const prep = prepItems[i]?.json || {};\n  const updateBody = {\n    type: 'page',\n    title: prep.title,\n    space: { key: $env.CONFLUENCE_SPACE_KEY },\n    body: { storage: { value: prep.html, representation: 'storage' } },\n    version: { number: currentVersion + 1 }\n  };\n  return { json: { method: 'PUT', url: $env.CONFLUENCE_BASE_URL + '/wiki/rest/api/content/' + prep.pageId, apiBody: updateBody, title: prep.title, pageId: prep.pageId, isUpdate: true, pageIndex: prep.pageIndex } };\n});"
      },
      "id": "k1000000-0000-0000-0000-000000000012",
      "name": "Build Update Bodies",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        150
      ]
    },
    {
      "parameters": {
        "method": "={{ $json.method }}",
        "url": "={{ $json.url }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.apiBody) }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "k1000000-0000-0000-0000-000000000004",
      "name": "Create or Update Page",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1340,
        300
      ],
      "credentials": {
        "httpBasicAuth": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const allPrepped = $('Prepare Pages').all();\nconst allResults = $input.all();\n\nconst pages = allPrepped.map((prep, i) => {\n  const result = allResults[i]?.json || {};\n  const url = (result._links?.base || '') + (result._links?.webui || '');\n  return {\n    id: result.id || prep.json.pageId,\n    title: prep.json.title,\n    url: url,\n    action: prep.json.isUpdate ? 'updated' : 'created'\n  };\n});\n\nreturn [{ json: { success: true, pages, pageCount: pages.length } }];"
      },
      "id": "k1000000-0000-0000-0000-000000000005",
      "name": "Aggregate Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1560,
        300
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}"
      },
      "id": "k1000000-0000-0000-0000-000000000006",
      "name": "Respond to Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1780,
        300
      ]
    }
  ],
  "connections": {
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Prepare Pages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Pages": {
      "main": [
        [
          {
            "node": "Needs Version?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Needs Version?": {
      "main": [
        [
          {
            "node": "Get Page Version",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Create or Update Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Page Version": {
      "main": [
        [
          {
            "node": "Build Update Bodies",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Update Bodies": {
      "main": [
        [
          {
            "node": "Create or Update Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create or Update Page": {
      "main": [
        [
          {
            "node": "Aggregate Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Results": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "tags": [
    {
      "name": "ai-pipeline"
    },
    {
      "name": "confluence"
    },
    {
      "name": "markdown"
    }
  ]
}