{
  "id": "ak2jPqnpYMKMo8Bp",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Complete SEO Audit Workflow with Automated HTML Summaries",
  "tags": [],
  "nodes": [
    {
      "id": "2fa7bca3-02c4-4ded-b4ea-b8dca5314257",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        144,
        -560
      ],
      "parameters": {
        "width": 796,
        "height": 644,
        "content": "## Complete SEO Audit Workflow with Automated HTML Reports via Gmail or Slack\n\n### Purpose  \nRun daily, in-depth audits of specified pages, checking everything from meta tags and heading structure to Core Web Vitals, accessibility, structured data, and security headers\u2014so you catch issues before they impact your rankings.\n\n### Benefits  \n- **Proactive Monitoring:** Identify on-page and technical SEO errors as they arise.  \n- **Time Savings:** Automate repetitive checks and free your team to focus on strategy.  \n- **Actionable Insights:** \u201cTop 3 Priorities\u201d and categorized issues speed up remediation.\n\n### Output  \n\u2022 A formatted HTML report delivered via Gmail (or Slack)  \n\u2022 Clear, actionable \u201cTop 3 Priorities,\u201d categorized On-Page vs. Technical issues, and next-steps links\n\n### Requirements & Customization  \n- **Gmail/Slack Credentials:** For report delivery.  \n- **Easy Customization:**  \n  - Edit URLs & recipients in the \u201cSet Variables\u201d node.  \n  - Tweak audit frequency in the Schedule Trigger.  \n  - Enhance the Code node with extra checks or third-party integrations.  \n"
      },
      "typeVersion": 1
    },
    {
      "id": "b64d81da-cc22-4fdd-9146-b76a29721047",
      "name": "Daily Audit Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        128,
        144
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "b12cf011-73cc-4890-bf15-1292201d3f60",
      "name": "Configure Target & Recipients",
      "type": "n8n-nodes-base.set",
      "position": [
        320,
        144
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "44337ce3-4e65-4d7b-9b1a-cf77f8412169",
              "name": "siteUrl",
              "type": "string",
              "value": "https://example.com/"
            },
            {
              "id": "60e53201-5d1b-4665-9f13-aad194cd653c",
              "name": "emailFrom",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "dc19de25-3f64-4244-9a3e-65d5178181c5",
              "name": "emailTo",
              "type": "string",
              "value": "user@example.com"
            }
          ]
        },
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "d84cbda7-a9e0-4c40-86da-e8914cf99e8f",
      "name": "HTTP Fetch Page Content",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        528,
        144
      ],
      "parameters": {
        "url": "={{$node[\"Configure Target & Recipients\"].json.siteUrl}}\n",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "f25b7ba0-54e9-4e5e-85f8-936f9ffd3930",
      "name": "Parse On\u2011Page Elements",
      "type": "n8n-nodes-base.html",
      "position": [
        736,
        144
      ],
      "parameters": {
        "options": {},
        "operation": "extractHtmlContent",
        "dataPropertyName": "=data",
        "extractionValues": {
          "values": [
            {
              "key": "title",
              "cssSelector": "title"
            },
            {
              "key": "metaDesc",
              "attribute": "content",
              "cssSelector": "meta[name=\"description\"]",
              "returnValue": "attribute"
            },
            {
              "key": "h1Tags",
              "cssSelector": "h1",
              "returnArray": true
            },
            {
              "key": "h2Tags",
              "cssSelector": "h2",
              "returnArray": true
            },
            {
              "key": "imgAlts",
              "attribute": "alt",
              "cssSelector": "img",
              "returnArray": true,
              "returnValue": "attribute"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "f097edcb-fb09-4eb7-96dc-398f8e3dbecd",
      "name": "Run SEO Audit Logic",
      "type": "n8n-nodes-base.code",
      "position": [
        944,
        144
      ],
      "parameters": {
        "jsCode": "// Collect results for each URL\nconst output = [];\n\nfor (const item of items) {\n  const d = item.json;\n  const issues = [];\n  const suggestions = {};\n\n  function addIssue(code, message, suggestion) {\n    issues.push({ code, message });\n    suggestions[code] = suggestion;\n  }\n\n  // 1. Title length\n  if (!d.title) {\n    addIssue('missing_title', 'Missing <title>', 'Add a descriptive <title> (30\u201360 chars).');\n  } else if (d.title.length < 30) {\n    addIssue('short_title', `Title too short (${d.title.length} chars)`, 'Increase title length to \u226530 chars.');\n  } else if (d.title.length > 60) {\n    addIssue('long_title', `Title too long (${d.title.length} chars)`, 'Reduce title to \u226460 chars.');\n  }\n\n  // 2. Meta description\n  if (!d.metaDesc) {\n    addIssue('missing_meta_desc', 'Missing meta description', 'Add <meta name=\"description\"> (50\u2013160 chars).');\n  } else if (d.metaDesc.length < 50) {\n    addIssue('short_meta_desc', `Meta description too short (${d.metaDesc.length} chars)`, 'Increase description to \u226550 chars.');\n  } else if (d.metaDesc.length > 160) {\n    addIssue('long_meta_desc', `Meta description too long (${d.metaDesc.length} chars)`, 'Shorten description to \u2264160 chars.');\n  }\n\n  // 3. Robots, viewport, canonical, lang\n  if (!d.metaRobots) {\n    addIssue('missing_robots', 'Missing meta robots tag', 'Add <meta name=\"robots\" content=\"index,follow\">.');\n  }\n  if (!d.metaViewport || !d.metaViewport.includes('width=device-width')) {\n    addIssue('viewport', 'Missing/incorrect viewport tag', 'Add <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">.');\n  }\n  if (!d.linkCanonical) {\n    addIssue('canonical', 'Missing canonical', 'Add <link rel=\"canonical\" href=\"YOUR_URL\">.');\n  }\n  if (!d.htmlLang) {\n    addIssue('lang', 'Missing lang attribute', 'Add <html lang=\"en\">.');\n  }\n\n  // 4. Word count\n  if (typeof d.wordCount === 'number' && d.wordCount < 300) {\n    addIssue('low_word_count', `Low word count (${d.wordCount} words)`, 'Aim for \u2265300 words.');\n  }\n\n  // 5. Headings\n  const h1 = (d.h1Tags || []).length;\n  const h2 = (d.h2Tags || []).length;\n  const h3 = (d.h3Tags || []).length;\n  if (h1 !== 1) {\n    addIssue('h1', `${h1} <h1> tags`, 'Use exactly one <h1>.');\n  }\n  if (h2 === 0) {\n    addIssue('h2', 'No <h2> tags', 'Add \u22651 <h2> for sections.');\n  }\n  if (h3 === 0) {\n    addIssue('h3', 'No <h3> tags', 'Use <h3> for subsections.');\n  }\n\n  // 6. Images alt text\n  if (Array.isArray(d.imgAlts)) {\n    const missing = d.imgAlts.filter(a => !a || !a.trim()).length;\n    if (missing) {\n      addIssue('img_alt', `${missing} images missing alt`, 'Provide descriptive alt text.');\n    }\n  }\n\n  // 7. Broken links\n  if (Array.isArray(d.links)) {\n    const broken = d.links.filter(l => l.status >= 400).length;\n    if (broken) {\n      addIssue('broken_links', `${broken} broken links`, 'Fix or remove broken URLs.');\n    }\n  }\n\n  // 8. Structured data\n  if (!d.structuredData || d.structuredData.length === 0) {\n    addIssue('schema', 'No structured data', 'Implement JSON-LD schema markup.');\n  }\n\n  // 9. Core Web Vitals\n  if (d.metrics) {\n    const { LCP, INP, CLS } = d.metrics;\n    if (LCP > 2500) {\n      addIssue('lcp', `LCP too slow (${LCP} ms)`, 'Optimize images and server response.');\n    }\n    if (INP > 200) {\n      addIssue('inp', `INP high (${INP} ms)`, 'Minimize JavaScript execution.');\n    }\n    if (CLS > 0.1) {\n      addIssue('cls', `CLS unstable (${CLS})`, 'Set image dimensions and pre-load fonts.');\n    }\n  }\n\n  // Summary\n  const pass = issues.length === 0;\n  const summary = pass ? 'All checks passed! \ud83c\udf89' : `${issues.length} issue${issues.length > 1 ? 's' : ''} detected`;\n\n  // Markdown report\n  let markdown = `## SEO Audit for ${d.siteUrl || d.url}\\n`;\n  markdown += `**Status:** ${pass ? 'PASS \u2705' : 'FAIL \u274c'}\\n`;\n  markdown += `**Summary:** ${summary}\\n`;\n  if (!pass) {\n    markdown += `\\n### Issues & Suggestions\\n`;\n    issues.forEach(({ code, message }) => {\n      markdown += `- **${message}**  \\n  _Suggestion:_ ${suggestions[code]}\\n`;\n    });\n  }\n\n  output.push({\n    json: {\n      siteUrl: d.siteUrl || d.url,\n      pass,\n      summary,\n      issues,\n      suggestions,\n      markdownReport: markdown,\n    },\n  });\n}\n\nreturn output;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "81cd1593-0669-4f68-bc12-cac12e30f327",
      "name": "Email Audit Report",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1168,
        144
      ],
      "parameters": {
        "sendTo": "={{$node[\"Configure Target & Recipients\"].json.emailTo}}\n\n",
        "message": "=<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\" />\n  <style>\n    body { font-family: Arial, sans-serif; color: #333; line-height: 1.4; padding: 20px; }\n    .header { display: flex; align-items: center; margin-bottom: 20px; }\n    .logo { height: 40px; margin-right: 15px; }\n    .title { font-size: 24px; color: #2a6ebb; margin: 0; }\n    .meta { margin: 5px 0 20px; }\n    .status-pass { color: green; font-weight: bold; }\n    .status-fail { color: red; font-weight: bold; }\n    .summary-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }\n    .summary-table td { padding: 8px; border: 1px solid #eee; }\n    .issues-container { display: flex; gap: 20px; }\n    .column { flex: 1; }\n    .column h4 { border-bottom: 1px solid #ddd; padding-bottom: 5px; }\n    .issues-list { list-style: none; padding: 0; margin: 0; }\n    .issues-list li { margin: 8px 0; }\n    .suggestion { display: block; margin-left: 15px; font-style: italic; color: #555; }\n    .priorities { background: #f9f9f9; padding: 15px; border-left: 4px solid #2a6ebb; margin-bottom: 20px; }\n    .next-steps { margin-top: 30px; font-size: 14px; }\n    .next-steps a { color: #2a6ebb; text-decoration: none; }\n    code { background: #eef; padding: 2px 4px; border-radius: 3px; }\n  </style>\n</head>\n<body>\n\n  <div class=\"header\">\n    <div>\n      <h1 class=\"title\">SEO Audit Report</h1>\n      <div class=\"meta\">Date: {{ new Date().toLocaleDateString() }}</div>\n    </div>\n  </div>\n\n  <p>\n    <strong>Page:</strong> {{ $node[\"Run SEO Audit Logic\"].json.siteUrl }}<br/>\n    <strong>Status:</strong>\n    <span class=\"{{ $node[\"Run SEO Audit Logic\"].json.pass ? 'status-pass' : 'status-fail' }}\">\n      {{ $node[\"Run SEO Audit Logic\"].json.pass ? 'PASS \u2705' : 'FAIL \u274c' }}\n    </span>\n  </p>\n\n  <div class=\"priorities\">\n    <strong>\ud83d\udea9 Top 3 Priorities:</strong>\n    <ol>\n      {{\n        $node[\"Run SEO Audit Logic\"].json.issues\n          .slice(0, 3)\n          .map(i => `<li>${i.message.replace(/</g,'&lt;').replace(/>/g,'&gt;')}</li>`)\n          .join('')\n      }}\n    </ol>\n  </div>\n\n  <table class=\"summary-table\">\n    <tr>\n      <td><strong>Total Issues</strong></td>\n      <td>{{ $node[\"Run SEO Audit Logic\"].json.issues.length }}</td>\n    </tr>\n    <tr>\n      <td><strong>Broken Links</strong></td>\n      <td>\n        {{\n          $node[\"Run SEO Audit Logic\"].json.issues.filter(i => i.code === 'broken_links').length\n        }}\n      </td>\n    </tr>\n    <tr>\n      <td><strong>CWV Alerts</strong></td>\n      <td>\n        {{\n          $node[\"Run SEO Audit Logic\"].json.issues\n            .filter(i => ['lcp','inp','cls'].includes(i.code))\n            .length\n        }}\n      </td>\n    </tr>\n  </table>\n\n  <div class=\"issues-container\">\n    <!-- On\u2011Page Issues -->\n    <div class=\"column\">\n      <h4>\ud83d\udcdd On\u2011Page Issues</h4>\n      <ul class=\"issues-list\">\n        {{\n          $node[\"Run SEO Audit Logic\"].json.issues\n            .filter(i => [\n              'missing_title','short_title','long_title',\n              'missing_meta_desc','short_meta_desc','long_meta_desc',\n              'h1','h2','h3','img_alt','schema'\n            ].includes(i.code))\n            .map(i => {\n              const msg = i.message.replace(/</g,'&lt;').replace(/>/g,'&gt;');\n              const sug = $node[\"Run SEO Audit Logic\"].json.suggestions[i.code]\n                            .replace(/</g,'&lt;').replace(/>/g,'&gt;');\n              return `\n                <li>\n                  \u26a0\ufe0f <strong>${msg}</strong>\n                  <span class=\"suggestion\">\n                    Suggestion: <code>${sug}</code>\n                  </span>\n                </li>`;\n            })\n            .join('')\n        }}\n      </ul>\n    </div>\n\n    <!-- Technical Issues -->\n    <div class=\"column\">\n      <h4>\ud83d\udd27 Technical Issues</h4>\n      <ul class=\"issues-list\">\n        {{\n          $node[\"Run SEO Audit Logic\"].json.issues\n            .filter(i => ![\n              'missing_title','short_title','long_title',\n              'missing_meta_desc','short_meta_desc','long_meta_desc',\n              'h1','h2','h3','img_alt','schema'\n            ].includes(i.code))\n            .map(i => {\n              const msg = i.message.replace(/</g,'&lt;').replace(/>/g,'&gt;');\n              const sug = $node[\"Run SEO Audit Logic\"].json.suggestions[i.code]\n                            .replace(/</g,'&lt;').replace(/>/g,'&gt;');\n              return `\n                <li>\n                  \u2699\ufe0f <strong>${msg}</strong>\n                  <span class=\"suggestion\">\n                    Suggestion: <code>${sug}</code>\n                  </span>\n                </li>`;\n            })\n            .join('')\n        }}\n      </ul>\n    </div>\n  </div>\n\n  <div class=\"next-steps\">\n    <p><strong>Next Steps:</strong></p>\n    <ol>\n      <li>Review the above suggestions and assign tickets in your tracker.</li>\n      <li>Re\u2011run this audit after fixes are deployed to confirm all PASS.</li>\n    </ol>\n  </div>\n\n</body>\n</html>\n",
        "options": {},
        "subject": "={{$node[\"Configure Target & Recipients\"].json.siteUrl}}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "bc8a99b9-d397-4881-a02e-6aab78b364dc",
  "connections": {
    "Daily Audit Trigger": {
      "main": [
        [
          {
            "node": "Configure Target & Recipients",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run SEO Audit Logic": {
      "main": [
        [
          {
            "node": "Email Audit Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Fetch Page Content": {
      "main": [
        [
          {
            "node": "Parse On\u2011Page Elements",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse On\u2011Page Elements": {
      "main": [
        [
          {
            "node": "Run SEO Audit Logic",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure Target & Recipients": {
      "main": [
        [
          {
            "node": "HTTP Fetch Page Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}