{
  "nodes": [
    {
      "id": "863c7ba9-dcbe-4cbb-a451-aff131934ef1",
      "name": "Get a database page",
      "type": "n8n-nodes-base.notion",
      "position": [
        -448,
        208
      ],
      "parameters": {
        "pageId": {
          "__rl": true,
          "mode": "url",
          "value": "={{ $json.notion_url }}"
        },
        "resource": "databasePage",
        "operation": "get"
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "80ccc7d0-f503-48c3-bf8f-5c98edcbe543",
      "name": "Get many child blocks",
      "type": "n8n-nodes-base.notion",
      "position": [
        -224,
        208
      ],
      "parameters": {
        "blockId": {
          "__rl": true,
          "mode": "url",
          "value": "={{ $json.url }}"
        },
        "resource": "block",
        "operation": "getAll",
        "returnAll": true,
        "simplifyOutput": false,
        "fetchNestedBlocks": true
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b62bd11d-eafb-49f5-af62-948ea9dea525",
      "name": "decode blocks",
      "type": "n8n-nodes-base.code",
      "position": [
        224,
        208
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const c = $json.content\n\nswitch ($json.type) {\n  case 'code':\n    $json.content = $json.code.text[0].plain_text\n    break;\n  case 'video':\n    if ($json.video.external.url.match('vimeo'))\n    // $json.content = `[vimeo src=\"${$json.video.external.url}\"]`\n      $json.content = `<!-- wp:shortcode -->[embed]${$json.video.external.url}[/embed]\n<!-- /wp:shortcode -->`\n    else if ($json.video.external.url.match('youtu'))\n      $json.content = `<!-- wp:shortcode -->\n[embed]${$json.video.external.url}[/embed]\n<!-- /wp:shortcode -->`\n    break\n  case 'heading_3':\n    $json.content = `<!-- wp:heading {\"level\":3} -->\n<h3>${$json.heading_3.text[0].plain_text}</h3>\n<!-- /wp:heading -->`\n    break\n  case 'heading_2':\n    $json.content = `<!-- wp:heading {\"level\":2} -->\n<h2>${$json.heading_2.text[0].plain_text}</h2>\n<!-- /wp:heading -->`\n    break\n  case 'heading_1':\n    $json.content = `<!-- wp:heading {\"level\":2} -->\n<h1>${$json.heading_1.text[0].plain_text}</h1>\n<!-- /wp:heading -->`\n    break\n  case 'paragraph':\n    $json.content = `\n<!-- wp:paragraph -->\n<p>${c}</p>\n<!-- /wp:paragraph -->`\n    break\n  case 'numbered_list_item':\n    $json.content = `\n<!-- wp:list {\"ordered\": true} -->\n<ol class=\\\"wp-block-list\\\">\n<!-- wp:list-item -->\n<li>${c}</li>\n<!-- /wp:list-item -->\n</ol>\n<!-- /wp:list -->`\n    break\n  case 'bulleted_list_item':\n    $json.content = `<!-- wp:list -->\n<ul class=\\\"wp-block-list\\\">\n<!-- wp:list-item -->\n<li>${c}</li>\n<!-- /wp:list-item -->\n</ul>\n<!-- /wp:list -->`\n    break\n  case 'image':\n    $json.content = `<figure class=\"wp-block-image\">\n<img src=\"${$json?.image?.external?.url || $json?.image?.file?.url}\" alt=\"\"/>\n</figure>`\n    break\n  case 'audio':\n    $json.content = `<!-- wp:audio -->\n<figure class=\"wp-block-audio\">\n  <audio controls src=\"${$json?.audio?.external?.url || $json?.audio?.file?.url}\"></audio>\n</figure>\n<!-- /wp:audio -->`\n    break\n  case 'embed':\n    $json.content = ` <!-- wp:embed {\"url\":\"${$json.embed.url}\"} /-->`\n    break\n  case 'divider':\n    $json.content = `<hr class=\"wp-block-separator\"/>`\n    break\n  default:\n    $json.content = `<!-- wp:html -->\n${$json.content}\n<!-- /wp:html -->`\n    break\n}\n\n\nreturn $json"
      },
      "typeVersion": 2
    },
    {
      "id": "11fb7b82-a913-4792-a33b-dff0751512af",
      "name": "decode paragraphs",
      "type": "n8n-nodes-base.code",
      "position": [
        0,
        208
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const annotateText = (t) => {\n  let v = t?.text?.content || t.plain_text\n  const a = t.annotations\n  v = a.bold ? `<strong>${v}</strong>` : v\n  v = a.italic ? `<em>${v}</em>` : v\n  v = a.strikethrough ? '<s>${v}</s>' : v\n  v = a.underline ? '<u>${v}</u>' : v\n  if (t?.text?.link) {\n    v = `<a href=\"${t.text.link.url}\">${v}</a>`\n  }\n  return v\n}\n\nswitch ($json.type) {\n  case 'paragraph':\n  case 'numbered_list_item':\n  case 'bulleted_list_item':\n    $json.content = $json[$json.type]\n      .text\n      .map(annotateText)\n      .join(' ')\n    break\n}\n\nreturn $json"
      },
      "typeVersion": 2
    },
    {
      "id": "bd2edec6-ffc7-4e80-884d-4fbc9ecb30a7",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1344,
        208
      ],
      "parameters": {
        "options": {
          "mergeLists": true
        },
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "wp"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "1d83b6b5-9bbe-4a8d-8a79-82ad98b7a28a",
      "name": "When Executed by Another Workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        -672,
        304
      ],
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "notion_url"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "1754c892-4aae-46f2-a90c-40fff5d9c88e",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -896,
        112
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "03f0e6ad-8a8b-40c0-a392-4441274b61f0",
      "name": "Edit Fields2",
      "type": "n8n-nodes-base.set",
      "position": [
        -672,
        112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "bb704bf6-9a59-49f0-872b-56469fa52263",
              "name": "notion_url",
              "type": "string",
              "value": ""
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "ef9e41a2-38b9-4a9e-87f6-bb19aade231f",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -496,
        -176
      ],
      "parameters": {
        "color": 6,
        "width": 416,
        "height": 656,
        "content": "## \ud83d\udce5 Extract: Fetch Notion Content\n\nThis stage retrieves all the content from the provided Notion page URL.\n\n**Get a database page:** Fetches the main page object. This is needed to get the page's canonical ID, which is used later for filtering.\n\n**Get many child blocks:** Fetches all blocks on the page. Crucially, the fetchNestedBlocks: true option is enabled, so it recursively gets all content, including blocks inside columns, toggles, etc."
      },
      "typeVersion": 1
    },
    {
      "id": "2af6a568-782c-44ed-b04c-5356f67f1139",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        -176
      ],
      "parameters": {
        "color": 5,
        "width": 880,
        "height": 656,
        "content": "## \u2699\ufe0f Transform: Convert Notion to Gutenberg HTML\n\nThis is the core logic of the workflow. It iterates over every block (in multiple passes) to build the final HTML.\n\n1. **decode paragraphs (Code):** Handles rich text. It loops through text-based blocks (paragraphs, lists) and converts Notion's annotations array (bold, italic, strikethrough, underline, and links) into the corresponding HTML tags (e.g., <strong>, <em>, <a href=\"...\">).\n\n2. **decode blocks (Code):** A large switch statement that maps each Notion block type (e.g., heading_1, image, video, divider) to its correct WordPress Gutenberg block structure (e.g., <figure class=\"wp-block-image\">..., <hr class=\"wp-block-separator\"/>).\n\n3. **drop unnecessary fields (Set):** Cleans up the data after conversion, keeping only the fields needed for the next step (id, parent_id, type, and the new content field).\n\n4. **nested blocks (Code):** This is a critical step for handling layouts. It runs once on all blocks to correctly rebuild column_list and column blocks. It finds all child blocks of each column and injects their HTML inside the parent column_list block, wrapping them in the correct and tags."
      },
      "typeVersion": 1
    },
    {
      "id": "18124fd4-c9f7-4ebc-9353-d5fde739cca3",
      "name": "drop unnecessary fields",
      "type": "n8n-nodes-base.set",
      "position": [
        448,
        208
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "0630be0c-738a-4f1b-b782-67de592305dc",
              "name": "id",
              "type": "string",
              "value": "={{ $json.id }}"
            },
            {
              "id": "71be8325-4dc1-4567-972a-74ee46c7fe50",
              "name": "parent_id",
              "type": "string",
              "value": "={{ $json.parent_id }}"
            },
            {
              "id": "f1629c55-71fa-4c4f-bb94-c09714827475",
              "name": "type",
              "type": "string",
              "value": "={{ $json.type }}"
            },
            {
              "id": "32db7ae9-9b52-46d8-a75d-61d6e2ba9594",
              "name": "content",
              "type": "string",
              "value": "={{ $json.content }}"
            },
            {
              "id": "efcfcaa9-4e72-4f57-8740-c89a68eceb0c",
              "name": "has_children",
              "type": "boolean",
              "value": "={{ $json.has_children }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "fd03cfb2-3fc1-4dff-ba44-d40d64cb5f5c",
      "name": "nested blocks",
      "type": "n8n-nodes-base.code",
      "position": [
        672,
        208
      ],
      "parameters": {
        "jsCode": "const all = $input.all()\n\nfor (const item of all) {\n  if (item.json.type !== 'column') continue\n\n  item.json.children = all\n    .filter(_ => _.json.parent_id == item.json.id)\n    .map(_ => _.json.content)\n    .join(\"\\n\")\n  item.json.children = \"<!-- wp:column -->\\n<div class=\\\"wp-block-column\\\">\\n\" + item.json.children + \"\\n</div>\\n<!-- /wp:column -->\\n\"\n}\n\nfor (const item of all) {\n  if (item.json.type !== 'column_list') continue\n\n  item.json.children = all\n    .filter(_ => _.json.parent_id == item.json.id)\n    .map(_ => _.json.children)\n    .join(\"\\n\")\n  item.json.children = \"<!-- wp:columns -->\\n<div class=\\\"wp-block-columns\\\">\\n\" + item.json.children + \"\\n</div>\\n<!-- /wp:columns -->\\n\"\n}\n\nreturn all"
      },
      "typeVersion": 2
    },
    {
      "id": "774f0093-478f-4eb9-979d-0068dba63ff3",
      "name": "choose content field",
      "type": "n8n-nodes-base.set",
      "position": [
        1120,
        208
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "bb93822c-5868-49a4-abbe-d72d8868fc47",
              "name": "wp",
              "type": "string",
              "value": "={{ $json.children || $json.content }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "bbdfd21e-df44-4ad8-b57a-08f6691c8536",
      "name": "only top level blocks",
      "type": "n8n-nodes-base.filter",
      "position": [
        896,
        208
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "2de1606e-f16f-4455-9c8e-8ccb54ce197c",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.parent_id.replace(/-/g, '') }}",
              "rightValue": "={{ $('Get a database page').item.json.id.replace(/-/g,'') }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "43a66246-0dfb-445d-b55f-da2c6d8be17a",
      "name": "join lines",
      "type": "n8n-nodes-base.set",
      "position": [
        1568,
        208
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "b995ade4-ee5c-4d7a-89a5-f01b039f7e62",
              "name": "wp",
              "type": "string",
              "value": "={{ $json.wp.join(\"\\n\") }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "8c7a766d-5499-47d5-abbf-451488ea24be",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        848,
        -176
      ],
      "parameters": {
        "color": 4,
        "width": 928,
        "height": 656,
        "content": "## \ud83e\udde9 Finalize: Assemble the Post Body\n\nThis stage takes all the individual HTML block strings and assembles them into a single, complete HTML document ready for WordPress.\n\n**only top level blocks (Filter):** Filters the list to only include blocks that are direct children of the page (i.e., their parent_id matches the main page ID). This is essential because all nested blocks (like those in columns) have already been moved inside their parent block's content in the previous step.\n\n**choose content field (Set):** Creates a single, consistently-named field wp that holds the final HTML for each block. It uses ($json.children || $json.content) to ensure it gets the content from nested blocks (like columns) or regular blocks.\n\n**Aggregate:** Merges all the separate items (each with its wp HTML string) into a single item containing an array of all the HTML strings.\n\n**join lines (Set):** Joins the array of HTML strings together, separated by newlines (\\n), into one final text string. This string is in the wp field and is the final output of the sub-workflow."
      },
      "typeVersion": 1
    },
    {
      "id": "31568ee0-614f-4563-b831-f389e0dc0cef",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1568,
        80
      ],
      "parameters": {
        "width": 1040,
        "height": 400,
        "content": "## \ud83d\ude80 Triggers (How to Run)\n\nThis workflow is designed as a sub-workflow.\n\n### Manual Test:\n\nClick \"Execute workflow\".\n\nYou must paste a valid Notion page URL into the Edit Fields2 node first.\n\n### Production (Sub-workflow):\n\nTrigger this from a parent workflow using the When Executed by Another Workflow trigger.\n\nYour parent workflow must pass a JSON object containing a notion_url key."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Aggregate": {
      "main": [
        [
          {
            "node": "join lines",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields2": {
      "main": [
        [
          {
            "node": "Get a database page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "decode blocks": {
      "main": [
        [
          {
            "node": "drop unnecessary fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "nested blocks": {
      "main": [
        [
          {
            "node": "only top level blocks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "decode paragraphs": {
      "main": [
        [
          {
            "node": "decode blocks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get a database page": {
      "main": [
        [
          {
            "node": "Get many child blocks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "choose content field": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get many child blocks": {
      "main": [
        [
          {
            "node": "decode paragraphs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "only top level blocks": {
      "main": [
        [
          {
            "node": "choose content field",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "drop unnecessary fields": {
      "main": [
        [
          {
            "node": "nested blocks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Get a database page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Edit Fields2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}