{ "id": "Tqa8dikBDLYEytx5", "meta": { "instanceId": "ddfdf733df99a65c801a91865dba5b7c087c95cc22a459ff3647e6deddf2aee6" }, "name": "Automated Content SEO Audit Report", "tags": [], "nodes": [ { "id": "b5f15675-35c9-42a1-b7eb-bfaf0b467a5a", "name": "Set Fields", "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": "When clicking ‘Start’", "type": "n8n-nodes-base.manualTrigger", "position": [ 60, 620 ], "parameters": {}, "typeVersion": 1 }, { "id": "3e5e8162-2815-429f-b6e8-6ea6ea70cf18", "name": "Check Task Status", "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": "If", "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": "Wait", "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": "Get RAW Audit Data", "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": "Extract 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": "Loop Over Items", "type": "n8n-nodes-base.splitInBatches", "position": [ 1480, 500 ], "parameters": { "options": {}, "batchSize": 100 }, "typeVersion": 3 }, { "id": "aebdd823-9a4d-4323-aadf-b7d92d601d57", "name": "Query 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": "Wait1", "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": "Map GSC Data to 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": "Merge GSC Data with RAW Data", "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": "Build Report Structure", "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": "Generate HTML Report", "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 `

No issues found.

`;\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 \n \n \n ${columns.map(col => ``).join('')}\n \n \n \n ${initialItems.map(item => `\n \n ${columns.map(col => ``).join('')}\n \n `).join('')}\n \n ${hasMoreItems ? `\n \n ${hiddenItems.map(item => `\n \n ${columns.map(col => ``).join('')}\n \n `).join('')}\n \n ` : ''}\n
${col.header}
${col.render(item)}
${col.render(item)}
\n ${hasMoreItems ? `\n
\n \n
\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 '

No source links found.

';\n }\n \n return `\n
\n \n \n \n \n \n \n \n \n \n ${sources.map(source => `\n \n \n \n \n \n `).join('')}\n \n \n
\n `;\n}\n\n// Return a single item with the HTML content\nreturn [{\n html: `\n\n\n \n \n Content Audit Report for ${websiteDomain} | ${companyName}\n \n \n\n\n
\n
\n
\n
\n \"${companyName}\n
\n

Content Audit Report

\n

for ${websiteDomain}

\n
\n
\n
\n

Generated on: ${formattedDate}

\n

By: ${companyName}

\n
\n
\n
\n
\n\n
\n
\n

Executive Summary

\n

This report provides a comprehensive analysis of content issues found on ${websiteDomain}. We've identified ${totalIssues} issues across ${auditData.summary.pages} pages that need attention to improve SEO performance and user experience.

\n \n
\n
\n
\n Pages Analyzed\n
\n
${auditData.summary.pages}
\n
\n \n
\n
\n Total Issues\n
\n
${totalIssues}
\n
\n \n
\n
\n Health Score\n
\n
${calculateHealthScore(auditData.summary.pages, totalIssues)}%
\n
\n
\n
\n
\n
\n \n
\n

Key Recommendations

\n
    \n ${getTopRecommendations(auditData).map(rec => `
  • ${rec.text} ${rec.priority}
  • `).join('')}\n
\n
\n
\n\n
\n

Issues Breakdown

\n \n
\n
\n

Content Quality

\n
\n
\n Outdated Content\n ${auditData.summary.issues.outdated}\n
\n
\n Thin Content\n ${auditData.summary.issues.thin}\n
\n
\n Readability Issues\n ${auditData.summary.issues.readability}\n
\n
\n Large HTML\n ${auditData.summary.issues.largeHTML}\n
\n
\n
\n \n
\n

Technical SEO

\n
\n
\n 404 Errors\n ${auditData.summary.issues['404']}\n
\n
\n Redirects\n ${auditData.summary.issues.redirects}\n
\n
\n Canonicalization Issues\n ${auditData.summary.issues.canonicalised}\n
\n
\n
\n \n
\n

On-Page SEO

\n
\n
\n Title Length Issues\n ${auditData.summary.issues.titleLen}\n
\n
\n Description Issues\n ${auditData.summary.issues.descriptionLen}\n
\n
\n Missing/Duplicate Meta\n ${auditData.summary.issues.missingOrDuplicateMeta}\n
\n
\n H1 Issues\n ${auditData.summary.issues.h1Issues}\n
\n
\n
\n \n
\n

Internal Linking

\n
\n
\n Excessive Click Depth\n ${auditData.summary.issues.excessiveClickDepth}\n
\n
\n Orphan Pages\n ${auditData.summary.issues.orphan}\n
\n
\n Low Internal Links\n ${auditData.summary.issues.lowInternalLinks}\n
\n
\n
\n \n
\n

Performance

\n
\n
\n Underperforming Pages\n ${auditData.summary.issues.underperforming}\n
\n
\n
\n
\n
\n\n \n
\n

Status Issues

\n \n

404 Errors (${(auditData.issues.statusIssues.pages404 || []).length})

\n ${(auditData.issues.statusIssues.pages404 || []).length === 0 ? \n `

No issues found.

` : \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 \n \n \n \n \n \n \n \n \n ${initialItems.map(item => `\n \n \n \n \n \n `).join('')}\n \n ${hasMoreItems ? `\n \n ${hiddenItems.map(item => `\n \n \n \n \n \n `).join('')}\n \n ` : ''}\n
URLSource LinksRecommendation
${item.url}\n ${item.sources && item.sources.length > 0 ? \n `\n
\n ${renderSourceLinks(item.sources)}\n
` : \n `No source links found`\n }\n
${item.todo}
${item.url}\n ${item.sources && item.sources.length > 0 ? \n `\n
\n ${renderSourceLinks(item.sources)}\n
` : \n `No source links found`\n }\n
${item.todo}
\n ${hasMoreItems ? `\n
\n \n
\n ` : ''}\n `;\n })()\n }\n \n

301 Redirects (${(auditData.issues.statusIssues.redirects301 || []).length})

\n ${(auditData.issues.statusIssues.redirects301 || []).length === 0 ? \n `

No issues found.

` : \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 \n \n \n \n \n \n \n \n \n ${initialItems.map(item => `\n \n \n \n \n \n `).join('')}\n \n ${hasMoreItems ? `\n \n ${hiddenItems.map(item => `\n \n \n \n \n \n `).join('')}\n \n ` : ''}\n
URLSource LinksRecommendation
${item.url}\n ${item.sources && item.sources.length > 0 ? \n `\n
\n ${renderSourceLinks(item.sources)}\n
` : \n `No source links found`\n }\n
${item.todo}
${item.url}\n ${item.sources && item.sources.length > 0 ? \n `\n
\n ${renderSourceLinks(item.sources)}\n
` : \n `No source links found`\n }\n
${item.todo}
\n ${hasMoreItems ? `\n
\n \n
\n ` : ''}\n `;\n })()\n }\n \n

Canonicalization Issues (${(auditData.issues.statusIssues.canonicalised || []).length})

\n ${renderTableSection(auditData.issues.statusIssues.canonicalised, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Canonical URL', render: item => item.canonical || '—' },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n
\n\n \n
\n

Content Quality Issues

\n \n

Outdated Content (${(auditData.issues.contentQuality.staleLastModified || []).length})

\n ${renderTableSection(auditData.issues.contentQuality.staleLastModified, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Last Modified', render: item => item.lastModified },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n

Thin Content (${(auditData.issues.contentQuality.thinContent || []).length})

\n ${renderTableSection(auditData.issues.contentQuality.thinContent, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Word Count', render: item => item.words },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n

Readability Issues (${(auditData.issues.contentQuality.readability || []).length})

\n ${renderTableSection(auditData.issues.contentQuality.readability, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'F-K Score', render: item => item.score.toFixed(1) },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n

Outdated Meta Years (${(auditData.issues.contentQuality.outdatedMetaYear || []).length})

\n ${renderTableSection(auditData.issues.contentQuality.outdatedMetaYear, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\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

Large HTML (${(auditData.issues.contentQuality.largeHTML || []).length})

\n ${renderTableSection(auditData.issues.contentQuality.largeHTML, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\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
\n \n \n
\n

Metadata & SEO Issues

\n \n

Title Length Issues (${(auditData.issues.metadataSEO.titleLength || []).length})

\n ${renderTableSection(auditData.issues.metadataSEO.titleLength, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Length', render: item => `${item.length} characters` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n

Description Length Issues (${(auditData.issues.metadataSEO.descriptionLength || []).length})

\n ${renderTableSection(auditData.issues.metadataSEO.descriptionLength, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Length', render: item => `${item.length} characters` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n

Missing Titles (${(auditData.issues.metadataSEO.missingTitle || []).length})

\n ${renderTableSection(auditData.issues.metadataSEO.missingTitle, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n

Missing Descriptions (${(auditData.issues.metadataSEO.missingDescription || []).length})

\n ${renderTableSection(auditData.issues.metadataSEO.missingDescription, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n

Duplicate Titles (${(auditData.issues.metadataSEO.duplicateTitle || []).length})

\n ${renderTableSection(auditData.issues.metadataSEO.duplicateTitle, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Title', render: item => item.title },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n

Duplicate Descriptions (${(auditData.issues.metadataSEO.duplicateDescription || []).length})

\n ${renderTableSection(auditData.issues.metadataSEO.duplicateDescription, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Description', render: item => item.description },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n

H1 Issues (${(auditData.issues.metadataSEO.h1Issues || []).length})

\n ${renderTableSection(auditData.issues.metadataSEO.h1Issues, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'H1 Count', render: item => item.h1Count },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n
\n \n \n
\n

Internal Linking Issues

\n \n

Excessive Click Depth (${(auditData.issues.internalLinking.excessiveClickDepth || []).length})

\n ${renderTableSection(auditData.issues.internalLinking.excessiveClickDepth, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Click Depth', render: item => item.depth },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n

Orphan Pages (${(auditData.issues.internalLinking.orphanPages || []).length})

\n ${renderTableSection(auditData.issues.internalLinking.orphanPages, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n

Low Internal Links (${(auditData.issues.internalLinking.lowInternalLinks || []).length})

\n ${renderTableSection(auditData.issues.internalLinking.lowInternalLinks, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\n { header: 'Internal Links', render: item => item.links },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n
\n \n \n
\n

Performance Issues

\n \n

Underperforming Content (${(auditData.issues.underperformingContent || []).length})

\n ${renderTableSection(auditData.issues.underperformingContent, [\n { header: 'URL', class: 'url-cell', render: item => `${item.url}` },\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
\n\n
\n

All Pages Overview

\n

Below is a summary of all pages analyzed with their respective issues flagged.

\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 \n \n \n \n \n \n \n \n \n \n ${initialItems.map(page => `\n \n \n \n \n \n \n `).join('')}\n \n ${hasMoreItems ? `\n \n ${hiddenItems.map(page => `\n \n \n \n \n \n \n `).join('')}\n \n ` : ''}\n
URLIssuesClicksImpressions
${page.url}${page.flags.map(flag => `${formatFlagName(flag)}`).join('')}${page.clicks !== null ? page.clicks : 'N/A'}${page.impressions !== null ? page.impressions : 'N/A'}
${page.url}${page.flags.map(flag => `${formatFlagName(flag)}`).join('')}${page.clicks !== null ? page.clicks : 'N/A'}${page.impressions !== null ? page.impressions : 'N/A'}
\n ${hasMoreItems ? `\n
\n \n
\n ` : ''}\n `;\n })()}\n
\n \n
\n

Recommended Next Steps

\n

Based on our analysis, we recommend the following actions to improve your content performance:

\n \n
\n

Priority Actions

\n
    \n ${auditData.summary.issues['404'] > 0 ? \n `
  • Fix 404 errors by restoring pages or implementing proper redirects
  • ` : ''}\n ${auditData.summary.issues.redirects > 0 ? \n `
  • Update internal links to point directly to final URLs instead of through redirects
  • ` : ''}\n ${auditData.summary.issues.thin > 0 ? \n `
  • Expand thin content pages to at least 1,500 words with valuable, unique information
  • ` : ''}\n ${auditData.summary.issues.outdated > 0 ? \n `
  • Update all content that hasn't been refreshed in the last 12 months
  • ` : ''}\n ${auditData.summary.issues.missingOrDuplicateMeta > 0 ? \n `
  • Add unique meta descriptions to all pages missing them
  • ` : ''}\n ${auditData.summary.issues.titleLen > 0 ? \n `
  • Optimize page titles to be between 40-60 characters
  • ` : ''}\n ${auditData.summary.issues.descriptionLen > 0 ? \n `
  • Optimize meta descriptions to be between 70-155 characters
  • ` : ''}\n ${auditData.summary.issues.readability > 0 ? \n `
  • Improve content readability by simplifying language and shortening sentences
  • ` : ''}\n ${auditData.summary.issues.underperforming > 0 ? \n `
  • Identify keywords with potential for pages with high impressions but low clicks
  • ` : ''}\n ${auditData.summary.issues.orphan > 0 ? \n `
  • Create internal links to orphan pages to improve crawlability
  • ` : ''}\n ${auditData.summary.issues.lowInternalLinks > 0 ? \n `
  • Improve internal linking between related content
  • ` : ''}\n
  • Implement a content calendar to regularly refresh content
  • \n
  • Conduct keyword research to identify new content opportunities
  • \n
\n
\n \n

Implementation Timeline

\n

We recommend addressing these issues in the following order:

\n \n
    \n
  1. Immediate (1-2 weeks): Fix technical issues like 404 errors, redirects, missing meta descriptions, and outdated year references.
  2. \n
  3. Short-term (2-4 weeks): Update thin content and improve readability on key pages.
  4. \n
  5. Medium-term (1-2 months): Refresh outdated content, especially on high-impression pages.
  6. \n
  7. Long-term (2-3 months): Implement a content calendar to regularly update content and prevent future staleness.
  8. \n
\n
\n
\n\n \n\n`\n}];" }, "typeVersion": 2 }, { "id": "b772f856-e1cf-44fd-8fc7-1ac5d8b033ca", "name": "Extract 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": "Loop Over Items1", "type": "n8n-nodes-base.splitInBatches", "position": [ 2100, 500 ], "parameters": { "options": {} }, "typeVersion": 3 }, { "id": "4defc61c-7f05-4b64-9b68-96f097a9ba92", "name": "Map URLs Data", "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": "Get Source URLs Data", "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": "Sticky Note", "type": "n8n-nodes-base.stickyNote", "position": [ 0, 0 ], "parameters": { "width": 940, "height": 580, "content": "## Content SEO Audit Report\nA workflow powered by DataForSEO and Google Search Analytics API that generate a comprehensive content audit report for any website up to 1000 pages, 100% customized to your brand's colors.\n\n### Set up instructions:\n1. Add a new credential \"Basic Auth\" by following this [guide](https://docs.n8n.io/integrations/builtin/credentials/httprequest/). You can get your DataForSEO API credentials [here](https://app.dataforseo.com/api-access). DataForSEO offer a free $1 credit when you register, which is plenty enough to test the workflow as the cost is about ~$0.20 per 500-page report. Finally, assign your Basic Auth account to the node \"Create Task\", \"Check Task Status\", \"Get Raw Audit Data\" and \"Get Source URLs Data\".\n2. Add a new credential \"Google OAuth2 API\" by following this [guide](https://docs.n8n.io/integrations/builtin/credentials/google/oauth-generic/). Assign your Google OAuth2 account to the node \"Query GSC API\".\n3. Update the \"Set Fields\" node with the following information:\n- dfs_domain: The website domain you want to crawl.\n- company_name: Your company name (Will be displayed on the final report)\n- company_website: Your company website URL (Will be displayed on the final report)\n- company_logo_url: Your company logo URL (Will be displayed on the final report)\n- brand_primary_color: Your primary brand color. (Will be used to customize the final report to your brand's colors)\n- brand_secondary_color: Your secondary brand color. (Will be used to customize the final report to your brand's colors)\n- gsc_property_type: Set to \"domain\" or \"url\" depending of the property type set in your Google Search Console account for the target website (dfs_domain).\n4. Start the workflow. Once done, download the HTML file in the last node \"Download Report\". \n\nVoilà! You have a comprehensive content audit report ready to be sent to your client via email, customized to your own branding.\n\n**Note**: The workflow take approximately 20 minutes to run for ~500 pages. If you want to customize this workflow for your own need, feel free to [contact us](https://customworkflows.ai/work-with-us)." }, "typeVersion": 1 }, { "id": "afd6a0aa-813c-4a3f-b844-ac1cf9f854c6", "name": "Download Report", "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 } ] ] } } }