自动化内容SEO审计报告
高级
这是一个AI, Marketing领域的自动化工作流,包含 21 个节点。主要使用 If, Set, Code, Wait, HttpRequest 等节点,结合人工智能技术实现智能自动化。 使用DataForSEO和Google Search Console生成全面的SEO审计报告
前置要求
- •可能需要目标 API 的认证凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "Tqa8dikBDLYEytx5",
"meta": {
"instanceId": "ddfdf733df99a65c801a91865dba5b7c087c95cc22a459ff3647e6deddf2aee6"
},
"name": "自动化内容SEO审计报告",
"tags": [],
"nodes": [
{
"id": "b5f15675-35c9-42a1-b7eb-bfaf0b467a5a",
"name": "设置字段",
"type": "n8n-nodes-base.set",
"position": [
280,
620
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e71886f0-104f-412b-9fef-d2b3738cebf0",
"name": "dfs_domain",
"type": "string",
"value": "yourclientdomain.com"
},
{
"id": "de35327e-1e32-4996-970a-50b8953c7709",
"name": "dfs_max_crawl_pages",
"type": "string",
"value": "1000"
},
{
"id": "0d6b4d1a-e57d-4e38-8aa5-e2ea5589a089",
"name": "dfs_enable_javascript",
"type": "string",
"value": "false"
},
{
"id": "d699e487-ab74-483f-8cd8-cdcfaca567d7",
"name": "company_name",
"type": "string",
"value": "Custom Workflows AI"
},
{
"id": "da123535-f678-4331-973a-07711b7aaaac",
"name": "company_website",
"type": "string",
"value": "https://customworkflows.ai"
},
{
"id": "e12486eb-7019-4639-85a9-c55b4c62beef",
"name": "company_logo_url",
"type": "string",
"value": "https://customworkflows.ai/images/logo.png"
},
{
"id": "9eef2015-e89c-4930-82a5-972111c1a4fe",
"name": "brand_primary_color",
"type": "string",
"value": "#252946"
},
{
"id": "dd4ff260-6008-49ec-a0e6-ad5c177eb8df",
"name": "brand_secondary_color",
"type": "string",
"value": "#0fd393"
},
{
"id": "d71a4d91-c5bf-49c4-b7d0-64e84dad6153",
"name": "gsc_property_type",
"type": "string",
"value": "domain"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "57a66b27-a253-4543-9d44-cd3afdbc3946",
"name": "点击\"开始\"时",
"type": "n8n-nodes-base.manualTrigger",
"position": [
60,
620
],
"parameters": {},
"typeVersion": 1
},
{
"id": "3e5e8162-2815-429f-b6e8-6ea6ea70cf18",
"name": "检查任务状态",
"type": "n8n-nodes-base.httpRequest",
"position": [
660,
620
],
"parameters": {
"url": "=https://api.dataforseo.com/v3/on_page/summary/{{ $json.tasks[0].id }}",
"options": {},
"sendHeaders": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "9ea481fe-8af6-43c2-881d-eb68f63b0424",
"name": "Create Task",
"type": "n8n-nodes-base.httpRequest",
"position": [
480,
620
],
"parameters": {
"url": "https://api.dataforseo.com/v3/on_page/task_post",
"method": "POST",
"options": {},
"jsonBody": "=[\n {\n \"target\": \"{{ $json.dfs_domain }}\",\n \"max_crawl_pages\": {{ $json.dfs_max_crawl_pages }},\n \"load_resources\": false,\n \"enable_javascript\": {{ $json.dfs_enable_javascript }},\n \"custom_js\": \"meta = {}; meta.url = document.URL; meta;\",\n \"tag\": \"{{ $json.dfs_domain + Math.floor(10000 + Math.random() * 90000) }}\"\n }\n]",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "0a0e696a-29a7-4b34-8299-102c72544153",
"name": "条件判断",
"type": "n8n-nodes-base.if",
"position": [
860,
620
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "7e13429d-9ead-4ae5-8ed6-c5730b05927d",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.tasks[0].result[0].crawl_progress }}",
"rightValue": "finished"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "a31db736-23e0-4db8-ab90-294cd87c9123",
"name": "等待",
"type": "n8n-nodes-base.wait",
"position": [
1060,
680
],
"webhookId": "f60d5346-5ddf-4819-a865-48e2d9e6103c",
"parameters": {
"unit": "minutes",
"amount": 1
},
"typeVersion": 1.1
},
{
"id": "8f95fd0b-e990-4c85-b21b-83d06d2121fe",
"name": "获取原始审计数据",
"type": "n8n-nodes-base.httpRequest",
"position": [
1060,
500
],
"parameters": {
"url": "https://api.dataforseo.com/v3/on_page/pages",
"method": "POST",
"options": {},
"jsonBody": "=[\n {\n \"id\": \"{{ $json.tasks[0].id }}\",\n \"limit\": \"1000\"\n }\n]",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "6cf221d9-c17e-4a5c-9c9a-c3176319df95",
"name": "提取URLs",
"type": "n8n-nodes-base.code",
"position": [
1260,
500
],
"parameters": {
"jsCode": "// Get input data from the previous node\nconst input = $input.all();\n\n// Initialize an array to store the new items\nconst output = [];\n\n// Loop through each input item\nfor (const item of input) {\n const tasks = item.json.tasks || [];\n for (const task of tasks) {\n const results = task.result || [];\n for (const result of results) {\n const items = result.items || [];\n for (const page of items) {\n // Only include URLs with status_code 200\n if (page.url && page.status_code === 200) {\n output.push({ json: { url: page.url } });\n }\n }\n }\n }\n}\n\n// Return all URLs with status code 200 as separate items\nreturn output;"
},
"typeVersion": 2
},
{
"id": "fbf18c28-dbd5-410b-87cb-5f5aef44727e",
"name": "遍历项目",
"type": "n8n-nodes-base.splitInBatches",
"position": [
1480,
500
],
"parameters": {
"options": {},
"batchSize": 100
},
"typeVersion": 3
},
{
"id": "aebdd823-9a4d-4323-aadf-b7d92d601d57",
"name": "查询GSC API",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueErrorOutput",
"maxTries": 5,
"position": [
1480,
680
],
"parameters": {
"url": "={{ \n $('Set Fields').first().json.gsc_property_type === 'domain' \n ? 'https://searchconsole.googleapis.com/webmasters/v3/sites/' + \n 'sc-domain:' + \n $node[\"Loop Over Items\"].json.url.replace(/https?:\\/\\/(www\\.)?([^\\/]+).*/, '$2') + \n '/searchAnalytics/query' \n : 'https://searchconsole.googleapis.com/webmasters/v3/sites/' + \n encodeURIComponent(\n $node[\"Loop Over Items\"].json.url.replace(/(https?:\\/\\/(?:www\\.)?[^\\/]+).*/, '$1')\n ) + \n '/searchAnalytics/query' \n}}",
"body": "={\n \"startDate\": \"{{ new Date(new Date().setDate(new Date().getDate() - 90)).toISOString().split('T')[0] }}\",\n \"endDate\": \"{{ new Date().toISOString().split('T')[0] }}\",\n \"dimensionFilterGroups\": [\n {\n \"filters\": [\n {\n \"dimension\": \"page\",\n \"operator\": \"equals\",\n \"expression\": \"{{ $node['Loop Over Items'].json.url }}\"\n }\n ]\n }\n ],\n \"aggregationType\": \"auto\",\n \"rowLimit\": 100\n}",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "raw",
"sendHeaders": true,
"authentication": "predefinedCredentialType",
"rawContentType": "JSON",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"nodeCredentialType": "googleOAuth2Api"
},
"retryOnFail": true,
"typeVersion": 4.2,
"waitBetweenTries": 5000
},
{
"id": "d9943a4b-7320-47ce-95fa-67eb28cabd26",
"name": "等待1",
"type": "n8n-nodes-base.wait",
"position": [
1680,
680
],
"webhookId": "8b2109f4-1aca-4585-8261-7dfc4ca2f95e",
"parameters": {
"unit": "minutes",
"amount": 1
},
"typeVersion": 1.1
},
{
"id": "f2f7e975-1db1-4566-b674-396ccaa775f5",
"name": "将GSC数据映射到URL",
"type": "n8n-nodes-base.set",
"position": [
1880,
680
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "342ff66d-cdfc-46e8-9605-db588c913eb0",
"name": "URL",
"type": "string",
"value": "={{ $('Loop Over Items').item.json.url }}"
},
{
"id": "5c547efc-0514-4641-8f05-c24b965993ad",
"name": "Clicks",
"type": "string",
"value": "={{ $('Query GSC API').item.json.rows[0].clicks }}"
},
{
"id": "340c3ced-061d-49f0-911d-bd8b9e433a7d",
"name": "Impressions",
"type": "string",
"value": "={{ $('Query GSC API').item.json.rows[0].impressions }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "4e42e1eb-4769-4e28-9f2f-3fb342baf971",
"name": "合并GSC数据与原始数据",
"type": "n8n-nodes-base.code",
"position": [
1680,
500
],
"parameters": {
"jsCode": "/*\n * Function node\n * Inputs: none (reads data from other nodes)\n * Output: ONE item whose .json is the enriched audit object\n */\n\n// 1. ---- Get the raw audit JSON ------------------------------------------\nlet rawAuditData = $node['Get RAW Audit Data'].json; // first item of that node\n\n// If that node delivered a JSON string, parse it:\nif (typeof rawAuditData === 'string') {\n\trawAuditData = JSON.parse(rawAuditData);\n}\n\n// 2. ---- Get the Google Search Console rows ------------------------------\nconst gscItems = $items('Loop Over Items'); // all items from that node\n\n// 3. ---- Build a fast lookup: URL -> { clicks, impressions } ------------\nconst gscLookup = {};\nfor (const { json } of gscItems) {\n const { URL, Clicks, Impressions } = json;\n if (URL) {\n gscLookup[URL] = {\n clicks: Clicks !== undefined ? Number(Clicks) || 0 : null,\n impressions: Impressions !== undefined ? Number(Impressions) || 0 : null,\n };\n }\n}\n\n// 4. ---- Enrich every page record with googleSearchConsoleData -------------\nconst itemsPath = (((rawAuditData.tasks || [])[0] || {}).result || [])[0]?.items || [];\n\nfor (const page of itemsPath) {\n const url = page.url;\n page.googleSearchConsoleData = gscLookup[url] || { clicks: null, impressions: null };\n}\n\n// 5. ---- Return ONE item with the updated audit data ----------------------\nreturn [\n\t{\n\t\tjson: rawAuditData, // <-- an actual object, so n8n is satisfied\n\t},\n];"
},
"typeVersion": 2
},
{
"id": "0b35fb68-6a0d-4eea-b29a-96550574c2b8",
"name": "构建报告结构",
"type": "n8n-nodes-base.code",
"position": [
2100,
320
],
"parameters": {
"jsCode": "/**\n * n8n – Function node\n * Input : • One item whose `json` is the crawl + GSC data\n * • All the items produced by the loop node “Loop Over Items1”\n * Output : ONE item whose `json` = { generatedAt, summary, issues, pages }\n * – Unchanged shape, just extra `sources`[] on 404 / 301 records\n */\n\n/* ────────────────────── helpers & constants ───────────────────── */\nconst CUR_YEAR = new Date().getFullYear();\nconst YEAR_RX = /20\\d{2}/g;\nconst TWELVE_MONTHS_MS = 1000 * 60 * 60 * 24 * 365.25;\nconst SIX_MONTHS_MS = TWELVE_MONTHS_MS / 2;\nconst LARGE_HTML_LIMIT = 2_000_000;\n\nconst ageInMs = (s) => Date.now() - Date.parse(s);\nconst ensureBucket = (parent, key) => (parent[key] ??= []);\nconst normalizeUrl = (u) => (u || '').replace(/\\/+$/, ''); // strip trailing “/”\n\n/* ────────────────────── main data sets ───────────────────────── */\nconst root = $node['Merge GSC Data with RAW Data'].json;\nconst pages = root.tasks?.[0]?.result?.[0]?.items ?? [];\n\n/* link-source items from the loop node */\nconst sourceItems = $items('Loop Over Items1') ?? [];\nconst linkSourceMap = {}; // { normalisedTargetUrl : [ {linkFrom,type,text},… ] }\n\nfor (const itm of sourceItems) {\n const j = itm.json || {};\n const tgt = normalizeUrl(j.URL);\n if (!tgt) continue;\n\n linkSourceMap[tgt] ??= [];\n for (const s of j.sources || []) {\n linkSourceMap[tgt].push({\n linkFrom: s.link_from,\n type : s.type,\n text : s.text,\n });\n }\n}\n\n/* ────────────────────── duplicate-meta look-ups ───────────────── */\nconst titleFreq = {};\nconst descFreq = {};\n\nfor (const p of pages) {\n const t = p.meta?.title?.trim();\n const d = p.meta?.description?.trim();\n if (t) titleFreq[t] = (titleFreq[t] || 0) + 1;\n if (d) descFreq[d] = (descFreq[d] || 0) + 1;\n}\n\n/* ────────────────────── report skeleton ──────────────────────── */\nconst issues = {\n statusIssues: {},\n contentQuality: {},\n metadataSEO: {},\n internalLinking: {},\n underperformingContent: [],\n};\n\nconst summary = { pages: pages.length };\nconst pagesWithFlags = [];\n\n/* ────────────────────── per-page loop ────────────────────────── */\nfor (const p of pages) {\n const url = p.url;\n const norm = normalizeUrl(url);\n const flags = [];\n\n const add = (sect, bucket, rec) => ensureBucket(issues[sect], bucket).push(rec);\n\n const isStatusOK = p.status_code === 200;\n\n /* 1 · 404 ---------------------------------------------------- */\n if (p.status_code === 404 || p.checks?.is_4xx_code) {\n flags.push('404');\n add('statusIssues', 'pages404', {\n url,\n sources: linkSourceMap[norm] ?? [], // ← new\n todo : 'Restore the page or 301-redirect it to a relevant URL.',\n });\n }\n\n /* 2 · 301 ---------------------------------------------------- */\n if (p.status_code === 301 || p.checks?.is_redirect) {\n flags.push('redirect_301');\n add('statusIssues', 'redirects301', {\n url,\n sources: linkSourceMap[norm] ?? [], // ← new\n todo : 'Update internal links so they point directly to the final URL (single-hop redirect).',\n });\n }\n\n /* 3 – 15 · all original checks (unchanged) ------------------ */\n /* Canonicalised */\n const canonicalised =\n (p.meta?.canonical && p.meta.canonical !== url) ||\n p.checks?.canonical_chain ||\n p.checks?.recursive_canonical;\n\n if (isStatusOK && canonicalised) {\n flags.push('canonicalised');\n add('statusIssues', 'canonicalised', {\n url,\n canonical: p.meta?.canonical,\n todo: `Verify that \"${p.meta?.canonical || '—'}\" is the correct canonical target and eliminate unintended duplicates.`,\n });\n }\n\n /* Outdated content (years + stale last-modified) */\n if (isStatusOK) {\n const titleYears = (p.meta?.title?.match(YEAR_RX) || []).filter((y) => Number(y) < CUR_YEAR);\n const descYears = (p.meta?.description?.match(YEAR_RX) || []).filter((y) => Number(y) < CUR_YEAR);\n\n if (titleYears.length) {\n flags.push('outdated_year_title');\n add('contentQuality', 'outdatedMetaYear', {\n url,\n field : 'title',\n years : titleYears.join(','),\n original : p.meta?.title,\n todo : `Title contains old year → ${titleYears.join(', ')}. Update to ${CUR_YEAR} or remove dates.`,\n });\n }\n if (descYears.length) {\n flags.push('outdated_year_description');\n add('contentQuality', 'outdatedMetaYear', {\n url,\n field : 'description',\n years : descYears.join(','),\n original : p.meta?.description,\n todo : `Meta description contains old year → ${descYears.join(', ')}. Update to ${CUR_YEAR} or remove dates.`,\n });\n }\n\n const lm = p.last_modified ??\n p.meta?.social_media_tags?.['og:updated_time'] ?? null;\n\n if (lm && ageInMs(lm) > TWELVE_MONTHS_MS) {\n flags.push('stale_last_modified');\n add('contentQuality', 'staleLastModified', {\n url,\n lastModified: lm,\n todo : 'Page not updated for 12+ months — refresh content.',\n });\n }\n }\n\n /* Thin content */\n if (isStatusOK) {\n const wc = p.meta?.content?.plain_text_word_count || 0;\n if (p.click_depth !== 0 && wc >= 1 && wc <= 1500) {\n flags.push('thin_content');\n add('contentQuality', 'thinContent', {\n url,\n words: wc,\n todo : 'Expand the piece beyond 1 500 words with valuable, unique information.',\n });\n }\n }\n\n /* Excessive click depth */\n if (isStatusOK && (p.click_depth || 0) > 4) {\n flags.push('excessive_click_depth');\n add('internalLinking', 'excessiveClickDepth', {\n url,\n depth: p.click_depth,\n todo : 'Surface this URL within ≤4 clicks via navigation or contextual links.',\n });\n }\n\n /* Large HTML */\n if (isStatusOK && ((p.size || 0) > LARGE_HTML_LIMIT || (p.total_dom_size || 0) > LARGE_HTML_LIMIT)) {\n flags.push('large_html');\n add('contentQuality', 'largeHTML', {\n url,\n size : p.size,\n totalDom: p.total_dom_size,\n todo : 'Reduce HTML payload (remove unused markup/JS, paginate, or lazy-load where possible).',\n });\n }\n\n /* Title length */\n if (isStatusOK && (p.meta?.title_length < 40 || p.meta?.title_length > 60)) {\n flags.push('title_length');\n add('metadataSEO', 'titleLength', {\n url,\n length: p.meta?.title_length,\n todo : `Write a meta title 40-60 characters long (currently ${p.meta?.title_length || 0}).`,\n });\n }\n\n /* Description length */\n if (isStatusOK) {\n const dl = p.meta?.description_length || 0;\n if (dl > 0 && (dl < 70 || dl > 155)) {\n flags.push('description_length');\n add('metadataSEO', 'descriptionLength', {\n url,\n length: dl,\n todo : `Write a meta description 70-155 characters long (currently ${dl}).`,\n });\n }\n }\n\n /* Missing / duplicate meta */\n if (isStatusOK) {\n if (p.checks?.no_title) {\n flags.push('missing_title');\n add('metadataSEO', 'missingTitle', { url, todo: 'Add a unique SEO title 40-60 characters long.' });\n }\n if (p.checks?.no_description) {\n flags.push('missing_description');\n add('metadataSEO', 'missingDescription', { url, todo: 'Add a unique meta description 70-155 characters long.' });\n }\n if (titleFreq[p.meta?.title?.trim()] > 1) {\n flags.push('duplicate_title');\n add('metadataSEO', 'duplicateTitle', { url, title: p.meta?.title, todo: 'Differentiate this title to avoid keyword cannibalisation.' });\n }\n if (p.meta?.description && descFreq[p.meta.description.trim()] > 1) {\n flags.push('duplicate_description');\n add('metadataSEO', 'duplicateDescription', { url, description: p.meta?.description, todo: 'Rewrite the meta description so each page is unique.' });\n }\n }\n\n /* H1 issues */\n if (isStatusOK) {\n const h1s = p.meta?.htags?.h1 ?? [];\n if (h1s.length !== 1) {\n flags.push('h1_issue');\n add('metadataSEO', 'h1Issues', { url, h1Count: h1s.length, todo: 'Ensure exactly one H1 tag per page that reflects the main topic.' });\n }\n }\n\n /* Readability */\n if (isStatusOK) {\n const fk = p.meta?.content?.flesch_kincaid_readability_index ?? 100;\n if (fk < 55) {\n flags.push('low_readability');\n add('contentQuality', 'readability', { url, score: fk, todo: `Simplify language, shorten sentences, and use lists to lift F-K score > 55 (currently ${fk.toFixed(2)}).` });\n }\n }\n\n /* Orphan pages */\n if (isStatusOK && p.checks?.is_orphan_page) {\n flags.push('orphan_page');\n add('internalLinking', 'orphanPages', { url, todo: 'Add at least one crawlable internal link pointing to this URL.' });\n }\n\n /* Low internal links */\n if (isStatusOK && (p.meta?.internal_links_count || 0) < 3) {\n flags.push('low_internal_links');\n add('internalLinking', 'lowInternalLinks', { url, links: p.meta?.inbound_links_count, todo: 'Add three or more relevant internal links to strengthen topical signals.' });\n }\n\n /* Under-performing content */\n if (isStatusOK) {\n const clicks = p.googleSearchConsoleData?.clicks ?? null;\n const impressions = p.googleSearchConsoleData?.impressions ?? null;\n const lm = p.last_modified ?? p.meta?.social_media_tags?.['og:updated_time'] ?? null;\n\n if (clicks !== null && clicks < 50 && (lm === null || ageInMs(lm) > SIX_MONTHS_MS)) {\n flags.push('underperforming');\n issues.underperformingContent.push({\n url,\n clicks,\n impressions,\n lastModified: lm,\n todo: `Only ${clicks} clicks in the last 90 days — refresh content, improve targeting, or consider pruning.`,\n });\n }\n }\n\n /* page-level flags record */\n pagesWithFlags.push({\n url,\n flags,\n clicks : p.googleSearchConsoleData?.clicks,\n impressions: p.googleSearchConsoleData?.impressions,\n });\n}\n\n/* ────────────────────── executive summary ────────────────────── */\nconst count = (sect, bucket) => issues[sect]?.[bucket]?.length || 0;\n\nsummary.issues = {\n '404' : count('statusIssues', 'pages404'),\n redirects : count('statusIssues', 'redirects301'),\n canonicalised : count('statusIssues', 'canonicalised'),\n outdated : count('contentQuality', 'outdatedMetaYear') +\n count('contentQuality', 'staleLastModified'),\n thin : count('contentQuality', 'thinContent'),\n excessiveClickDepth : count('internalLinking', 'excessiveClickDepth'),\n largeHTML : count('contentQuality', 'largeHTML'),\n titleLen : count('metadataSEO', 'titleLength'),\n descriptionLen : count('metadataSEO', 'descriptionLength'),\n missingOrDuplicateMeta:\n count('metadataSEO', 'missingTitle') +\n count('metadataSEO', 'missingDescription') +\n count('metadataSEO', 'duplicateTitle') +\n count('metadataSEO', 'duplicateDescription'),\n h1Issues : count('metadataSEO', 'h1Issues'),\n readability : count('contentQuality', 'readability'),\n orphan : count('internalLinking', 'orphanPages'),\n lowInternalLinks : count('internalLinking', 'lowInternalLinks'),\n underperforming : issues.underperformingContent.length,\n};\n\n/* ────────────────────── final report ─────────────────────────── */\nreturn [{\n json: {\n generatedAt: new Date().toISOString(),\n summary,\n issues,\n pages: pagesWithFlags,\n },\n}];"
},
"typeVersion": 2
},
{
"id": "2227e1c7-890a-4b99-ad20-5b5645ba884b",
"name": "生成HTML报告",
"type": "n8n-nodes-base.code",
"position": [
2320,
320
],
"parameters": {
"jsCode": "// Get the audit data and company information\nconst auditData = $('Build Report Structure').item.json;\nconst websiteDomain = $('Set Fields').first().json.dfs_domain;\nconst companyName = $('Set Fields').first().json.company_name;\nconst companyWebsite = $('Set Fields').first().json.company_website;\nconst companyLogoUrl = $('Set Fields').first().json.company_logo_url;\nconst primaryColor = $('Set Fields').first().json.brand_primary_color;\nconst secondaryColor = $('Set Fields').first().json.brand_secondary_color;\n\n// Format date nicely\nconst formattedDate = new Date(auditData.generatedAt).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric'\n});\n\n// Calculate total issues\nconst totalIssues = Object.values(auditData.summary.issues).reduce((sum, count) => sum + count, 0);\n\n// Define issue gravity weights for health score calculation\nconst issueGravity = {\n // Content Quality\n outdated: 2, // Medium\n thin: 3, // High\n readability: 1, // Low\n largeHTML: 2, // Medium\n // Technical SEO\n '404': 3, // High\n redirects: 2, // Medium\n canonicalised: 3, // High\n // On-Page SEO\n titleLen: 1, // Low\n descriptionLen: 1, // Low\n missingOrDuplicateMeta: 1, // Low\n h1Issues: 3, // High\n // Internal Linking\n excessiveClickDepth: 3, // High\n orphan: 3, // High\n lowInternalLinks: 3, // High\n // Performance\n underperforming: 3 // High\n};\n\n// Calculate health score based on issue gravity\nfunction calculateHealthScore(pages, issues) {\n // Calculate weighted sum of issues\n let weightedIssues = 0;\n let maxPossibleWeightedIssues = 0;\n \n // Process each issue type with its gravity weight\n for (const [issueType, count] of Object.entries(auditData.summary.issues)) {\n const gravity = issueGravity[issueType] || 1; // Default to Low if not defined\n weightedIssues += count * gravity;\n \n // Assume worst case: all pages have this issue\n maxPossibleWeightedIssues += pages * gravity;\n }\n \n // Cap the maximum penalty to avoid too severe scores with many pages\n const maxPenalty = Math.min(pages * 5, 100);\n \n // Calculate score: start at 100 and subtract weighted penalty\n const weightedPenalty = Math.min(maxPenalty, (weightedIssues / Math.max(1, pages)) * 2);\n const score = 100 - weightedPenalty;\n \n return Math.max(0, Math.round(score));\n}\n\n// Get health score color based on value\nfunction getHealthScoreColor(score) {\n if (score >= 80) return '#4caf50'; // Green\n if (score >= 60) return '#ff9800'; // Orange\n return '#f44336'; // Red\n}\n\n// Get top recommendations\nfunction getTopRecommendations(audit) {\n const recommendations = [];\n const priorityMap = {\n 3: \"high\", // High gravity issues\n 2: \"medium\", // Medium gravity issues\n 1: \"low\" // Low gravity issues\n };\n \n // Check for high gravity issues first\n if ((audit.issues.contentQuality.thinContent || []).length > 0) {\n recommendations.push({\n text: \"Expand thin content pages to improve topical depth and authority\",\n priority: priorityMap[issueGravity.thin] || \"high\"\n });\n }\n \n if ((audit.issues.statusIssues.pages404 || []).length > 0) {\n recommendations.push({\n text: \"Fix 404 errors by restoring pages or implementing proper redirects\",\n priority: priorityMap[issueGravity['404']] || \"high\"\n });\n }\n \n if ((audit.issues.metadataSEO.h1Issues || []).length > 0) {\n recommendations.push({\n text: \"Fix H1 tag issues to improve on-page SEO and content hierarchy\",\n priority: priorityMap[issueGravity.h1Issues] || \"high\"\n });\n }\n \n if ((audit.issues.internalLinking.orphanPages || []).length > 0) {\n recommendations.push({\n text: \"Create internal links to orphan pages to improve crawlability\",\n priority: priorityMap[issueGravity.orphan] || \"high\"\n });\n }\n \n if ((audit.issues.underperformingContent || []).length > 0) {\n recommendations.push({\n text: \"Optimize underperforming pages to improve search visibility\",\n priority: priorityMap[issueGravity.underperforming] || \"high\"\n });\n }\n \n if ((audit.issues.statusIssues.canonicalised || []).length > 0) {\n recommendations.push({\n text: \"Fix canonicalization issues to consolidate ranking signals\",\n priority: priorityMap[issueGravity.canonicalised] || \"high\"\n });\n }\n \n // Medium gravity issues\n if ((audit.issues.contentQuality.staleLastModified || []).length > 0) {\n recommendations.push({\n text: \"Update stale content with fresh information and current year references\",\n priority: priorityMap[issueGravity.outdated] || \"medium\"\n });\n }\n \n if ((audit.issues.statusIssues.redirects301 || []).length > 0) {\n recommendations.push({\n text: \"Update internal links to point directly to final URLs instead of through redirects\",\n priority: priorityMap[issueGravity.redirects] || \"medium\"\n });\n }\n \n if ((audit.issues.contentQuality.largeHTML || []).length > 0) {\n recommendations.push({\n text: \"Reduce HTML size for better page performance and loading speed\",\n priority: priorityMap[issueGravity.largeHTML] || \"medium\"\n });\n }\n \n // Low gravity issues\n if ((audit.issues.metadataSEO.missingDescription || []).length > 0) {\n recommendations.push({\n text: \"Add missing meta descriptions to improve click-through rates\",\n priority: priorityMap[issueGravity.missingOrDuplicateMeta] || \"low\"\n });\n }\n \n if ((audit.issues.contentQuality.readability || []).length > 0) {\n recommendations.push({\n text: \"Improve content readability to enhance user experience\",\n priority: priorityMap[issueGravity.readability] || \"low\"\n });\n }\n \n // Fallback if not enough recommendations\n if (recommendations.length < 3) {\n recommendations.push({\n text: \"Implement a regular content audit schedule to maintain freshness\",\n priority: \"low\"\n });\n }\n \n // Return top 5 recommendations, prioritizing high gravity issues first\n return recommendations\n .sort((a, b) => {\n const priorityOrder = { \"high\": 0, \"medium\": 1, \"low\": 2 };\n return priorityOrder[a.priority] - priorityOrder[b.priority];\n })\n .slice(0, 5);\n}\n\n// Format flag names for display\nfunction formatFlagName(flag) {\n return flag\n .split('_')\n .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\n// Utility to lighten a color\nfunction lightenColor(hex, percent) {\n hex = hex.replace('#', '');\n let r = parseInt(hex.substring(0, 2), 16);\n let g = parseInt(hex.substring(2, 4), 16);\n let b = parseInt(hex.substring(4, 6), 16);\n r = Math.min(255, Math.round(r + (255 - r) * (percent / 100)));\n g = Math.min(255, Math.round(g + (255 - g) * (percent / 100)));\n b = Math.min(255, Math.round(b + (255 - b) * (percent / 100)));\n return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;\n}\n\n// Utility to darken a color\nfunction darkenColor(hex, percent) {\n hex = hex.replace('#', '');\n let r = parseInt(hex.substring(0, 2), 16);\n let g = parseInt(hex.substring(2, 4), 16);\n let b = parseInt(hex.substring(4, 6), 16);\n r = Math.max(0, Math.round(r * (1 - percent / 100)));\n g = Math.max(0, Math.round(g * (1 - percent / 100)));\n b = Math.max(0, Math.round(b * (1 - percent / 100)));\n return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;\n}\n\n// Helper function to render a table section or \"No issues found\" message\nfunction renderTableSection(items, columns) {\n if (!items || items.length === 0) {\n return `<p class=\"section-empty\">No issues found.</p>`;\n }\n \n const showInitial = 10; // Number of rows to show initially\n const hasMoreItems = items.length > showInitial;\n const initialItems = hasMoreItems ? items.slice(0, showInitial) : items;\n const hiddenItems = hasMoreItems ? items.slice(showInitial) : [];\n \n return `\n <table class=\"paginated-table\">\n <thead>\n <tr>\n ${columns.map(col => `<th>${col.header}</th>`).join('')}\n </tr>\n </thead>\n <tbody class=\"initial-rows\">\n ${initialItems.map(item => `\n <tr>\n ${columns.map(col => `<td class=\"${col.class || ''}\">${col.render(item)}</td>`).join('')}\n </tr>\n `).join('')}\n </tbody>\n ${hasMoreItems ? `\n <tbody class=\"hidden-rows\" style=\"display: none;\">\n ${hiddenItems.map(item => `\n <tr>\n ${columns.map(col => `<td class=\"${col.class || ''}\">${col.render(item)}</td>`).join('')}\n </tr>\n `).join('')}\n </tbody>\n ` : ''}\n </table>\n ${hasMoreItems ? `\n <div class=\"table-pagination\">\n <button class=\"show-more-button\" onclick=\"toggleRows(this)\">Show All (${items.length} rows)</button>\n </div>\n ` : ''}\n `;\n}\n\n// Helper function to render source links for 404 and 301 pages\nfunction renderSourceLinks(sources) {\n if (!sources || sources.length === 0) {\n return '<p class=\"no-sources\">No source links found.</p>';\n }\n \n return `\n <div class=\"source-links\">\n <table class=\"source-links-table\">\n <thead>\n <tr>\n <th>Source URL</th>\n <th>Type</th>\n <th>Anchor Text</th>\n </tr>\n </thead>\n <tbody>\n ${sources.map(source => `\n <tr>\n <td class=\"url-cell\"><a href=\"${source.linkFrom}\" target=\"_blank\">${source.linkFrom}</a></td>\n <td>${source.type || 'N/A'}</td>\n <td>${source.text || 'N/A'}</td>\n </tr>\n `).join('')}\n </tbody>\n </table>\n </div>\n `;\n}\n\n// Return a single item with the HTML content\nreturn [{\n html: `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Content Audit Report for ${websiteDomain} | ${companyName}</title>\n <style>\n :root {\n --primary-color: ${primaryColor};\n --secondary-color: ${secondaryColor};\n --primary-light: ${lightenColor(primaryColor, 85)};\n --secondary-light: ${lightenColor(secondaryColor, 85)};\n --primary-dark: ${darkenColor(primaryColor, 20)};\n --text-color: #333;\n --light-gray: #f5f5f5;\n --medium-gray: #e0e0e0;\n --dark-gray: #757575;\n --success-color: #4caf50;\n --warning-color: #ff9800;\n --danger-color: #f44336;\n }\n \n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n body {\n font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n line-height: 1.6;\n color: var(--text-color);\n background-color: #fff;\n }\n \n .container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 0 20px;\n }\n \n header {\n background-color: var(--primary-color);\n color: white;\n padding: 30px 0;\n margin-bottom: 40px;\n }\n \n .header-content {\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n \n .logo-container {\n display: flex;\n align-items: center;\n }\n \n .logo {\n max-height: 60px;\n margin-right: 20px;\n }\n \n .report-info {\n text-align: right;\n }\n \n h1 {\n font-size: 1.8rem;\n margin-bottom: 0px;\n color: white;\n }\n \n h2 {\n font-size: 1.8rem;\n margin: 40px 0 20px;\n color: var(--primary-color);\n border-bottom: 2px solid var(--primary-light);\n padding-bottom: 10px;\n }\n \n h3 {\n font-size: 1.4rem;\n margin: 30px 0 15px;\n color: var(--primary-dark);\n }\n \n h4 {\n font-size: 1.2rem;\n margin: 20px 0 10px;\n color: var(--secondary-color);\n }\n \n p {\n margin-bottom: 15px;\n }\n \n .summary-cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n gap: 20px;\n margin: 30px 0;\n }\n \n .card {\n background-color: white;\n border-radius: 8px;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n padding: 20px;\n transition: transform 0.3s ease;\n }\n \n .card:hover {\n transform: translateY(-5px);\n }\n \n .card-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 15px;\n }\n \n .card-title {\n font-size: 1.2rem;\n font-weight: 600;\n color: var(--primary-color);\n }\n \n .card-value {\n font-size: 2.5rem;\n font-weight: 700;\n color: var(--secondary-color);\n }\n \n .issues-summary {\n display: flex;\n justify-content: space-between;\n flex-wrap: wrap;\n gap: 15px;\n margin: 30px 0;\n }\n \n .issue-category {\n flex: 1;\n min-width: 250px;\n background-color: var(--light-gray);\n border-radius: 8px;\n padding: 20px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n }\n \n .issue-category h3 {\n color: var(--primary-color);\n margin-top: 0;\n border-bottom: 1px solid var(--medium-gray);\n padding-bottom: 10px;\n }\n \n .issue-item {\n display: flex;\n justify-content: space-between;\n padding: 8px 0;\n border-bottom: 1px solid var(--medium-gray);\n }\n \n .issue-item:last-child {\n border-bottom: none;\n }\n \n .issue-name {\n color: var(--text-color);\n }\n \n .issue-count {\n font-weight: 600;\n color: var(--secondary-color);\n }\n \n table {\n width: 100%;\n border-collapse: collapse;\n margin: 20px 0 40px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n }\n \n th {\n background-color: var(--primary-color);\n color: white;\n text-align: left;\n padding: 12px 15px;\n }\n \n tr:nth-child(even) {\n background-color: var(--light-gray);\n }\n \n td {\n padding: 10px 15px;\n border-bottom: 1px solid var(--medium-gray);\n }\n \n .url-cell {\n max-width: 300px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n \n .url-cell a {\n color: var(--primary-color);\n text-decoration: none;\n }\n \n .url-cell a:hover {\n text-decoration: underline;\n }\n \n .todo-cell {\n max-width: 400px;\n }\n \n .flag {\n display: inline-block;\n padding: 3px 8px;\n border-radius: 4px;\n margin: 2px;\n font-size: 0.8rem;\n background-color: var(--primary-light);\n color: var(--primary-dark);\n }\n \n .pages-table {\n margin-top: 30px;\n }\n \n .pages-table th {\n position: sticky;\n top: 0;\n }\n \n footer {\n margin-top: 60px;\n padding: 30px 0;\n background-color: var(--primary-light);\n color: var(--primary-dark);\n text-align: center;\n }\n \n .footer-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n \n .company-info {\n margin-bottom: 20px;\n }\n \n .company-website {\n color: var(--primary-color);\n text-decoration: none;\n font-weight: 600;\n }\n \n .company-website:hover {\n text-decoration: underline;\n }\n \n .date-generated {\n font-style: italic;\n color: var(--dark-gray);\n }\n \n .progress-bar-container {\n width: 100%;\n background-color: var(--light-gray);\n border-radius: 10px;\n margin: 10px 0;\n overflow: hidden;\n }\n \n .progress-bar {\n height: 10px;\n background-color: var(--secondary-color);\n border-radius: 10px;\n }\n \n .recommendations {\n background-color: var(--secondary-light);\n border-left: 4px solid var(--secondary-color);\n padding: 15px;\n margin: 20px 0;\n border-radius: 0 4px 4px 0;\n }\n \n .recommendations h4 {\n color: var(--secondary-color);\n margin-top: 0;\n }\n \n .recommendations ul {\n margin-left: 20px;\n }\n \n .recommendations li {\n margin-bottom: 8px;\n }\n \n .priority-tag {\n display: inline-block;\n padding: 3px 8px;\n border-radius: 4px;\n margin-left: 8px;\n font-size: 0.8rem;\n font-weight: 600;\n }\n \n .high {\n background-color: rgba(244, 67, 54, 0.1);\n color: var(--danger-color);\n }\n \n .medium {\n background-color: rgba(255, 152, 0, 0.1);\n color: var(--warning-color);\n }\n \n .low {\n background-color: rgba(76, 175, 80, 0.1);\n color: var(--success-color);\n }\n \n .section-empty {\n font-style: italic;\n color: var(--dark-gray);\n padding: 15px;\n background-color: var(--light-gray);\n border-radius: 4px;\n text-align: center;\n }\n \n .source-links {\n margin-top: 10px;\n margin-bottom: 20px;\n padding: 10px;\n background-color: var(--light-gray);\n border-radius: 4px;\n border-left: 3px solid var(--secondary-color);\n }\n \n .source-links h4 {\n margin-top: 0;\n margin-bottom: 10px;\n color: var(--secondary-color);\n font-size: 1rem;\n }\n \n .source-links-table {\n margin: 0;\n box-shadow: none;\n }\n \n .source-links-table th {\n background-color: var(--secondary-color);\n font-size: 0.9rem;\n padding: 8px 10px;\n }\n \n .source-links-table td {\n font-size: 0.9rem;\n padding: 6px 10px;\n }\n \n .no-sources {\n font-style: italic;\n color: var(--dark-gray);\n margin: 5px 0;\n }\n \n .toggle-sources {\n background-color: var(--secondary-light);\n color: var(--secondary-color);\n border: 1px solid var(--secondary-color);\n border-radius: 4px;\n padding: 5px 10px;\n font-size: 0.8rem;\n cursor: pointer;\n margin-top: 5px;\n transition: background-color 0.3s;\n }\n \n .toggle-sources:hover {\n background-color: var(--secondary-color);\n color: white;\n }\n \n .sources-container {\n margin-top: 10px;\n }\n \n .show-more-button {\n background-color: var(--primary-color);\n color: white;\n border: none;\n border-radius: 4px;\n padding: 8px 16px;\n font-size: 0.9rem;\n font-weight: 600;\n cursor: pointer;\n margin: 10px auto;\n display: block;\n transition: all 0.3s ease;\n box-shadow: 0 2px 5px rgba(0,0,0,0.1);\n }\n \n .show-more-button:hover {\n background-color: var(--primary-dark);\n box-shadow: 0 3px 7px rgba(0,0,0,0.2);\n transform: translateY(-2px);\n }\n \n .table-pagination {\n text-align: center;\n margin-top: -20px;\n margin-bottom: 30px;\n }\n \n @media print {\n body {\n font-size: 12pt;\n }\n \n .container {\n width: 100%;\n max-width: none;\n padding: 0;\n }\n \n header {\n padding: 15px 0;\n }\n \n h1 {\n font-size: 20pt;\n }\n \n h2 {\n font-size: 18pt;\n margin-top: 20px;\n }\n \n h3 {\n font-size: 14pt;\n }\n \n .card:hover {\n transform: none;\n }\n \n table {\n page-break-inside: avoid;\n }\n \n tr {\n page-break-inside: avoid;\n }\n \n .no-print {\n display: none;\n }\n \n @page {\n margin: 1.5cm;\n }\n }\n </style>\n <script>\n // JavaScript to toggle source links visibility\n document.addEventListener('DOMContentLoaded', function() {\n document.querySelectorAll('.toggle-sources').forEach(button => {\n button.addEventListener('click', function() {\n const container = this.nextElementSibling;\n if (container.style.display === 'none' || !container.style.display) {\n container.style.display = 'block';\n this.textContent = 'Hide Source Links';\n } else {\n container.style.display = 'none';\n this.textContent = 'Show Source Links';\n }\n });\n });\n });\n \n // JavaScript to toggle table rows visibility\n function toggleRows(button) {\n const table = button.closest('.table-pagination').previousElementSibling;\n const hiddenRows = table.querySelector('.hidden-rows');\n const totalRows = hiddenRows.querySelectorAll('tr').length + table.querySelector('.initial-rows').querySelectorAll('tr').length;\n \n if (hiddenRows.style.display === 'none' || !hiddenRows.style.display) {\n hiddenRows.style.display = 'table-row-group';\n button.textContent = 'Show Less';\n } else {\n hiddenRows.style.display = 'none';\n button.textContent = 'Show All (' + totalRows + ' items)';\n }\n }\n </script>\n</head>\n<body>\n <header>\n <div class=\"container\">\n <div class=\"header-content\">\n <div class=\"logo-container\">\n <img src=\"${companyLogoUrl}\" alt=\"${companyName} Logo\" class=\"logo\">\n <div>\n <h1>Content Audit Report</h1>\n <p>for ${websiteDomain}</p>\n </div>\n </div>\n <div class=\"report-info\">\n <p>Generated on: ${formattedDate}</p>\n <p>By: ${companyName}</p>\n </div>\n </div>\n </div>\n </header>\n\n <main class=\"container\">\n <section id=\"executive-summary\">\n <h2>Executive Summary</h2>\n <p>This report provides a comprehensive analysis of content issues found on <strong>${websiteDomain}</strong>. We've identified ${totalIssues} issues across ${auditData.summary.pages} pages that need attention to improve SEO performance and user experience.</p>\n \n <div class=\"summary-cards\">\n <div class=\"card\">\n <div class=\"card-header\">\n <span class=\"card-title\">Pages Analyzed</span>\n </div>\n <div class=\"card-value\">${auditData.summary.pages}</div>\n </div>\n \n <div class=\"card\">\n <div class=\"card-header\">\n <span class=\"card-title\">Total Issues</span>\n </div>\n <div class=\"card-value\">${totalIssues}</div>\n </div>\n \n <div class=\"card\">\n <div class=\"card-header\">\n <span class=\"card-title\">Health Score</span>\n </div>\n <div class=\"card-value\" style=\"color: ${getHealthScoreColor(calculateHealthScore(auditData.summary.pages, totalIssues))};\">${calculateHealthScore(auditData.summary.pages, totalIssues)}%</div>\n <div class=\"progress-bar-container\">\n <div class=\"progress-bar\" style=\"width: ${calculateHealthScore(auditData.summary.pages, totalIssues)}%\"></div>\n </div>\n </div>\n </div>\n \n <div class=\"recommendations\">\n <h4>Key Recommendations</h4>\n <ul>\n ${getTopRecommendations(auditData).map(rec => `<li>${rec.text} <span class=\"priority-tag ${rec.priority}\">${rec.priority}</span></li>`).join('')}\n </ul>\n </div>\n </section>\n\n <section id=\"issues-breakdown\">\n <h2>Issues Breakdown</h2>\n \n <div class=\"issues-summary\">\n <div class=\"issue-category\">\n <h3>Content Quality</h3>\n <div class=\"issues-list\">\n <div class=\"issue-item\">\n <span class=\"issue-name\">Outdated Content</span>\n <span class=\"issue-count\">${auditData.summary.issues.outdated}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Thin Content</span>\n <span class=\"issue-count\">${auditData.summary.issues.thin}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Readability Issues</span>\n <span class=\"issue-count\">${auditData.summary.issues.readability}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Large HTML</span>\n <span class=\"issue-count\">${auditData.summary.issues.largeHTML}</span>\n </div>\n </div>\n </div>\n \n <div class=\"issue-category\">\n <h3>Technical SEO</h3>\n <div class=\"issues-list\">\n <div class=\"issue-item\">\n <span class=\"issue-name\">404 Errors</span>\n <span class=\"issue-count\">${auditData.summary.issues['404']}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Redirects</span>\n <span class=\"issue-count\">${auditData.summary.issues.redirects}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Canonicalization Issues</span>\n <span class=\"issue-count\">${auditData.summary.issues.canonicalised}</span>\n </div>\n </div>\n </div>\n \n <div class=\"issue-category\">\n <h3>On-Page SEO</h3>\n <div class=\"issues-list\">\n <div class=\"issue-item\">\n <span class=\"issue-name\">Title Length Issues</span>\n <span class=\"issue-count\">${auditData.summary.issues.titleLen}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Description Issues</span>\n <span class=\"issue-count\">${auditData.summary.issues.descriptionLen}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Missing/Duplicate Meta</span>\n <span class=\"issue-count\">${auditData.summary.issues.missingOrDuplicateMeta}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">H1 Issues</span>\n <span class=\"issue-count\">${auditData.summary.issues.h1Issues}</span>\n </div>\n </div>\n </div>\n \n <div class=\"issue-category\">\n <h3>Internal Linking</h3>\n <div class=\"issues-list\">\n <div class=\"issue-item\">\n <span class=\"issue-name\">Excessive Click Depth</span>\n <span class=\"issue-count\">${auditData.summary.issues.excessiveClickDepth}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Orphan Pages</span>\n <span class=\"issue-count\">${auditData.summary.issues.orphan}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Low Internal Links</span>\n <span class=\"issue-count\">${auditData.summary.issues.lowInternalLinks}</span>\n </div>\n </div>\n </div>\n \n <div class=\"issue-category\">\n <h3>Performance</h3>\n <div class=\"issues-list\">\n <div class=\"issue-item\">\n <span class=\"issue-name\">Underperforming Pages</span>\n <span class=\"issue-count\">${auditData.summary.issues.underperforming}</span>\n </div>\n </div>\n </div>\n </div>\n </section>\n\n <!-- Status Issues Section -->\n <section id=\"status-issues\">\n <h2>Status Issues</h2>\n \n <h3>404 Errors (${(auditData.issues.statusIssues.pages404 || []).length})</h3>\n ${(auditData.issues.statusIssues.pages404 || []).length === 0 ? \n `<p class=\"section-empty\">No issues found.</p>` : \n (() => {\n const items = auditData.issues.statusIssues.pages404 || [];\n const showInitial = 10; // Number of rows to show initially\n const hasMoreItems = items.length > showInitial;\n const initialItems = hasMoreItems ? items.slice(0, showInitial) : items;\n const hiddenItems = hasMoreItems ? items.slice(showInitial) : [];\n \n return `\n <table class=\"paginated-table\">\n <thead>\n <tr>\n <th>URL</th>\n <th>Source Links</th>\n <th>Recommendation</th>\n </tr>\n </thead>\n <tbody class=\"initial-rows\">\n ${initialItems.map(item => `\n <tr>\n <td class=\"url-cell\"><a href=\"${item.url}\" target=\"_blank\">${item.url}</a></td>\n <td>\n ${item.sources && item.sources.length > 0 ? \n `<button class=\"toggle-sources\">Show Source Links (${item.sources.length})</button>\n <div class=\"sources-container\" style=\"display: none;\">\n ${renderSourceLinks(item.sources)}\n </div>` : \n `<span class=\"no-sources\">No source links found</span>`\n }\n </td>\n <td class=\"todo-cell\">${item.todo}</td>\n </tr>\n `).join('')}\n </tbody>\n ${hasMoreItems ? `\n <tbody class=\"hidden-rows\" style=\"display: none;\">\n ${hiddenItems.map(item => `\n <tr>\n <td class=\"url-cell\"><a href=\"${item.url}\" target=\"_blank\">${item.url}</a></td>\n <td>\n ${item.sources && item.sources.length > 0 ? \n `<button class=\"toggle-sources\">Show Source Links (${item.sources.length})</button>\n <div class=\"sources-container\" style=\"display: none;\">\n ${renderSourceLinks(item.sources)}\n </div>` : \n `<span class=\"no-sources\">No source links found</span>`\n }\n </td>\n <td class=\"todo-cell\">${item.todo}</td>\n </tr>\n `).join('')}\n </tbody>\n ` : ''}\n </table>\n ${hasMoreItems ? `\n <div class=\"table-pagination\">\n <button class=\"show-more-button\" onclick=\"toggleRows(this)\">Show All (${items.length} rows)</button>\n </div>\n ` : ''}\n `;\n })()\n }\n \n <h3>301 Redirects (${(auditData.issues.statusIssues.redirects301 || []).length})</h3>\n ${(auditData.issues.statusIssues.redirects301 || []).length === 0 ? \n `<p class=\"section-empty\">No issues found.</p>` : \n (() => {\n const items = auditData.issues.statusIssues.redirects301 || [];\n const showInitial = 10; // Number of rows to show initially\n const hasMoreItems = items.length > showInitial;\n const initialItems = hasMoreItems ? items.slice(0, showInitial) : items;\n const hiddenItems = hasMoreItems ? items.slice(showInitial) : [];\n \n return `\n <table class=\"paginated-table\">\n <thead>\n <tr>\n <th>URL</th>\n <th>Source Links</th>\n <th>Recommendation</th>\n </tr>\n </thead>\n <tbody class=\"initial-rows\">\n ${initialItems.map(item => `\n <tr>\n <td class=\"url-cell\"><a href=\"${item.url}\" target=\"_blank\">${item.url}</a></td>\n <td>\n ${item.sources && item.sources.length > 0 ? \n `<button class=\"toggle-sources\">Show Source Links (${item.sources.length})</button>\n <div class=\"sources-container\" style=\"display: none;\">\n ${renderSourceLinks(item.sources)}\n </div>` : \n `<span class=\"no-sources\">No source links found</span>`\n }\n </td>\n <td class=\"todo-cell\">${item.todo}</td>\n </tr>\n `).join('')}\n </tbody>\n ${hasMoreItems ? `\n <tbody class=\"hidden-rows\" style=\"display: none;\">\n ${hiddenItems.map(item => `\n <tr>\n <td class=\"url-cell\"><a href=\"${item.url}\" target=\"_blank\">${item.url}</a></td>\n <td>\n ${item.sources && item.sources.length > 0 ? \n `<button class=\"toggle-sources\">Show Source Links (${item.sources.length})</button>\n <div class=\"sources-container\" style=\"display: none;\">\n ${renderSourceLinks(item.sources)}\n </div>` : \n `<span class=\"no-sources\">No source links found</span>`\n }\n </td>\n <td class=\"todo-cell\">${item.todo}</td>\n </tr>\n `).join('')}\n </tbody>\n ` : ''}\n </table>\n ${hasMoreItems ? `\n <div class=\"table-pagination\">\n <button class=\"show-more-button\" onclick=\"toggleRows(this)\">Show All (${items.length} rows)</button>\n </div>\n ` : ''}\n `;\n })()\n }\n \n <h3>Canonicalization Issues (${(auditData.issues.statusIssues.canonicalised || []).length})</h3>\n ${renderTableSection(auditData.issues.statusIssues.canonicalised, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Canonical URL', render: item => item.canonical || '—' },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n </section>\n\n <!-- Content Quality Issues Section -->\n <section id=\"content-quality-issues\">\n <h2>Content Quality Issues</h3>\n \n <h3>Outdated Content (${(auditData.issues.contentQuality.staleLastModified || []).length})</h3>\n ${renderTableSection(auditData.issues.contentQuality.staleLastModified, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Last Modified', render: item => item.lastModified },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Thin Content (${(auditData.issues.contentQuality.thinContent || []).length})</h3>\n ${renderTableSection(auditData.issues.contentQuality.thinContent, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Word Count', render: item => item.words },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Readability Issues (${(auditData.issues.contentQuality.readability || []).length})</h3>\n ${renderTableSection(auditData.issues.contentQuality.readability, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'F-K Score', render: item => item.score.toFixed(1) },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Outdated Meta Years (${(auditData.issues.contentQuality.outdatedMetaYear || []).length})</h3>\n ${renderTableSection(auditData.issues.contentQuality.outdatedMetaYear, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Field', render: item => item.field },\n { header: 'Years', render: item => item.years },\n { header: 'Original Text', render: item => item.original },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Large HTML (${(auditData.issues.contentQuality.largeHTML || []).length})</h3>\n ${renderTableSection(auditData.issues.contentQuality.largeHTML, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Size (bytes)', render: item => item.size ? item.size.toLocaleString() : 'N/A' },\n { header: 'DOM Size (bytes)', render: item => item.totalDom ? item.totalDom.toLocaleString() : 'N/A' },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n </section>\n \n <!-- Metadata & SEO Issues Section -->\n <section id=\"metadata-seo-issues\">\n <h2>Metadata & SEO Issues</h2>\n \n <h3>Title Length Issues (${(auditData.issues.metadataSEO.titleLength || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.titleLength, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Length', render: item => `${item.length} characters` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Description Length Issues (${(auditData.issues.metadataSEO.descriptionLength || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.descriptionLength, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Length', render: item => `${item.length} characters` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Missing Titles (${(auditData.issues.metadataSEO.missingTitle || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.missingTitle, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Missing Descriptions (${(auditData.issues.metadataSEO.missingDescription || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.missingDescription, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Duplicate Titles (${(auditData.issues.metadataSEO.duplicateTitle || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.duplicateTitle, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Title', render: item => item.title },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Duplicate Descriptions (${(auditData.issues.metadataSEO.duplicateDescription || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.duplicateDescription, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Description', render: item => item.description },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>H1 Issues (${(auditData.issues.metadataSEO.h1Issues || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.h1Issues, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'H1 Count', render: item => item.h1Count },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n </section>\n \n <!-- Internal Linking Issues Section -->\n <section id=\"internal-linking-issues\">\n <h2>Internal Linking Issues</h2>\n \n <h3>Excessive Click Depth (${(auditData.issues.internalLinking.excessiveClickDepth || []).length})</h3>\n ${renderTableSection(auditData.issues.internalLinking.excessiveClickDepth, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Click Depth', render: item => item.depth },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Orphan Pages (${(auditData.issues.internalLinking.orphanPages || []).length})</h3>\n ${renderTableSection(auditData.issues.internalLinking.orphanPages, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Low Internal Links (${(auditData.issues.internalLinking.lowInternalLinks || []).length})</h3>\n ${renderTableSection(auditData.issues.internalLinking.lowInternalLinks, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Internal Links', render: item => item.links },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n </section>\n \n <!-- Performance Issues Section -->\n <section id=\"performance-issues\">\n <h2>Performance Issues</h2>\n \n <h3>Underperforming Content (${(auditData.issues.underperformingContent || []).length})</h3>\n ${renderTableSection(auditData.issues.underperformingContent, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Clicks', render: item => item.clicks },\n { header: 'Impressions', render: item => item.impressions },\n { header: 'Last Modified', render: item => item.lastModified },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n </section>\n\n <section id=\"all-pages\">\n <h2>All Pages Overview</h2>\n <p>Below is a summary of all pages analyzed with their respective issues flagged.</p>\n \n ${(() => {\n const items = auditData.pages || [];\n const showInitial = 10; // Number of rows to show initially\n const hasMoreItems = items.length > showInitial;\n const initialItems = hasMoreItems ? items.slice(0, showInitial) : items;\n const hiddenItems = hasMoreItems ? items.slice(showInitial) : [];\n \n return `\n <table class=\"paginated-table pages-table\">\n <thead>\n <tr>\n <th>URL</th>\n <th>Issues</th>\n <th>Clicks</th>\n <th>Impressions</th>\n </tr>\n </thead>\n <tbody class=\"initial-rows\">\n ${initialItems.map(page => `\n <tr>\n <td class=\"url-cell\"><a href=\"${page.url}\" target=\"_blank\">${page.url}</a></td>\n <td>${page.flags.map(flag => `<span class=\"flag\">${formatFlagName(flag)}</span>`).join('')}</td>\n <td>${page.clicks !== null ? page.clicks : 'N/A'}</td>\n <td>${page.impressions !== null ? page.impressions : 'N/A'}</td>\n </tr>\n `).join('')}\n </tbody>\n ${hasMoreItems ? `\n <tbody class=\"hidden-rows\" style=\"display: none;\">\n ${hiddenItems.map(page => `\n <tr>\n <td class=\"url-cell\"><a href=\"${page.url}\" target=\"_blank\">${page.url}</a></td>\n <td>${page.flags.map(flag => `<span class=\"flag\">${formatFlagName(flag)}</span>`).join('')}</td>\n <td>${page.clicks !== null ? page.clicks : 'N/A'}</td>\n <td>${page.impressions !== null ? page.impressions : 'N/A'}</td>\n </tr>\n `).join('')}\n </tbody>\n ` : ''}\n </table>\n ${hasMoreItems ? `\n <div class=\"table-pagination\">\n <button class=\"show-more-button\" onclick=\"toggleRows(this)\">Show All (${items.length} rows)</button>\n </div>\n ` : ''}\n `;\n })()}\n </section>\n \n <section id=\"next-steps\">\n <h2>Recommended Next Steps</h2>\n <p>Based on our analysis, we recommend the following actions to improve your content performance:</p>\n \n <div class=\"recommendations\">\n <h4>Priority Actions</h4>\n <ul>\n ${auditData.summary.issues['404'] > 0 ? \n `<li>Fix 404 errors by restoring pages or implementing proper redirects</li>` : ''}\n ${auditData.summary.issues.redirects > 0 ? \n `<li>Update internal links to point directly to final URLs instead of through redirects</li>` : ''}\n ${auditData.summary.issues.thin > 0 ? \n `<li>Expand thin content pages to at least 1,500 words with valuable, unique information</li>` : ''}\n ${auditData.summary.issues.outdated > 0 ? \n `<li>Update all content that hasn't been refreshed in the last 12 months</li>` : ''}\n ${auditData.summary.issues.missingOrDuplicateMeta > 0 ? \n `<li>Add unique meta descriptions to all pages missing them</li>` : ''}\n ${auditData.summary.issues.titleLen > 0 ? \n `<li>Optimize page titles to be between 40-60 characters</li>` : ''}\n ${auditData.summary.issues.descriptionLen > 0 ? \n `<li>Optimize meta descriptions to be between 70-155 characters</li>` : ''}\n ${auditData.summary.issues.readability > 0 ? \n `<li>Improve content readability by simplifying language and shortening sentences</li>` : ''}\n ${auditData.summary.issues.underperforming > 0 ? \n `<li>Identify keywords with potential for pages with high impressions but low clicks</li>` : ''}\n ${auditData.summary.issues.orphan > 0 ? \n `<li>Create internal links to orphan pages to improve crawlability</li>` : ''}\n ${auditData.summary.issues.lowInternalLinks > 0 ? \n `<li>Improve internal linking between related content</li>` : ''}\n <li>Implement a content calendar to regularly refresh content</li>\n <li>Conduct keyword research to identify new content opportunities</li>\n </ul>\n </div>\n \n <h3>Implementation Timeline</h3>\n <p>We recommend addressing these issues in the following order:</p>\n \n <ol>\n <li><strong>Immediate (1-2 weeks):</strong> Fix technical issues like 404 errors, redirects, missing meta descriptions, and outdated year references.</li>\n <li><strong>Short-term (2-4 weeks):</strong> Update thin content and improve readability on key pages.</li>\n <li><strong>Medium-term (1-2 months):</strong> Refresh outdated content, especially on high-impression pages.</li>\n <li><strong>Long-term (2-3 months):</strong> Implement a content calendar to regularly update content and prevent future staleness.</li>\n </ol>\n </section>\n </main>\n\n <footer>\n <div class=\"container\">\n <div class=\"footer-content\">\n <div class=\"company-info\">\n <p>Report generated by <strong>${companyName}</strong></p>\n <a href=\"${companyWebsite}\" class=\"company-website\" target=\"_blank\">${companyWebsite}</a>\n </div>\n <p class=\"date-generated\">Generated on ${formattedDate}</p>\n </div>\n </div>\n </footer>\n</body>\n</html>`\n}];"
},
"typeVersion": 2
},
{
"id": "b772f856-e1cf-44fd-8fc7-1ac5d8b033ca",
"name": "提取404和301",
"type": "n8n-nodes-base.code",
"position": [
1880,
500
],
"parameters": {
"jsCode": "// Get input data from the updated node\nconst input = $('Get RAW Audit Data').first().json;\n\n// Initialize an array to store the new items\nconst output = [];\n\n// Loop through tasks\nconst tasks = input.tasks || [];\nfor (const task of tasks) {\n const results = task.result || [];\n for (const result of results) {\n const items = result.items || [];\n for (const page of items) {\n // Only include URLs with status_code 404 or 301\n if (page.url && (page.status_code === 404 || page.status_code === 301)) {\n output.push({ json: { url: page.url, status_code: page.status_code } });\n }\n }\n }\n}\n\n// Return filtered URLs with status codes 404 or 301\nreturn output;\n"
},
"typeVersion": 2
},
{
"id": "2bc70a8c-5c2d-4cb5-be4f-8d051f32ad23",
"name": "遍历项目1循环",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2100,
500
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "4defc61c-7f05-4b64-9b68-96f097a9ba92",
"name": "映射URLs数据",
"type": "n8n-nodes-base.code",
"position": [
2520,
500
],
"parameters": {
"jsCode": "// Get the input data\nconst input = items[0].json;\n\n// Access the items array\nconst linkItems = input.tasks[0].result[0].items;\n\n// Extract the target URL and status code from the first item\nconst url = linkItems[0].link_to;\nconst pageStatus = linkItems[0].page_to_status_code;\n\n// Build the output object\nconst output = {\n URL: url,\n page_to_status_code: pageStatus,\n sources: linkItems.map(item => ({\n type: item.type,\n link_from: item.link_from,\n text: item.text\n }))\n};\n\n// Return formatted output\nreturn [{ json: output }];\n"
},
"typeVersion": 2
},
{
"id": "bbf44181-0ea7-48b2-b89e-143d72460d27",
"name": "获取源URLs数据",
"type": "n8n-nodes-base.httpRequest",
"position": [
2320,
500
],
"parameters": {
"url": "https://api.dataforseo.com/v3/on_page/links",
"method": "POST",
"options": {},
"jsonBody": "=[\n {\n \"id\": \"{{ $('Get RAW Audit Data').first().json.tasks[0].id }}\",\n \"page_to\": \"{{ $json.url }}\"\n }\n]",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "cae4d8e7-5a63-417d-a025-3f6631ead225",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"width": 940,
"height": 580,
"content": "## 内容SEO审计报告"
},
"typeVersion": 1
},
{
"id": "afd6a0aa-813c-4a3f-b844-ac1cf9f854c6",
"name": "下载报告",
"type": "n8n-nodes-base.convertToFile",
"position": [
2500,
320
],
"parameters": {
"options": {
"fileName": "={{ $('Set Fields').first().json.dfs_domain }}-content-audit-{{ new Date().toLocaleString('en-US', { month: 'long' }) + '-' + new Date().getFullYear() }}.html"
},
"operation": "toText",
"sourceProperty": "html",
"binaryPropertyName": "=content audit report"
},
"typeVersion": 1.1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "c6db2f12-2e4f-4f40-acf9-6664c9feb45e",
"connections": {
"If": {
"main": [
[
{
"node": "Get RAW Audit Data",
"type": "main",
"index": 0
}
],
[
{
"node": "Wait",
"type": "main",
"index": 0
}
]
]
},
"Wait": {
"main": [
[
{
"node": "Check Task Status",
"type": "main",
"index": 0
}
]
]
},
"Wait1": {
"main": [
[
{
"node": "Map GSC Data to URL",
"type": "main",
"index": 0
}
]
]
},
"Set Fields": {
"main": [
[
{
"node": "Create Task",
"type": "main",
"index": 0
}
]
]
},
"Create Task": {
"main": [
[
{
"node": "Check Task Status",
"type": "main",
"index": 0
}
]
]
},
"Extract URLs": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Map URLs Data": {
"main": [
[
{
"node": "Loop Over Items1",
"type": "main",
"index": 0
}
]
]
},
"Query GSC API": {
"main": [
[
{
"node": "Wait1",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[
{
"node": "Merge GSC Data with RAW Data",
"type": "main",
"index": 0
}
],
[
{
"node": "Query GSC API",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items1": {
"main": [
[
{
"node": "Build Report Structure",
"type": "main",
"index": 0
}
],
[
{
"node": "Get Source URLs Data",
"type": "main",
"index": 0
}
]
]
},
"Check Task Status": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"Extract 404 & 301": {
"main": [
[
{
"node": "Loop Over Items1",
"type": "main",
"index": 0
}
]
]
},
"Get RAW Audit Data": {
"main": [
[
{
"node": "Extract URLs",
"type": "main",
"index": 0
}
]
]
},
"Map GSC Data to URL": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Generate HTML Report": {
"main": [
[
{
"node": "Download Report",
"type": "main",
"index": 0
}
]
]
},
"Get Source URLs Data": {
"main": [
[
{
"node": "Map URLs Data",
"type": "main",
"index": 0
}
]
]
},
"Build Report Structure": {
"main": [
[
{
"node": "Generate HTML Report",
"type": "main",
"index": 0
}
]
]
},
"When clicking ‘Start’": {
"main": [
[
{
"node": "Set Fields",
"type": "main",
"index": 0
}
]
]
},
"Merge GSC Data with RAW Data": {
"main": [
[
{
"node": "Extract 404 & 301",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - 人工智能, 营销
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
我的工作流
基于Reddit和Anthropic的每周数字公关故事自动推荐
If
Set
Code
+11
29 节点Custom Workflows AI
人工智能
高级服务页面 SEO 蓝图报告
高级服务页面 SEO 蓝图报告生成器
Set
Code
Wait
+7
33 节点Custom Workflows AI
人工智能
(Duc)深度研究市场模板
集成PerplexityAI研究和OpenAI内容的多层级WordPress博客生成器
If
Set
Xml
+28
132 节点Daniel Ng
人工智能
病毒式标题/缩略图生成
自动化病毒式YouTube标题和缩略图创建(FLUX.1 + Apify)
If
Set
Code
+13
41 节点Nasser
人工智能
YouTube 自动化
自动化 YouTube 视频排程和 AI 元数据生成 🎬
If
Set
Code
+11
33 节点JPres
人工智能
使用 Elevenlabs 和 Hailuo AI 自动创建 YouTube 短视频
使用 Elevenlabs 和 Hailuo AI 自动创建 YouTube 短视频
If
Set
Code
+10
38 节点Nguyen Quoc Hai
人工智能
工作流信息
难度等级
高级
节点数量21
分类2
节点类型9
作者
Custom Workflows AI
@customworkflowsaiWe specializes in crafting tailored automation solutions that help businesses streamline their operations and boost productivity. With expertise in creating custom n8n workflows, we transform complex business processes into seamless, automated systems that save time and reduce manual effort.
外部链接
在 n8n.io 查看 →
分享此工作流