竞争对手内容差距分析器:自动化网站主题映射
高级
这是一个Miscellaneous, AI Summarization, Multimodal AI领域的自动化工作流,包含 30 个节点。主要使用 If, Set, Code, Gmail, Merge 等节点。 使用Gemini AI、Apify和Google Sheets分析竞争对手内容差距
前置要求
- •Google 账号和 Gmail API 凭证
- •HTTP Webhook 端点(n8n 会自动生成)
- •可能需要目标 API 的认证凭证
- •Google Sheets API 凭证
- •Google Gemini API Key
使用的节点 (30)
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "MVDc7al8dnpBvlD2",
"meta": {
"instanceId": "d1dc073e8e3059a23e2730f69cb1b90065a2ac39039fea0727fdf9bee77a9131",
"templateCredsSetupCompleted": true
},
"name": "竞争对手内容差距分析器:自动化网站主题映射",
"tags": [],
"nodes": [
{
"id": "8d912fd2-61b3-41df-9631-c7eaf6050bc9",
"name": "AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
1696,
-32
],
"parameters": {
"text": "=You are a competitive content analyst.\nAnalyze the competitor website page provided in `markdown`.\n\nReturn ONLY valid JSON matching this schema:\n\n{\n \"page_url\": \"string\",\n \"title\": \"string\",\n \"content_type\": \"string\",\n \"main_topics\": [\n {\n \"topic\": \"string\",\n \"level\": \"number (must start at 1 for top-level)\",\n \"subtopics\": [\n {\n \"topic\": \"string\",\n \"level\": \"number (increment by 1 for each deeper heading)\",\n \"subtopics\": []\n }\n ]\n }\n ],\n \"key_entities\": [\"string\"],\n \"depth_score_1_to_5\": \"number\"\n}\n\nRules:\n- Strict JSON output. No extra text, no explanations.\n- content_type: choose from [\"landing\",\"blog\",\"service\",\"about\"].\n- \"main_topics\" must always start at level 1.\n- Subtopics increment level by +1 relative to their parent.\n- If no subtopics, return [].\n- Always close JSON properly.\n\nContent to analyze:\n{{ $json[\"markdown\"] }}\n",
"options": {},
"promptType": "define"
},
"typeVersion": 2.2
},
{
"id": "d09b94e5-09d8-4477-9034-843079795ea3",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
80,
-928
],
"parameters": {
"width": 304,
"height": 304,
"content": "## 竞争对手内容差距分析器:自动化网站主题映射"
},
"typeVersion": 1
},
{
"id": "a9a4fa59-bc0a-4175-afd8-46855bb271d3",
"name": "准备 Apify 输入",
"type": "n8n-nodes-base.code",
"position": [
576,
64
],
"parameters": {
"jsCode": "// 1) Read POST body (supports Webhook { body: {...} } or raw JSON)\nconst src = items?.[0]?.json ?? {};\nconst bodyRaw = (src.body ?? src ?? {});\nconst body = (typeof bodyRaw === 'string')\n ? (() => { try { return JSON.parse(bodyRaw); } catch { return {}; } })()\n : bodyRaw;\n\n// 2) Helpers\nfunction normalizeDomain(input = '') {\n const s = String(input).trim();\n if (!s) return '';\n return s\n .replace(/^https?:\\/\\//i, '') // remove protocol\n .replace(/^www\\./i, '') // remove www\n .replace(/\\/+$/g, ''); // remove trailing slashes\n}\nfunction splitList(input) {\n if (Array.isArray(input)) return input.map(String);\n return String(input || '')\n .split(/[,\\n\\r;]+/)\n .map(s => s.trim())\n .filter(Boolean);\n}\nfunction toGlobs(list) {\n return splitList(list).map(p => (p.endsWith('*') ? p : `${p}*`));\n}\n\n// 3) Map incoming fields\nconst row = {\n client_name: body.client_name ?? body.name ?? '',\n client_domain: body.client_domain ?? '',\n competitors: body.competitors ?? body.competitor ?? body.url ?? '',\n include_paths: body.include_paths ?? '',\n exclude_paths: body.exclude_paths ?? '',\n max_pages: body.max_pages ?? '',\n crawl_depth: body.crawl_depth ?? '',\n notify_email: body.notify_email ?? body.email ?? '',\n processed: (typeof body.processed === 'boolean'\n ? body.processed\n : String(body.processed ?? '').toLowerCase() === 'true'),\n rowIndex: body.rowIndex ?? null,\n};\n\n// 4) Early exit if sender marked it processed\nif (row.processed === true) return [];\n\n// 5) Build Apify fields\nconst competitorDomain = normalizeDomain(row.competitors);\nconst clientDomain = normalizeDomain(row.client_domain);\n\nconst startUrls = competitorDomain\n ? [{ url: `https://${competitorDomain}/`, method: 'GET' }]\n : [];\n\nif (!startUrls.length) {\n return [{ json: { error: 'NO_START_URLS', raw_competitors: row.competitors || '' } }];\n}\n\nconst includeArr = toGlobs(row.include_paths);\nconst excludeArr = toGlobs(row.exclude_paths);\n\nconst maxPages = Number(row.max_pages) || 20;\nconst crawlDepth = Number(row.crawl_depth) || 1;\n\n// 6) Output for next nodes\nreturn [{\n json: {\n client_name: String(row.client_name || ''),\n client_domain: clientDomain,\n competitor_domain: competitorDomain,\n startUrls,\n includeArr,\n excludeArr,\n max_pages_num: maxPages,\n crawl_depth_num: crawlDepth,\n notify_email: String(row.notify_email || ''),\n rowIndex: row.rowIndex,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "82bce0ee-a888-4e7e-9dc6-47bdcf3e18e9",
"name": "爬取竞争对手网站",
"type": "@apify/n8n-nodes-apify.apify",
"position": [
1024,
-32
],
"parameters": {
"actorId": {
"__rl": true,
"mode": "list",
"value": "aYG0l9s7dbB7j3gbS",
"cachedResultUrl": "https://console.apify.com/actors/aYG0l9s7dbB7j3gbS/input",
"cachedResultName": "Website Content Crawler (apify/website-content-crawler)"
},
"timeout": {},
"customBody": "={{ JSON.stringify({\n crawlerType: \"cheerio\",\n startUrls: $json.startUrls, \n useSitemaps: false,\n maxDepth: $json.crawl_depth_num || 0,\n maxRequestsPerCrawl: $json.max_pages_num || 20,\nmaxConcurrency:1,\n respectRobotsTxtFile: true,\n ignoreHttpsErrors: true,\n proxyConfiguration: { useApifyProxy: true },\n removeCookieWarnings: true,\n removeElementsCssSelector: \"nav, footer, script, style, noscript, svg, img[src^='data:']\",\n blockMedia: true,\n saveMarkdown: true,\n saveHtml: false,\n saveFiles: false,\n saveScreenshots: false\n}) }}\n"
},
"credentials": {
"apifyApi": {
"id": "Uzk0v4R6yNh8OxbO",
"name": "Apify account"
}
},
"typeVersion": 1
},
{
"id": "d7eb2fc0-464d-450e-9be9-ac7e2e9488c3",
"name": "获取爬取数据集",
"type": "n8n-nodes-base.httpRequest",
"position": [
1248,
-32
],
"parameters": {
"url": "={{ \"https://api.apify.com/v2/datasets/\" + ($json.data?.defaultDatasetId || $json.defaultDatasetId || \"auD8MlRMr6mZQpRS1\") + \"/items?clean=true&format=json&offset=0&limit=1\" }}",
"options": {}
},
"typeVersion": 4.2
},
{
"id": "9e970002-63b5-4f1b-8189-b59eb92a9471",
"name": "提取页面元数据",
"type": "n8n-nodes-base.code",
"position": [
1472,
-32
],
"parameters": {
"jsCode": "// Renamed variables for clarity; behavior unchanged.\nconst sourceItem = items?.[0]?.json;\nif (!sourceItem) return [];\n\nconst markdownText = String(sourceItem.markdown || sourceItem.text || '');\nconst wordCount = markdownText.trim().split(/\\s+/).filter(Boolean).length;\n\nfunction getTitleFromMarkdown(markdown, url) {\n const lines = markdown.split(/\\r?\\n/).map(line => line.trim());\n\n const h1Line = lines.find(line => /^#\\s+/.test(line));\n if (h1Line) return h1Line.replace(/^#\\s+/, '').trim();\n\n const firstMeaningfulLine = lines.find(line => line.length > 0 && line.length < 120);\n if (firstMeaningfulLine) return firstMeaningfulLine;\n\n try {\n const parsedUrl = new URL(url);\n const pathSegments = parsedUrl.pathname.split('/').filter(Boolean);\n return (pathSegments.pop() || parsedUrl.hostname).replace(/[-_]/g, ' ');\n } catch {\n return url;\n }\n}\n\nreturn [{\n json: {\n page_url: sourceItem.url,\n title: getTitleFromMarkdown(markdownText, sourceItem.url),\n word_count: wordCount,\n reading_time_min: Math.max(1, Math.ceil(wordCount / 200)),\n excerpt: markdownText.slice(0, 600),\n markdown: markdownText\n }\n}];"
},
"typeVersion": 2
},
{
"id": "7e929b2d-89d8-4a36-a13e-f82ae18f3f03",
"name": "分析页面内容",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
1776,
192
],
"parameters": {
"options": {}
},
"credentials": {
"googlePalmApi": {
"id": "qQGrvqnSPqWFH6I6",
"name": "Google Gemini(PaLM) Api account 5"
}
},
"typeVersion": 1
},
{
"id": "f8c1643b-4382-49c0-9ec3-35876bd52eb2",
"name": "解析和规范化 Gemini JSON",
"type": "n8n-nodes-base.code",
"position": [
2048,
-32
],
"parameters": {
"jsCode": "\nfunction stripCodeFences(text = \"\") {\n \n return String(text)\n .replace(/^\\s*```(?:json)?\\s*/i, \"\")\n .replace(/\\s*```\\s*$/i, \"\")\n .trim();\n}\n\nfunction safeParseModelJson(payload) {\n \n if (payload && (payload.page_url || payload.main_topics || payload.key_entities)) return payload;\n\n let raw = payload?.output ?? payload;\n if (typeof raw !== \"string\") raw = JSON.stringify(raw ?? {});\n raw = stripCodeFences(raw);\n\n let obj;\n try { obj = JSON.parse(raw); } catch {}\n\n\n if (Array.isArray(obj)) {\n if (obj[0]?.output) {\n try { obj = JSON.parse(stripCodeFences(obj[0].output)); } catch { obj = obj[0] || {}; }\n } else {\n obj = obj[0] || {};\n }\n } else if (obj && typeof obj === \"object\" && typeof obj.output === \"string\") {\n try { obj = JSON.parse(stripCodeFences(obj.output)); } catch {}\n }\n\n return obj || {};\n}\n\nfunction normalizeTopicTree(nodes = [], level = 1) {\n return nodes.map(node => ({\n topic: String(node?.topic ?? \"\"),\n level: Number(node?.level) || level,\n subtopics: normalizeTopicTree(node?.subtopics || [], (Number(node?.level) || level) + 1),\n }));\n}\n\nfunction toTopicTree(modelObj) {\n let t = modelObj.main_topics;\n if (typeof t === \"string\") {\n try { t = JSON.parse(t); } catch { t = []; }\n }\n if (!Array.isArray(t)) t = [];\n return normalizeTopicTree(t, 1);\n}\n\nfunction toBulletLines(tree, indent = \"\") {\n const lines = [];\n for (const node of tree) {\n const text = String(node.topic || \"\").trim();\n if (text) lines.push(`${indent}- ${text}`);\n if (node.subtopics?.length) lines.push(...toBulletLines(node.subtopics, indent + \" \"));\n }\n return lines;\n}\n\nfunction parseEntitiesToBullets(value) {\n let arr = value;\n\n if (typeof arr === \"string\") {\n const s = arr.trim();\n if (/^- /m.test(s)) {\n // Already looks like \"- item\" lines\n arr = s.split(/\\r?\\n/).map(l => l.replace(/^\\s*-\\s*/, \"\").trim()).filter(Boolean);\n } else {\n try { arr = JSON.parse(s); }\n catch { arr = s.split(\",\").map(x => x.trim()).filter(Boolean); }\n }\n }\n\n if (!Array.isArray(arr)) arr = [];\n return arr.length ? arr.map(e => `- ${String(e)}`).join(\"\\n\") : \"- (none)\";\n}\n\nfunction chooseUrl(...candidates) {\n for (const c of candidates) {\n const v = String(c ?? \"\").trim();\n if (v && !/^n\\/?a$/i.test(v)) return v; // reject \"N/A\", \"n/a\"\n }\n return \"\";\n}\n\n// ---------- main ----------\nconst inputPayload = items?.[0]?.json ?? {};\nconst modelObj = safeParseModelJson(inputPayload);\n\n// Build bullets\nconst topicTree = toTopicTree(modelObj);\nconst mainTopicsFlat = topicTree.length ? toBulletLines(topicTree).join(\"\\n\") : \"- (none)\";\nconst keyEntitiesFlat = parseEntitiesToBullets(modelObj.key_entities);\n\n// Prefer the true URL from \"Extract Page Metadata\"\nlet urlFromMeta;\ntry {\n urlFromMeta =\n $items(\"Extract Page Metadata\", 0, 0)?.json?.page_url ||\n $items(\"Extract Page Metadata\", 0, 0)?.json?.url;\n} catch {}\n\nconst pageUrl = chooseUrl(\n urlFromMeta,\n inputPayload?.page_url,\n modelObj?.page_url\n);\n\n// Output\nreturn [{\n json: {\n page_url: pageUrl,\n main_topics_flat: mainTopicsFlat,\n key_entities_flat: keyEntitiesFlat,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "e5c2649e-d2ea-49a0-a3ad-5f9916843da2",
"name": "推导工作表名称",
"type": "n8n-nodes-base.code",
"position": [
2272,
-32
],
"parameters": {
"jsCode": "\nfunction toSheetName(raw) {\n let s = String(raw || '').trim();\n if (!s) return 'Page';\n if (!/^https?:\\/\\//i.test(s)) s = 'https://' + s;\n\n try {\n const u = new URL(s);\n const host = u.hostname.replace(/^www\\./, '');\n let path = (u.pathname || '/')\n .replace(/\\/+/g, '/')\n .replace(/^\\/|\\/$/g, '');\n let name = path ? `${host}_${path}` : host;\n name = name.replace(/[:\\\\\\/\\?\\*\\[\\]]/g, '·').replace(/\\s+/g, ' ').trim();\n return name || 'Page';\n } catch {\n let name = s.replace(/^https?:\\/\\//i, '').replace(/[:\\\\\\/\\?\\*\\[\\]]/g, '·').trim();\n return name || 'Page';\n }\n}\n\n// Make N random digits as a string (e.g., \"48291\")\nfunction randDigits(n = 5) {\n let out = '';\n for (let i = 0; i < n; i++) out += Math.floor(Math.random() * 10);\n return out;\n}\n\nconst MAX_LEN = 90; // Sheets tab name limit\nconst DIGITS = 5; // how many random digits to append\n\nreturn items.map(({ json }) => {\n const url = json.page_url ?? json.url ?? json.pageUrl ?? '';\n const base = toSheetName(url);\n\n const suffix = '_' + randDigits(DIGITS); \n const roomForBase = Math.max(0, MAX_LEN - suffix.length);\n const safeBase = base.length > roomForBase ? base.slice(0, roomForBase) : base;\n\n const sheet_name = `${safeBase}${suffix}`;\n\n return {\n json: {\n ...json,\n sheet_name,\n _sheet_name_src: url \n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "7b85f71d-7bdf-4de2-a572-494347582699",
"name": "合并",
"type": "n8n-nodes-base.merge",
"position": [
2720,
-16
],
"parameters": {},
"typeVersion": 3.2
},
{
"id": "6182edc0-197d-40b6-bdd0-a00b6d0fa73e",
"name": "保存请求数据",
"type": "n8n-nodes-base.googleSheets",
"position": [
1024,
160
],
"parameters": {
"columns": {
"value": {
"max_pages": "={{ $json.max_pages_num }}",
"client_name": "={{ $json.client_name }}",
"competitors": "={{ $json.competitor_domain }}",
"crawl_depth": "={{ $json.crawl_depth_num }}",
"notify_email": "={{ $json.notify_email }}",
"exclude_paths": "={{ $json.excludeArr }}",
"include_paths": "={{ $json.includeArr }}"
},
"schema": [
{
"id": "client_name",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "client_name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "competitors",
"type": "string",
"display": true,
"required": false,
"displayName": "competitors",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "include_paths",
"type": "string",
"display": true,
"required": false,
"displayName": "include_paths",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "exclude_paths",
"type": "string",
"display": true,
"required": false,
"displayName": "exclude_paths",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "max_pages",
"type": "string",
"display": true,
"required": false,
"displayName": "max_pages",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "crawl_depth",
"type": "string",
"display": true,
"required": false,
"displayName": "crawl_depth",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "notify_email",
"type": "string",
"display": true,
"required": false,
"displayName": "notify_email",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1OvrhykXjzBI0_VqXurrQOMMtvBWfBVlttZVI1LhcRoQ/edit#gid=0",
"cachedResultName": "CONFIG"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1OvrhykXjzBI0_VqXurrQOMMtvBWfBVlttZVI1LhcRoQ",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1OvrhykXjzBI0_VqXurrQOMMtvBWfBVlttZVI1LhcRoQ/edit?usp=drivesdk",
"cachedResultName": "Content Gap Analyzer"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "wHDA0XakCCVOLVNe",
"name": "Google Sheets account 2"
}
},
"typeVersion": 4.7
},
{
"id": "2b28b4f7-e07d-4d14-950c-61a2d36fdb34",
"name": "检查数据是否存在",
"type": "n8n-nodes-base.if",
"position": [
800,
64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "c87f5a6e-e9c8-4c57-a5b3-2c77371cd529",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.startUrls[0].method }}",
"rightValue": "GET"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "7273236a-42f6-4c62-8f79-52e039e15433",
"name": "是否有内容可邮件发送",
"type": "n8n-nodes-base.if",
"position": [
3392,
-16
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1cdaf14-a4af-4ec9-8db2-4a8ddb78f4c4",
"operator": {
"type": "string",
"operation": "regex"
},
"leftValue": "={{ $json.page_url }}",
"rightValue": "^https?://"
},
{
"id": "8600a236-03c6-45db-91fe-89f93926c97e",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.main_topics }}",
"rightValue": ""
},
{
"id": "ba4c3349-2b5c-4122-873e-fd93e5f3b944",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.key_words }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "c97679a0-efc5-43ab-b603-c40173c6d4e7",
"name": "保存收集的数据",
"type": "n8n-nodes-base.googleSheets",
"position": [
3168,
-16
],
"parameters": {
"columns": {
"value": {},
"schema": [
{
"id": "page_url",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "page_url",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "main_topics",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "main_topics",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "key_words",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "key_words",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $('Derive Sheet Name').item.json.sheet_name }}"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1OvrhykXjzBI0_VqXurrQOMMtvBWfBVlttZVI1LhcRoQ",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1OvrhykXjzBI0_VqXurrQOMMtvBWfBVlttZVI1LhcRoQ/edit?usp=drivesdk",
"cachedResultName": "Content Gap Analyzer"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "wHDA0XakCCVOLVNe",
"name": "Google Sheets account 2"
}
},
"executeOnce": true,
"typeVersion": 4.7
},
{
"id": "ecec493c-7b3b-4746-90f7-1b28ce224d9c",
"name": "为数据创建工作表",
"type": "n8n-nodes-base.googleSheets",
"position": [
2512,
64
],
"parameters": {
"title": "={{ $json.sheet_name }}",
"options": {},
"operation": "create",
"documentId": {
"__rl": true,
"mode": "list",
"value": "1OvrhykXjzBI0_VqXurrQOMMtvBWfBVlttZVI1LhcRoQ",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1OvrhykXjzBI0_VqXurrQOMMtvBWfBVlttZVI1LhcRoQ/edit?usp=drivesdk",
"cachedResultName": "Content Gap Analyzer"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "wHDA0XakCCVOLVNe",
"name": "Google Sheets account 2"
}
},
"executeOnce": false,
"notesInFlow": false,
"retryOnFail": false,
"typeVersion": 4.7
},
{
"id": "0964d490-2039-4658-be5f-c9b374fab9dc",
"name": "发送报告",
"type": "n8n-nodes-base.gmail",
"position": [
3616,
-32
],
"webhookId": "1d1f6479-dd01-4dee-97cc-ff1a3b706dcb",
"parameters": {
"sendTo": "={{ $('Prepare Apify Input').item.json.notify_email }}",
"message": "=<div style=\"font-family: Arial, sans-serif; color: #333;\">\n <h2 style=\"color:#4CAF50;\">📊 Competitor SEO Content Analysis Report</h2>\n\n <p><b>Page URL:</b> <a href=\"{{ $json.page_url }}\" target=\"_blank\">{{ $json.page_url }}</a></p>\n\n <h3>Main Topics</h3>\n <pre style=\"background:#f9f9f9; padding:10px; border:1px solid #ddd; white-space:pre-wrap; margin:0;\">\n{{ $json.main_topics }}\n </pre>\n\n <h3>Keywords Identified</h3>\n <pre style=\"background:#f9f9f9; padding:10px; border:1px solid #ddd; white-space:pre-wrap; margin:0;\">\n{{ $json.key_words }}\n </pre>\n</div>",
"options": {},
"subject": "=SEO Audit Report: {{ $json.page_url }}"
},
"credentials": {
"gmailOAuth2": {
"id": "jkKHvU2Pb9X5WJk5",
"name": "Gmail account"
}
},
"executeOnce": true,
"typeVersion": 2.1
},
{
"id": "f5ee629d-0845-4680-9104-672f7e2a3570",
"name": "便签2",
"type": "n8n-nodes-base.stickyNote",
"position": [
960,
336
],
"parameters": {
"color": 5,
"height": 112,
"content": "### 保存请求数据"
},
"typeVersion": 1
},
{
"id": "b846b79a-f3ab-4b05-9bbe-41ecf82f8626",
"name": "便签3",
"type": "n8n-nodes-base.stickyNote",
"position": [
944,
-192
],
"parameters": {
"color": 5,
"width": 256,
"height": 144,
"content": "### 爬取竞争对手网站"
},
"typeVersion": 1
},
{
"id": "a684c3ba-f7a2-446f-9f27-5fc7dade6d3c",
"name": "便签4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1680,
320
],
"parameters": {
"color": 5,
"width": 272,
"height": 112,
"content": "### 分析页面"
},
"typeVersion": 1
},
{
"id": "0a3c9884-2fe0-4759-941f-7fe2af68b682",
"name": "便利贴5",
"type": "n8n-nodes-base.stickyNote",
"position": [
3536,
128
],
"parameters": {
"color": 5,
"width": 256,
"height": 128,
"content": "### 发送报告"
},
"typeVersion": 1
},
{
"id": "81fa829e-90ef-41a4-aa23-c65887297f6d",
"name": "便签1",
"type": "n8n-nodes-base.stickyNote",
"position": [
480,
-928
],
"parameters": {
"color": 3,
"width": 336,
"height": 304,
"content": "## 优势"
},
"typeVersion": 1
},
{
"id": "df5d7ff4-6e19-48fd-b3ec-7e9a302a7d81",
"name": "便签 6",
"type": "n8n-nodes-base.stickyNote",
"position": [
912,
-928
],
"parameters": {
"color": 6,
"width": 288,
"height": 176,
"content": "## 目标受众"
},
"typeVersion": 1
},
{
"id": "b74a1d37-14b2-4cf4-8b1e-5ffc0c54ec84",
"name": "便签 7",
"type": "n8n-nodes-base.stickyNote",
"position": [
1280,
-928
],
"parameters": {
"color": 5,
"width": 406,
"height": 176,
"content": "## 所需 API"
},
"typeVersion": 1
},
{
"id": "c8dde663-90f0-46e7-bebe-de63a7c58f74",
"name": "便签8",
"type": "n8n-nodes-base.stickyNote",
"position": [
1744,
-928
],
"parameters": {
"color": 2,
"width": 496,
"height": 176,
"content": "## 轻松自定义"
},
"typeVersion": 1
},
{
"id": "f3181117-2221-4b73-9632-2572ab4f2a89",
"name": "便签 9",
"type": "n8n-nodes-base.stickyNote",
"position": [
2448,
208
],
"parameters": {
"color": 5,
"width": 224,
"height": 112,
"content": "### 为数据创建工作表"
},
"typeVersion": 1
},
{
"id": "874709c8-edd9-4084-b3bd-ad925b039f40",
"name": "便签10",
"type": "n8n-nodes-base.stickyNote",
"position": [
96,
-48
],
"parameters": {
"color": 5,
"width": 208,
"height": 208,
"content": "### Webhook — 由此开始"
},
"typeVersion": 1
},
{
"id": "00661d71-a7b1-485e-bff7-a1888df54b85",
"name": "便签11",
"type": "n8n-nodes-base.stickyNote",
"position": [
416,
-496
],
"parameters": {
"color": 5,
"width": 448,
"height": 336,
"content": "### 提交表单"
},
"typeVersion": 1
},
{
"id": "cd5b853c-8331-442b-b4c4-d772c6bdc4f8",
"name": "由此开始",
"type": "n8n-nodes-base.webhook",
"position": [
352,
-32
],
"webhookId": "300e7628-acda-4e87-ad89-6179f6d89260",
"parameters": {
"path": "competitors",
"options": {},
"responseMode": "responseNode",
"multipleMethods": true
},
"typeVersion": 2.1
},
{
"id": "be10836c-5727-4607-9dfb-12d50ad1f12b",
"name": "提交表单",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
576,
-128
],
"parameters": {
"options": {},
"respondWith": "text",
"responseBody": "=<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\" />\n <title>Competitor Crawl Request</title>\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\" />\n <style>\n :root{\n --bg-0:#0b1220;\n --bg-1:#0f172a;\n --bg-2:#111827;\n --card:#0b1220;\n --text:#e5e7eb;\n --muted:#94a3b8;\n --line:#1f2937;\n --accent:#7c3aed; /* violet */\n --accent-2:#06b6d4; /* cyan */\n --ok:#10b981; /* green */\n --err:#ef4444; /* red */\n }\n *{box-sizing:border-box}\n html,body{height:100%}\n body{\n margin:0;\n background:\n radial-gradient(1200px 600px at 20% -10%, rgba(124,58,237,.18), transparent 60%),\n radial-gradient(1000px 400px at 120% 10%, rgba(6,182,212,.20), transparent 60%),\n linear-gradient(180deg, #0b1220, #0b1220 60%, #0a1020 100%);\n color:var(--text);\n font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial, \"Noto Sans\", \"Apple Color Emoji\",\"Segoe UI Emoji\";\n display:flex; align-items:center; justify-content:center;\n padding:24px;\n }\n .card{\n width:100%;\n max-width: 720px;\n background: linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.01));\n border:1px solid var(--line);\n border-radius: 16px;\n box-shadow: 0 10px 30px rgba(0,0,0,.45), inset 0 1px 0 rgba(255,255,255,.03);\n backdrop-filter: blur(8px);\n overflow: hidden;\n }\n .card header{\n padding:20px 24px;\n border-bottom:1px solid var(--line);\n display:flex; align-items:center; gap:12px;\n background: linear-gradient(180deg, rgba(124,58,237,.15), rgba(124,58,237,.02));\n }\n .logo{\n width:36px; height:36px; border-radius:10px;\n background: conic-gradient(from 210deg, var(--accent), var(--accent-2));\n box-shadow: 0 0 0 2px rgba(255,255,255,.06), 0 8px 16px rgba(124,58,237,.2);\n }\n h1{font-size:1.15rem; margin:0}\n .sub{font-size:.9rem; color:var(--muted); margin-top:2px}\n form{padding:20px 24px 24px}\n .grid{\n display:grid;\n grid-template-columns: 1fr;\n gap:16px;\n }\n @media (min-width: 720px){\n .grid{ grid-template-columns: 1fr 1fr; }\n .grid .full{ grid-column: 1 / -1; }\n }\n label{\n display:block; font-size:.92rem; font-weight:600; margin-bottom:8px; color:#c7d2fe;\n }\n input, select, textarea{\n width:100%;\n background:#0d1426;\n color:var(--text);\n border:1px solid #1e293b;\n border-radius:10px;\n padding:12px 12px;\n font-size: 0.98rem;\n outline:none;\n transition: border .15s, box-shadow .15s, transform .02s;\n }\n input::placeholder, textarea::placeholder{ color:#6b7280 }\n input:focus, select:focus, textarea:focus{\n border-color: var(--accent);\n box-shadow: 0 0 0 3px rgba(124,58,237,.2);\n }\n select[multiple]{\n min-height:132px;\n padding:10px;\n }\n small.help{ display:block; color:var(--muted); margin-top:6px; }\n .row{\n display:flex; gap:10px; align-items:center; margin-top:6px;\n }\n .row input{ flex:1 }\n .pill{\n display:inline-flex; align-items:center; gap:8px;\n padding:10px 14px; border-radius:999px;\n border:1px solid #233046; background:#0e1629;\n font-weight:700; letter-spacing:.2px;\n color:#e9d5ff;\n cursor:pointer; user-select:none;\n transition: transform .02s, background .15s, border .15s;\n }\n .pill:hover{ background:#121b31; border-color:#2a3a57 }\n .submit{\n display:flex; gap:12px; align-items:center; justify-content:flex-end;\n padding:18px 24px; border-top:1px solid var(--line);\n background: linear-gradient(180deg, rgba(6,182,212,.08), rgba(6,182,212,.02));\n }\n button{\n all:unset;\n background: linear-gradient(90deg, var(--accent), var(--accent-2));\n padding:12px 18px; border-radius:12px; cursor:pointer; font-weight:700;\n box-shadow: 0 10px 22px rgba(124,58,237,.25);\n }\n button[disabled]{ opacity:.7; cursor:not-allowed; box-shadow:none }\n .msg{ padding:12px 16px; font-weight:600; }\n .success{ color: var(--ok); }\n .error{ color: var(--err); }\n .inline-note{ font-size:.85rem; color:var(--muted); margin-left:10px }\n </style>\n</head>\n<body>\n <div class=\"card\" role=\"region\" aria-label=\"Competitor Crawl Request\">\n <header>\n <div class=\"logo\" aria-hidden=\"true\"></div>\n <div>\n <h1>Competitor Crawl Request</h1>\n <div class=\"sub\">Send a domain and optional scoping rules for the crawl</div>\n </div>\n </header>\n\n <form id=\"requestForm\" novalidate>\n <div class=\"grid\">\n <div class=\"full\">\n <label for=\"competitors\">Competitor URL / Domain</label>\n <input id=\"competitors\" name=\"competitors\" type=\"text\" required placeholder=\"https://example.com or example.com\" />\n <small class=\"help\">A full URL or bare domain is fine — we’ll normalize it.</small>\n </div>\n\n <div>\n <label for=\"name\">Your Name</label>\n <input id=\"name\" name=\"name\" type=\"text\" required placeholder=\"Your name…\" autocomplete=\"name\" />\n </div>\n\n <div>\n <label for=\"email\">Email (for report)</label>\n <input id=\"email\" name=\"email\" type=\"email\" required placeholder=\"you@domain.com\" autocomplete=\"email\" inputmode=\"email\" />\n <small class=\"help\">We’ll email the summary here.</small>\n </div>\n\n <div class=\"full\">\n <label for=\"include_paths\">Include paths (multi-select)</label>\n <select id=\"include_paths\" multiple>\n <option value=\"/\">/</option>\n <option value=\"/blog/\">/blog/</option>\n <option value=\"/services/\">/services/</option>\n <option value=\"/products/\">/products/</option>\n <option value=\"/news/\">/news/</option>\n <option value=\"/portfolio/\">/portfolio/</option>\n </select>\n <div class=\"row\">\n <input id=\"include_custom\" type=\"text\" placeholder=\"Custom paths (comma-separated, e.g. /guide/, /docs/ )\" />\n <span class=\"pill\" id=\"add_include\">+ Add</span>\n </div>\n <small class=\"help\">Hold Ctrl/⌘ to select multiple. Custom items are appended automatically.</small>\n </div>\n\n <div class=\"full\">\n <label for=\"exclude_paths\">Exclude paths (multi-select)</label>\n <select id=\"exclude_paths\" multiple>\n <option value=\"/privacy\">/privacy</option>\n <option value=\"/terms\">/terms</option>\n <option value=\"/login\">/login</option>\n <option value=\"/cart\">/cart</option>\n <option value=\"/checkout\">/checkout</option>\n <option value=\"/wp-admin\">/wp-admin</option>\n <option value=\"/tag/\">/tag/</option>\n <option value=\"/category/\">/category/</option>\n <option value=\"/feed\">/feed</option>\n </select>\n <div class=\"row\">\n <input id=\"exclude_custom\" type=\"text\" placeholder=\"Custom paths (comma-separated, e.g. /search, /admin )\" />\n <span class=\"pill\" id=\"add_exclude\">+ Add</span>\n </div>\n <small class=\"help\">Use this to skip utility/low-value areas.</small>\n </div>\n\n <div>\n <label for=\"max_pages\">Max pages</label>\n <select id=\"max_pages\">\n <option value=\"5\">5</option>\n <option value=\"10\">10</option>\n <option value=\"20\" selected>20</option>\n <option value=\"50\">50</option>\n <option value=\"100\">100</option>\n <option value=\"200\">200</option>\n </select>\n <span class=\"inline-note\">Default 20</span>\n </div>\n\n <div>\n <label for=\"crawl_depth\">Crawl depth</label>\n <select id=\"crawl_depth\">\n <option value=\"0\">0 (only start URL)</option>\n <option value=\"1\" selected>1</option>\n <option value=\"2\">2</option>\n <option value=\"3\">3</option>\n </select>\n <span class=\"inline-note\">Default 1</span>\n </div>\n </div>\n\n <div class=\"submit\">\n <div id=\"response\" class=\"msg\" aria-live=\"polite\"></div>\n <button id=\"submitBtn\" type=\"submit\">Send Request</button>\n </div>\n </form>\n </div>\n\n <script>\n const $ = (id) => document.getElementById(id);\n\n function showMsg(text, type) {\n const el = $('response');\n el.className = 'msg ' + (type || '');\n el.textContent = text;\n }\n\n function normalizeUrl(input) {\n const s = (input || '').trim();\n if (!s) return '';\n return /^https?:\\/\\//i.test(s) ? s : `https://${s}`;\n }\n\n function selectedValues(selectEl) {\n return Array.from(selectEl.options)\n .filter(o => o.selected)\n .map(o => o.value.trim())\n .filter(Boolean);\n }\n\n function parseCustom(inputEl) {\n return (inputEl.value || '')\n .split(',')\n .map(s => s.trim())\n .filter(Boolean);\n }\n\n function addCustomToSelect(inputEl, selectEl) {\n parseCustom(inputEl).forEach(v => {\n const opt = document.createElement('option');\n opt.value = v;\n opt.textContent = v;\n opt.selected = true;\n selectEl.appendChild(opt);\n });\n inputEl.value = '';\n }\n\n // Wire add buttons\n $('add_include').addEventListener('click', () => addCustomToSelect($('include_custom'), $('include_paths')));\n $('add_exclude').addEventListener('click', () => addCustomToSelect($('exclude_custom'), $('exclude_paths')));\n\n $('requestForm').addEventListener('submit', async (e) => {\n e.preventDefault();\n showMsg('', '');\n\n if (!e.target.checkValidity()) {\n e.target.reportValidity();\n return;\n }\n\n const name = $('name').value.trim();\n const email = $('email').value.trim();\n const competitors = normalizeUrl($('competitors').value.trim());\n\n const emailOk = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n if (!emailOk) { $('email').focus(); return showMsg('❌ Please enter a valid email.', 'error'); }\n\n if (!competitors) { $('competitors').focus(); return showMsg('❌ Please enter a competitor URL.', 'error'); }\n\n // Merge dropdown selections + any custom text entries\n const includeSel = selectedValues($('include_paths'));\n const excludeSel = selectedValues($('exclude_paths'));\n const includeCustom = parseCustom($('include_custom'));\n const excludeCustom = parseCustom($('exclude_custom'));\n\n const include_paths = [...includeSel, ...includeCustom].join(', ');\n const exclude_paths = [...excludeSel, ...excludeCustom].join(', ');\n\n const max_pages = $('max_pages').value;\n const crawl_depth = $('crawl_depth').value;\n\n const submitBtn = $('submitBtn');\n submitBtn.setAttribute('disabled','true');\n submitBtn.textContent = 'Sending…';\n\n try {\n const webhookUrl = 'https://n8nworkflow.eu/webhook-test/competitors'; // use production /webhook/... when you deploy\n const res = await fetch(webhookUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n name,\n email,\n competitors,\n include_paths,\n exclude_paths,\n max_pages,\n crawl_depth\n })\n });\n\n if (res.ok) {\n showMsg('✅ Request sent. We’ll email you when it’s processed.', 'success');\n e.target.reset();\n } else {\n const txt = await res.text().catch(() => '');\n showMsg('❌ Failed to send. ' + (txt || ''), 'error');\n }\n } catch (err) {\n showMsg('❌ Network error. Please try again.', 'error');\n } finally {\n submitBtn.removeAttribute('disabled');\n submitBtn.textContent = 'Send Request';\n }\n });\n </script>\n</body>\n</html>"
},
"executeOnce": false,
"retryOnFail": true,
"typeVersion": 1.4
},
{
"id": "76b234af-da62-4017-a648-e5a8d7a61c51",
"name": "准备工作表行",
"type": "n8n-nodes-base.set",
"position": [
2944,
-16
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "367b45c6-c47d-43de-ae52-ea269cdbe938",
"name": "page_url",
"type": "string",
"value": "={{ $('Parse and Normalize Gemini JSON').item.json.page_url }}"
},
{
"id": "420d1ace-2bb3-4fbf-ada2-9301ca570f9a",
"name": "main_topics",
"type": "string",
"value": "={{ $('Parse and Normalize Gemini JSON').item.json.main_topics_flat }}"
},
{
"id": "707356b5-ae9f-4706-820d-213916363049",
"name": "key_words",
"type": "string",
"value": "={{ $('Parse and Normalize Gemini JSON').item.json.key_entities_flat }}"
}
]
}
},
"typeVersion": 3.4
}
],
"active": false,
"pinData": {},
"settings": {
"timezone": "Europe/Helsinki",
"callerPolicy": "workflowsFromSameOwner",
"executionOrder": "v1"
},
"versionId": "30265337-2486-4efa-90c8-7dfb0cca682e",
"connections": {
"Merge": {
"main": [
[
{
"node": "Prepare Sheet Row",
"type": "main",
"index": 0
}
]
]
},
"AI Agent": {
"main": [
[
{
"node": "Parse and Normalize Gemini JSON",
"type": "main",
"index": 0
}
]
]
},
"Start Here": {
"main": [
[
{
"node": "Submission form",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Apify Input",
"type": "main",
"index": 0
}
]
]
},
"Submission form": {
"main": [
[]
]
},
"Derive Sheet Name": {
"main": [
[
{
"node": "Create sheet for the data",
"type": "main",
"index": 0
},
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Prepare Sheet Row": {
"main": [
[
{
"node": "Save the data collected",
"type": "main",
"index": 0
}
]
]
},
"Prepare Apify Input": {
"main": [
[
{
"node": "Check if the data exist",
"type": "main",
"index": 0
}
]
]
},
"Analyze Page Content": {
"ai_languageModel": [
[
{
"node": "AI Agent",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Has content to email": {
"main": [
[
{
"node": "Send report ",
"type": "main",
"index": 0
}
]
]
},
"Extract Page Metadata": {
"main": [
[
{
"node": "AI Agent",
"type": "main",
"index": 0
}
]
]
},
"Fetch Crawled Dataset": {
"main": [
[
{
"node": "Extract Page Metadata",
"type": "main",
"index": 0
}
]
]
},
"Check if the data exist": {
"main": [
[
{
"node": "Crawl Competitor Website",
"type": "main",
"index": 0
},
{
"node": "Save Data Of The request",
"type": "main",
"index": 0
}
]
]
},
"Save the data collected": {
"main": [
[
{
"node": "Has content to email",
"type": "main",
"index": 0
}
]
]
},
"Crawl Competitor Website": {
"main": [
[
{
"node": "Fetch Crawled Dataset",
"type": "main",
"index": 0
}
]
]
},
"Save Data Of The request": {
"main": [
[]
]
},
"Create sheet for the data": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Parse and Normalize Gemini JSON": {
"main": [
[
{
"node": "Derive Sheet Name",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - 杂项, AI 摘要总结, 多模态 AI
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
AI简历优化器
使用Gemini分析和邮件报告将简历匹配到职位描述
Set
Code
Gmail
+9
18 节点Mychel Garzon
AI 摘要总结
来自多个招聘网站的求职自动化
使用 5 个招聘平台和 AI 简历生成器自动化求职与申请
If
Set
Code
+14
34 节点Gerald Denor
个人效率
使用Groq AI和GhostGenius比较LinkedIn个人资料与职位描述
使用Groq AI和GhostGenius比较LinkedIn个人资料与职位描述的匹配度
If
Set
Code
+8
17 节点Stephan Koning
杂项
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
If
Set
Code
+20
52 节点Daniel Lianes
杂项
欺诈预订检测器:使用AI识别可疑旅行交易
欺诈预订检测器:使用Google Gemini识别可疑旅行交易
If
Set
Code
+8
20 节点Oneclick AI Squad
安全运维
在可视化参考库中探索n8n节点
在可视化参考库中探索n8n节点
If
Ftp
Set
+93
113 节点I versus AI
其他