Workflow: Wait Code Automate

Workflow Details

Download Workflow
{
    "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.399999999999999911182158029987476766109466552734375
        },
        {
            "id": "57a66b27-a253-4543-9d44-cd3afdbc3946",
            "name": "When clicking \u2018Start\u2019",
            "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.20000000000000017763568394002504646778106689453125
        },
        {
            "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.20000000000000017763568394002504646778106689453125
        },
        {
            "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.20000000000000017763568394002504646778106689453125
        },
        {
            "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.100000000000000088817841970012523233890533447265625
        },
        {
            "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.20000000000000017763568394002504646778106689453125
        },
        {
            "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.20000000000000017763568394002504646778106689453125,
            "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.100000000000000088817841970012523233890533447265625
        },
        {
            "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.399999999999999911182158029987476766109466552734375
        },
        {
            "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 \u2013 Function node\n * Input  : \u2022 One item whose `json` is the crawl + GSC data\n *          \u2022 All the items produced by the loop node \u201cLoop Over Items1\u201d\n * Output : ONE item whose `json` = { generatedAt, summary, issues, pages }\n *          \u2013 Unchanged shape, just extra `sources`[] on 404 \/ 301 records\n *\/\n\n\/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 helpers & constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\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 \u201c\/\u201d\n\n\/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 main data sets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\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},\u2026 ] }\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\/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 duplicate-meta look-ups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\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\/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 report skeleton \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst issues = {\n  statusIssues:         {},\n  contentQuality:       {},\n  metadataSEO:          {},\n  internalLinking:      {},\n  underperformingContent: [],\n};\n\nconst summary        = { pages: pages.length };\nconst pagesWithFlags = [];\n\n\/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 per-page loop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\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 \u00b7 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] ?? [],      \/\/ \u2190 new\n      todo  : 'Restore the page or 301-redirect it to a relevant URL.',\n    });\n  }\n\n  \/* 2 \u00b7 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] ?? [],      \/\/ \u2190 new\n      todo  : 'Update internal links so they point directly to the final URL (single-hop redirect).',\n    });\n  }\n\n  \/* 3 \u00ad\u2013 15 \u00b7 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 || '\u2014'}\" 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 \u2192 ${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 \u2192 ${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 \u2014 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 \u22644 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 \u2014 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\/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 executive summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\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\/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 final report \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\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 `<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 || '\u2014' },\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": "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.20000000000000017763568394002504646778106689453125
        },
        {
            "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\u00e0! 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.100000000000000088817841970012523233890533447265625
        }
    ],
    "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 \u2018Start\u2019": {
            "main": [
                [
                    {
                        "node": "Set Fields",
                        "type": "main",
                        "index": 0
                    }
                ]
            ]
        },
        "Merge GSC Data with RAW Data": {
            "main": [
                [
                    {
                        "node": "Extract 404 & 301",
                        "type": "main",
                        "index": 0
                    }
                ]
            ]
        }
    }
}
Back to Workflows

Related Workflows

Manual Import Triggered
View
Splitout Limit Send Webhook
View
Webhook Nocodb Create Webhook
View
Get all the stories starting with `release` and publish them
View
Stripe Payment Order Sync – Auto Retrieve Customer & Product Purchased
View
Shopify Automate Triggered
View
Aggregate Telegram Automate Triggered
View
HTTP Filter Monitor Webhook
View
Get Comments from Facebook Page
View
Auto WordPress Blog Generator (GPT + Postgres + WP Media)
View