Automatisierter Inhalts-SEO-Audit-Bericht
Dies ist ein AI, Marketing-Bereich Automatisierungsworkflow mit 21 Nodes. Hauptsächlich werden If, Set, Code, Wait, HttpRequest und andere Nodes verwendet, kombiniert mit KI-Technologie für intelligente Automatisierung. Mit DataForSEO und Google Search Console umfassende SEO-Prüfberichte erstellen
- •Möglicherweise sind Ziel-API-Anmeldedaten erforderlich
Verwendete Nodes (21)
Kategorie
{
"id": "Tqa8dikBDLYEytx5",
"meta": {
"instanceId": "ddfdf733df99a65c801a91865dba5b7c087c95cc22a459ff3647e6deddf2aee6"
},
"name": "Automated Content SEO Audit Report",
"tags": [],
"nodes": [
{
"id": "b5f15675-35c9-42a1-b7eb-bfaf0b467a5a",
"name": "Felder setzen",
"type": "n8n-nodes-base.set",
"position": [
280,
620
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e71886f0-104f-412b-9fef-d2b3738cebf0",
"name": "dfs_domain",
"type": "string",
"value": "yourclientdomain.com"
},
{
"id": "de35327e-1e32-4996-970a-50b8953c7709",
"name": "dfs_max_crawl_pages",
"type": "string",
"value": "1000"
},
{
"id": "0d6b4d1a-e57d-4e38-8aa5-e2ea5589a089",
"name": "dfs_enable_javascript",
"type": "string",
"value": "false"
},
{
"id": "d699e487-ab74-483f-8cd8-cdcfaca567d7",
"name": "company_name",
"type": "string",
"value": "Custom Workflows AI"
},
{
"id": "da123535-f678-4331-973a-07711b7aaaac",
"name": "company_website",
"type": "string",
"value": "https://customworkflows.ai"
},
{
"id": "e12486eb-7019-4639-85a9-c55b4c62beef",
"name": "company_logo_url",
"type": "string",
"value": "https://customworkflows.ai/images/logo.png"
},
{
"id": "9eef2015-e89c-4930-82a5-972111c1a4fe",
"name": "brand_primary_color",
"type": "string",
"value": "#252946"
},
{
"id": "dd4ff260-6008-49ec-a0e6-ad5c177eb8df",
"name": "brand_secondary_color",
"type": "string",
"value": "#0fd393"
},
{
"id": "d71a4d91-c5bf-49c4-b7d0-64e84dad6153",
"name": "gsc_property_type",
"type": "string",
"value": "domain"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "57a66b27-a253-4543-9d44-cd3afdbc3946",
"name": "Bei Klick auf 'Start'",
"type": "n8n-nodes-base.manualTrigger",
"position": [
60,
620
],
"parameters": {},
"typeVersion": 1
},
{
"id": "3e5e8162-2815-429f-b6e8-6ea6ea70cf18",
"name": "Aufgabenstatus prüfen",
"type": "n8n-nodes-base.httpRequest",
"position": [
660,
620
],
"parameters": {
"url": "=https://api.dataforseo.com/v3/on_page/summary/{{ $json.tasks[0].id }}",
"options": {},
"sendHeaders": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "9ea481fe-8af6-43c2-881d-eb68f63b0424",
"name": "Aufgabe erstellen",
"type": "n8n-nodes-base.httpRequest",
"position": [
480,
620
],
"parameters": {
"url": "https://api.dataforseo.com/v3/on_page/task_post",
"method": "POST",
"options": {},
"jsonBody": "=[\n {\n \"target\": \"{{ $json.dfs_domain }}\",\n \"max_crawl_pages\": {{ $json.dfs_max_crawl_pages }},\n \"load_resources\": false,\n \"enable_javascript\": {{ $json.dfs_enable_javascript }},\n \"custom_js\": \"meta = {}; meta.url = document.URL; meta;\",\n \"tag\": \"{{ $json.dfs_domain + Math.floor(10000 + Math.random() * 90000) }}\"\n }\n]",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "0a0e696a-29a7-4b34-8299-102c72544153",
"name": "Wenn",
"type": "n8n-nodes-base.if",
"position": [
860,
620
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "7e13429d-9ead-4ae5-8ed6-c5730b05927d",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.tasks[0].result[0].crawl_progress }}",
"rightValue": "finished"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "a31db736-23e0-4db8-ab90-294cd87c9123",
"name": "Warten",
"type": "n8n-nodes-base.wait",
"position": [
1060,
680
],
"webhookId": "f60d5346-5ddf-4819-a865-48e2d9e6103c",
"parameters": {
"unit": "minutes",
"amount": 1
},
"typeVersion": 1.1
},
{
"id": "8f95fd0b-e990-4c85-b21b-83d06d2121fe",
"name": "ROHDaten für Audit abrufen",
"type": "n8n-nodes-base.httpRequest",
"position": [
1060,
500
],
"parameters": {
"url": "https://api.dataforseo.com/v3/on_page/pages",
"method": "POST",
"options": {},
"jsonBody": "=[\n {\n \"id\": \"{{ $json.tasks[0].id }}\",\n \"limit\": \"1000\"\n }\n]",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "6cf221d9-c17e-4a5c-9c9a-c3176319df95",
"name": "URLs extrahieren",
"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": "Über Elemente iterieren",
"type": "n8n-nodes-base.splitInBatches",
"position": [
1480,
500
],
"parameters": {
"options": {},
"batchSize": 100
},
"typeVersion": 3
},
{
"id": "aebdd823-9a4d-4323-aadf-b7d92d601d57",
"name": "Google Search Console abfragen API",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueErrorOutput",
"maxTries": 5,
"position": [
1480,
680
],
"parameters": {
"url": "={{ \n $('Set Fields').first().json.gsc_property_type === 'domain' \n ? 'https://searchconsole.googleapis.com/webmasters/v3/sites/' + \n 'sc-domain:' + \n $node[\"Loop Over Items\"].json.url.replace(/https?:\\/\\/(www\\.)?([^\\/]+).*/, '$2') + \n '/searchAnalytics/query' \n : 'https://searchconsole.googleapis.com/webmasters/v3/sites/' + \n encodeURIComponent(\n $node[\"Loop Over Items\"].json.url.replace(/(https?:\\/\\/(?:www\\.)?[^\\/]+).*/, '$1')\n ) + \n '/searchAnalytics/query' \n}}",
"body": "={\n \"startDate\": \"{{ new Date(new Date().setDate(new Date().getDate() - 90)).toISOString().split('T')[0] }}\",\n \"endDate\": \"{{ new Date().toISOString().split('T')[0] }}\",\n \"dimensionFilterGroups\": [\n {\n \"filters\": [\n {\n \"dimension\": \"page\",\n \"operator\": \"equals\",\n \"expression\": \"{{ $node['Loop Over Items'].json.url }}\"\n }\n ]\n }\n ],\n \"aggregationType\": \"auto\",\n \"rowLimit\": 100\n}",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "raw",
"sendHeaders": true,
"authentication": "predefinedCredentialType",
"rawContentType": "JSON",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"nodeCredentialType": "googleOAuth2Api"
},
"retryOnFail": true,
"typeVersion": 4.2,
"waitBetweenTries": 5000
},
{
"id": "d9943a4b-7320-47ce-95fa-67eb28cabd26",
"name": "Warten1",
"type": "n8n-nodes-base.wait",
"position": [
1680,
680
],
"webhookId": "8b2109f4-1aca-4585-8261-7dfc4ca2f95e",
"parameters": {
"unit": "minutes",
"amount": 1
},
"typeVersion": 1.1
},
{
"id": "f2f7e975-1db1-4566-b674-396ccaa775f5",
"name": "GSC-Daten URLs zuordnen",
"type": "n8n-nodes-base.set",
"position": [
1880,
680
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "342ff66d-cdfc-46e8-9605-db588c913eb0",
"name": "URL",
"type": "string",
"value": "={{ $('Loop Over Items').item.json.url }}"
},
{
"id": "5c547efc-0514-4641-8f05-c24b965993ad",
"name": "Clicks",
"type": "string",
"value": "={{ $('Query GSC API').item.json.rows[0].clicks }}"
},
{
"id": "340c3ced-061d-49f0-911d-bd8b9e433a7d",
"name": "Impressions",
"type": "string",
"value": "={{ $('Query GSC API').item.json.rows[0].impressions }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "4e42e1eb-4769-4e28-9f2f-3fb342baf971",
"name": "GSC-Daten mit ROH-Daten zusammenführen",
"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": "Berichtsstruktur aufbauen",
"type": "n8n-nodes-base.code",
"position": [
2100,
320
],
"parameters": {
"jsCode": "/**\n * n8n – Function node\n * Input : • One item whose `json` is the crawl + GSC data\n * • All the items produced by the loop node “Loop Over Items1”\n * Output : ONE item whose `json` = { generatedAt, summary, issues, pages }\n * – Unchanged shape, just extra `sources`[] on 404 / 301 records\n */\n\n/* ────────────────────── helpers & constants ───────────────────── */\nconst CUR_YEAR = new Date().getFullYear();\nconst YEAR_RX = /20\\d{2}/g;\nconst TWELVE_MONTHS_MS = 1000 * 60 * 60 * 24 * 365.25;\nconst SIX_MONTHS_MS = TWELVE_MONTHS_MS / 2;\nconst LARGE_HTML_LIMIT = 2_000_000;\n\nconst ageInMs = (s) => Date.now() - Date.parse(s);\nconst ensureBucket = (parent, key) => (parent[key] ??= []);\nconst normalizeUrl = (u) => (u || '').replace(/\\/+$/, ''); // strip trailing “/”\n\n/* ────────────────────── main data sets ───────────────────────── */\nconst root = $node['Merge GSC Data with RAW Data'].json;\nconst pages = root.tasks?.[0]?.result?.[0]?.items ?? [];\n\n/* link-source items from the loop node */\nconst sourceItems = $items('Loop Over Items1') ?? [];\nconst linkSourceMap = {}; // { normalisedTargetUrl : [ {linkFrom,type,text},… ] }\n\nfor (const itm of sourceItems) {\n const j = itm.json || {};\n const tgt = normalizeUrl(j.URL);\n if (!tgt) continue;\n\n linkSourceMap[tgt] ??= [];\n for (const s of j.sources || []) {\n linkSourceMap[tgt].push({\n linkFrom: s.link_from,\n type : s.type,\n text : s.text,\n });\n }\n}\n\n/* ────────────────────── duplicate-meta look-ups ───────────────── */\nconst titleFreq = {};\nconst descFreq = {};\n\nfor (const p of pages) {\n const t = p.meta?.title?.trim();\n const d = p.meta?.description?.trim();\n if (t) titleFreq[t] = (titleFreq[t] || 0) + 1;\n if (d) descFreq[d] = (descFreq[d] || 0) + 1;\n}\n\n/* ────────────────────── report skeleton ──────────────────────── */\nconst issues = {\n statusIssues: {},\n contentQuality: {},\n metadataSEO: {},\n internalLinking: {},\n underperformingContent: [],\n};\n\nconst summary = { pages: pages.length };\nconst pagesWithFlags = [];\n\n/* ────────────────────── per-page loop ────────────────────────── */\nfor (const p of pages) {\n const url = p.url;\n const norm = normalizeUrl(url);\n const flags = [];\n\n const add = (sect, bucket, rec) => ensureBucket(issues[sect], bucket).push(rec);\n\n const isStatusOK = p.status_code === 200;\n\n /* 1 · 404 ---------------------------------------------------- */\n if (p.status_code === 404 || p.checks?.is_4xx_code) {\n flags.push('404');\n add('statusIssues', 'pages404', {\n url,\n sources: linkSourceMap[norm] ?? [], // ← new\n todo : 'Restore the page or 301-redirect it to a relevant URL.',\n });\n }\n\n /* 2 · 301 ---------------------------------------------------- */\n if (p.status_code === 301 || p.checks?.is_redirect) {\n flags.push('redirect_301');\n add('statusIssues', 'redirects301', {\n url,\n sources: linkSourceMap[norm] ?? [], // ← new\n todo : 'Update internal links so they point directly to the final URL (single-hop redirect).',\n });\n }\n\n /* 3 – 15 · all original checks (unchanged) ------------------ */\n /* Canonicalised */\n const canonicalised =\n (p.meta?.canonical && p.meta.canonical !== url) ||\n p.checks?.canonical_chain ||\n p.checks?.recursive_canonical;\n\n if (isStatusOK && canonicalised) {\n flags.push('canonicalised');\n add('statusIssues', 'canonicalised', {\n url,\n canonical: p.meta?.canonical,\n todo: `Verify that \"${p.meta?.canonical || '—'}\" is the correct canonical target and eliminate unintended duplicates.`,\n });\n }\n\n /* Outdated content (years + stale last-modified) */\n if (isStatusOK) {\n const titleYears = (p.meta?.title?.match(YEAR_RX) || []).filter((y) => Number(y) < CUR_YEAR);\n const descYears = (p.meta?.description?.match(YEAR_RX) || []).filter((y) => Number(y) < CUR_YEAR);\n\n if (titleYears.length) {\n flags.push('outdated_year_title');\n add('contentQuality', 'outdatedMetaYear', {\n url,\n field : 'title',\n years : titleYears.join(','),\n original : p.meta?.title,\n todo : `Title contains old year → ${titleYears.join(', ')}. Update to ${CUR_YEAR} or remove dates.`,\n });\n }\n if (descYears.length) {\n flags.push('outdated_year_description');\n add('contentQuality', 'outdatedMetaYear', {\n url,\n field : 'description',\n years : descYears.join(','),\n original : p.meta?.description,\n todo : `Meta description contains old year → ${descYears.join(', ')}. Update to ${CUR_YEAR} or remove dates.`,\n });\n }\n\n const lm = p.last_modified ??\n p.meta?.social_media_tags?.['og:updated_time'] ?? null;\n\n if (lm && ageInMs(lm) > TWELVE_MONTHS_MS) {\n flags.push('stale_last_modified');\n add('contentQuality', 'staleLastModified', {\n url,\n lastModified: lm,\n todo : 'Page not updated for 12+ months — refresh content.',\n });\n }\n }\n\n /* Thin content */\n if (isStatusOK) {\n const wc = p.meta?.content?.plain_text_word_count || 0;\n if (p.click_depth !== 0 && wc >= 1 && wc <= 1500) {\n flags.push('thin_content');\n add('contentQuality', 'thinContent', {\n url,\n words: wc,\n todo : 'Expand the piece beyond 1 500 words with valuable, unique information.',\n });\n }\n }\n\n /* Excessive click depth */\n if (isStatusOK && (p.click_depth || 0) > 4) {\n flags.push('excessive_click_depth');\n add('internalLinking', 'excessiveClickDepth', {\n url,\n depth: p.click_depth,\n todo : 'Surface this URL within ≤4 clicks via navigation or contextual links.',\n });\n }\n\n /* Large HTML */\n if (isStatusOK && ((p.size || 0) > LARGE_HTML_LIMIT || (p.total_dom_size || 0) > LARGE_HTML_LIMIT)) {\n flags.push('large_html');\n add('contentQuality', 'largeHTML', {\n url,\n size : p.size,\n totalDom: p.total_dom_size,\n todo : 'Reduce HTML payload (remove unused markup/JS, paginate, or lazy-load where possible).',\n });\n }\n\n /* Title length */\n if (isStatusOK && (p.meta?.title_length < 40 || p.meta?.title_length > 60)) {\n flags.push('title_length');\n add('metadataSEO', 'titleLength', {\n url,\n length: p.meta?.title_length,\n todo : `Write a meta title 40-60 characters long (currently ${p.meta?.title_length || 0}).`,\n });\n }\n\n /* Description length */\n if (isStatusOK) {\n const dl = p.meta?.description_length || 0;\n if (dl > 0 && (dl < 70 || dl > 155)) {\n flags.push('description_length');\n add('metadataSEO', 'descriptionLength', {\n url,\n length: dl,\n todo : `Write a meta description 70-155 characters long (currently ${dl}).`,\n });\n }\n }\n\n /* Missing / duplicate meta */\n if (isStatusOK) {\n if (p.checks?.no_title) {\n flags.push('missing_title');\n add('metadataSEO', 'missingTitle', { url, todo: 'Add a unique SEO title 40-60 characters long.' });\n }\n if (p.checks?.no_description) {\n flags.push('missing_description');\n add('metadataSEO', 'missingDescription', { url, todo: 'Add a unique meta description 70-155 characters long.' });\n }\n if (titleFreq[p.meta?.title?.trim()] > 1) {\n flags.push('duplicate_title');\n add('metadataSEO', 'duplicateTitle', { url, title: p.meta?.title, todo: 'Differentiate this title to avoid keyword cannibalisation.' });\n }\n if (p.meta?.description && descFreq[p.meta.description.trim()] > 1) {\n flags.push('duplicate_description');\n add('metadataSEO', 'duplicateDescription', { url, description: p.meta?.description, todo: 'Rewrite the meta description so each page is unique.' });\n }\n }\n\n /* H1 issues */\n if (isStatusOK) {\n const h1s = p.meta?.htags?.h1 ?? [];\n if (h1s.length !== 1) {\n flags.push('h1_issue');\n add('metadataSEO', 'h1Issues', { url, h1Count: h1s.length, todo: 'Ensure exactly one H1 tag per page that reflects the main topic.' });\n }\n }\n\n /* Readability */\n if (isStatusOK) {\n const fk = p.meta?.content?.flesch_kincaid_readability_index ?? 100;\n if (fk < 55) {\n flags.push('low_readability');\n add('contentQuality', 'readability', { url, score: fk, todo: `Simplify language, shorten sentences, and use lists to lift F-K score > 55 (currently ${fk.toFixed(2)}).` });\n }\n }\n\n /* Orphan pages */\n if (isStatusOK && p.checks?.is_orphan_page) {\n flags.push('orphan_page');\n add('internalLinking', 'orphanPages', { url, todo: 'Add at least one crawlable internal link pointing to this URL.' });\n }\n\n /* Low internal links */\n if (isStatusOK && (p.meta?.internal_links_count || 0) < 3) {\n flags.push('low_internal_links');\n add('internalLinking', 'lowInternalLinks', { url, links: p.meta?.inbound_links_count, todo: 'Add three or more relevant internal links to strengthen topical signals.' });\n }\n\n /* Under-performing content */\n if (isStatusOK) {\n const clicks = p.googleSearchConsoleData?.clicks ?? null;\n const impressions = p.googleSearchConsoleData?.impressions ?? null;\n const lm = p.last_modified ?? p.meta?.social_media_tags?.['og:updated_time'] ?? null;\n\n if (clicks !== null && clicks < 50 && (lm === null || ageInMs(lm) > SIX_MONTHS_MS)) {\n flags.push('underperforming');\n issues.underperformingContent.push({\n url,\n clicks,\n impressions,\n lastModified: lm,\n todo: `Only ${clicks} clicks in the last 90 days — refresh content, improve targeting, or consider pruning.`,\n });\n }\n }\n\n /* page-level flags record */\n pagesWithFlags.push({\n url,\n flags,\n clicks : p.googleSearchConsoleData?.clicks,\n impressions: p.googleSearchConsoleData?.impressions,\n });\n}\n\n/* ────────────────────── executive summary ────────────────────── */\nconst count = (sect, bucket) => issues[sect]?.[bucket]?.length || 0;\n\nsummary.issues = {\n '404' : count('statusIssues', 'pages404'),\n redirects : count('statusIssues', 'redirects301'),\n canonicalised : count('statusIssues', 'canonicalised'),\n outdated : count('contentQuality', 'outdatedMetaYear') +\n count('contentQuality', 'staleLastModified'),\n thin : count('contentQuality', 'thinContent'),\n excessiveClickDepth : count('internalLinking', 'excessiveClickDepth'),\n largeHTML : count('contentQuality', 'largeHTML'),\n titleLen : count('metadataSEO', 'titleLength'),\n descriptionLen : count('metadataSEO', 'descriptionLength'),\n missingOrDuplicateMeta:\n count('metadataSEO', 'missingTitle') +\n count('metadataSEO', 'missingDescription') +\n count('metadataSEO', 'duplicateTitle') +\n count('metadataSEO', 'duplicateDescription'),\n h1Issues : count('metadataSEO', 'h1Issues'),\n readability : count('contentQuality', 'readability'),\n orphan : count('internalLinking', 'orphanPages'),\n lowInternalLinks : count('internalLinking', 'lowInternalLinks'),\n underperforming : issues.underperformingContent.length,\n};\n\n/* ────────────────────── final report ─────────────────────────── */\nreturn [{\n json: {\n generatedAt: new Date().toISOString(),\n summary,\n issues,\n pages: pagesWithFlags,\n },\n}];"
},
"typeVersion": 2
},
{
"id": "2227e1c7-890a-4b99-ad20-5b5645ba884b",
"name": "HTML-Bericht generieren",
"type": "n8n-nodes-base.code",
"position": [
2320,
320
],
"parameters": {
"jsCode": "// Get the audit data and company information\nconst auditData = $('Build Report Structure').item.json;\nconst websiteDomain = $('Set Fields').first().json.dfs_domain;\nconst companyName = $('Set Fields').first().json.company_name;\nconst companyWebsite = $('Set Fields').first().json.company_website;\nconst companyLogoUrl = $('Set Fields').first().json.company_logo_url;\nconst primaryColor = $('Set Fields').first().json.brand_primary_color;\nconst secondaryColor = $('Set Fields').first().json.brand_secondary_color;\n\n// Format date nicely\nconst formattedDate = new Date(auditData.generatedAt).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric'\n});\n\n// Calculate total issues\nconst totalIssues = Object.values(auditData.summary.issues).reduce((sum, count) => sum + count, 0);\n\n// Define issue gravity weights for health score calculation\nconst issueGravity = {\n // Content Quality\n outdated: 2, // Medium\n thin: 3, // High\n readability: 1, // Low\n largeHTML: 2, // Medium\n // Technical SEO\n '404': 3, // High\n redirects: 2, // Medium\n canonicalised: 3, // High\n // On-Page SEO\n titleLen: 1, // Low\n descriptionLen: 1, // Low\n missingOrDuplicateMeta: 1, // Low\n h1Issues: 3, // High\n // Internal Linking\n excessiveClickDepth: 3, // High\n orphan: 3, // High\n lowInternalLinks: 3, // High\n // Performance\n underperforming: 3 // High\n};\n\n// Calculate health score based on issue gravity\nfunction calculateHealthScore(pages, issues) {\n // Calculate weighted sum of issues\n let weightedIssues = 0;\n let maxPossibleWeightedIssues = 0;\n \n // Process each issue type with its gravity weight\n for (const [issueType, count] of Object.entries(auditData.summary.issues)) {\n const gravity = issueGravity[issueType] || 1; // Default to Low if not defined\n weightedIssues += count * gravity;\n \n // Assume worst case: all pages have this issue\n maxPossibleWeightedIssues += pages * gravity;\n }\n \n // Cap the maximum penalty to avoid too severe scores with many pages\n const maxPenalty = Math.min(pages * 5, 100);\n \n // Calculate score: start at 100 and subtract weighted penalty\n const weightedPenalty = Math.min(maxPenalty, (weightedIssues / Math.max(1, pages)) * 2);\n const score = 100 - weightedPenalty;\n \n return Math.max(0, Math.round(score));\n}\n\n// Get health score color based on value\nfunction getHealthScoreColor(score) {\n if (score >= 80) return '#4caf50'; // Green\n if (score >= 60) return '#ff9800'; // Orange\n return '#f44336'; // Red\n}\n\n// Get top recommendations\nfunction getTopRecommendations(audit) {\n const recommendations = [];\n const priorityMap = {\n 3: \"high\", // High gravity issues\n 2: \"medium\", // Medium gravity issues\n 1: \"low\" // Low gravity issues\n };\n \n // Check for high gravity issues first\n if ((audit.issues.contentQuality.thinContent || []).length > 0) {\n recommendations.push({\n text: \"Expand thin content pages to improve topical depth and authority\",\n priority: priorityMap[issueGravity.thin] || \"high\"\n });\n }\n \n if ((audit.issues.statusIssues.pages404 || []).length > 0) {\n recommendations.push({\n text: \"Fix 404 errors by restoring pages or implementing proper redirects\",\n priority: priorityMap[issueGravity['404']] || \"high\"\n });\n }\n \n if ((audit.issues.metadataSEO.h1Issues || []).length > 0) {\n recommendations.push({\n text: \"Fix H1 tag issues to improve on-page SEO and content hierarchy\",\n priority: priorityMap[issueGravity.h1Issues] || \"high\"\n });\n }\n \n if ((audit.issues.internalLinking.orphanPages || []).length > 0) {\n recommendations.push({\n text: \"Create internal links to orphan pages to improve crawlability\",\n priority: priorityMap[issueGravity.orphan] || \"high\"\n });\n }\n \n if ((audit.issues.underperformingContent || []).length > 0) {\n recommendations.push({\n text: \"Optimize underperforming pages to improve search visibility\",\n priority: priorityMap[issueGravity.underperforming] || \"high\"\n });\n }\n \n if ((audit.issues.statusIssues.canonicalised || []).length > 0) {\n recommendations.push({\n text: \"Fix canonicalization issues to consolidate ranking signals\",\n priority: priorityMap[issueGravity.canonicalised] || \"high\"\n });\n }\n \n // Medium gravity issues\n if ((audit.issues.contentQuality.staleLastModified || []).length > 0) {\n recommendations.push({\n text: \"Update stale content with fresh information and current year references\",\n priority: priorityMap[issueGravity.outdated] || \"medium\"\n });\n }\n \n if ((audit.issues.statusIssues.redirects301 || []).length > 0) {\n recommendations.push({\n text: \"Update internal links to point directly to final URLs instead of through redirects\",\n priority: priorityMap[issueGravity.redirects] || \"medium\"\n });\n }\n \n if ((audit.issues.contentQuality.largeHTML || []).length > 0) {\n recommendations.push({\n text: \"Reduce HTML size for better page performance and loading speed\",\n priority: priorityMap[issueGravity.largeHTML] || \"medium\"\n });\n }\n \n // Low gravity issues\n if ((audit.issues.metadataSEO.missingDescription || []).length > 0) {\n recommendations.push({\n text: \"Add missing meta descriptions to improve click-through rates\",\n priority: priorityMap[issueGravity.missingOrDuplicateMeta] || \"low\"\n });\n }\n \n if ((audit.issues.contentQuality.readability || []).length > 0) {\n recommendations.push({\n text: \"Improve content readability to enhance user experience\",\n priority: priorityMap[issueGravity.readability] || \"low\"\n });\n }\n \n // Fallback if not enough recommendations\n if (recommendations.length < 3) {\n recommendations.push({\n text: \"Implement a regular content audit schedule to maintain freshness\",\n priority: \"low\"\n });\n }\n \n // Return top 5 recommendations, prioritizing high gravity issues first\n return recommendations\n .sort((a, b) => {\n const priorityOrder = { \"high\": 0, \"medium\": 1, \"low\": 2 };\n return priorityOrder[a.priority] - priorityOrder[b.priority];\n })\n .slice(0, 5);\n}\n\n// Format flag names for display\nfunction formatFlagName(flag) {\n return flag\n .split('_')\n .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n}\n\n// Utility to lighten a color\nfunction lightenColor(hex, percent) {\n hex = hex.replace('#', '');\n let r = parseInt(hex.substring(0, 2), 16);\n let g = parseInt(hex.substring(2, 4), 16);\n let b = parseInt(hex.substring(4, 6), 16);\n r = Math.min(255, Math.round(r + (255 - r) * (percent / 100)));\n g = Math.min(255, Math.round(g + (255 - g) * (percent / 100)));\n b = Math.min(255, Math.round(b + (255 - b) * (percent / 100)));\n return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;\n}\n\n// Utility to darken a color\nfunction darkenColor(hex, percent) {\n hex = hex.replace('#', '');\n let r = parseInt(hex.substring(0, 2), 16);\n let g = parseInt(hex.substring(2, 4), 16);\n let b = parseInt(hex.substring(4, 6), 16);\n r = Math.max(0, Math.round(r * (1 - percent / 100)));\n g = Math.max(0, Math.round(g * (1 - percent / 100)));\n b = Math.max(0, Math.round(b * (1 - percent / 100)));\n return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;\n}\n\n// Helper function to render a table section or \"No issues found\" message\nfunction renderTableSection(items, columns) {\n if (!items || items.length === 0) {\n return `<p class=\"section-empty\">No issues found.</p>`;\n }\n \n const showInitial = 10; // Number of rows to show initially\n const hasMoreItems = items.length > showInitial;\n const initialItems = hasMoreItems ? items.slice(0, showInitial) : items;\n const hiddenItems = hasMoreItems ? items.slice(showInitial) : [];\n \n return `\n <table class=\"paginated-table\">\n <thead>\n <tr>\n ${columns.map(col => `<th>${col.header}</th>`).join('')}\n </tr>\n </thead>\n <tbody class=\"initial-rows\">\n ${initialItems.map(item => `\n <tr>\n ${columns.map(col => `<td class=\"${col.class || ''}\">${col.render(item)}</td>`).join('')}\n </tr>\n `).join('')}\n </tbody>\n ${hasMoreItems ? `\n <tbody class=\"hidden-rows\" style=\"display: none;\">\n ${hiddenItems.map(item => `\n <tr>\n ${columns.map(col => `<td class=\"${col.class || ''}\">${col.render(item)}</td>`).join('')}\n </tr>\n `).join('')}\n </tbody>\n ` : ''}\n </table>\n ${hasMoreItems ? `\n <div class=\"table-pagination\">\n <button class=\"show-more-button\" onclick=\"toggleRows(this)\">Show All (${items.length} rows)</button>\n </div>\n ` : ''}\n `;\n}\n\n// Helper function to render source links for 404 and 301 pages\nfunction renderSourceLinks(sources) {\n if (!sources || sources.length === 0) {\n return '<p class=\"no-sources\">No source links found.</p>';\n }\n \n return `\n <div class=\"source-links\">\n <table class=\"source-links-table\">\n <thead>\n <tr>\n <th>Source URL</th>\n <th>Type</th>\n <th>Anchor Text</th>\n </tr>\n </thead>\n <tbody>\n ${sources.map(source => `\n <tr>\n <td class=\"url-cell\"><a href=\"${source.linkFrom}\" target=\"_blank\">${source.linkFrom}</a></td>\n <td>${source.type || 'N/A'}</td>\n <td>${source.text || 'N/A'}</td>\n </tr>\n `).join('')}\n </tbody>\n </table>\n </div>\n `;\n}\n\n// Return a single item with the HTML content\nreturn [{\n html: `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Content Audit Report for ${websiteDomain} | ${companyName}</title>\n <style>\n :root {\n --primary-color: ${primaryColor};\n --secondary-color: ${secondaryColor};\n --primary-light: ${lightenColor(primaryColor, 85)};\n --secondary-light: ${lightenColor(secondaryColor, 85)};\n --primary-dark: ${darkenColor(primaryColor, 20)};\n --text-color: #333;\n --light-gray: #f5f5f5;\n --medium-gray: #e0e0e0;\n --dark-gray: #757575;\n --success-color: #4caf50;\n --warning-color: #ff9800;\n --danger-color: #f44336;\n }\n \n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n body {\n font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n line-height: 1.6;\n color: var(--text-color);\n background-color: #fff;\n }\n \n .container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 0 20px;\n }\n \n header {\n background-color: var(--primary-color);\n color: white;\n padding: 30px 0;\n margin-bottom: 40px;\n }\n \n .header-content {\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n \n .logo-container {\n display: flex;\n align-items: center;\n }\n \n .logo {\n max-height: 60px;\n margin-right: 20px;\n }\n \n .report-info {\n text-align: right;\n }\n \n h1 {\n font-size: 1.8rem;\n margin-bottom: 0px;\n color: white;\n }\n \n h2 {\n font-size: 1.8rem;\n margin: 40px 0 20px;\n color: var(--primary-color);\n border-bottom: 2px solid var(--primary-light);\n padding-bottom: 10px;\n }\n \n h3 {\n font-size: 1.4rem;\n margin: 30px 0 15px;\n color: var(--primary-dark);\n }\n \n h4 {\n font-size: 1.2rem;\n margin: 20px 0 10px;\n color: var(--secondary-color);\n }\n \n p {\n margin-bottom: 15px;\n }\n \n .summary-cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n gap: 20px;\n margin: 30px 0;\n }\n \n .card {\n background-color: white;\n border-radius: 8px;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n padding: 20px;\n transition: transform 0.3s ease;\n }\n \n .card:hover {\n transform: translateY(-5px);\n }\n \n .card-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin-bottom: 15px;\n }\n \n .card-title {\n font-size: 1.2rem;\n font-weight: 600;\n color: var(--primary-color);\n }\n \n .card-value {\n font-size: 2.5rem;\n font-weight: 700;\n color: var(--secondary-color);\n }\n \n .issues-summary {\n display: flex;\n justify-content: space-between;\n flex-wrap: wrap;\n gap: 15px;\n margin: 30px 0;\n }\n \n .issue-category {\n flex: 1;\n min-width: 250px;\n background-color: var(--light-gray);\n border-radius: 8px;\n padding: 20px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);\n }\n \n .issue-category h3 {\n color: var(--primary-color);\n margin-top: 0;\n border-bottom: 1px solid var(--medium-gray);\n padding-bottom: 10px;\n }\n \n .issue-item {\n display: flex;\n justify-content: space-between;\n padding: 8px 0;\n border-bottom: 1px solid var(--medium-gray);\n }\n \n .issue-item:last-child {\n border-bottom: none;\n }\n \n .issue-name {\n color: var(--text-color);\n }\n \n .issue-count {\n font-weight: 600;\n color: var(--secondary-color);\n }\n \n table {\n width: 100%;\n border-collapse: collapse;\n margin: 20px 0 40px;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n }\n \n th {\n background-color: var(--primary-color);\n color: white;\n text-align: left;\n padding: 12px 15px;\n }\n \n tr:nth-child(even) {\n background-color: var(--light-gray);\n }\n \n td {\n padding: 10px 15px;\n border-bottom: 1px solid var(--medium-gray);\n }\n \n .url-cell {\n max-width: 300px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n \n .url-cell a {\n color: var(--primary-color);\n text-decoration: none;\n }\n \n .url-cell a:hover {\n text-decoration: underline;\n }\n \n .todo-cell {\n max-width: 400px;\n }\n \n .flag {\n display: inline-block;\n padding: 3px 8px;\n border-radius: 4px;\n margin: 2px;\n font-size: 0.8rem;\n background-color: var(--primary-light);\n color: var(--primary-dark);\n }\n \n .pages-table {\n margin-top: 30px;\n }\n \n .pages-table th {\n position: sticky;\n top: 0;\n }\n \n footer {\n margin-top: 60px;\n padding: 30px 0;\n background-color: var(--primary-light);\n color: var(--primary-dark);\n text-align: center;\n }\n \n .footer-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n }\n \n .company-info {\n margin-bottom: 20px;\n }\n \n .company-website {\n color: var(--primary-color);\n text-decoration: none;\n font-weight: 600;\n }\n \n .company-website:hover {\n text-decoration: underline;\n }\n \n .date-generated {\n font-style: italic;\n color: var(--dark-gray);\n }\n \n .progress-bar-container {\n width: 100%;\n background-color: var(--light-gray);\n border-radius: 10px;\n margin: 10px 0;\n overflow: hidden;\n }\n \n .progress-bar {\n height: 10px;\n background-color: var(--secondary-color);\n border-radius: 10px;\n }\n \n .recommendations {\n background-color: var(--secondary-light);\n border-left: 4px solid var(--secondary-color);\n padding: 15px;\n margin: 20px 0;\n border-radius: 0 4px 4px 0;\n }\n \n .recommendations h4 {\n color: var(--secondary-color);\n margin-top: 0;\n }\n \n .recommendations ul {\n margin-left: 20px;\n }\n \n .recommendations li {\n margin-bottom: 8px;\n }\n \n .priority-tag {\n display: inline-block;\n padding: 3px 8px;\n border-radius: 4px;\n margin-left: 8px;\n font-size: 0.8rem;\n font-weight: 600;\n }\n \n .high {\n background-color: rgba(244, 67, 54, 0.1);\n color: var(--danger-color);\n }\n \n .medium {\n background-color: rgba(255, 152, 0, 0.1);\n color: var(--warning-color);\n }\n \n .low {\n background-color: rgba(76, 175, 80, 0.1);\n color: var(--success-color);\n }\n \n .section-empty {\n font-style: italic;\n color: var(--dark-gray);\n padding: 15px;\n background-color: var(--light-gray);\n border-radius: 4px;\n text-align: center;\n }\n \n .source-links {\n margin-top: 10px;\n margin-bottom: 20px;\n padding: 10px;\n background-color: var(--light-gray);\n border-radius: 4px;\n border-left: 3px solid var(--secondary-color);\n }\n \n .source-links h4 {\n margin-top: 0;\n margin-bottom: 10px;\n color: var(--secondary-color);\n font-size: 1rem;\n }\n \n .source-links-table {\n margin: 0;\n box-shadow: none;\n }\n \n .source-links-table th {\n background-color: var(--secondary-color);\n font-size: 0.9rem;\n padding: 8px 10px;\n }\n \n .source-links-table td {\n font-size: 0.9rem;\n padding: 6px 10px;\n }\n \n .no-sources {\n font-style: italic;\n color: var(--dark-gray);\n margin: 5px 0;\n }\n \n .toggle-sources {\n background-color: var(--secondary-light);\n color: var(--secondary-color);\n border: 1px solid var(--secondary-color);\n border-radius: 4px;\n padding: 5px 10px;\n font-size: 0.8rem;\n cursor: pointer;\n margin-top: 5px;\n transition: background-color 0.3s;\n }\n \n .toggle-sources:hover {\n background-color: var(--secondary-color);\n color: white;\n }\n \n .sources-container {\n margin-top: 10px;\n }\n \n .show-more-button {\n background-color: var(--primary-color);\n color: white;\n border: none;\n border-radius: 4px;\n padding: 8px 16px;\n font-size: 0.9rem;\n font-weight: 600;\n cursor: pointer;\n margin: 10px auto;\n display: block;\n transition: all 0.3s ease;\n box-shadow: 0 2px 5px rgba(0,0,0,0.1);\n }\n \n .show-more-button:hover {\n background-color: var(--primary-dark);\n box-shadow: 0 3px 7px rgba(0,0,0,0.2);\n transform: translateY(-2px);\n }\n \n .table-pagination {\n text-align: center;\n margin-top: -20px;\n margin-bottom: 30px;\n }\n \n @media print {\n body {\n font-size: 12pt;\n }\n \n .container {\n width: 100%;\n max-width: none;\n padding: 0;\n }\n \n header {\n padding: 15px 0;\n }\n \n h1 {\n font-size: 20pt;\n }\n \n h2 {\n font-size: 18pt;\n margin-top: 20px;\n }\n \n h3 {\n font-size: 14pt;\n }\n \n .card:hover {\n transform: none;\n }\n \n table {\n page-break-inside: avoid;\n }\n \n tr {\n page-break-inside: avoid;\n }\n \n .no-print {\n display: none;\n }\n \n @page {\n margin: 1.5cm;\n }\n }\n </style>\n <script>\n // JavaScript to toggle source links visibility\n document.addEventListener('DOMContentLoaded', function() {\n document.querySelectorAll('.toggle-sources').forEach(button => {\n button.addEventListener('click', function() {\n const container = this.nextElementSibling;\n if (container.style.display === 'none' || !container.style.display) {\n container.style.display = 'block';\n this.textContent = 'Hide Source Links';\n } else {\n container.style.display = 'none';\n this.textContent = 'Show Source Links';\n }\n });\n });\n });\n \n // JavaScript to toggle table rows visibility\n function toggleRows(button) {\n const table = button.closest('.table-pagination').previousElementSibling;\n const hiddenRows = table.querySelector('.hidden-rows');\n const totalRows = hiddenRows.querySelectorAll('tr').length + table.querySelector('.initial-rows').querySelectorAll('tr').length;\n \n if (hiddenRows.style.display === 'none' || !hiddenRows.style.display) {\n hiddenRows.style.display = 'table-row-group';\n button.textContent = 'Show Less';\n } else {\n hiddenRows.style.display = 'none';\n button.textContent = 'Show All (' + totalRows + ' items)';\n }\n }\n </script>\n</head>\n<body>\n <header>\n <div class=\"container\">\n <div class=\"header-content\">\n <div class=\"logo-container\">\n <img src=\"${companyLogoUrl}\" alt=\"${companyName} Logo\" class=\"logo\">\n <div>\n <h1>Content Audit Report</h1>\n <p>for ${websiteDomain}</p>\n </div>\n </div>\n <div class=\"report-info\">\n <p>Generated on: ${formattedDate}</p>\n <p>By: ${companyName}</p>\n </div>\n </div>\n </div>\n </header>\n\n <main class=\"container\">\n <section id=\"executive-summary\">\n <h2>Executive Summary</h2>\n <p>This report provides a comprehensive analysis of content issues found on <strong>${websiteDomain}</strong>. We've identified ${totalIssues} issues across ${auditData.summary.pages} pages that need attention to improve SEO performance and user experience.</p>\n \n <div class=\"summary-cards\">\n <div class=\"card\">\n <div class=\"card-header\">\n <span class=\"card-title\">Pages Analyzed</span>\n </div>\n <div class=\"card-value\">${auditData.summary.pages}</div>\n </div>\n \n <div class=\"card\">\n <div class=\"card-header\">\n <span class=\"card-title\">Total Issues</span>\n </div>\n <div class=\"card-value\">${totalIssues}</div>\n </div>\n \n <div class=\"card\">\n <div class=\"card-header\">\n <span class=\"card-title\">Health Score</span>\n </div>\n <div class=\"card-value\" style=\"color: ${getHealthScoreColor(calculateHealthScore(auditData.summary.pages, totalIssues))};\">${calculateHealthScore(auditData.summary.pages, totalIssues)}%</div>\n <div class=\"progress-bar-container\">\n <div class=\"progress-bar\" style=\"width: ${calculateHealthScore(auditData.summary.pages, totalIssues)}%\"></div>\n </div>\n </div>\n </div>\n \n <div class=\"recommendations\">\n <h4>Key Recommendations</h4>\n <ul>\n ${getTopRecommendations(auditData).map(rec => `<li>${rec.text} <span class=\"priority-tag ${rec.priority}\">${rec.priority}</span></li>`).join('')}\n </ul>\n </div>\n </section>\n\n <section id=\"issues-breakdown\">\n <h2>Issues Breakdown</h2>\n \n <div class=\"issues-summary\">\n <div class=\"issue-category\">\n <h3>Content Quality</h3>\n <div class=\"issues-list\">\n <div class=\"issue-item\">\n <span class=\"issue-name\">Outdated Content</span>\n <span class=\"issue-count\">${auditData.summary.issues.outdated}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Thin Content</span>\n <span class=\"issue-count\">${auditData.summary.issues.thin}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Readability Issues</span>\n <span class=\"issue-count\">${auditData.summary.issues.readability}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Large HTML</span>\n <span class=\"issue-count\">${auditData.summary.issues.largeHTML}</span>\n </div>\n </div>\n </div>\n \n <div class=\"issue-category\">\n <h3>Technical SEO</h3>\n <div class=\"issues-list\">\n <div class=\"issue-item\">\n <span class=\"issue-name\">404 Errors</span>\n <span class=\"issue-count\">${auditData.summary.issues['404']}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Redirects</span>\n <span class=\"issue-count\">${auditData.summary.issues.redirects}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Canonicalization Issues</span>\n <span class=\"issue-count\">${auditData.summary.issues.canonicalised}</span>\n </div>\n </div>\n </div>\n \n <div class=\"issue-category\">\n <h3>On-Page SEO</h3>\n <div class=\"issues-list\">\n <div class=\"issue-item\">\n <span class=\"issue-name\">Title Length Issues</span>\n <span class=\"issue-count\">${auditData.summary.issues.titleLen}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Description Issues</span>\n <span class=\"issue-count\">${auditData.summary.issues.descriptionLen}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Missing/Duplicate Meta</span>\n <span class=\"issue-count\">${auditData.summary.issues.missingOrDuplicateMeta}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">H1 Issues</span>\n <span class=\"issue-count\">${auditData.summary.issues.h1Issues}</span>\n </div>\n </div>\n </div>\n \n <div class=\"issue-category\">\n <h3>Internal Linking</h3>\n <div class=\"issues-list\">\n <div class=\"issue-item\">\n <span class=\"issue-name\">Excessive Click Depth</span>\n <span class=\"issue-count\">${auditData.summary.issues.excessiveClickDepth}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Orphan Pages</span>\n <span class=\"issue-count\">${auditData.summary.issues.orphan}</span>\n </div>\n <div class=\"issue-item\">\n <span class=\"issue-name\">Low Internal Links</span>\n <span class=\"issue-count\">${auditData.summary.issues.lowInternalLinks}</span>\n </div>\n </div>\n </div>\n \n <div class=\"issue-category\">\n <h3>Performance</h3>\n <div class=\"issues-list\">\n <div class=\"issue-item\">\n <span class=\"issue-name\">Underperforming Pages</span>\n <span class=\"issue-count\">${auditData.summary.issues.underperforming}</span>\n </div>\n </div>\n </div>\n </div>\n </section>\n\n <!-- Status Issues Section -->\n <section id=\"status-issues\">\n <h2>Status Issues</h2>\n \n <h3>404 Errors (${(auditData.issues.statusIssues.pages404 || []).length})</h3>\n ${(auditData.issues.statusIssues.pages404 || []).length === 0 ? \n `<p class=\"section-empty\">No issues found.</p>` : \n (() => {\n const items = auditData.issues.statusIssues.pages404 || [];\n const showInitial = 10; // Number of rows to show initially\n const hasMoreItems = items.length > showInitial;\n const initialItems = hasMoreItems ? items.slice(0, showInitial) : items;\n const hiddenItems = hasMoreItems ? items.slice(showInitial) : [];\n \n return `\n <table class=\"paginated-table\">\n <thead>\n <tr>\n <th>URL</th>\n <th>Source Links</th>\n <th>Recommendation</th>\n </tr>\n </thead>\n <tbody class=\"initial-rows\">\n ${initialItems.map(item => `\n <tr>\n <td class=\"url-cell\"><a href=\"${item.url}\" target=\"_blank\">${item.url}</a></td>\n <td>\n ${item.sources && item.sources.length > 0 ? \n `<button class=\"toggle-sources\">Show Source Links (${item.sources.length})</button>\n <div class=\"sources-container\" style=\"display: none;\">\n ${renderSourceLinks(item.sources)}\n </div>` : \n `<span class=\"no-sources\">No source links found</span>`\n }\n </td>\n <td class=\"todo-cell\">${item.todo}</td>\n </tr>\n `).join('')}\n </tbody>\n ${hasMoreItems ? `\n <tbody class=\"hidden-rows\" style=\"display: none;\">\n ${hiddenItems.map(item => `\n <tr>\n <td class=\"url-cell\"><a href=\"${item.url}\" target=\"_blank\">${item.url}</a></td>\n <td>\n ${item.sources && item.sources.length > 0 ? \n `<button class=\"toggle-sources\">Show Source Links (${item.sources.length})</button>\n <div class=\"sources-container\" style=\"display: none;\">\n ${renderSourceLinks(item.sources)}\n </div>` : \n `<span class=\"no-sources\">No source links found</span>`\n }\n </td>\n <td class=\"todo-cell\">${item.todo}</td>\n </tr>\n `).join('')}\n </tbody>\n ` : ''}\n </table>\n ${hasMoreItems ? `\n <div class=\"table-pagination\">\n <button class=\"show-more-button\" onclick=\"toggleRows(this)\">Show All (${items.length} rows)</button>\n </div>\n ` : ''}\n `;\n })()\n }\n \n <h3>301 Redirects (${(auditData.issues.statusIssues.redirects301 || []).length})</h3>\n ${(auditData.issues.statusIssues.redirects301 || []).length === 0 ? \n `<p class=\"section-empty\">No issues found.</p>` : \n (() => {\n const items = auditData.issues.statusIssues.redirects301 || [];\n const showInitial = 10; // Number of rows to show initially\n const hasMoreItems = items.length > showInitial;\n const initialItems = hasMoreItems ? items.slice(0, showInitial) : items;\n const hiddenItems = hasMoreItems ? items.slice(showInitial) : [];\n \n return `\n <table class=\"paginated-table\">\n <thead>\n <tr>\n <th>URL</th>\n <th>Source Links</th>\n <th>Recommendation</th>\n </tr>\n </thead>\n <tbody class=\"initial-rows\">\n ${initialItems.map(item => `\n <tr>\n <td class=\"url-cell\"><a href=\"${item.url}\" target=\"_blank\">${item.url}</a></td>\n <td>\n ${item.sources && item.sources.length > 0 ? \n `<button class=\"toggle-sources\">Show Source Links (${item.sources.length})</button>\n <div class=\"sources-container\" style=\"display: none;\">\n ${renderSourceLinks(item.sources)}\n </div>` : \n `<span class=\"no-sources\">No source links found</span>`\n }\n </td>\n <td class=\"todo-cell\">${item.todo}</td>\n </tr>\n `).join('')}\n </tbody>\n ${hasMoreItems ? `\n <tbody class=\"hidden-rows\" style=\"display: none;\">\n ${hiddenItems.map(item => `\n <tr>\n <td class=\"url-cell\"><a href=\"${item.url}\" target=\"_blank\">${item.url}</a></td>\n <td>\n ${item.sources && item.sources.length > 0 ? \n `<button class=\"toggle-sources\">Show Source Links (${item.sources.length})</button>\n <div class=\"sources-container\" style=\"display: none;\">\n ${renderSourceLinks(item.sources)}\n </div>` : \n `<span class=\"no-sources\">No source links found</span>`\n }\n </td>\n <td class=\"todo-cell\">${item.todo}</td>\n </tr>\n `).join('')}\n </tbody>\n ` : ''}\n </table>\n ${hasMoreItems ? `\n <div class=\"table-pagination\">\n <button class=\"show-more-button\" onclick=\"toggleRows(this)\">Show All (${items.length} rows)</button>\n </div>\n ` : ''}\n `;\n })()\n }\n \n <h3>Canonicalization Issues (${(auditData.issues.statusIssues.canonicalised || []).length})</h3>\n ${renderTableSection(auditData.issues.statusIssues.canonicalised, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Canonical URL', render: item => item.canonical || '—' },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n </section>\n\n <!-- Content Quality Issues Section -->\n <section id=\"content-quality-issues\">\n <h2>Content Quality Issues</h3>\n \n <h3>Outdated Content (${(auditData.issues.contentQuality.staleLastModified || []).length})</h3>\n ${renderTableSection(auditData.issues.contentQuality.staleLastModified, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Last Modified', render: item => item.lastModified },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Thin Content (${(auditData.issues.contentQuality.thinContent || []).length})</h3>\n ${renderTableSection(auditData.issues.contentQuality.thinContent, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Word Count', render: item => item.words },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Readability Issues (${(auditData.issues.contentQuality.readability || []).length})</h3>\n ${renderTableSection(auditData.issues.contentQuality.readability, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'F-K Score', render: item => item.score.toFixed(1) },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Outdated Meta Years (${(auditData.issues.contentQuality.outdatedMetaYear || []).length})</h3>\n ${renderTableSection(auditData.issues.contentQuality.outdatedMetaYear, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Field', render: item => item.field },\n { header: 'Years', render: item => item.years },\n { header: 'Original Text', render: item => item.original },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Large HTML (${(auditData.issues.contentQuality.largeHTML || []).length})</h3>\n ${renderTableSection(auditData.issues.contentQuality.largeHTML, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Size (bytes)', render: item => item.size ? item.size.toLocaleString() : 'N/A' },\n { header: 'DOM Size (bytes)', render: item => item.totalDom ? item.totalDom.toLocaleString() : 'N/A' },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n </section>\n \n <!-- Metadata & SEO Issues Section -->\n <section id=\"metadata-seo-issues\">\n <h2>Metadata & SEO Issues</h2>\n \n <h3>Title Length Issues (${(auditData.issues.metadataSEO.titleLength || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.titleLength, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Length', render: item => `${item.length} characters` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Description Length Issues (${(auditData.issues.metadataSEO.descriptionLength || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.descriptionLength, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Length', render: item => `${item.length} characters` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Missing Titles (${(auditData.issues.metadataSEO.missingTitle || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.missingTitle, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Missing Descriptions (${(auditData.issues.metadataSEO.missingDescription || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.missingDescription, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Duplicate Titles (${(auditData.issues.metadataSEO.duplicateTitle || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.duplicateTitle, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Title', render: item => item.title },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Duplicate Descriptions (${(auditData.issues.metadataSEO.duplicateDescription || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.duplicateDescription, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Description', render: item => item.description },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>H1 Issues (${(auditData.issues.metadataSEO.h1Issues || []).length})</h3>\n ${renderTableSection(auditData.issues.metadataSEO.h1Issues, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'H1 Count', render: item => item.h1Count },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n </section>\n \n <!-- Internal Linking Issues Section -->\n <section id=\"internal-linking-issues\">\n <h2>Internal Linking Issues</h2>\n \n <h3>Excessive Click Depth (${(auditData.issues.internalLinking.excessiveClickDepth || []).length})</h3>\n ${renderTableSection(auditData.issues.internalLinking.excessiveClickDepth, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Click Depth', render: item => item.depth },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Orphan Pages (${(auditData.issues.internalLinking.orphanPages || []).length})</h3>\n ${renderTableSection(auditData.issues.internalLinking.orphanPages, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n \n <h3>Low Internal Links (${(auditData.issues.internalLinking.lowInternalLinks || []).length})</h3>\n ${renderTableSection(auditData.issues.internalLinking.lowInternalLinks, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Internal Links', render: item => item.links },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n </section>\n \n <!-- Performance Issues Section -->\n <section id=\"performance-issues\">\n <h2>Performance Issues</h2>\n \n <h3>Underperforming Content (${(auditData.issues.underperformingContent || []).length})</h3>\n ${renderTableSection(auditData.issues.underperformingContent, [\n { header: 'URL', class: 'url-cell', render: item => `<a href=\"${item.url}\" target=\"_blank\">${item.url}</a>` },\n { header: 'Clicks', render: item => item.clicks },\n { header: 'Impressions', render: item => item.impressions },\n { header: 'Last Modified', render: item => item.lastModified },\n { header: 'Recommendation', class: 'todo-cell', render: item => item.todo }\n ])}\n </section>\n\n <section id=\"all-pages\">\n <h2>All Pages Overview</h2>\n <p>Below is a summary of all pages analyzed with their respective issues flagged.</p>\n \n ${(() => {\n const items = auditData.pages || [];\n const showInitial = 10; // Number of rows to show initially\n const hasMoreItems = items.length > showInitial;\n const initialItems = hasMoreItems ? items.slice(0, showInitial) : items;\n const hiddenItems = hasMoreItems ? items.slice(showInitial) : [];\n \n return `\n <table class=\"paginated-table pages-table\">\n <thead>\n <tr>\n <th>URL</th>\n <th>Issues</th>\n <th>Clicks</th>\n <th>Impressions</th>\n </tr>\n </thead>\n <tbody class=\"initial-rows\">\n ${initialItems.map(page => `\n <tr>\n <td class=\"url-cell\"><a href=\"${page.url}\" target=\"_blank\">${page.url}</a></td>\n <td>${page.flags.map(flag => `<span class=\"flag\">${formatFlagName(flag)}</span>`).join('')}</td>\n <td>${page.clicks !== null ? page.clicks : 'N/A'}</td>\n <td>${page.impressions !== null ? page.impressions : 'N/A'}</td>\n </tr>\n `).join('')}\n </tbody>\n ${hasMoreItems ? `\n <tbody class=\"hidden-rows\" style=\"display: none;\">\n ${hiddenItems.map(page => `\n <tr>\n <td class=\"url-cell\"><a href=\"${page.url}\" target=\"_blank\">${page.url}</a></td>\n <td>${page.flags.map(flag => `<span class=\"flag\">${formatFlagName(flag)}</span>`).join('')}</td>\n <td>${page.clicks !== null ? page.clicks : 'N/A'}</td>\n <td>${page.impressions !== null ? page.impressions : 'N/A'}</td>\n </tr>\n `).join('')}\n </tbody>\n ` : ''}\n </table>\n ${hasMoreItems ? `\n <div class=\"table-pagination\">\n <button class=\"show-more-button\" onclick=\"toggleRows(this)\">Show All (${items.length} rows)</button>\n </div>\n ` : ''}\n `;\n })()}\n </section>\n \n <section id=\"next-steps\">\n <h2>Recommended Next Steps</h2>\n <p>Based on our analysis, we recommend the following actions to improve your content performance:</p>\n \n <div class=\"recommendations\">\n <h4>Priority Actions</h4>\n <ul>\n ${auditData.summary.issues['404'] > 0 ? \n `<li>Fix 404 errors by restoring pages or implementing proper redirects</li>` : ''}\n ${auditData.summary.issues.redirects > 0 ? \n `<li>Update internal links to point directly to final URLs instead of through redirects</li>` : ''}\n ${auditData.summary.issues.thin > 0 ? \n `<li>Expand thin content pages to at least 1,500 words with valuable, unique information</li>` : ''}\n ${auditData.summary.issues.outdated > 0 ? \n `<li>Update all content that hasn't been refreshed in the last 12 months</li>` : ''}\n ${auditData.summary.issues.missingOrDuplicateMeta > 0 ? \n `<li>Add unique meta descriptions to all pages missing them</li>` : ''}\n ${auditData.summary.issues.titleLen > 0 ? \n `<li>Optimize page titles to be between 40-60 characters</li>` : ''}\n ${auditData.summary.issues.descriptionLen > 0 ? \n `<li>Optimize meta descriptions to be between 70-155 characters</li>` : ''}\n ${auditData.summary.issues.readability > 0 ? \n `<li>Improve content readability by simplifying language and shortening sentences</li>` : ''}\n ${auditData.summary.issues.underperforming > 0 ? \n `<li>Identify keywords with potential for pages with high impressions but low clicks</li>` : ''}\n ${auditData.summary.issues.orphan > 0 ? \n `<li>Create internal links to orphan pages to improve crawlability</li>` : ''}\n ${auditData.summary.issues.lowInternalLinks > 0 ? \n `<li>Improve internal linking between related content</li>` : ''}\n <li>Implement a content calendar to regularly refresh content</li>\n <li>Conduct keyword research to identify new content opportunities</li>\n </ul>\n </div>\n \n <h3>Implementation Timeline</h3>\n <p>We recommend addressing these issues in the following order:</p>\n \n <ol>\n <li><strong>Immediate (1-2 weeks):</strong> Fix technical issues like 404 errors, redirects, missing meta descriptions, and outdated year references.</li>\n <li><strong>Short-term (2-4 weeks):</strong> Update thin content and improve readability on key pages.</li>\n <li><strong>Medium-term (1-2 months):</strong> Refresh outdated content, especially on high-impression pages.</li>\n <li><strong>Long-term (2-3 months):</strong> Implement a content calendar to regularly update content and prevent future staleness.</li>\n </ol>\n </section>\n </main>\n\n <footer>\n <div class=\"container\">\n <div class=\"footer-content\">\n <div class=\"company-info\">\n <p>Report generated by <strong>${companyName}</strong></p>\n <a href=\"${companyWebsite}\" class=\"company-website\" target=\"_blank\">${companyWebsite}</a>\n </div>\n <p class=\"date-generated\">Generated on ${formattedDate}</p>\n </div>\n </div>\n </footer>\n</body>\n</html>`\n}];"
},
"typeVersion": 2
},
{
"id": "b772f856-e1cf-44fd-8fc7-1ac5d8b033ca",
"name": "404 & 301 extrahieren",
"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": "Über Elemente iterieren1",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2100,
500
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "4defc61c-7f05-4b64-9b68-96f097a9ba92",
"name": "URL-Daten zuordnen",
"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": "Quell-URL-Daten abrufen",
"type": "n8n-nodes-base.httpRequest",
"position": [
2320,
500
],
"parameters": {
"url": "https://api.dataforseo.com/v3/on_page/links",
"method": "POST",
"options": {},
"jsonBody": "=[\n {\n \"id\": \"{{ $('Get RAW Audit Data').first().json.tasks[0].id }}\",\n \"page_to\": \"{{ $json.url }}\"\n }\n]",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "cae4d8e7-5a63-417d-a025-3f6631ead225",
"name": "Haftnotiz",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"width": 940,
"height": 580,
"content": "## Content SEO Audit Report\nA workflow powered by DataForSEO and Google Search Analytics API that generate a comprehensive content audit report for any website up to 1000 pages, 100% customized to your brand's colors.\n\n### Set up instructions:\n1. Add a new credential \"Basic Auth\" by following this [guide](https://docs.n8n.io/integrations/builtin/credentials/httprequest/). You can get your DataForSEO API credentials [here](https://app.dataforseo.com/api-access). DataForSEO offer a free $1 credit when you register, which is plenty enough to test the workflow as the cost is about ~$0.20 per 500-page report. Finally, assign your Basic Auth account to the node \"Create Task\", \"Check Task Status\", \"Get Raw Audit Data\" and \"Get Source URLs Data\".\n2. Add a new credential \"Google OAuth2 API\" by following this [guide](https://docs.n8n.io/integrations/builtin/credentials/google/oauth-generic/). Assign your Google OAuth2 account to the node \"Query GSC API\".\n3. Update the \"Set Fields\" node with the following information:\n- dfs_domain: The website domain you want to crawl.\n- company_name: Your company name (Will be displayed on the final report)\n- company_website: Your company website URL (Will be displayed on the final report)\n- company_logo_url: Your company logo URL (Will be displayed on the final report)\n- brand_primary_color: Your primary brand color. (Will be used to customize the final report to your brand's colors)\n- brand_secondary_color: Your secondary brand color. (Will be used to customize the final report to your brand's colors)\n- gsc_property_type: Set to \"domain\" or \"url\" depending of the property type set in your Google Search Console account for the target website (dfs_domain).\n4. Start the workflow. Once done, download the HTML file in the last node \"Download Report\". \n\nVoilà! You have a comprehensive content audit report ready to be sent to your client via email, customized to your own branding.\n\n**Note**: The workflow take approximately 20 minutes to run for ~500 pages. If you want to customize this workflow for your own need, feel free to [contact us](https://customworkflows.ai/work-with-us)."
},
"typeVersion": 1
},
{
"id": "afd6a0aa-813c-4a3f-b844-ac1cf9f854c6",
"name": "Bericht herunterladen",
"type": "n8n-nodes-base.convertToFile",
"position": [
2500,
320
],
"parameters": {
"options": {
"fileName": "={{ $('Set Fields').first().json.dfs_domain }}-content-audit-{{ new Date().toLocaleString('en-US', { month: 'long' }) + '-' + new Date().getFullYear() }}.html"
},
"operation": "toText",
"sourceProperty": "html",
"binaryPropertyName": "=content audit report"
},
"typeVersion": 1.1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "c6db2f12-2e4f-4f40-acf9-6664c9feb45e",
"connections": {
"0a0e696a-29a7-4b34-8299-102c72544153": {
"main": [
[
{
"node": "8f95fd0b-e990-4c85-b21b-83d06d2121fe",
"type": "main",
"index": 0
}
],
[
{
"node": "a31db736-23e0-4db8-ab90-294cd87c9123",
"type": "main",
"index": 0
}
]
]
},
"a31db736-23e0-4db8-ab90-294cd87c9123": {
"main": [
[
{
"node": "3e5e8162-2815-429f-b6e8-6ea6ea70cf18",
"type": "main",
"index": 0
}
]
]
},
"d9943a4b-7320-47ce-95fa-67eb28cabd26": {
"main": [
[
{
"node": "f2f7e975-1db1-4566-b674-396ccaa775f5",
"type": "main",
"index": 0
}
]
]
},
"b5f15675-35c9-42a1-b7eb-bfaf0b467a5a": {
"main": [
[
{
"node": "9ea481fe-8af6-43c2-881d-eb68f63b0424",
"type": "main",
"index": 0
}
]
]
},
"9ea481fe-8af6-43c2-881d-eb68f63b0424": {
"main": [
[
{
"node": "3e5e8162-2815-429f-b6e8-6ea6ea70cf18",
"type": "main",
"index": 0
}
]
]
},
"6cf221d9-c17e-4a5c-9c9a-c3176319df95": {
"main": [
[
{
"node": "fbf18c28-dbd5-410b-87cb-5f5aef44727e",
"type": "main",
"index": 0
}
]
]
},
"4defc61c-7f05-4b64-9b68-96f097a9ba92": {
"main": [
[
{
"node": "2bc70a8c-5c2d-4cb5-be4f-8d051f32ad23",
"type": "main",
"index": 0
}
]
]
},
"aebdd823-9a4d-4323-aadf-b7d92d601d57": {
"main": [
[
{
"node": "d9943a4b-7320-47ce-95fa-67eb28cabd26",
"type": "main",
"index": 0
}
]
]
},
"fbf18c28-dbd5-410b-87cb-5f5aef44727e": {
"main": [
[
{
"node": "4e42e1eb-4769-4e28-9f2f-3fb342baf971",
"type": "main",
"index": 0
}
],
[
{
"node": "aebdd823-9a4d-4323-aadf-b7d92d601d57",
"type": "main",
"index": 0
}
]
]
},
"2bc70a8c-5c2d-4cb5-be4f-8d051f32ad23": {
"main": [
[
{
"node": "0b35fb68-6a0d-4eea-b29a-96550574c2b8",
"type": "main",
"index": 0
}
],
[
{
"node": "bbf44181-0ea7-48b2-b89e-143d72460d27",
"type": "main",
"index": 0
}
]
]
},
"3e5e8162-2815-429f-b6e8-6ea6ea70cf18": {
"main": [
[
{
"node": "0a0e696a-29a7-4b34-8299-102c72544153",
"type": "main",
"index": 0
}
]
]
},
"b772f856-e1cf-44fd-8fc7-1ac5d8b033ca": {
"main": [
[
{
"node": "2bc70a8c-5c2d-4cb5-be4f-8d051f32ad23",
"type": "main",
"index": 0
}
]
]
},
"8f95fd0b-e990-4c85-b21b-83d06d2121fe": {
"main": [
[
{
"node": "6cf221d9-c17e-4a5c-9c9a-c3176319df95",
"type": "main",
"index": 0
}
]
]
},
"f2f7e975-1db1-4566-b674-396ccaa775f5": {
"main": [
[
{
"node": "fbf18c28-dbd5-410b-87cb-5f5aef44727e",
"type": "main",
"index": 0
}
]
]
},
"2227e1c7-890a-4b99-ad20-5b5645ba884b": {
"main": [
[
{
"node": "afd6a0aa-813c-4a3f-b844-ac1cf9f854c6",
"type": "main",
"index": 0
}
]
]
},
"bbf44181-0ea7-48b2-b89e-143d72460d27": {
"main": [
[
{
"node": "4defc61c-7f05-4b64-9b68-96f097a9ba92",
"type": "main",
"index": 0
}
]
]
},
"0b35fb68-6a0d-4eea-b29a-96550574c2b8": {
"main": [
[
{
"node": "2227e1c7-890a-4b99-ad20-5b5645ba884b",
"type": "main",
"index": 0
}
]
]
},
"57a66b27-a253-4543-9d44-cd3afdbc3946": {
"main": [
[
{
"node": "b5f15675-35c9-42a1-b7eb-bfaf0b467a5a",
"type": "main",
"index": 0
}
]
]
},
"4e42e1eb-4769-4e28-9f2f-3fb342baf971": {
"main": [
[
{
"node": "b772f856-e1cf-44fd-8fc7-1ac5d8b033ca",
"type": "main",
"index": 0
}
]
]
}
}
}Wie verwende ich diesen Workflow?
Kopieren Sie den obigen JSON-Code, erstellen Sie einen neuen Workflow in Ihrer n8n-Instanz und wählen Sie "Aus JSON importieren". Fügen Sie die Konfiguration ein und passen Sie die Anmeldedaten nach Bedarf an.
Für welche Szenarien ist dieser Workflow geeignet?
Experte - Künstliche Intelligenz, Marketing
Ist es kostenpflichtig?
Dieser Workflow ist völlig kostenlos. Beachten Sie jedoch, dass Drittanbieterdienste (wie OpenAI API), die im Workflow verwendet werden, möglicherweise kostenpflichtig sind.
Verwandte Workflows
Custom Workflows AI
@customworkflowsaiWe specializes in crafting tailored automation solutions that help businesses streamline their operations and boost productivity. With expertise in creating custom n8n workflows, we transform complex business processes into seamless, automated systems that save time and reduce manual effort.
Diesen Workflow teilen