AutomationFlowsWeb Scraping › Daily SEO Audit Workflow with HTML Reports via Gmail/slack

Daily SEO Audit Workflow with HTML Reports via Gmail/slack

ByMychel Garzon @mychel-garzon on n8n.io

Boost your online visibility and stay ahead of the curve with this Automated SEO Audit Workflow Template. This solution streamlines the crucial task of monitoring your website’s health, proactively identifying issues, and ensuring peak performance without the manual grind. It’s…

Cron / scheduled trigger★★★★☆ complexity7 nodesHTTP RequestGmail
Web Scraping Trigger: Cron / scheduled Nodes: 7 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #6698 — we link there as the canonical source.

This workflow follows the Gmail → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "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
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Boost your online visibility and stay ahead of the curve with this Automated SEO Audit Workflow Template. This solution streamlines the crucial task of monitoring your website’s health, proactively identifying issues, and ensuring peak performance without the manual grind. It’s…

Source: https://n8n.io/workflows/6698/ — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

How it works

HTTP Request, Html Extract, Sqlite +2
Web Scraping

Automatically send beautifully formatted news digests from any RSS feed (e.g., Prothom Alo) directly to your Gmail inbox on a schedule using this n8n workflow. Ideal for news curators, bloggers, media

Gmail, XML, HTTP Request
Web Scraping

As n8n instances scale, teams often lose track of sub-workflows—who uses them, where they are referenced, and whether they can be safely updated. This leads to inefficiencies like unnecessary copies o

HTTP Request, n8n, N8N Trigger +1
Web Scraping

This workflow is an improvement of this workflow by Greg Brzezinka.

HTTP Request, Email Send, XML +1
Web Scraping

N8N-Workflow-Github-Manager. Uses github, httpRequest, n8n. Scheduled trigger; 38 nodes.

GitHub, HTTP Request, n8n