AutomationFlowsContent & Video › Publish Google Docs to Wordpress with Advance Rankmath SEO & Gemini Analysis

Publish Google Docs to Wordpress with Advance Rankmath SEO & Gemini Analysis

ByCuong Nguyen @cuongnguyen on n8n.io

This workflow is designed for Content Marketing Teams, Agencies, and Professional Editors who prefer writing in Google Docs but need a seamless way to publish to WordPress.

Event trigger★★★★☆ complexityAI-powered24 nodesHTTP RequestGoogle GeminiGoogle DriveGoogle Drive Trigger
Content & Video Trigger: Event Nodes: 24 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #11989 — we link there as the canonical source.

This workflow follows the Google Drive → Google Drive Trigger recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "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
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow is designed for Content Marketing Teams, Agencies, and Professional Editors who prefer writing in Google Docs but need a seamless way to publish to WordPress.

Source: https://n8n.io/workflows/11989/ — original creator credit. Request a take-down →

More Content & Video workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Content & Video

The best content automation in the market! This advanced workflow not only creates and publishes SEO-optimized blog posts to your WordPress website but also backs up all content and images to a design

OpenAI, Output Parser Structured, Chain Llm +8
Content & Video

✍️🌄 Your First Wordpress Content Creator - Quick Start. Uses manualTrigger, lmChatOpenAi, outputParserStructured, agent. Event-driven trigger; 39 nodes.

OpenAI Chat, Output Parser Structured, Agent +4
Content & Video

Enrich Faq Sections On Your Website Pages At Scale With Ai. Uses manualTrigger, lmChatOpenAi, splitInBatches, splitOut. Event-driven trigger; 36 nodes.

OpenAI Chat, Google Drive, Google Sheets +6
Content & Video

Strapi Splitout. Uses manualTrigger, lmChatOpenAi, splitInBatches, splitOut. Event-driven trigger; 36 nodes.

OpenAI Chat, Google Drive, Google Sheets +6
Content & Video

2434. Uses lmChatOpenAi, googleDrive, googleSheets, executeWorkflowTrigger. Event-driven trigger; 36 nodes.

OpenAI Chat, Google Drive, Google Sheets +6