{
  "id": "GNd5jFx8yDJEitDg",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Google Drive to WordPress: Auto-Publish Post with Advanced RankMath SEO & Gemini Analysis",
  "tags": [],
  "nodes": [
    {
      "id": "381d7d21-a665-438a-95e4-6f3ae037e6cd",
      "name": "HTTP Request (Export HTML)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        96,
        240
      ],
      "parameters": {
        "url": "=https://www.googleapis.com/drive/v3/files/{{ $('Google Drive Trigger').item.json.id }}/export ",
        "options": {},
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {
              "name": "supportsAllDrives",
              "value": "true"
            },
            {
              "name": "mimeType",
              "value": "text/html"
            }
          ]
        },
        "nodeCredentialType": "googleDriveOAuth2Api"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "fbc679cb-f5ee-43bb-88b9-df5e63b4b551",
      "name": "Code (Extract Body)",
      "type": "n8n-nodes-base.code",
      "position": [
        272,
        240
      ],
      "parameters": {
        "jsCode": "const raw = $node[\"HTTP Request (Export HTML)\"].json.data;\n\n// 1) Extract body content\nconst bodyMatch = raw.match(/<body[^>]*>([\\s\\S]*?)<\\/body>/i);\nlet body = bodyMatch ? bodyMatch[1] : raw;\n\n// 2) remove unnecessary <style> and tags.\nbody = body\n  .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n  .replace(/<meta[^>]*>/gi, '')\n  .replace(/<\\/?html[^>]*>/gi, '')\n  .replace(/<\\/?head[^>]*>/gi, '')\n  .replace(/<\\/?body[^>]*>/gi, '')\n  .trim();\n\n// 3) strip unnecessary attributes \nbody = body\n  .replace(/\\s(style|class|id|dir|lang|width|height|border|cellpadding|cellspacing)=(\".*?\"|'.*?'|[^\\s>]+)/gi, '')\n  .replace(/\\s(data-[a-z0-9_-]+|aria-[a-z0-9_-]+)=(\".*?\"|'.*?'|[^\\s>]+)/gi, '');\n\n// 4) Peel off the span wrapper (repeat several times)\nfor (let i = 0; i < 5; i++) {\n  body = body.replace(/<span[^>]*>([\\s\\S]*?)<\\/span>/gi, '$1');\n}\n\n// 5) Remove empty paragraphs around <hr>\nbody = body\n  .replace(/<p>\\s*<\\/p>/gi, '')\n  .replace(/<hr>\\s*/gi, '<hr>');\n\nfunction decodeHtmlEntities(str) {\n  if (typeof str !== 'string') return str;\n\n  // numeric entities\n  str = str.replace(/&#(\\d+);/g, (_, n) => String.fromCodePoint(parseInt(n, 10)));\n  str = str.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)));\n\n  // named entities (Latin-1 + some common ones)\n  const map = {\n    '&quot;': '\"', '&apos;': \"'\", '&amp;': '&', '&lt;': '<', '&gt;': '>',\n    '&nbsp;': ' ',\n    '&ndash;': '\u2013', '&mdash;': '\u2014', '&hellip;': '\u2026',\n\n    '&lsquo;': '\u2018', '&rsquo;': '\u2019', '&ldquo;': '\u201c', '&rdquo;': '\u201d',\n    '&laquo;': '\u00ab', '&raquo;': '\u00bb',\n\n    // Latin-1 letters (enough to handle &ocirc; &acirc; &agrave; ...)\n    '&Agrave;':'\u00c0','&Aacute;':'\u00c1','&Acirc;':'\u00c2','&Atilde;':'\u00c3','&Auml;':'\u00c4','&Aring;':'\u00c5','&AElig;':'\u00c6','&Ccedil;':'\u00c7',\n    '&Egrave;':'\u00c8','&Eacute;':'\u00c9','&Ecirc;':'\u00ca','&Euml;':'\u00cb','&Igrave;':'\u00cc','&Iacute;':'\u00cd','&Icirc;':'\u00ce','&Iuml;':'\u00cf',\n    '&ETH;':'\u00d0','&Ntilde;':'\u00d1','&Ograve;':'\u00d2','&Oacute;':'\u00d3','&Ocirc;':'\u00d4','&Otilde;':'\u00d5','&Ouml;':'\u00d6','&Oslash;':'\u00d8',\n    '&Ugrave;':'\u00d9','&Uacute;':'\u00da','&Ucirc;':'\u00db','&Uuml;':'\u00dc','&Yacute;':'\u00dd','&THORN;':'\u00de','&szlig;':'\u00df',\n    '&agrave;':'\u00e0','&aacute;':'\u00e1','&acirc;':'\u00e2','&atilde;':'\u00e3','&auml;':'\u00e4','&aring;':'\u00e5','&aelig;':'\u00e6','&ccedil;':'\u00e7',\n    '&egrave;':'\u00e8','&eacute;':'\u00e9','&ecirc;':'\u00ea','&euml;':'\u00eb','&igrave;':'\u00ec','&iacute;':'\u00ed','&icirc;':'\u00ee','&iuml;':'\u00ef',\n    '&eth;':'\u00f0','&ntilde;':'\u00f1','&ograve;':'\u00f2','&oacute;':'\u00f3','&ocirc;':'\u00f4','&otilde;':'\u00f5','&ouml;':'\u00f6','&oslash;':'\u00f8',\n    '&ugrave;':'\u00f9','&uacute;':'\u00fa','&ucirc;':'\u00fb','&uuml;':'\u00fc','&yacute;':'\u00fd','&thorn;':'\u00fe','&yuml;':'\u00ff',\n  };\n\n  // decode named\n  str = str.replace(/&[A-Za-z]+?;/g, (m) => (map[m] !== undefined ? map[m] : m));\n\n  return str;\n}\n\nbody = decodeHtmlEntities(body)\n  .replace(/[ \\t]+\\n/g, '\\n')\n  .replace(/\\n{3,}/g, '\\n\\n')\n  .replace(/>\\s+</g, '><')\n  .trim();\n\nreturn [{\n  json: {\n    fileId:$('Google Drive Trigger').first().json.id,\n    title: $('Google Drive Trigger').first().json.name,\n    htmlBody: body,\n    bodyLength: body.length,\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c80e5132-39fe-4cab-905f-55fdbae1b273",
      "name": "Code (Prepare Metadata)",
      "type": "n8n-nodes-base.code",
      "position": [
        464,
        240
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nlet html = item.htmlBody || '';\nconst title = (item.title || '').trim();\n\n// ---- Normalize lightweight HTML for consistent output ---\nhtml = html\n  // Ensure that <hr> closes automatically if necessary (depending on the render location).\n  .replace(/<hr\\s*>/gi, '<hr />')\n  // Normalize line breaks between blocks.\n  .replace(/>\\s+</g, '><')\n  .trim();\n\n// --- Convert HTML to plain text (good enough for excerpts, searches, and word counts) ---\nfunction htmlToText(inputHtml) {\n  if (!inputHtml) return '';\n  return inputHtml\n    .replace(/<br\\s*\\/?>/gi, '\\n')\n    .replace(/<\\/(p|div|h1|h2|h3|h4|h5|h6|li|ul|ol|blockquote|section|article)>/gi, '\\n')\n    .replace(/<hr\\s*\\/?>/gi, '\\n---\\n')\n    .replace(/<[^>]+>/g, '')         // strip tags\n    .replace(/[ \\t]+\\n/g, '\\n')\n    .replace(/\\n{3,}/g, '\\n\\n')\n    .trim();\n}\n\nconst textBody = htmlToText(html);\n\n// word count (separated by whitespace)\nconst words = textBody\n  .replace(/[\u201c\u201d\"']/g, '')\n  .split(/\\s+/)\n  .filter(Boolean);\n\nconst wordCount = words.length;\nconst readingTimeMin = Math.max(1, Math.round(wordCount / 200)); // 200 wpm\n\n// The excerpt should be 160-200 characters long (depending on your preference).\nfunction makeExcerpt(text, maxLen = 180) {\n  const t = (text || '').replace(/\\s+/g, ' ').trim();\n  if (t.length <= maxLen) return t;\n  return t.slice(0, maxLen).replace(/\\s+\\S*$/, '').trim() + '\u2026';\n}\nconst excerpt = makeExcerpt(textBody, 180);\n\n// Vietnamese slug -> Latin\nfunction slugifyVN(str) {\n  if (!str) return '';\n  return str\n    .normalize('NFD')\n    .replace(/[\\u0300-\\u036f]/g, '')  // remove accents\n    .replace(/\u0111/g, 'd')\n    .replace(/\u0110/g, 'd')\n    .toLowerCase()\n    .replace(/[^a-z0-9]+/g, '-')\n    .replace(/^-+|-+$/g, '')\n    .replace(/-+/g, '-');\n}\n\nconst slug = slugifyVN(title);\n\n// output final\nreturn [{\n  json: {\n    fileId: item.fileId,\n    title,\n    slug,\n    excerpt,\n    htmlBody: html,\n    textBody,\n    bodyLength: html.length,\n    wordCount,\n    readingTimeMin,\n    updatedAt: new Date().toISOString(),\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "50bdef2e-2e69-4615-a0bf-f2f67ba1919f",
      "name": "HTTP Request (WP - Find Post by Slug)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        720,
        352
      ],
      "parameters": {
        "url": "={{ $('CONFIG - Edit Settings Here').item.json.wp_base_url }}/wp-json/wp/v2/posts",
        "options": {
          "response": {
            "response": {
              "fullResponse": true,
              "responseFormat": "json"
            }
          }
        },
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {
              "name": "slug",
              "value": "={{$json.slug}}"
            },
            {
              "name": "per_page",
              "value": "1"
            },
            {
              "name": "status",
              "value": "any"
            }
          ]
        },
        "nodeCredentialType": "wordpressApi"
      },
      "credentials": {
        "wordpressApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3,
      "alwaysOutputData": true
    },
    {
      "id": "78bf0faa-9ddd-489e-80ae-53b772b6dc48",
      "name": "Merge (Meta + FindResult)",
      "type": "n8n-nodes-base.merge",
      "position": [
        944,
        256
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3.2
    },
    {
      "id": "547a6404-aa5e-4e07-ad7c-1d622add3a4f",
      "name": "IF (Found Post?)",
      "type": "n8n-nodes-base.if",
      "position": [
        1088,
        256
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "8f416a3b-f2a9-4396-af18-f0ed50f4b853",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.body?.length ?? 0 }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "29c70539-b58d-43e8-b97b-50987f1628af",
      "name": "HTTP Request: WP - Create Post",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1264,
        352
      ],
      "parameters": {
        "url": "={{ $('CONFIG - Edit Settings Here').item.json.wp_base_url }}/wp-json/wp/v2/posts",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "title",
              "value": "={{ $json.title }}"
            },
            {
              "name": "slug",
              "value": "={{ $json.slug }}"
            },
            {
              "name": "status",
              "value": "draft"
            },
            {
              "name": "content",
              "value": "={{ $json.htmlBody }}"
            },
            {
              "name": "excerpt",
              "value": "={{ $json.excerpt }}"
            }
          ]
        },
        "nodeCredentialType": "wordpressApi"
      },
      "credentials": {
        "wordpressApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "31130a63-f884-4bd8-b2a0-c5e9f96a361e",
      "name": "HTTP Request - WP Update Post",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1264,
        128
      ],
      "parameters": {
        "url": "={{ $('CONFIG - Edit Settings Here').item.json.wp_base_url }}/wp-json/wp/v2/posts/{{$json.body[0].id}}",
        "method": "PUT",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "title",
              "value": "={{ $json.title }}"
            },
            {
              "name": "slug",
              "value": "={{ $json.slug }}"
            },
            {
              "name": "status",
              "value": "draft"
            },
            {
              "name": "content",
              "value": "={{ $json.htmlBody }}"
            },
            {
              "name": "excerpt",
              "value": "={{ $json.excerpt }}"
            }
          ]
        },
        "nodeCredentialType": "wordpressApi"
      },
      "credentials": {
        "wordpressApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3,
      "alwaysOutputData": false
    },
    {
      "id": "6448aad4-74c5-4fb7-8e7d-eddb3c3f784e",
      "name": "SET - SEO Input",
      "type": "n8n-nodes-base.set",
      "position": [
        1520,
        128
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "3127c888-813c-4463-81a7-e7422e24b444",
              "name": "postId",
              "type": "number",
              "value": "={{$json.id || $json.body?.[0]?.id}}"
            },
            {
              "id": "3d2632e5-bb76-45ab-b19c-a72008fc5c1e",
              "name": "title",
              "type": "string",
              "value": "={{$json.title?.raw || $json.title || $json.name || ''}}"
            },
            {
              "id": "b71112ba-4a5e-4898-99f9-77cecad4fa9d",
              "name": "excerpt.raw",
              "type": "string",
              "value": "={{$json.excerpt?.raw || $json.excerpt || ''}}"
            },
            {
              "id": "158f4948-06c7-467f-b333-94cc4c2a2962",
              "name": "htmlBody",
              "type": "string",
              "value": "={{ $('Code (Prepare Metadata)').item.json.htmlBody }}"
            },
            {
              "id": "2c3d8a86-29fa-4ab2-b808-909f87f1403d",
              "name": "textBody",
              "type": "string",
              "value": "={{ $('Code (Prepare Metadata)').item.json.textBody }}"
            },
            {
              "id": "227090f6-b8ac-4352-88e4-667d560f9f49",
              "name": "fileId",
              "type": "string",
              "value": "={{ $('Code (Prepare Metadata)').item.json.fileId }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "7faf5ea5-17d4-4dab-9e21-c5933ef1c707",
      "name": "GEMINI - SEO JSON",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "maxTries": 5,
      "position": [
        1680,
        272
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-2.5-flash",
          "cachedResultName": "models/gemini-2.5-flash"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=You are an expert SEO Editor specializing in WordPress and RankMath optimization.\nYour task is to analyze the provided content and generate a strictly valid JSON object.\n\nINPUT DATA:\n- Title: {{ $json.title }}\n- Post ID: {{ $json.postId }}\n- Content Excerpt: {{ $json.excerpt.raw }}\n- Base URL: {{ $('CONFIG - Edit Settings Here').item.json.wp_base_url }}\n\nREQUIREMENTS:\n1. focus_keyword: Identify the most relevant Main Keyword from the content (in the same language as the content).\n2. seo_title: Create a catchy SEO title (max 60 chars). Must include the 'focus_keyword' at the beginning if possible.\n3. meta_description: Write a compelling meta description (150-160 chars). Must include the 'focus_keyword'.\n4. slug: Create a URL-friendly slug (lowercase, no accents, hyphens only).\n   - Must contain the 'focus_keyword' (converted to latin).\n   - Total URL length must be < 75 chars.\n5. opening_sentence: Write a captivating opening sentence (max 240 chars) to be injected at the start of the blog post.\n   - Must naturally contain the 'focus_keyword'.\n   - Do NOT use phrases like \"In this article...\". Jump straight into the value.\n\nOUTPUT FORMAT (JSON ONLY):\n{\n  \"seo_title\": \"string\",\n  \"meta_description\": \"string\",\n  \"focus_keyword\": \"string\",\n  \"opening_sentence\": \"string\",\n  \"slug\": \"string\",\n  \"postid\": {{ $json.postId }}\n}\n\nIMPORTANT: Return ONLY the JSON. No markdown, no explanations.\nINPUT:\nTITLE: {{$json.title}}\nPOSTID: {{ $json.postId }}\nCONTENT: {{ $json.excerpt.raw }}\nBASE_URL: {{ $('CONFIG - Edit Settings Here').item.json.wp_base_url }}\nMAX_URL_LEN: 75"
            }
          ]
        }
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": false,
      "typeVersion": 1,
      "waitBetweenTries": 5000
    },
    {
      "id": "348624cd-6a04-441b-8008-7b922368eee6",
      "name": "CODE - Parse & QA SEO",
      "type": "n8n-nodes-base.code",
      "position": [
        2176,
        144
      ],
      "parameters": {
        "jsCode": "function extractJson(text) {\n  if (!text || typeof text !== 'string') return null;\n\n  const m = text.match(/```json\\s*([\\s\\S]*?)\\s*```/i) || text.match(/```\\s*([\\s\\S]*?)\\s*```/);\n  const candidate = m?.[1] ?? text;\n\n  // Try parsing directly; if that fails, try cutting from { ... }\n  try { return JSON.parse(candidate); } catch (e) {}\n\n  const start = candidate.indexOf('{');\n  const end = candidate.lastIndexOf('}');\n  if (start !== -1 && end !== -1 && end > start) {\n    return JSON.parse(candidate.slice(start, end + 1));\n  }\n  return null;\n}\n\nreturn $input.all().map(item => {\n  const j = item.json;\n\n  const postId = Number(j.postId); // <- retrieved from SET - SEO Input via Merge\n  if (!Number.isFinite(postId) || postId <= 0) throw new Error(`Missing postId after merge`);\n\n  const raw = j.content?.parts?.[0]?.text ?? '';\n  const parsed = extractJson(raw) ?? {};\n\n  return {\n    json: {\n      postId,\n      seo_title: parsed.seo_title ?? '',\n      meta_description: parsed.meta_description ?? '',\n      focus_keyword: parsed.focus_keyword ?? '',\n      slug: parsed.slug ?? j.slug ?? '',\n      _debug_rawText: raw,\n      htmlBody: j.htmlBody ?? '',\n      textBody: j.textBody ?? '',\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "b7e4b632-b0cc-45c4-a10a-c6a4f40d2ae0",
      "name": "HTTP - WP Update SEO (RankMath)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2672,
        288
      ],
      "parameters": {
        "url": "={{ $('CONFIG - Edit Settings Here').item.json.wp_base_url }}/wp-json/wp/v2/posts/{{$json.postId}}",
        "method": "PATCH",
        "options": {},
        "sendBody": true,
        "authentication": "predefinedCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "status",
              "value": "draft"
            },
            {
              "name": "slug",
              "value": "={{ $json.slug }}"
            },
            {
              "name": "excerpt",
              "value": "={{ $('SET - SEO Input').item.json.excerpt.raw }}"
            },
            {
              "name": "meta",
              "value": "={{ $json.meta }}"
            },
            {
              "name": "content",
              "value": "={{ $json.contentPatched }}"
            }
          ]
        },
        "nodeCredentialType": "wordpressApi"
      },
      "credentials": {
        "wordpressApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "8e535160-4509-4d61-8d92-975659b616d8",
      "name": "CODE - Build WP Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        2368,
        144
      ],
      "parameters": {
        "jsCode": "function clampText(s, max) {\n  s = (s ?? '').toString().trim();\n  if (!s) return '';\n  return s.length > max ? s.slice(0, max - 1).trimEnd() + '\u2026' : s;\n}\n\nfunction removeAccents(str) {\n  return (str ?? '')\n    .toString()\n    .normalize('NFD')\n    .replace(/[\\u0300-\\u036f]/g, '')\n    .replace(/\u0111/g, 'd')\n    .replace(/\u0110/g, 'D');\n}\n\nfunction norm(str) {\n  return removeAccents(str).toLowerCase().replace(/\\s+/g, ' ').trim();\n}\n\n// Inject keywords at the beginning of the text (preferably inserting them in the first <p>). Use markers to prevent duplicate insertions during playback.\nfunction injectKeywordToHtml({ html, keyword }) {\n  html = (html ?? '').toString();\n  keyword = (keyword ?? '').toString().trim();\n  if (!html || !keyword) return html;\n\n  const marker = `<!--n8n_kw_injected:${removeAccents(keyword).toLowerCase()}-->`;\n  if (html.includes(marker)) return html;\n\n  // If the content already has keywords, just check the \"heading\".\n  const hasKw = norm(html).includes(norm(keyword));\n\n  // Use the first 250 characters (light stripe tag) to check \"beginning\".\n  const headText = html\n    .slice(0, 800)\n    .replace(/<[^>]+>/g, ' ')\n    .replace(/\\s+/g, ' ')\n    .trim();\n\n  const kwInBeginning = norm(headText).includes(norm(keyword));\n\n  // If the keyword is already in the content and in the first paragraph -> do nothing.\n  if (hasKw && kwInBeginning) return html;\n\n  // If there is a <p> at the beginning: insert the keyword at the beginning of the <p>\n  if (/<p\\b[^>]*>/i.test(html)) {\n    html = html.replace(/<p\\b([^>]*)>/i, (m, attrs) => {\n      // If the keyword is not already at the beginning of the article, add a keyword prefix.\n      return `<p${attrs}><strong>${keyword}</strong>: `;\n    });\n  } else {\n    // Without <p>, prepend 1 paragraph\n    html = `<p><strong>${keyword}</strong>: </p>` + html;\n  }\n\n  // If the keyword is not already present in the content, inserting it this way will also create an occurrence in the content.\n  return marker + html;\n}\n\nreturn $input.all().map((item) => {\n  const j = item.json;\n\n  const postId = Number(j.postId);\n  if (!Number.isFinite(postId) || postId <= 0) {\n    throw new Error(`Missing/invalid postId in Build WP Payload. postId=${j.postId}`);\n  }\n\n  const seo_title = clampText(j.seo_title, 70);\n  const meta_description = clampText(j.meta_description, 170);\n  const focus_keyword = (j.focus_keyword ?? '').toString().trim();\n  const slug = (j.slug ?? '').toString().trim();\n\n  const meta = {\n    rank_math_title: seo_title,\n    rank_math_description: meta_description,\n    rank_math_focus_keyword: focus_keyword,\n  };\n\n  Object.keys(meta).forEach((k) => {\n    if (!meta[k]) delete meta[k];\n  });\n\n  const htmlBody = j.htmlBody ?? '';\n  const contentPatched = injectKeywordToHtml({ html: htmlBody, keyword: focus_keyword });\n\n  return {\n    json: {\n      postId,\n      slug,\n      meta,\n      contentPatched,\n      _seo: { seo_title, meta_description, focus_keyword },\n    },\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "0b749465-5b6e-466f-a2a3-08beff0d5c0c",
      "name": "HTTP - WP Get Post (Verify)",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3072,
        288
      ],
      "parameters": {
        "url": "={{ $('CONFIG - Edit Settings Here').item.json.wp_base_url }}/wp-json/wp/v2/posts/{{ $json.id }}?context=edit",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "wordpressApi"
      },
      "credentials": {
        "wordpressApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "57865b48-413c-49fc-a58e-42b06a1feba4",
      "name": "MERGE - Attach postId",
      "type": "n8n-nodes-base.merge",
      "position": [
        1968,
        144
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3.2
    },
    {
      "id": "004dd064-d904-471d-98b6-d1f9f1743ac2",
      "name": "Google Drive - Move to Published",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        3248,
        144
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('SET - SEO Input').item.json.fileId }}"
        },
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "folderId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('CONFIG - Edit Settings Here').item.json.drive_published_folder_id }}"
        },
        "operation": "move"
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "1a253992-69a8-4a1f-a4f4-b9b13a23a049",
      "name": "Google Drive Trigger",
      "type": "n8n-nodes-base.googleDriveTrigger",
      "position": [
        -304,
        128
      ],
      "parameters": {
        "event": "fileCreated",
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerOn": "specificFolder",
        "folderToWatch": {
          "__rl": true,
          "mode": "list",
          "value": ""
        }
      },
      "credentials": {
        "googleDriveOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "executeOnce": false,
      "typeVersion": 1
    },
    {
      "id": "01138756-449d-41e9-8feb-e194c9036242",
      "name": "CONFIG - Edit Settings Here",
      "type": "n8n-nodes-base.set",
      "position": [
        -112,
        240
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ff890f96-5ab0-42b4-9a05-24f476600436",
              "name": "wp_base_url",
              "type": "string",
              "value": "Input your domain here"
            },
            {
              "id": "8fe095df-b9da-406d-9730-2955a5f6ea91",
              "name": "drive_published_folder_id",
              "type": "string",
              "value": "Enter the ID of the Google Drive folder where you want to move published files."
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d339fd1a-8db4-4ae3-9b55-a2f9f70d4d7d",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -368,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 976,
        "height": 368,
        "content": "## 1. Input & Cleaning\nMonitors Google Drive for new files, sanitizes raw HTML, and prepares metadata for processing."
      },
      "typeVersion": 1
    },
    {
      "id": "d7aa0a4c-972b-4396-a5e3-ccbb828f86be",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        672,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 496,
        "content": "## 2. Duplicate Check\nQueries WordPress to check if the post exists. Routes to \"Create\" or \"Update\" path to prevent duplicates."
      },
      "typeVersion": 1
    },
    {
      "id": "73fac425-7102-4470-ad0b-24bf08738363",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1488,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 1024,
        "height": 432,
        "content": "## 3. AI SEO Analysis\nGemini analyzes content to generate Focus Keyword, SEO Title, and Meta Description in structured JSON."
      },
      "typeVersion": 1
    },
    {
      "id": "05a7dcb9-8be3-4f44-923d-a1d869179c63",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2544,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 896,
        "height": 432,
        "content": "## 4. Publish & Cleanup\nUpdates WordPress content, maps custom RankMath meta keys via API, and moves files to the \"Published\" folder."
      },
      "typeVersion": 1
    },
    {
      "id": "b93e82bf-7a05-46db-9690-255a4c8ecb4b",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1104,
        -80
      ],
      "parameters": {
        "width": 704,
        "height": 608,
        "content": "# Google Drive to WordPress Publisher\n**Auto-publish content with Advanced RankMath SEO & Gemini Analysis.**\n\nThis workflow automates the \"Document Ops\" process for professional content teams. It creates a seamless bridge between writing in Google Docs and publishing on WordPress, ensuring high-quality SEO standards without manual effort.\n\n### How it works\n1.  **Ingestion:** The workflow monitors a specific Google Drive folder for new HTML or Doc files.\n2.  **Sanitization:** It cleans the raw HTML from Google Docs, removing messy styles and tags to ensure clean code.\n3.  **Smart Logic:** It checks if the post already exists (via slug) to decide whether to Create a new draft or Update an existing one.\n4.  **AI SEO Agent:** Gemini AI analyzes the content to extract the Focus Keyword, SEO Title, and Meta Description, optimizing for RankMath.\n5.  **Publishing:** The content is pushed to WordPress via REST API, mapping custom RankMath meta keys that standard nodes often miss.\n6.  **Archiving:** Processed files are moved to a \"Published\" folder.\n\n### Setup steps\n1.  **Configure Credentials:** Set up your Google Drive, Gemini (PaLM), and WordPress credentials.\n2.  **Global Config:** In the `CONFIG` node, enter your WordPress URL and the ID of your \"Published\" folder in Drive.\n3.  **RankMath API Helper:** You **MUST** install the custom PHP snippet provided in the template description to allow n8n to update RankMath metadata (Critical Step).\n4.  **Trigger:** Select your \"Drafts\" folder in the Google Drive Trigger node."
      },
      "typeVersion": 1
    },
    {
      "id": "a108d140-3fad-4cab-9f7f-d26c07cd4f64",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2560,
        176
      ],
      "parameters": {
        "color": 3,
        "width": 336,
        "height": 256,
        "content": "### \u26a0\ufe0f CRITICAL REQUIREMENT\nThis node requires the **RankMath Helper PHP Snippet** to be installed on your WordPress site.\nWithout it, RankMath fields cannot be updated via API. Check the template description!"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "e1e272a4-c865-4468-a88d-73cd0fd1b308",
  "connections": {
    "SET - SEO Input": {
      "main": [
        [
          {
            "node": "GEMINI - SEO JSON",
            "type": "main",
            "index": 0
          },
          {
            "node": "MERGE - Attach postId",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF (Found Post?)": {
      "main": [
        [
          {
            "node": "HTTP Request - WP Update Post",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "HTTP Request: WP - Create Post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GEMINI - SEO JSON": {
      "main": [
        [
          {
            "node": "MERGE - Attach postId",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Code (Extract Body)": {
      "main": [
        [
          {
            "node": "Code (Prepare Metadata)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Drive Trigger": {
      "main": [
        [
          {
            "node": "CONFIG - Edit Settings Here",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CODE - Parse & QA SEO": {
      "main": [
        [
          {
            "node": "CODE - Build WP Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "MERGE - Attach postId": {
      "main": [
        [
          {
            "node": "CODE - Parse & QA SEO",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CODE - Build WP Payload": {
      "main": [
        [
          {
            "node": "HTTP - WP Update SEO (RankMath)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code (Prepare Metadata)": {
      "main": [
        [
          {
            "node": "HTTP Request (WP - Find Post by Slug)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge (Meta + FindResult)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge (Meta + FindResult)": {
      "main": [
        [
          {
            "node": "IF (Found Post?)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request (Export HTML)": {
      "main": [
        [
          {
            "node": "Code (Extract Body)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CONFIG - Edit Settings Here": {
      "main": [
        [
          {
            "node": "HTTP Request (Export HTML)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP - WP Get Post (Verify)": {
      "main": [
        [
          {
            "node": "Google Drive - Move to Published",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request - WP Update Post": {
      "main": [
        [
          {
            "node": "SET - SEO Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request: WP - Create Post": {
      "main": [
        [
          {
            "node": "SET - SEO Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP - WP Update SEO (RankMath)": {
      "main": [
        [
          {
            "node": "HTTP - WP Get Post (Verify)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request (WP - Find Post by Slug)": {
      "main": [
        [
          {
            "node": "Merge (Meta + FindResult)",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  }
}