← Back to Cookbook

SEO Audit

Scrapes a page and produces an SEO audit with scores, issues, and fixes.

firecrawlai

Source

/**
 * Audits a webpage for SEO issues using scraping and AI analysis.
 */

form seo_audit {
  label: "SEO Audit"
  schema: @json {
    {
      "type": "object",
      "required": ["url"],
      "properties": {
        "url": { "type": "string", "format": "uri", "title": "Page URL" },
        "target_keyword": { "type": "string", "title": "Target Keyword" },
        "competitors": { "type": "string", "title": "Competitor URLs (comma-separated)" }
      }
    }
  }
}

graph run_seo_audit {
  label: "Run SEO Audit"

  root {
    type: code
    label: "Extract request"
    code: @ts { return context.nodes.root.input }
    outputSchema: @json {
      {
        "type": "object",
        "properties": {
          "url": { "type": "string", "format": "uri" },
          "target_keyword": { "type": "string" },
          "competitors": { "type": "string" }
        }
      }
    }
  }

  node scrape_page {
    type: firecrawl
    label: "Scrape target page"
    url: @ts { return context.nodes.root.output.url }
    onlyMainContent: false
    formats: ["markdown", "html"]
  }

  node analyze_seo {
    type: ai
    label: "Analyze SEO"
    kind: object
    model: "google/gemini-2.5-flash"
    prompt: @ts {
      const req = context.nodes.root.output
      const page = context.nodes.scrape_page.output
      return "Perform an SEO audit of this page.\n\nURL: " + req.url + "\nTarget keyword: " + (req.target_keyword || "not specified") + "\n\nPage content:\n" + JSON.stringify(page)
    }
    schema: @json {
      {
        "type": "object",
        "required": ["score", "issues", "recommendations"],
        "properties": {
          "score": { "type": "number", "minimum": 0, "maximum": 100 },
          "issues": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "category": { "type": "string" },
                "severity": { "type": "string", "enum": ["critical", "warning", "info"] },
                "description": { "type": "string" },
                "fix": { "type": "string" }
              }
            }
          },
          "recommendations": { "type": "array", "items": { "type": "string" } },
          "keyword_density": { "type": "number" },
          "has_meta_description": { "type": "boolean" },
          "has_h1": { "type": "boolean" },
          "word_count": { "type": "number" }
        }
      }
    }
  }

  node format_report {
    type: code
    label: "Format report"
    code: @ts {
      const audit = context.nodes.analyze_seo.output
      const critical = audit.issues.filter(function(i) { return i.severity === "critical" })
      return {
        url: context.nodes.root.output.url,
        score: audit.score,
        critical_issues: critical.length,
        total_issues: audit.issues.length,
        issues: audit.issues,
        recommendations: audit.recommendations,
        word_count: audit.word_count
      }
    }
  }

  flow {
    root -> scrape_page
    scrape_page -> analyze_seo
    analyze_seo -> format_report
  }
}

trigger on_audit {
  form:seo_audit -> run_seo_audit
  enabled: true
}

Flow

Trigger → graph

Graph nodes