<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aaron VanSledright</title>
    <description>The latest articles on DEV Community by Aaron VanSledright (@avansledright).</description>
    <link>https://dev.to/avansledright</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3817240%2Fb7f5c68f-432d-4d27-91d4-6c7a7b572314.jpg</url>
      <title>DEV Community: Aaron VanSledright</title>
      <link>https://dev.to/avansledright</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/avansledright"/>
    <language>en</language>
    <item>
      <title>Catching SEO Regressions in Vercel Preview Deploys</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 07 Sep 2026 14:00:05 +0000</pubDate>
      <link>https://dev.to/avansledright/catching-seo-regressions-in-vercel-preview-deploys-5enk</link>
      <guid>https://dev.to/avansledright/catching-seo-regressions-in-vercel-preview-deploys-5enk</guid>
      <description>&lt;p&gt;Vercel preview deploys are the most underused SEO QA surface in modern frontend teams. Every PR gets a unique, real URL that loads in a real browser. That's exactly the environment an SEO audit needs — and it's a few minutes of YAML away from being a hard PR check.&lt;/p&gt;

&lt;p&gt;This post is the production setup for catching SEO regressions on Vercel previews using SEO Score API. It's a sibling of &lt;a href="https://dev.to/blog/seo-analyzer-api-nextjs-build"&gt;Wiring an SEO analyzer API into a Next.js build&lt;/a&gt; but focused on the Vercel-specific gotchas.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the workflow
&lt;/h2&gt;

&lt;p&gt;Three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Wait for the Vercel preview to be live (it takes 30s–3min after PR push).&lt;/li&gt;
&lt;li&gt;Audit the preview URL with SEO Score API.&lt;/li&gt;
&lt;li&gt;Fail the PR check if the score is below the threshold.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first step is where most teams get it wrong on the first try. The Vercel preview URL is available in the PR comment within seconds, but the &lt;em&gt;deploy&lt;/em&gt; isn't ready for several minutes. Audit too early and you'll audit a 404 or an in-progress build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.github/workflows/seo-preview.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Preview Check&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;seo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for Vercel preview to be ready&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;patrickedqvist/wait-for-vercel-preview@v1.3.1&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;preview&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;max_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;600&lt;/span&gt;  &lt;span class="c1"&gt;# 10 min — Next.js builds can be slow&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Audit preview&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;audit&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEO_KEY }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;URL="${{ steps.preview.outputs.url }}"&lt;/span&gt;
          &lt;span class="s"&gt;RESP=$(curl -s -H "X-API-Key: $KEY" \&lt;/span&gt;
            &lt;span class="s"&gt;"https://seoscoreapi.com/audit?url=$URL")&lt;/span&gt;
          &lt;span class="s"&gt;SCORE=$(echo "$RESP" | jq '.score // 0')&lt;/span&gt;
          &lt;span class="s"&gt;echo "score=$SCORE" &amp;gt;&amp;gt; $GITHUB_OUTPUT&lt;/span&gt;
          &lt;span class="s"&gt;echo "## SEO Score: $SCORE" &amp;gt;&amp;gt; $GITHUB_STEP_SUMMARY&lt;/span&gt;
          &lt;span class="s"&gt;echo "$RESP" | jq -r '.priorities[] | "- \(.issue)"' &amp;gt;&amp;gt; $GITHUB_STEP_SUMMARY&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Threshold check&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;[ "${{ steps.audit.outputs.score }}" -ge 85 ] || exit 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;wait-for-vercel-preview&lt;/code&gt; action listens for the Vercel deployment status webhook on the PR. It returns the URL only once the preview is actually serving traffic — that's the part you can't skip.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned the hard way
&lt;/h2&gt;

&lt;p&gt;A few things that bit us in production on the first version:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Don't audit immediately.&lt;/strong&gt; Even after Vercel reports the preview is live, the CDN propagation takes another 5–10 seconds for global routes. Insert a &lt;code&gt;sleep 15&lt;/code&gt; after &lt;code&gt;wait-for-vercel-preview&lt;/code&gt; if you see intermittent 502s from the audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Branch URLs change on rebase.&lt;/strong&gt; The Vercel preview URL incorporates the branch name and commit SHA. After a force-push, the URL changes. &lt;code&gt;wait-for-vercel-preview&lt;/code&gt; handles this if you call it fresh in every workflow run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Auth-gated previews are invisible to us.&lt;/strong&gt; If your Vercel preview deploys are behind Vercel Authentication (paid plans default to on), SEO Score API sees the auth wall, not your app. Turn off Vercel Authentication for previews, or use Vercel's &lt;code&gt;protection-bypass-for-automation&lt;/code&gt; token and pass it as a header — we accept a &lt;code&gt;?bypass_token&lt;/code&gt; query parameter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The first audit cold-starts the page.&lt;/strong&gt; Next.js with on-demand rendering means the first request to a route compiles it. The audit will include that cold-start time. If your performance score is consistently 8–10 points lower than production, this is why. Run a warmup &lt;code&gt;curl $URL&lt;/code&gt; step before the audit step and the numbers settle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Set the threshold against &lt;code&gt;main&lt;/code&gt;, not against the score you wish you had.&lt;/strong&gt; A gate that fails every PR teaches developers to bypass it. See &lt;a href="https://dev.to/blog/pre-deploy-seo-gates"&gt;Pre-Deploy SEO Gates&lt;/a&gt; for how to pick the right threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-page auditing
&lt;/h2&gt;

&lt;p&gt;The example above audits one URL. Most teams want to audit a small list of routes — the ones that matter for SEO, like &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;/pricing&lt;/code&gt;, &lt;code&gt;/signup&lt;/code&gt;, and the top 3–5 product pages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;URLS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/pricing"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/signup"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/docs"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;FAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;u &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;URLS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;S&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://seoscoreapi.com/audit?url=&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="s1"&gt;'.score // 0'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  &lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt; → &lt;/span&gt;&lt;span class="nv"&gt;$S&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$S&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 85 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;FAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="nv"&gt;$FAIL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four URLs × ~10 PRs/day × 22 working days = ~880 audits/month. That fits in the Basic plan ($15/mo, 1,000 audits) with margin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Posting the report back to the PR
&lt;/h2&gt;

&lt;p&gt;GitHub's &lt;code&gt;STEP_SUMMARY&lt;/code&gt; works, but a sticky PR comment is better — same content, doesn't get buried. The &lt;code&gt;marocchino/sticky-pull-request-comment&lt;/code&gt; action handles the dedup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;marocchino/sticky-pull-request-comment@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seo-score&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;### SEO Preview Score: ${{ steps.audit.outputs.score }}&lt;/span&gt;
      &lt;span class="s"&gt;Top issues:&lt;/span&gt;
      &lt;span class="s"&gt;${{ steps.audit.outputs.priorities }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comment updates in place on every push to the PR, so the latest score is always at the top of the conversation thread. Developers actually read those.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to skip the gate
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;seo-skip&lt;/code&gt; PR label that makes the workflow exit 0 is a one-line addition and it dramatically improves adoption. There are legitimate cases — intentional title changes, A/B test pages, copy experiments — where the gate should be silenced for one PR. Make that explicit and the rest of the team will stop disabling the workflow file when they're under deadline pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this gives you
&lt;/h2&gt;

&lt;p&gt;Three months in, the win isn't the headline number going up — it's the &lt;em&gt;absence of regressions&lt;/em&gt;. The gate catches the accidentally-deleted &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; tag, the broken canonical, the meta description someone overwrote in a CMS migration. Those don't show up in the metrics dashboard until traffic drops weeks later. The gate catches them before the PR merges.&lt;/p&gt;

&lt;p&gt;That's the whole case for the Vercel preview audit.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Wiring an SEO Analyzer API into a Next.js Build</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 31 Aug 2026 14:00:04 +0000</pubDate>
      <link>https://dev.to/avansledright/wiring-an-seo-analyzer-api-into-a-nextjs-build-1cjf</link>
      <guid>https://dev.to/avansledright/wiring-an-seo-analyzer-api-into-a-nextjs-build-1cjf</guid>
      <description>&lt;p&gt;The most useful place to run an &lt;a href="https://dev.to/seo-audit-api"&gt;SEO analyzer API&lt;/a&gt; against a Next.js app isn't production — it's the preview deploy. By the time a regression hits production, you've already shipped it. By the time you find it in production, three more PRs have landed on top.&lt;/p&gt;

&lt;p&gt;This post is the end-to-end setup for running SEO Score API against every Vercel preview deploy of a Next.js app, with a GitHub Action that fails the PR check if the score drops below a threshold. ~30 lines of YAML and one secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why preview URLs and not production
&lt;/h2&gt;

&lt;p&gt;Three reasons preview-deploy auditing beats production-deploy auditing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You can block bad changes.&lt;/strong&gt; A failed check on a preview is a PR you don't merge. A failed check on production is an incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each PR is a clean diff.&lt;/strong&gt; The score on &lt;code&gt;main&lt;/code&gt; is a moving target; the score delta between &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;feature/new-pricing&lt;/code&gt; is a number you can argue about in a PR review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JS-rendered pages need a real URL.&lt;/strong&gt; SEO Score API renders pages in headless Chrome, which means the preview deploy needs to actually be live. Vercel previews are. Local builds usually aren't.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Vercel side
&lt;/h2&gt;

&lt;p&gt;Vercel comments the preview URL on every PR. The format is stable enough to parse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This pull request is being deployed to Vercel.
✅ Preview: https://your-app-git-feature-branch-team.vercel.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're going to grab that URL via the &lt;a href="https://vercel.com/docs/rest-api" rel="noopener noreferrer"&gt;Vercel API&lt;/a&gt; instead of parsing the comment — same data, more reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The GitHub Actions workflow
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;.github/workflows/seo-check.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Check&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;opened&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;synchronize&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;seo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for Vercel preview&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wait&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;patrickedqvist/wait-for-vercel-preview@v1.3.1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;max_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Audit preview URL&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEO_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.wait.outputs.url }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;RESP=$(curl -s -H "X-API-Key: $KEY" \&lt;/span&gt;
            &lt;span class="s"&gt;"https://seoscoreapi.com/audit?url=$URL")&lt;/span&gt;
          &lt;span class="s"&gt;SCORE=$(echo "$RESP" | jq '.score')&lt;/span&gt;
          &lt;span class="s"&gt;GRADE=$(echo "$RESP" | jq -r '.grade')&lt;/span&gt;
          &lt;span class="s"&gt;echo "Score: $SCORE ($GRADE) on $URL"&lt;/span&gt;
          &lt;span class="s"&gt;if [ "$SCORE" -lt 85 ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "::error::SEO score $SCORE is below threshold (85)"&lt;/span&gt;
            &lt;span class="s"&gt;echo "$RESP" | jq '.priorities'&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The action waits for the Vercel preview to be live (up to 5 minutes), audits it, and fails the check if the score is below 85. The failing case dumps the &lt;code&gt;priorities&lt;/code&gt; array into the log so the PR author can see what to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting the threshold
&lt;/h2&gt;

&lt;p&gt;85 is the default we recommend, but it depends on your baseline. The right way to pick a threshold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the audit against &lt;code&gt;main&lt;/code&gt; to get your current baseline.&lt;/li&gt;
&lt;li&gt;Set the threshold 2 points below that baseline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your &lt;code&gt;main&lt;/code&gt; scores 92, threshold at 90. That way the gate fails on real regressions but tolerates the ±1 noise from network conditions and external API variance.&lt;/p&gt;

&lt;p&gt;For sites that haven't been optimized yet (&lt;code&gt;main&lt;/code&gt; scores 65), threshold at 65. Hold the line, then ratchet up as you improve. The &lt;a href="https://dev.to/blog/pre-deploy-seo-gates"&gt;pre-deploy SEO gates&lt;/a&gt; post has more on threshold strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auditing more than the homepage
&lt;/h2&gt;

&lt;p&gt;The example above audits one URL — the preview root. Most apps have at least pricing, signup, and a handful of feature pages that matter just as much. The fix is a small bash loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;URLS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/pricing"&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/signup"&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;/docs"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;FAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;for &lt;/span&gt;u &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;URLS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;S&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"https://seoscoreapi.com/audit?url=&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="s1"&gt;'.score'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  &lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="s2"&gt; → &lt;/span&gt;&lt;span class="nv"&gt;$S&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$S&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 85 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;FAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="nv"&gt;$FAIL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That'll burn one audit per URL per PR. On the Basic plan ($15/mo, 1,000 audits), that comfortably covers a team shipping ~10 PRs/day audited against 3 URLs each.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this catches that Lighthouse doesn't
&lt;/h2&gt;

&lt;p&gt;Lighthouse runs in your CI by default if you use Vercel — but Lighthouse is Web Vitals first, SEO second. SEO Score API runs 83 checks specifically about SEO: title structure, meta description presence, canonical tags, schema validity, OpenGraph completeness, internal linking, AI-readability signals, robot directives.&lt;/p&gt;

&lt;p&gt;You can run both. They overlap maybe 10% — there's no good reason not to gate on both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do when a developer's PR fails the gate
&lt;/h2&gt;

&lt;p&gt;Two things help adoption:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Print the &lt;code&gt;priorities&lt;/code&gt; array in the failure log.&lt;/strong&gt; The default response includes a sorted list of top remediation suggestions. Show them in the GitHub check output and you've turned the gate from "no, your PR is bad" into "yes, and here's what to fix."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow override via PR label.&lt;/strong&gt; Add a &lt;code&gt;seo-skip&lt;/code&gt; label that makes the workflow exit 0. There are legitimate cases (intentional title changes, A/B test pages) where the gate should be silenced. Don't make it impossible — just make it explicit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both of those keep the gate from becoming the kind of CI check that everyone learns to ignore.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Automate SEO Audits with n8n and SEO Score API</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 24 Aug 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/avansledright/how-to-automate-seo-audits-with-n8n-and-seo-score-api-1j9k</link>
      <guid>https://dev.to/avansledright/how-to-automate-seo-audits-with-n8n-and-seo-score-api-1j9k</guid>
      <description>&lt;p&gt;n8n is an open-source workflow automation platform — think Zapier but self-hosted and developer-friendly. Combined with the SEO Score API, you can build powerful SEO automation workflows without writing backend code. Schedule audits, route results to Slack or Google Sheets, and trigger alerts entirely through a visual editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is n8n and how does it relate to SEO automation?
&lt;/h2&gt;

&lt;p&gt;n8n is a self-hostable, node-based workflow automation tool that connects APIs, databases, and services without custom glue code. For SEO automation, it means you can schedule audits, route results to Slack or Google Sheets, and trigger alerts — all through a visual editor, with no backend server to maintain. Unlike hosted tools like Zapier, n8n runs on your own infrastructure, which means no per-task pricing and no data leaving your environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does the SEO Score API n8n node work?
&lt;/h2&gt;

&lt;p&gt;The SEO Score API n8n node is a purpose-built community node that wraps the &lt;code&gt;/audit&lt;/code&gt; and &lt;code&gt;/audit/batch&lt;/code&gt; endpoints. You configure your API key once in the node credentials, then connect it into any workflow. It handles authentication, passes your URL, and returns the full audit JSON — score, grade, and issue list — as node output data for downstream nodes to consume. Every field in the response is addressable using n8n's expression syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where do you install n8n-nodes-seoscoreapi?
&lt;/h2&gt;

&lt;p&gt;Install the official community node directly from the n8n UI under &lt;strong&gt;Settings &amp;gt; Community Nodes &amp;gt; Install&lt;/strong&gt;, entering &lt;code&gt;n8n-nodes-seoscoreapi&lt;/code&gt;. On a self-hosted instance, you can also run &lt;code&gt;npm install n8n-nodes-seoscoreapi&lt;/code&gt; and restart the n8n process. Once installed, the SEO Score API node appears in your node palette alongside other integrations. You only need to enter your API key once — n8n stores it in the node credentials and reuses it across every workflow that calls the node.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you set up an automated SEO audit workflow in n8n?
&lt;/h2&gt;

&lt;p&gt;The three most common SEO automation workflows are weekly reports, bulk client audits, and competitive monitoring. Each follows the same pattern: a trigger node fires on a schedule, the SEO Score API node runs the audit, and downstream nodes route the results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weekly SEO Audit Report&lt;/strong&gt; — audit your site every Monday and send results to Slack. Add a Cron node set to run at 9am on Mondays, connect a SEO Score API node with your target URL, add an IF node to check whether the score is below your threshold (for example, 80), then add a Slack node to post results to your channel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bulk Client Audits&lt;/strong&gt; — import client URLs from a Google Sheet, audit each one, and write results back. Use a Google Sheets node to read URLs from column A, loop through them with a SEO Score API node, and write score and grade back to columns B and C. This workflow replaces hours of manual auditing with a single scheduled run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Competitive Monitoring&lt;/strong&gt; — track competitor SEO scores over time and alert on significant changes. Set a Cron node to run daily at midnight, audit competitor URLs with the SEO Score API node, use an IF node to check for score changes greater than 5 points, and send an email alert when something shifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the SEO Score API n8n node output?
&lt;/h2&gt;

&lt;p&gt;The node returns the full audit JSON object as its output data, including &lt;code&gt;score&lt;/code&gt; (0–100), &lt;code&gt;grade&lt;/code&gt; (A+ to F), &lt;code&gt;priorities&lt;/code&gt; (an array of issues with severity and description), and all 28 individual check results. Downstream nodes can reference any field using n8n's expression syntax — for example, &lt;code&gt;{{ $json.score }}&lt;/code&gt; to branch on score or &lt;code&gt;{{ $json.grade }}&lt;/code&gt; to include in a notification message. The structured output makes it easy to filter issues by severity, extract specific checks, or aggregate results across multiple URLs in the same workflow run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the HTTP Request Node as an alternative
&lt;/h2&gt;

&lt;p&gt;If you prefer not to install a community node, the built-in HTTP Request node works directly with the SEO Score API. Set the method to GET, enter &lt;code&gt;https://seoscoreapi.com/audit&lt;/code&gt; as the URL, and add your target URL as a query parameter. Include your API key in the &lt;code&gt;X-API-Key&lt;/code&gt; request header. The response is identical to the community node output — you get the same score, grade, and check data, and can reference it downstream using the same n8n expression syntax. The community node is slightly more convenient for credentials management, but the HTTP Request approach works on any n8n instance without additional packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;The free plan gives you 2 audits per day — enough to build and test your first workflow. Create a free account at &lt;a href="https://dev.to/#signup"&gt;seoscoreapi.com&lt;/a&gt;, install the n8n community node or configure the HTTP Request node, and connect your first workflow in under 5 minutes. For agencies auditing 10 or more client sites, the Pro plan at $39 per month includes batch auditing across multiple URLs in a single workflow step, along with monitoring and historical score tracking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;Here are the most common questions about automating SEO audits with n8n and SEO Score API.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is n8n and how does it relate to SEO automation?
&lt;/h3&gt;

&lt;p&gt;n8n is an open-source, self-hostable workflow automation platform that connects APIs and services through a visual node editor. For SEO, it lets you schedule audits, route results to Slack or Sheets, and trigger alerts — without writing or maintaining custom backend code. Because n8n is self-hosted, there are no per-task fees and your audit data stays in your own environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does the SEO Score API n8n node work?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;n8n-nodes-seoscoreapi&lt;/code&gt; community node wraps the SEO Score API endpoints. You enter your API key in the node credentials once, point it at a URL, and it returns the full audit JSON — score, grade, priorities, and all 28 check results — as structured output for subsequent workflow nodes. Credentials are stored securely and reused across every workflow that includes the node.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you set up an automated SEO audit workflow in n8n?
&lt;/h3&gt;

&lt;p&gt;Add a Cron trigger node set to your desired schedule, connect a SEO Score API node with your target URL, then branch with an IF node on score thresholds. Connect a Slack, email, or Google Sheets node to deliver results. The whole workflow takes under five minutes to configure and runs automatically on every scheduled trigger without manual intervention.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does the SEO Score API n8n node output?
&lt;/h3&gt;

&lt;p&gt;The node outputs the full audit JSON including &lt;code&gt;score&lt;/code&gt;, &lt;code&gt;grade&lt;/code&gt;, &lt;code&gt;priorities&lt;/code&gt; (issues with severity labels), and all individual check results. You can reference any field downstream using n8n expressions like &lt;code&gt;{{ $json.score }}&lt;/code&gt; to power conditional logic or notification messages. The &lt;code&gt;priorities&lt;/code&gt; array includes severity labels — critical, warning, and info — so you can filter alerts to only the most important issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where do you install n8n-nodes-seoscoreapi?
&lt;/h3&gt;

&lt;p&gt;Install via the n8n UI at &lt;strong&gt;Settings &amp;gt; Community Nodes &amp;gt; Install&lt;/strong&gt;, entering &lt;code&gt;n8n-nodes-seoscoreapi&lt;/code&gt;. Alternatively, install via npm with &lt;code&gt;npm install n8n-nodes-seoscoreapi&lt;/code&gt; on a self-hosted instance. The node then appears in your node palette under the SEO Score API category. No restart is required on cloud-hosted n8n instances after installation.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Claude Code SEO Skill: Audits in Your Terminal</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/avansledright/claude-code-seo-skill-audits-in-your-terminal-1cnl</link>
      <guid>https://dev.to/avansledright/claude-code-seo-skill-audits-in-your-terminal-1cnl</guid>
      <description>&lt;p&gt;The Claude Code SEO Skill is a custom slash-command integration that runs SEO Score API audits directly in your terminal — no browser tabs, no copy-pasting URLs, no context switching. It supports single-URL audits, batch checks, site comparisons, and specific signal inspections.&lt;/p&gt;

&lt;p&gt;Built as a wrapper around SEO Score API, the skill translates natural-language requests into API calls and formats results as readable terminal output. Type &lt;code&gt;/seo-score&lt;/code&gt;, describe what you want, and get structured scores and priorities in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is the Claude Code SEO Skill?
&lt;/h2&gt;

&lt;p&gt;The Claude Code SEO skill is a custom slash-command integration that lets you run SEO audits directly inside Claude Code sessions. The skill is a wrapper around the SEO Score API — it translates natural language requests into API calls and formats the results as readable terminal output, without requiring you to leave your coding environment.&lt;/p&gt;

&lt;p&gt;The skill supports auditing individual URLs, batch-checking multiple pages, comparing two sites side by side, and inspecting specific SEO signals like meta tags, heading structure, and canonical configuration. It's designed for developers who want SEO data inline with their workflow, not as a separate tool to manage.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Install the SEO Score Skill in Claude Code?
&lt;/h2&gt;

&lt;p&gt;Installing the skill takes under two minutes. Clone or download the skill files into your Claude Code skills directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.claude/skills/seo-score/scripts
curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://seoscoreapi.com/skills/seo-score/SKILL.md &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; ~/.claude/skills/seo-score/SKILL.md
curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://seoscoreapi.com/skills/seo-score/seo_audit.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; ~/.claude/skills/seo-score/scripts/seo_audit.py
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ~/.claude/skills/seo-score/scripts/seo_audit.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then set your API key as an environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SEO_SCORE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"your_key_here"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API key is optional for single URL audits — the skill uses the demo endpoint if no key is present, which allows a few free audits without signup. For batch audits, comparisons, and higher volume use, you'll need a key. Get one free at &lt;a href="https://seoscoreapi.com" rel="noopener noreferrer"&gt;seoscoreapi.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once installed, restart Claude Code and the &lt;code&gt;/seo-score&lt;/code&gt; command becomes available in any session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Commands Does the SEO Score Skill Support?
&lt;/h2&gt;

&lt;p&gt;The skill responds to natural language, so you don't need to memorize a command syntax. These phrasings all work:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit a URL:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"Audit https://example.com"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"Check the SEO score for my homepage"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"What's the SEO score for this URL?"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Batch check multiple pages:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"Audit these five URLs: [list]"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"Check SEO for all pages in my sitemap"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"Run a batch audit on these pages"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Compare two sites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"Compare https://mysite.com vs https://competitor.com"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"Which site scores better for SEO?"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"Show me the SEO difference between these two domains"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Inspect specific signals:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"What meta tags does example.com use?"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"Check if this page has a canonical tag"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"Show me the heading structure for this URL"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The skill formats output as structured terminal text — score, grade, top issues, and a list of passing and failing checks. For batch runs, it displays a summary table so you can spot weak pages at a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Does the Skill Connect to the SEO Score API?
&lt;/h2&gt;

&lt;p&gt;The skill calls the SEO Score API over HTTPS. When you make a request, Claude Code invokes the &lt;code&gt;seo_audit.py&lt;/code&gt; script in the background, which sends a &lt;code&gt;GET&lt;/code&gt; request to the &lt;code&gt;/audit&lt;/code&gt; endpoint (or &lt;code&gt;/audit/batch&lt;/code&gt; for multi-URL requests) with your API key in the &lt;code&gt;X-API-Key&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;The API response — a JSON object containing score, grade, up to 82 individual check results, and fix recommendations — is passed back to Claude, which formats and summarizes it for you. On Starter plan and above, the response also includes SXO (Search Experience), AEO (Answer Engine), and AIO (AI Optimization) scores, each with their own grade and check breakdown. No data is stored locally; the skill is a thin client that calls the API and displays the results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request flow:
Claude Code → seo_audit.py → SEO Score API → JSON response → Claude formats output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill respects the rate limits of your plan. If you hit a limit mid-session, it will tell you and suggest when to retry. For high-volume use cases like auditing a full site during a build, the Pro or Ultra plan is recommended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which SEO Score API Plan Works with the Claude Code Skill?
&lt;/h2&gt;

&lt;p&gt;All plans work with the skill, including the free tier:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Audits&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;2/day&lt;/td&gt;
&lt;td&gt;Quick checks during development&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;200/month&lt;/td&gt;
&lt;td&gt;Regular audits on personal or small client projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;1,000/month&lt;/td&gt;
&lt;td&gt;Teams running audits across multiple projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;5,000/month&lt;/td&gt;
&lt;td&gt;Agencies or CI/CD pipelines with high audit volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultra&lt;/td&gt;
&lt;td&gt;25,000/month&lt;/td&gt;
&lt;td&gt;Enterprise use or large-scale automated workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For individual developers, the &lt;strong&gt;Starter plan at $5/month&lt;/strong&gt; covers most use cases — 200 audits is enough for daily checks across several projects with headroom to spare. If you're using the skill in a CI/CD pipeline or running audits automatically on each deploy, &lt;strong&gt;Pro&lt;/strong&gt; gives you the rate limits to handle that without throttling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use the Skill Instead of the API Directly?
&lt;/h2&gt;

&lt;p&gt;The skill adds value in specific developer scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;During active development&lt;/strong&gt; — check SEO signals on a staging URL without switching context to a browser or API client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When reviewing PRs&lt;/strong&gt; — ask Claude to audit a preview deployment URL as part of your code review&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;While writing content&lt;/strong&gt; — confirm a page's meta tags and heading structure are correct before publishing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For quick comparisons&lt;/strong&gt; — benchmark your score against a competitor while you're already in a session working on something else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you prefer scripting directly against the API — for example, in a CI pipeline, a Python script, or an n8n workflow — the skill is not the right tool for that. Use the API or one of the official SDKs (Python, Node.js) instead. The skill is optimized for interactive, in-session use inside Claude Code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does the Skill Output Look Like?
&lt;/h2&gt;

&lt;p&gt;After running an audit, you'll see output structured like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SEO Audit: https://example.com
Score: 78/100 (B)

Top Issues:
  - [CRITICAL] Missing Open Graph image tag
  - [HIGH] H1 tag not found
  - [MEDIUM] Page load time exceeds 3s threshold

SXO: 81/100 (A-)  AEO: 52/100 (C+)  AIO: 68/100 (B)

Report: https://seoscoreapi.com/report/example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SXO, AEO, and AIO scores appear automatically on Starter plan and above — no extra flags needed. The full report link is shareable and can be pasted into a PR comment, Slack message, or client email without any additional setup.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Build an AI SEO Agent: Automated Website Auditing with Python</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 10 Aug 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/avansledright/build-an-ai-seo-agent-automated-website-auditing-with-python-jd</link>
      <guid>https://dev.to/avansledright/build-an-ai-seo-agent-automated-website-auditing-with-python-jd</guid>
      <description>&lt;p&gt;AI agents are transforming SEO workflows. Instead of manually running audits, an AI agent can crawl your site, identify issues, prioritize fixes, and generate recommendations — all autonomously.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an AI SEO Agent?
&lt;/h2&gt;

&lt;p&gt;An AI SEO agent is an automated program that combines structured audit data with language model reasoning to identify, prioritize, and explain SEO issues without human intervention. Rather than generating a static report, the agent interprets results and produces actionable recommendations on demand. The key distinction from a traditional script is the LLM layer: instead of hard-coded logic, the agent reasons about what the data means and what to do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the seoscoreapi Python SDK do?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;seoscoreapi&lt;/code&gt; Python SDK wraps the SEO Score API REST endpoints into simple function calls. It handles authentication, request formatting, and response parsing — so you call &lt;code&gt;audit("https://example.com", api_key=key)&lt;/code&gt; and get back a structured dict with score, grade, and prioritized issue list. The SDK also exposes &lt;code&gt;batch_audit()&lt;/code&gt; for auditing multiple URLs in a single call, and &lt;code&gt;signup()&lt;/code&gt; for programmatically creating a free API key without visiting the dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;An AI SEO agent uses three components: the SEO Score API to provide raw audit data (82 checks per URL on paid plans), a Python orchestration layer that loops through pages and collects results, and an LLM to interpret those results and generate human-readable recommendations. These three layers are loosely coupled — you can swap the LLM (GPT-4, Claude, Gemini) or extend the orchestration without touching the audit layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual Auditing vs. AI Agent: What Changes?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Manual workflow&lt;/th&gt;
&lt;th&gt;AI SEO agent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Audit frequency&lt;/td&gt;
&lt;td&gt;Weekly or monthly&lt;/td&gt;
&lt;td&gt;Continuous or scheduled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URLs covered&lt;/td&gt;
&lt;td&gt;5-20 by hand&lt;/td&gt;
&lt;td&gt;500+ via batch API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Issue interpretation&lt;/td&gt;
&lt;td&gt;Human reads raw data&lt;/td&gt;
&lt;td&gt;LLM generates plain-English recommendations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reporting&lt;/td&gt;
&lt;td&gt;Manual copy-paste&lt;/td&gt;
&lt;td&gt;Automated structured output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alert on regressions&lt;/td&gt;
&lt;td&gt;Check manually&lt;/td&gt;
&lt;td&gt;Automated diff + notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per audit&lt;/td&gt;
&lt;td&gt;Engineer time&lt;/td&gt;
&lt;td&gt;$0-0.02 per URL via API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How do you build an AI SEO agent with Python?
&lt;/h2&gt;

&lt;p&gt;Building the agent takes three steps. First, install the dependencies with &lt;code&gt;pip install seoscoreapi openai&lt;/code&gt;. Then create the audit function: call &lt;code&gt;signup("agent@yourdomain.com")&lt;/code&gt; to get a free API key, then pass that key and your target URL to &lt;code&gt;audit()&lt;/code&gt;. The response includes a numeric score, letter grade, and a &lt;code&gt;priorities&lt;/code&gt; list where each item has a severity label (&lt;code&gt;critical&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, or &lt;code&gt;info&lt;/code&gt;) and a description of the issue. The third step is LLM analysis: pass the structured result into a prompt asking for recommendations, then call your LLM of choice to interpret the data and generate plain-English fixes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;seoscoreapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signup&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent@yourdomain.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Analyze this SEO audit and provide 3 actionable recommendations:
Score: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/100 (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;grade&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)
Issues: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;priorities&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do you automate weekly SEO audits?
&lt;/h2&gt;

&lt;p&gt;To run audits on a schedule, use &lt;code&gt;batch_audit()&lt;/code&gt; to audit an entire site in a single call, then wrap the script in a cron job or workflow tool like n8n. Pass a list of URLs — homepage, about, pricing, blog — and get back a list of structured results you can loop through. Combine with an email or Slack notification step to receive weekly score summaries and issue alerts automatically. For CI/CD pipelines, add the audit to your deploy script and fail the build if any URL drops below your target score threshold.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;seoscoreapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;batch_audit&lt;/span&gt;

&lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/about&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/blog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;batch_audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What does the agent output after an audit?
&lt;/h2&gt;

&lt;p&gt;After running an audit, the agent returns a structured response containing a numeric score (0–100), a letter grade (A+ to F), and a prioritized list of issues with severity labels (&lt;code&gt;critical&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;). Each issue includes the check name, description, and recommended fix, making it ready to pipe directly into an LLM prompt or a downstream reporting tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agency automation:&lt;/strong&gt; Build an agent that audits all client sites weekly and sends reports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content QA:&lt;/strong&gt; Audit every new blog post before publishing to catch missing meta tags or heading issues&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitive intelligence:&lt;/strong&gt; Monitor competitor SEO scores daily and alert on changes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CrewAI / LangChain:&lt;/strong&gt; Add SEO auditing as a callable tool in your AI agent framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD quality gate:&lt;/strong&gt; Run the agent on every deploy and fail the build if score drops below threshold&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk site migrations:&lt;/strong&gt; Audit all pages before and after a migration to detect regressions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client onboarding:&lt;/strong&gt; Auto-generate a first-audit report for new agency clients on signup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content freshness monitoring:&lt;/strong&gt; Alert when a high-ranking page's SEO score drops significantly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://dev.to/#signup"&gt;free tier&lt;/a&gt; gives you 2 audits per day to build and test your agent. For production workloads, &lt;a href="https://dev.to/#pricing"&gt;paid plans&lt;/a&gt; start at $5 per month and include batch auditing, monitoring, and up to 25,000 audits per month on the Ultra plan. No credit card is required to start — sign up with your email address and get your API key immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an AI SEO agent?
&lt;/h3&gt;

&lt;p&gt;An AI SEO agent is an automated program that runs structured website audits and uses a language model to interpret results, prioritize issues, and generate actionable recommendations — removing the need for manual review at every step. The LLM layer transforms raw check data into plain-English fixes that developers and content teams can act on immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does the seoscoreapi Python SDK do?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;seoscoreapi&lt;/code&gt; SDK wraps the SEO Score API into simple Python function calls like &lt;code&gt;audit()&lt;/code&gt; and &lt;code&gt;batch_audit()&lt;/code&gt;. It handles authentication and response parsing, returning structured data with scores, grades, and issue priorities ready for downstream processing or LLM input. A &lt;code&gt;signup()&lt;/code&gt; helper lets you programmatically create a free API key without a browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you build an AI SEO agent with Python?
&lt;/h3&gt;

&lt;p&gt;Install &lt;code&gt;seoscoreapi&lt;/code&gt; and &lt;code&gt;openai&lt;/code&gt; via pip, call &lt;code&gt;audit()&lt;/code&gt; with your target URL and API key, then pass the structured result — score, grade, and priorities — into an LLM prompt. The LLM interprets the data and generates specific, actionable recommendations based on detected issues. The entire setup takes under 20 lines of Python and runs from any script or scheduler.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you automate weekly SEO audits?
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;batch_audit()&lt;/code&gt; to audit multiple URLs in one call, then wrap the script in a cron job or n8n workflow to run on a schedule. Pair with a Slack or email notification step to receive weekly score summaries and issue alerts without any manual trigger. For CI/CD use, add a score threshold check to fail the pipeline when quality drops below your target.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does the agent output after an audit?
&lt;/h3&gt;

&lt;p&gt;The audit response includes a numeric score (0–100), a letter grade, and a prioritized issues list with severity labels (&lt;code&gt;critical&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;). Each entry contains the check name, a description of the problem, and the recommended fix — structured for direct use in reports or LLM prompts. The consistent response format means the same prompt template works across every URL you audit.&lt;/p&gt;

</description>
      <category>python</category>
      <category>seo</category>
      <category>api</category>
      <category>ai</category>
    </item>
    <item>
      <title>Add SEO Checks to Your CI/CD Pipeline</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 03 Aug 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/avansledright/add-seo-checks-to-your-cicd-pipeline-2876</link>
      <guid>https://dev.to/avansledright/add-seo-checks-to-your-cicd-pipeline-2876</guid>
      <description>&lt;h1&gt;
  
  
  Add SEO Checks to Your CI/CD Pipeline
&lt;/h1&gt;

&lt;p&gt;Every deployment is a chance to accidentally break SEO. A developer removes a canonical tag, a build step strips meta descriptions, a config change adds &lt;code&gt;noindex&lt;/code&gt; to production — and you don't find out until rankings drop weeks later. The fix is treating SEO like any other quality gate: run checks on every deployment and fail the build if something critical breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Do SEO Checks Belong in CI/CD?
&lt;/h2&gt;

&lt;p&gt;SEO regressions are invisible at deploy time. Unlike a broken API endpoint or a failing unit test, a missing &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; tag or an accidental &lt;code&gt;noindex&lt;/code&gt; directive has no immediate error signal — it silently degrades over days or weeks as search engines recrawl and update their indexes. By the time rankings drop, the causal commit is buried under dozens of subsequent changes.&lt;/p&gt;

&lt;p&gt;CI/CD SEO checks create the same feedback loop you have for code quality: if a deployment introduces an SEO regression, the pipeline fails, the developer sees the issue immediately, and it never reaches production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Checks Matter Most in a Pipeline?
&lt;/h2&gt;

&lt;p&gt;Not all 54 audit checks are equally critical at deploy time. These are the high-priority checks that justify a build failure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;th&gt;Failure threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Meta title&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Missing or empty title tanks click-through rate&lt;/td&gt;
&lt;td&gt;Must be present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Meta description&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Direct impact on SERP snippet quality&lt;/td&gt;
&lt;td&gt;Must be present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Canonical tag&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Duplicate content and indexation issues&lt;/td&gt;
&lt;td&gt;Must be present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Noindex directive&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Accidentally blocking pages from Google&lt;/td&gt;
&lt;td&gt;Must not be set on key pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Required for ranking; marks page as insecure&lt;/td&gt;
&lt;td&gt;Must pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overall score&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Composite health indicator&lt;/td&gt;
&lt;td&gt;Define your own threshold (e.g., ≥ 70)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;Add this workflow to &lt;code&gt;.github/workflows/seo-check.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Quality Gate&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deployment_status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;seo-audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event.deployment_status.state == 'success' || github.event_name == 'push'&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.11"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install SEO Score API SDK&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install seoscoreapi&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run SEO audit&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SEO_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEO_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;AUDIT_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://your-production-site.com"&lt;/span&gt;
          &lt;span class="na"&gt;SCORE_THRESHOLD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;70"&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;python - &amp;lt;&amp;lt;'EOF'&lt;/span&gt;
          &lt;span class="s"&gt;import os, sys&lt;/span&gt;
          &lt;span class="s"&gt;from seoscoreapi import audit&lt;/span&gt;

          &lt;span class="s"&gt;result = audit(os.environ["AUDIT_URL"], api_key=os.environ["SEO_API_KEY"])&lt;/span&gt;
          &lt;span class="s"&gt;score = result["score"]&lt;/span&gt;
          &lt;span class="s"&gt;grade = result["grade"]&lt;/span&gt;
          &lt;span class="s"&gt;threshold = int(os.environ["SCORE_THRESHOLD"])&lt;/span&gt;

          &lt;span class="s"&gt;print(f"SEO Score: {score}/100 ({grade})")&lt;/span&gt;

          &lt;span class="s"&gt;failures = []&lt;/span&gt;
          &lt;span class="s"&gt;checks = result.get("audit", {})&lt;/span&gt;

          &lt;span class="s"&gt;if checks.get("meta", {}).get("title") == "fail":&lt;/span&gt;
              &lt;span class="s"&gt;failures.append("CRITICAL: Missing meta title")&lt;/span&gt;
          &lt;span class="s"&gt;if checks.get("meta", {}).get("description") == "fail":&lt;/span&gt;
              &lt;span class="s"&gt;failures.append("CRITICAL: Missing meta description")&lt;/span&gt;
          &lt;span class="s"&gt;if checks.get("meta", {}).get("canonical") == "fail":&lt;/span&gt;
              &lt;span class="s"&gt;failures.append("CRITICAL: Missing canonical tag")&lt;/span&gt;
          &lt;span class="s"&gt;if score &amp;lt; threshold:&lt;/span&gt;
              &lt;span class="s"&gt;failures.append(f"Score {score} is below threshold {threshold}")&lt;/span&gt;

          &lt;span class="s"&gt;if failures:&lt;/span&gt;
              &lt;span class="s"&gt;print("\nSEO Quality Gate FAILED:")&lt;/span&gt;
              &lt;span class="s"&gt;for f in failures:&lt;/span&gt;
                  &lt;span class="s"&gt;print(f"  - {f}")&lt;/span&gt;
              &lt;span class="s"&gt;sys.exit(1)&lt;/span&gt;
          &lt;span class="s"&gt;else:&lt;/span&gt;
              &lt;span class="s"&gt;print("SEO Quality Gate PASSED")&lt;/span&gt;
          &lt;span class="s"&gt;EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store your API key as a GitHub Actions secret: &lt;strong&gt;Settings &amp;gt; Secrets and variables &amp;gt; Actions &amp;gt; New repository secret&lt;/strong&gt; named &lt;code&gt;SEO_API_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Fail a Build If Score Drops Below a Threshold
&lt;/h2&gt;

&lt;p&gt;The pattern above uses &lt;code&gt;sys.exit(1)&lt;/code&gt; to signal failure to the CI runner. Choose your threshold based on your baseline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;70&lt;/strong&gt; — reasonable starting point for most sites&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;80&lt;/strong&gt; — appropriate for established sites where any regression is significant&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom&lt;/strong&gt; — run an initial audit, note your current score, then set the threshold 5–10 points below it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To track score over time, write the result to a GitHub Actions output or post it as a PR comment using the GitHub API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;comment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEO Score: **&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/100** (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;grade&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;pr_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PR_NUMBER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GITHUB_REPOSITORY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/issues/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pr_number&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/comments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GITHUB_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  GitLab CI Example
&lt;/h2&gt;

&lt;p&gt;Add to &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;seo-audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip install seoscoreapi&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;python - &amp;lt;&amp;lt;'EOF'&lt;/span&gt;
      &lt;span class="s"&gt;import os, sys&lt;/span&gt;
      &lt;span class="s"&gt;from seoscoreapi import audit&lt;/span&gt;

      &lt;span class="s"&gt;result = audit("https://your-site.com", api_key=os.environ["SEO_API_KEY"])&lt;/span&gt;
      &lt;span class="s"&gt;print(f"Score: {result['score']}/100 ({result['grade']})")&lt;/span&gt;

      &lt;span class="s"&gt;if result["score"] &amp;lt; 70:&lt;/span&gt;
          &lt;span class="s"&gt;print(f"FAIL: Score {result['score']} is below threshold 70")&lt;/span&gt;
          &lt;span class="s"&gt;sys.exit(1)&lt;/span&gt;
      &lt;span class="s"&gt;print("PASS: SEO quality gate cleared")&lt;/span&gt;
      &lt;span class="s"&gt;EOF&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;SEO_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$SEO_API_KEY&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;merge_requests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;SEO_API_KEY&lt;/code&gt; in &lt;strong&gt;Settings &amp;gt; CI/CD &amp;gt; Variables&lt;/strong&gt; in your GitLab project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Python Script for Custom Pipelines
&lt;/h2&gt;

&lt;p&gt;For non-GitHub/GitLab pipelines (Jenkins, CircleCI, Bitbucket Pipelines, or a local pre-deploy hook):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;seo_gate.py — run before any deployment to catch SEO regressions.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;seoscoreapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;

&lt;span class="n"&gt;AUDIT_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUDIT_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://your-site.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEO_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SCORE_THRESHOLD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;70&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AUDIT_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;grade&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grade&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;priorities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priorities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;SEO Audit: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AUDIT_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Score: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/100 (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;grade&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;critical_issues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;priorities&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;critical_issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;critical_issues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; critical issue(s) found:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;critical_issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;issue&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;THRESHOLD&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;critical_issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;SEO Quality Gate: FAILED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;SEO Quality Gate: PASSED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it as a pipeline step: &lt;code&gt;SEO_API_KEY=your_key python seo_gate.py&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/#signup"&gt;Get a free API key&lt;/a&gt; — 2 audits/day, no credit card required&lt;/li&gt;
&lt;li&gt;Add the workflow YAML to your repo&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;SEO_API_KEY&lt;/code&gt; as a pipeline secret&lt;/li&gt;
&lt;li&gt;Push to main and watch the quality gate run&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For teams running audits on every PR across multiple environments, the &lt;a href="https://dev.to/#pricing"&gt;Basic plan ($15/mo)&lt;/a&gt; gives you 1,000 audits/month at 30 RPM.&lt;/p&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why should SEO checks run in CI/CD rather than on a schedule?
&lt;/h3&gt;

&lt;p&gt;Scheduled audits tell you that something broke — CI/CD checks tell you which deployment broke it. Running checks at deploy time means you catch regressions in the same pull request that introduced them, before they ever reach production and before any search engine recrawls the affected pages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which SEO checks are most important to fail a build on?
&lt;/h3&gt;

&lt;p&gt;Fail the build on missing meta title, missing meta description, missing canonical tag, and any &lt;code&gt;noindex&lt;/code&gt; directive on pages that should be indexed. These are high-impact, unambiguous regressions. Use a score threshold (e.g., ≥ 70) as a secondary gate to catch broader degradation across multiple minor issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you set up the SEO audit GitHub Action?
&lt;/h3&gt;

&lt;p&gt;Add a workflow file to &lt;code&gt;.github/workflows/seo-check.yml&lt;/code&gt; with a Python step that installs &lt;code&gt;seoscoreapi&lt;/code&gt;, runs &lt;code&gt;audit()&lt;/code&gt; against your production URL, checks for critical failures, and calls &lt;code&gt;sys.exit(1)&lt;/code&gt; if any are found. Store your API key as a GitHub Actions secret named &lt;code&gt;SEO_API_KEY&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the SEO audit check a staging URL or the live site?
&lt;/h3&gt;

&lt;p&gt;You can audit either. For catching regressions before they go live, point the audit at your staging or preview URL after deployment but before promoting to production. For production monitoring, audit the live URL as a post-deploy verification step.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many API calls does a CI/CD pipeline use?
&lt;/h3&gt;

&lt;p&gt;Each audit call counts as one request against your monthly quota. A team pushing to main once a day uses about 30 audits/month — well within the free tier. If you audit on every PR (e.g., 10 PRs/day), the Starter plan at $5/mo (200 audits/month) covers most teams comfortably.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>SEO Quality Gates with GitHub Actions</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 27 Jul 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/avansledright/seo-quality-gates-with-github-actions-2bli</link>
      <guid>https://dev.to/avansledright/seo-quality-gates-with-github-actions-2bli</guid>
      <description>&lt;p&gt;A broken meta tag in one pull request can silently tank your search rankings for weeks. By the time you spot the traffic drop in Google Search Console, the damage is done. The fix is simple: treat SEO like a test suite and run it automatically on every PR.&lt;/p&gt;

&lt;p&gt;This guide walks through a real, production-ready GitHub Actions workflow using the &lt;a href="https://github.com/SeoScoreAPI/seo-audit-action" rel="noopener noreferrer"&gt;SEO Audit Action&lt;/a&gt;. By the end, you'll have a workflow that audits your deployed site, posts a score report as a PR comment, and fails the build if the score drops below your threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does the GitHub Actions SEO Audit Do?
&lt;/h2&gt;

&lt;p&gt;Here's what the workflow does on every pull request:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Waits for your preview deployment (Vercel, Netlify, Cloudflare Pages, etc.)&lt;/li&gt;
&lt;li&gt;Runs a 28-check SEO audit against the preview URL&lt;/li&gt;
&lt;li&gt;Posts the score, grade, and top issues as a PR comment&lt;/li&gt;
&lt;li&gt;Blocks the merge if the score is below 80&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full audit covers meta tags, technical SEO, Open Graph, performance, and accessibility — the same checks you'd get from running &lt;code&gt;curl https://seoscoreapi.com/audit?url=yoursite.com&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Are the Prerequisites?
&lt;/h2&gt;

&lt;p&gt;You need two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A free SEO Score API key&lt;/strong&gt; — &lt;a href="https://seoscoreapi.com/#signup" rel="noopener noreferrer"&gt;sign up here&lt;/a&gt; (takes 10 seconds, no credit card)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A preview deployment&lt;/strong&gt; in your pipeline — any system that gives you a URL for the PR branch (Vercel, Netlify, Render, etc.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Add your API key as a repository secret:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Settings &amp;gt; Secrets and variables &amp;gt; Actions &amp;gt; New repository secret&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;SEO_SCORE_API_KEY&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Value: your API key&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Do You Add It to a Workflow?
&lt;/h2&gt;

&lt;p&gt;Start with the simplest version. Create &lt;code&gt;.github/workflows/seo-audit.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Audit&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;seo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Audit&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seo&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SeoScoreAPI/seo-audit-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://your-site.com"&lt;/span&gt;
          &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEO_SCORE_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a working quality gate in 15 lines. If the SEO score is below 80, the workflow fails and the PR can't merge (assuming you have branch protection rules enabled).&lt;/p&gt;

&lt;p&gt;The action produces a markdown summary in the GitHub Actions UI automatically — you'll see the score, grade, issue count, and top 5 issues right in the workflow run.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Get PR Comments on Audit Results?
&lt;/h2&gt;

&lt;p&gt;The summary in the Actions tab is useful, but most reviewers look at the PR conversation, not the workflow logs. Let's post the results as a PR comment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Audit&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;seo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Audit&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seo&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SeoScoreAPI/seo-audit-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://your-site.com"&lt;/span&gt;
          &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEO_SCORE_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Comment on PR&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const score = '${{ steps.seo.outputs.score }}';&lt;/span&gt;
            &lt;span class="s"&gt;const grade = '${{ steps.seo.outputs.grade }}';&lt;/span&gt;
            &lt;span class="s"&gt;const issues = '${{ steps.seo.outputs.issues }}';&lt;/span&gt;
            &lt;span class="s"&gt;const reportUrl = '${{ steps.seo.outputs.report-url }}';&lt;/span&gt;
            &lt;span class="s"&gt;const passed = parseInt(score) &amp;gt;= 80;&lt;/span&gt;

            &lt;span class="s"&gt;const body = [&lt;/span&gt;
              &lt;span class="s"&gt;`## SEO Audit Results`,&lt;/span&gt;
              &lt;span class="s"&gt;``,&lt;/span&gt;
              &lt;span class="s"&gt;`| Metric | Value |`,&lt;/span&gt;
              &lt;span class="s"&gt;`|--------|-------|`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Score | **${score}/100** |`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Grade | **${grade}** |`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Issues | ${issues} |`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Threshold | 80 |`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Status | ${passed ? 'Passed' : 'Failed'} |`,&lt;/span&gt;
              &lt;span class="s"&gt;``,&lt;/span&gt;
              &lt;span class="s"&gt;`[View Full Report](${reportUrl})`,&lt;/span&gt;
            &lt;span class="s"&gt;].join('\n');&lt;/span&gt;

            &lt;span class="s"&gt;github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
              &lt;span class="s"&gt;body&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;if: always()&lt;/code&gt; is important — it ensures the comment is posted even when the audit step fails due to a low score. Without it, a failing score would skip the comment step and you'd have to dig into the logs to find out what went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Example: Vercel Preview Deployments
&lt;/h2&gt;

&lt;p&gt;Most teams don't audit a static production URL — they audit the preview deployment that Vercel (or Netlify, etc.) creates for each PR. Here's how to wire that up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Audit on Preview&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.html'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.jsx'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.tsx'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.vue'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.svelte'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;public/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;seo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for Vercel Preview&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;patrickedqvist/wait-for-vercel-preview@v1.3.2&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;preview&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;max_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Audit&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seo&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SeoScoreAPI/seo-audit-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.preview.outputs.url }}&lt;/span&gt;
          &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEO_SCORE_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Comment on PR&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const score = '${{ steps.seo.outputs.score }}';&lt;/span&gt;
            &lt;span class="s"&gt;const grade = '${{ steps.seo.outputs.grade }}';&lt;/span&gt;
            &lt;span class="s"&gt;const issues = '${{ steps.seo.outputs.issues }}';&lt;/span&gt;
            &lt;span class="s"&gt;const reportUrl = '${{ steps.seo.outputs.report-url }}';&lt;/span&gt;
            &lt;span class="s"&gt;const previewUrl = '${{ steps.preview.outputs.url }}';&lt;/span&gt;
            &lt;span class="s"&gt;const passed = parseInt(score) &amp;gt;= 80;&lt;/span&gt;

            &lt;span class="s"&gt;const emoji = passed ? '✅' : '❌';&lt;/span&gt;

            &lt;span class="s"&gt;const body = [&lt;/span&gt;
              &lt;span class="s"&gt;`## ${emoji} SEO Audit`,&lt;/span&gt;
              &lt;span class="s"&gt;``,&lt;/span&gt;
              &lt;span class="s"&gt;`Audited: ${previewUrl}`,&lt;/span&gt;
              &lt;span class="s"&gt;``,&lt;/span&gt;
              &lt;span class="s"&gt;`| Metric | Value |`,&lt;/span&gt;
              &lt;span class="s"&gt;`|--------|-------|`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Score | **${score}/100** |`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Grade | **${grade}** |`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Issues | ${issues} |`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Threshold | 80 |`,&lt;/span&gt;
              &lt;span class="s"&gt;``,&lt;/span&gt;
              &lt;span class="s"&gt;`[Full Report](${reportUrl})`,&lt;/span&gt;
            &lt;span class="s"&gt;].join('\n');&lt;/span&gt;

            &lt;span class="s"&gt;github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
              &lt;span class="s"&gt;body&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth noting about this version:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path filters&lt;/strong&gt; — the &lt;code&gt;paths&lt;/code&gt; block ensures the audit only runs when files that affect SEO are changed. Backend-only PRs (API routes, database migrations, config changes) skip the audit entirely. This is important because audits cost API calls, and you don't want to burn them on changes that can't affect your SEO score.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Preview URL&lt;/strong&gt; — instead of hardcoding a URL, we pull it from the Vercel preview deployment. Every PR gets its own preview URL, so you're auditing the actual changes in that PR, not the current production site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeout&lt;/strong&gt; — &lt;code&gt;max_timeout: 300&lt;/code&gt; gives Vercel 5 minutes to deploy. Adjust this based on your build times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Netlify Variant
&lt;/h2&gt;

&lt;p&gt;If you're on Netlify instead of Vercel, swap the preview step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for Netlify Preview&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jakepartusch/wait-for-netlify-action@v1.4&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;preview&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;site_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your-netlify-site"&lt;/span&gt;
          &lt;span class="na"&gt;max_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the workflow stays identical — just reference &lt;code&gt;${{ steps.preview.outputs.url }}&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-Page Audits
&lt;/h2&gt;

&lt;p&gt;For sites where SEO matters on more than one page, use a matrix strategy to audit multiple routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;seo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/pricing'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/blog'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/docs'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Audit - ${{ matrix.path }}&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seo&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SeoScoreAPI/seo-audit-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://your-site.com${{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;matrix.path&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
          &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEO_SCORE_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs 4 parallel audits — one per page. If any page drops below 80, that specific job fails. You can see exactly which page regressed in the Actions UI.&lt;/p&gt;

&lt;p&gt;For larger sites (10+ pages), use the &lt;a href="https://seoscoreapi.com/docs" rel="noopener noreferrer"&gt;batch audit endpoint&lt;/a&gt; with a script step instead of the matrix approach to use fewer API calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Set Up Branch Protection?
&lt;/h2&gt;

&lt;p&gt;The workflow only blocks merges if you configure branch protection:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Settings &amp;gt; Branches &amp;gt; Branch protection rules&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add a rule for &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check &lt;strong&gt;Require status checks to pass before merging&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Search for and add the &lt;strong&gt;SEO Audit&lt;/strong&gt; check&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now any PR where the SEO score drops below your threshold literally cannot be merged until the issues are fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing Your Threshold
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;90&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Marketing sites, landing pages where SEO directly drives revenue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;80&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Most production sites — catches real issues without false positives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;70&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apps with some public-facing pages where SEO is secondary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;60&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Internal tools with minimal SEO requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start at &lt;strong&gt;70&lt;/strong&gt; if your site has existing issues, then ratchet up to &lt;strong&gt;80&lt;/strong&gt; once the team has fixed the backlog. Don't start at 90 — you'll frustrate developers with failures on day one and they'll disable the check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Plans Support CI/CD Use?
&lt;/h2&gt;

&lt;p&gt;The free tier gives you &lt;strong&gt;2 audits/day&lt;/strong&gt;, which works for solo projects with a couple PRs per week. For teams:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Audits/mo&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Good For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;$5/mo&lt;/td&gt;
&lt;td&gt;Small team, 1-2 PRs/day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;$15/mo&lt;/td&gt;
&lt;td&gt;Active team, multi-page audits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;$39/mo&lt;/td&gt;
&lt;td&gt;Multiple repos, matrix audits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All paid plans support CI/CD use. The free tier is sufficient for personal projects; Starter or above is recommended for active teams. &lt;a href="https://seoscoreapi.com/#signup" rel="noopener noreferrer"&gt;Sign up free&lt;/a&gt; and upgrade when your PR volume outgrows the free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Complete Workflow
&lt;/h2&gt;

&lt;p&gt;Here's the full production-ready workflow with all the pieces — preview deployment, PR comments, path filtering, and the quality gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SEO Quality Gate&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.html'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.jsx'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.tsx'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.vue'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;**.svelte'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content/**'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;public/**'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;seo-audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wait for Preview Deploy&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;patrickedqvist/wait-for-vercel-preview@v1.3.2&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;preview&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;max_timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run SEO Audit&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;seo&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SeoScoreAPI/seo-audit-action@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.preview.outputs.url }}&lt;/span&gt;
          &lt;span class="na"&gt;api-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEO_SCORE_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Post Results to PR&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/github-script@v7&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;const score = '${{ steps.seo.outputs.score }}';&lt;/span&gt;
            &lt;span class="s"&gt;const grade = '${{ steps.seo.outputs.grade }}';&lt;/span&gt;
            &lt;span class="s"&gt;const issues = '${{ steps.seo.outputs.issues }}';&lt;/span&gt;
            &lt;span class="s"&gt;const reportUrl = '${{ steps.seo.outputs.report-url }}';&lt;/span&gt;
            &lt;span class="s"&gt;const previewUrl = '${{ steps.preview.outputs.url }}';&lt;/span&gt;
            &lt;span class="s"&gt;const passed = parseInt(score) &amp;gt;= 80;&lt;/span&gt;

            &lt;span class="s"&gt;const emoji = passed ? '✅' : '❌';&lt;/span&gt;
            &lt;span class="s"&gt;const status = passed ? 'Passed' : 'Failed — fix issues before merging';&lt;/span&gt;

            &lt;span class="s"&gt;const body = [&lt;/span&gt;
              &lt;span class="s"&gt;`## ${emoji} SEO Audit`,&lt;/span&gt;
              &lt;span class="s"&gt;``,&lt;/span&gt;
              &lt;span class="s"&gt;`**${status}**`,&lt;/span&gt;
              &lt;span class="s"&gt;``,&lt;/span&gt;
              &lt;span class="s"&gt;`| Metric | Value |`,&lt;/span&gt;
              &lt;span class="s"&gt;`|--------|-------|`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Preview | ${previewUrl} |`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Score | **${score}/100** |`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Grade | **${grade}** |`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Issues | ${issues} |`,&lt;/span&gt;
              &lt;span class="s"&gt;`| Threshold | 80 |`,&lt;/span&gt;
              &lt;span class="s"&gt;``,&lt;/span&gt;
              &lt;span class="s"&gt;`[View Full Report →](${reportUrl})`,&lt;/span&gt;
            &lt;span class="s"&gt;].join('\n');&lt;/span&gt;

            &lt;span class="s"&gt;github.rest.issues.createComment({&lt;/span&gt;
              &lt;span class="s"&gt;owner: context.repo.owner,&lt;/span&gt;
              &lt;span class="s"&gt;repo: context.repo.repo,&lt;/span&gt;
              &lt;span class="s"&gt;issue_number: context.issue.number,&lt;/span&gt;
              &lt;span class="s"&gt;body&lt;/span&gt;
            &lt;span class="s"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copy that into &lt;code&gt;.github/workflows/seo-audit.yml&lt;/code&gt;, add your &lt;code&gt;SEO_SCORE_API_KEY&lt;/code&gt; secret, enable branch protection, and you have an automated SEO quality gate protecting your site from regressions on every pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What does the GitHub Actions SEO Audit action do?
&lt;/h3&gt;

&lt;p&gt;The SEO Audit Action runs a 28-check SEO audit against your site on every pull request. It checks meta tags, technical SEO, Open Graph, performance, and accessibility, then posts a score and grade as a PR comment and fails the build if the score drops below your configured threshold.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you add the SEO audit to a GitHub Actions workflow?
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/seo-audit.yml&lt;/code&gt;, add the &lt;code&gt;SeoScoreAPI/seo-audit-action@v1&lt;/code&gt; step with your &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;api-key&lt;/code&gt; (stored as a repository secret), and a &lt;code&gt;threshold&lt;/code&gt; value. The action installs automatically — no additional dependencies or setup scripts required.&lt;/p&gt;

&lt;h3&gt;
  
  
  What score threshold triggers a build failure?
&lt;/h3&gt;

&lt;p&gt;The threshold is configurable via the &lt;code&gt;threshold&lt;/code&gt; input parameter. The default recommendation is &lt;strong&gt;80&lt;/strong&gt; for most production sites. Scores below your threshold fail the workflow step, which blocks the PR from merging when branch protection is enabled. Start at 70 for sites with existing issues, then raise it over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do PR comments with audit results work?
&lt;/h3&gt;

&lt;p&gt;Add a second step using &lt;code&gt;actions/github-script@v7&lt;/code&gt; with &lt;code&gt;if: always()&lt;/code&gt;. It reads the &lt;code&gt;score&lt;/code&gt;, &lt;code&gt;grade&lt;/code&gt;, &lt;code&gt;issues&lt;/code&gt;, and &lt;code&gt;report-url&lt;/code&gt; outputs from the audit step and posts them as a formatted comment on the pull request. The &lt;code&gt;if: always()&lt;/code&gt; ensures the comment appears even when the audit fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which plans support CI/CD use with GitHub Actions?
&lt;/h3&gt;

&lt;p&gt;All plans, including the free tier (2 audits/day), support CI/CD use. The free tier works for solo projects with a few PRs per week. For active teams, the Starter plan ($5/mo, 200 audits/month) or Basic plan ($15/mo, 1,000 audits/month) is recommended depending on PR volume and multi-page audit needs.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Node SDK 1.3: Run SEO Audits, Track Trends, and Get Slack Alerts from Any Node Project</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 20 Jul 2026 14:00:05 +0000</pubDate>
      <link>https://dev.to/avansledright/node-sdk-13-run-seo-audits-track-trends-and-get-slack-alerts-from-any-node-project-46e4</link>
      <guid>https://dev.to/avansledright/node-sdk-13-run-seo-audits-track-trends-and-get-slack-alerts-from-any-node-project-46e4</guid>
      <description>&lt;p&gt;The Node.js client for SEO Score API is at &lt;code&gt;1.3.0&lt;/code&gt; on npm. It mirrors the Python release we shipped at the same time: a new &lt;code&gt;compare()&lt;/code&gt; function for side-by-side URL diffs, full &lt;code&gt;history()&lt;/code&gt; and &lt;code&gt;historyDomains()&lt;/code&gt; timeseries access, and webhook alerts on the monitor system (Slack-formatted automatically when you point it at an incoming webhook).&lt;/p&gt;

&lt;p&gt;Install or upgrade with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;seoscoreapi@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with pnpm/yarn — same name, no native dependencies, runs anywhere Node 18+ has &lt;code&gt;fetch&lt;/code&gt; available.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new in 1.3
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compare(urls, apiKey)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /compare&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Basic+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;history(url, apiKey, opts)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /history&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Starter+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;historyDomains(apiKey)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /history/domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Starter+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;addMonitor(url, apiKey, { webhookUrl, alertThreshold })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /monitors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;addMonitor&lt;/code&gt; is backwards compatible — the legacy &lt;code&gt;addMonitor(url, apiKey, "weekly")&lt;/code&gt; form still works, but if you want webhooks you'll pass an options object instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Compare your page against 2–4 competitors in one call
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;compare()&lt;/code&gt; accepts 2–5 URLs and returns each one's score plus a &lt;code&gt;diff&lt;/code&gt; block telling you who is ahead, per category, per pair:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compare&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;seoscoreapi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://acme.com/pricing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://competitor-a.com/pricing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://competitor-b.com/pricing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category_gaps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: leader &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;leader&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (+&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one's Basic plan ($15/mo) and up. It's the call we use to power the agency-style "where do we stand?" dashboards customers have been building on top of the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pull historical scores for any URL
&lt;/h2&gt;

&lt;p&gt;If your account has been running audits on a paid plan, every audit's full score breakdown is already in our database. &lt;code&gt;history()&lt;/code&gt; returns the full timeseries plus a summary block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;seoscoreapi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://acme.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; audits tracked`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`First: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;first_score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Latest: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latest_score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Total delta: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total_delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retention windows are tied to your tier — Starter sees 30 days, Basic 90, Pro a year, Ultra unlimited. The deeper write-up on what's in the history block is in the &lt;a href="https://dev.to/blog/historical-seo-score-tracking"&gt;historical comparison post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. One-shot view of every domain you've audited
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;historyDomains()&lt;/code&gt; returns one row per domain on your key, with the latest score and a 30-day trend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;historyDomains&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;seoscoreapi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;historyDomains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trend_30d&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trend_30d&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Worst trends this month:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;arrow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trend_30d&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;↓&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;↑&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latest_score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;arrow&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trend_30d&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Single API call, one row per domain. Use it for a Slack digest, an agency dashboard, or to feed an LLM that's writing a quarterly recap.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Get Slack alerts on score drops
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;addMonitor()&lt;/code&gt; now takes a &lt;code&gt;webhookUrl&lt;/code&gt; and &lt;code&gt;alertThreshold&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addMonitor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;seoscoreapi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;addMonitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://acme.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;daily&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;webhookUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://hooks.slack.com/services/T0/B0/xxxx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alertThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// alert when the score drops 5+ points&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the webhook URL points at &lt;code&gt;hooks.slack.com&lt;/code&gt;, the payload is auto-formatted as Block Kit so it renders cleanly. Any other https endpoint gets the raw event JSON — wire it into PagerDuty, Discord, n8n, or your own service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three places to drop this into a Node project
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Express middleware that runs an SEO audit on demand
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;audit&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;seoscoreapi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/seo-check&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;top_issues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priorities&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;result.history.delta&lt;/code&gt; field is what makes this useful in production: every audit response on a paid plan tells you whether the score moved since last time and by how much.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next.js API route for an internal dashboard
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/seo-snapshot/route.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;historyDomains&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;seoscoreapi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;historyDomains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latest_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latest_grade&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;trend_30d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trend_30d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire backend for an internal "all our brands at a glance" page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stand-alone weekly CLI you run from cron
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;historyDomains&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;seoscoreapi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;historyDomains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dropped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trend_30d&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;All domains stable.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; domains down 2+ points:`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;latest_score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trend_30d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; 30d)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pipe the output to email or wire it to a Slack incoming webhook, schedule it weekly with &lt;code&gt;cron&lt;/code&gt; or a CI pipeline, and you've got an automated Monday-morning report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading from 1.2
&lt;/h2&gt;

&lt;p&gt;Strict additive update. &lt;code&gt;addMonitor()&lt;/code&gt; accepts the new options object form &lt;em&gt;or&lt;/em&gt; the legacy &lt;code&gt;(url, apiKey, "daily")&lt;/code&gt; string form, so existing code keeps working. The new functions are pure additions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;seoscoreapi@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full reference is in the &lt;a href="https://github.com/avansledright/seoscoreapi.com/tree/main/sdks/node" rel="noopener noreferrer"&gt;README&lt;/a&gt; and at &lt;a href="https://dev.to/docs"&gt;/docs&lt;/a&gt;. If you don't have a key yet, &lt;a href="https://dev.to/"&gt;grab a free one&lt;/a&gt; or &lt;a href="https://dev.to/upgrade?tier=starter"&gt;start on Starter for $5/mo&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>seo</category>
      <category>api</category>
      <category>ai</category>
    </item>
    <item>
      <title>Python SDK 1.3: Historical Trends, URL Comparisons, and Slack Alerts in Three Lines Each</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 13 Jul 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/avansledright/python-sdk-13-historical-trends-url-comparisons-and-slack-alerts-in-three-lines-each-4igo</link>
      <guid>https://dev.to/avansledright/python-sdk-13-historical-trends-url-comparisons-and-slack-alerts-in-three-lines-each-4igo</guid>
      <description>&lt;p&gt;The Python client for SEO Score API hit &lt;code&gt;1.3.0&lt;/code&gt; today. It picks up everything we shipped through the latest sprint: side-by-side URL comparison, the &lt;code&gt;/history&lt;/code&gt; and &lt;code&gt;/history/domains&lt;/code&gt; endpoints, and webhook alerts (Slack-formatted by default) on the monitor system.&lt;/p&gt;

&lt;p&gt;If you already have &lt;code&gt;seoscoreapi&lt;/code&gt; installed, upgrade with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; seoscoreapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the rest of this post is a tour of what the new functions actually do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in 1.3
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compare(urls, api_key)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /compare&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Basic+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;history(url, api_key, limit=100, since=None)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /history&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Starter+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;history_domains(api_key)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;GET /history/domains&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Starter+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;add_monitor(..., webhook_url, alert_threshold)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;POST /monitors&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;competitive_audit()&lt;/code&gt; and &lt;code&gt;report_url()&lt;/code&gt; were already in 1.2 — they're listed here for completeness because they pair well with the new history functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Compare your page against competitors in one call
&lt;/h2&gt;

&lt;p&gt;Before 1.3, doing a head-to-head comparison meant calling &lt;code&gt;/audit&lt;/code&gt; for each URL and computing diffs yourself. Now &lt;code&gt;compare()&lt;/code&gt; does it server-side and returns a structured &lt;code&gt;diff&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;seoscoreapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;compare&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://acme.com/pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://competitor-a.com/pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://competitor-b.com/pricing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url_data&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;grade&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Gap analysis:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;diff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category_gaps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: leader is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;leader&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (+&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;gap&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;compare()&lt;/code&gt; accepts 2–5 URLs in a single request and returns each URL's full audit, plus the &lt;code&gt;diff&lt;/code&gt; object that tells you who is ahead in every category. It's Basic plan ($15/mo) and up.&lt;/p&gt;

&lt;p&gt;This is the hook agency people have been asking for — drop it into a Streamlit or Slack-bot and you've got a "where do we stand against the top 3 competitors?" tool in 30 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pull the full audit history for any URL
&lt;/h2&gt;

&lt;p&gt;If you've been auditing a URL on a paid plan, every audit's score breakdown is already in our database. &lt;code&gt;history()&lt;/code&gt; returns the full timeseries plus a summary block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;seoscoreapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://acme.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tracked: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; audits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  First: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;first_score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Latest: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;latest_score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Total delta: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_delta&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Min/Max: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;min_score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;summary&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max_score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Plot it
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;
&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;savefig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acme-trend.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;history()&lt;/code&gt; accepts &lt;code&gt;limit&lt;/code&gt; and &lt;code&gt;since&lt;/code&gt; parameters if you want to scope the response. The retention windows are tied to your tier:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Retention&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Starter ($5/mo)&lt;/td&gt;
&lt;td&gt;30 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic ($15/mo)&lt;/td&gt;
&lt;td&gt;90 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro ($39/mo)&lt;/td&gt;
&lt;td&gt;1 year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultra ($99/mo)&lt;/td&gt;
&lt;td&gt;Unlimited&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a deeper write-up on what's in the history block, see the &lt;a href="https://dev.to/blog/historical-seo-score-tracking"&gt;historical comparison post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Get a one-shot view of every domain you track
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;history_domains()&lt;/code&gt; is the "all your stuff at a glance" call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;seoscoreapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;history_domains&lt;/span&gt;

&lt;span class="n"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;history_domains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Sort by 30-day trend, worst first
&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trend_30d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;domains&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;arrow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;↓&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trend_30d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;↑&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;latest_score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;arrow&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;trend_30d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One call, one row per domain you've ever audited. Use it for an agency-wide "who needs attention this week?" dashboard or a Slack digest.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Get alerted in Slack when a score drops
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;add_monitor()&lt;/code&gt; now takes &lt;code&gt;webhook_url&lt;/code&gt; and &lt;code&gt;alert_threshold&lt;/code&gt;. If the webhook URL is a Slack incoming-webhook, the payload is auto-formatted as Block Kit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;seoscoreapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;add_monitor&lt;/span&gt;

&lt;span class="nf"&gt;add_monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://acme.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;frequency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;daily&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;webhook_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://hooks.slack.com/services/T0/B0/xxxx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;alert_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# alert when the score drops 5+ points
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 5-point drop on Acme's homepage now pings your &lt;code&gt;#seo-alerts&lt;/code&gt; channel within minutes of the next scheduled audit run. Other https endpoints get the raw event JSON, so you can route alerts into PagerDuty, Discord, your own SIEM — anything that takes a webhook.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;alert_threshold&lt;/code&gt; is in score points, not percent. Five points is a reasonable starting value: small enough to catch real regressions, large enough to ignore the kind of noise that comes from a slow Core Web Vitals reading on a single audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete worked example: the 30-line agency monitor
&lt;/h2&gt;

&lt;p&gt;Putting it together, here's the entire script we run for a small in-house team. It uses every new function in 1.3:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;seoscoreapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;history_domains&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;compare&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Find URLs that dropped 2+ points this month
&lt;/span&gt;&lt;span class="n"&gt;domains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;history_domains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dropped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;domains&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trend_30d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trend_30d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;## &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; URLs to investigate&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 2. For each, pull the timeseries to see when the drop happened
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;### &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Compare your worst one against two competitors
&lt;/span&gt;&lt;span class="n"&gt;worst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dropped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;worst&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;domain&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://competitor-a.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://competitor-b.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;grade&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a daily report that used to take a half-day in spreadsheets, in 30 lines, billable as a fixed-price retainer add-on. The full SDK reference is in the &lt;a href="https://github.com/avansledright/seoscoreapi.com/tree/main/sdks/python" rel="noopener noreferrer"&gt;README&lt;/a&gt; and the auto-generated &lt;a href="https://dev.to/docs"&gt;API docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading from 1.2
&lt;/h2&gt;

&lt;p&gt;Nothing breaks. &lt;code&gt;add_monitor()&lt;/code&gt; gets two new optional kwargs, but every existing call works as-is. The new functions (&lt;code&gt;compare&lt;/code&gt;, &lt;code&gt;history&lt;/code&gt;, &lt;code&gt;history_domains&lt;/code&gt;) were either new in this release or added in 1.2 — if you've been on 1.2 already, this is a strict additive update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; seoscoreapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't have a key yet, &lt;a href="https://dev.to/"&gt;grab a free one&lt;/a&gt; or &lt;a href="https://dev.to/upgrade?tier=starter"&gt;start on Starter for $5/mo&lt;/a&gt; to get historical tracking on day one.&lt;/p&gt;

</description>
      <category>python</category>
      <category>seo</category>
      <category>api</category>
      <category>ai</category>
    </item>
    <item>
      <title>SEO Audit API for Developers: Complete Guide</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 06 Jul 2026 14:00:04 +0000</pubDate>
      <link>https://dev.to/avansledright/seo-audit-api-for-developers-complete-guide-2a7n</link>
      <guid>https://dev.to/avansledright/seo-audit-api-for-developers-complete-guide-2a7n</guid>
      <description>&lt;h1&gt;
  
  
  SEO Audit API for Developers: Complete Guide
&lt;/h1&gt;

&lt;p&gt;The SEO Score API is a REST API that runs a full technical SEO audit on any URL and returns a structured JSON response with a score, grade, and prioritized list of issues — covering meta tags, performance, accessibility, structured data, and AI readiness in a single call. For the endpoint overview, response shape, and full check list, see the &lt;a href="https://dev.to/seo-audit-api"&gt;SEO audit API&lt;/a&gt; reference; this guide walks through integrating it as a developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the SEO Score API?
&lt;/h2&gt;

&lt;p&gt;The SEO Score API performs instant, headless-browser-backed audits across 54 checks and returns structured results in under 5 seconds. There's no scraping to manage, no Playwright setup required on your end, and no rate-limit gymnastics — just a single GET request per URL. It's designed for developers who want to add SEO intelligence to their applications, dashboards, or CI/CD pipelines without building the audit engine themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does authentication work?
&lt;/h2&gt;

&lt;p&gt;Every request must include your API key in the &lt;code&gt;X-API-Key&lt;/code&gt; header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://seoscoreapi.com/audit?url=https://example.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: YOUR_KEY_HERE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can get a free key (2 audits/day) at &lt;a href="https://dev.to/#signup"&gt;seoscoreapi.com/#signup&lt;/a&gt; — no credit card required. Paid plans start at $5/mo for 200 audits/month.&lt;/p&gt;

&lt;h2&gt;
  
  
  The /audit Endpoint
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GET /audit?url={url}&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Response example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;74&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"grade"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priorities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"issue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Missing meta description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"recommendation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Add a 120–165 character meta description tag."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"issue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Page load time 2.8s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"recommendation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Optimize images and enable compression to get below 2s."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"audit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"canonical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"performance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"load_time_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"compression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accessibility"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"lang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"alt_tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"structured_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"json_ld"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"og_tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pass"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ai_readiness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"robots_ai"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"llms_txt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fail"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Python SDK Example
&lt;/h2&gt;

&lt;p&gt;Install the SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;seoscoreapi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run an audit in 5 lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;seoscoreapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signup&lt;/span&gt;

&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;you@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# one-time free key creation
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Score: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;score&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/100 (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;grade&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Iterate over issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priorities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;issue&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Fix: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;recommendation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  JavaScript / curl Examples
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Node.js (fetch):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://seoscoreapi.com/audit?url=https://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_KEY_HERE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Score: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/100 (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grade&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;curl with jq:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://seoscoreapi.com/audit?url=https://example.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: YOUR_KEY"&lt;/span&gt; | jq &lt;span class="s1"&gt;'{score, grade, issues: .priorities | length}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The 5 Audit Categories
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What it covers&lt;/th&gt;
&lt;th&gt;Example checks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Meta&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;On-page SEO fundamentals&lt;/td&gt;
&lt;td&gt;Title tag, meta description, canonical, robots meta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Speed and delivery signals&lt;/td&gt;
&lt;td&gt;Load time, compression, render-blocking scripts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Accessibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Usability and standards&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lang&lt;/code&gt; attribute, image alt text, heading structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Structured Data&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rich result eligibility&lt;/td&gt;
&lt;td&gt;JSON-LD presence, Open Graph tags, schema types&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI Readiness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LLM discoverability&lt;/td&gt;
&lt;td&gt;AI bot access in robots.txt, llms.txt, SSR, fact density&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On paid plans (Starter and above), three additional scoring dimensions are returned: &lt;strong&gt;SXO&lt;/strong&gt; (search experience), &lt;strong&gt;AEO&lt;/strong&gt; (answer engine eligibility), and &lt;strong&gt;AIO&lt;/strong&gt; (AI optimization) — each with their own score, grade, and check breakdown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Handling
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;401 Unauthorized&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Missing or invalid API key&lt;/td&gt;
&lt;td&gt;Check the &lt;code&gt;X-API-Key&lt;/code&gt; header value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;422 Unprocessable Entity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Invalid or missing URL parameter&lt;/td&gt;
&lt;td&gt;Ensure &lt;code&gt;url&lt;/code&gt; is a valid, fully-qualified URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;429 Too Many Requests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rate limit or daily quota exceeded&lt;/td&gt;
&lt;td&gt;Back off and retry, or upgrade your plan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All error responses include a JSON body with a &lt;code&gt;detail&lt;/code&gt; field explaining the specific problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free key&lt;/strong&gt; — &lt;a href="https://dev.to/#signup"&gt;Sign up here&lt;/a&gt; for 2 audits/day, no credit card&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python SDK&lt;/strong&gt; — &lt;code&gt;pip install seoscoreapi&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm SDK&lt;/strong&gt; — &lt;code&gt;npm install seoscoreapi&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid plans&lt;/strong&gt; — start at $5/mo for 200 audits/month at &lt;a href="https://dev.to/#pricing"&gt;seoscoreapi.com/#pricing&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the SEO Score API?
&lt;/h3&gt;

&lt;p&gt;The SEO Score API is a REST API that runs instant technical SEO audits on any URL using a headless browser. It returns a structured JSON response with a 0–100 score, letter grade, and prioritized issue list covering 54 checks across meta, performance, accessibility, structured data, and AI readiness.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you authenticate with the SEO Score API?
&lt;/h3&gt;

&lt;p&gt;Authentication uses an &lt;code&gt;X-API-Key&lt;/code&gt; header on every request. Get a free key at seoscoreapi.com — it gives you 2 audits per day with no credit card required. Paid plans starting at $5/mo increase limits to 200–25,000 audits per month.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does the /audit endpoint return?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;/audit&lt;/code&gt; endpoint returns a JSON object with &lt;code&gt;score&lt;/code&gt; (0–100), &lt;code&gt;grade&lt;/code&gt; (A+ to F), &lt;code&gt;priorities&lt;/code&gt; (an array of issues with severity and recommended fix), and a full &lt;code&gt;audit&lt;/code&gt; object broken into five categories. Paid plans also include &lt;code&gt;sxo&lt;/code&gt;, &lt;code&gt;aeo&lt;/code&gt;, and &lt;code&gt;aio&lt;/code&gt; scoring objects.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you handle 429 rate limit errors?
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;429&lt;/code&gt; response means you've hit either your per-minute rate limit (RPM) or your daily/monthly quota. Implement exponential backoff and retry after the interval suggested in the response. For sustained high-volume needs, upgrade to a higher plan — Pro allows 60 RPM and 5,000 audits/month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a Python or JavaScript SDK?
&lt;/h3&gt;

&lt;p&gt;Yes. Install the Python SDK with &lt;code&gt;pip install seoscoreapi&lt;/code&gt; and the JavaScript SDK with &lt;code&gt;npm install seoscoreapi&lt;/code&gt;. Both wrap the REST API with typed function calls, handle authentication, and return parsed response objects — no raw HTTP requests needed for common operations.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>SEO audits in Claude + Cursor: our new MCP server</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 29 Jun 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/avansledright/seo-audits-in-claude-cursor-our-new-mcp-server-1494</link>
      <guid>https://dev.to/avansledright/seo-audits-in-claude-cursor-our-new-mcp-server-1494</guid>
      <description>&lt;h1&gt;
  
  
  SEO audits in Claude + Cursor: our new MCP server
&lt;/h1&gt;

&lt;p&gt;We just shipped an &lt;a href="https://github.com/avansledright/seoscoreapi.com/tree/main/sdks/mcp" rel="noopener noreferrer"&gt;official Model Context Protocol server&lt;/a&gt;. Install it once and Claude Desktop, Claude Code, Cursor, Windsurf, and any MCP-aware AI tool gets direct access to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full SEO audits (&lt;code&gt;audit&lt;/code&gt;, &lt;code&gt;batch_audit&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Your monitoring (&lt;code&gt;add_monitor&lt;/code&gt;, &lt;code&gt;list_monitors&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Audit-score history (&lt;code&gt;history&lt;/code&gt;, &lt;code&gt;history_domains&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Public report URLs (&lt;code&gt;report_url&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Plan and usage (&lt;code&gt;usage&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observed backlinks&lt;/strong&gt; (&lt;code&gt;backlinks&lt;/code&gt;, Basic plan and up)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's nine tools your AI can reach for the moment you ask "audit this URL" or "what backlinks point at my-startup.com?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MCP matters for SEO
&lt;/h2&gt;

&lt;p&gt;Most AI tools today guess at SEO. The model has a fuzzy idea of what a good page looks like, an even fuzzier idea of how Google ranks anything in 2026, and zero idea what your specific page actually scores. So you get advice like &lt;em&gt;"make sure your meta description is between 150 and 160 characters"&lt;/em&gt; — true, generic, and unverifiable.&lt;/p&gt;

&lt;p&gt;MCP changes the shape of that conversation. The AI doesn't need to guess; it can call our scoring engine directly. The same 82-check audit your CI pipeline runs is now one tool call away from any prompt.&lt;/p&gt;

&lt;p&gt;The difference in practice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before MCP&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Hey Claude, can you audit my site?"&lt;/em&gt;&lt;br&gt;
"I can't access URLs directly, but here are some general tips: make sure your title tags are 50-60 characters, write descriptive meta descriptions..."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;After MCP&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Hey Claude, can you audit my site?"&lt;/em&gt;&lt;br&gt;
&lt;em&gt;(Claude calls the &lt;code&gt;audit&lt;/code&gt; tool against &lt;code&gt;https://seoscoreapi.com&lt;/code&gt;.)&lt;/em&gt;&lt;br&gt;
"Your site scores 97.9 / A+. The only category with a flag is meta — your homepage description is 154 chars, well within range. AI readability scores 96/A+. The top priority would be... actually you have zero priorities right now. You're clean."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not a parlor trick. That's grounded answer-engine output, and it's exactly the kind of answer LLMs are starting to be measured on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install in 30 seconds
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;-y&lt;/span&gt; seoscoreapi-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, in your AI client's config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"seoscoreapi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"seoscoreapi-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"SEO_SCORE_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ssa_your_key_here"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://dev.to/#signup"&gt;Get a free API key.&lt;/a&gt; The free tier covers 2 audits per day with no card required — enough to wire up the MCP server and run your first batch.&lt;/p&gt;

&lt;p&gt;Full setup instructions for &lt;a href="https://github.com/avansledright/seoscoreapi.com/tree/main/sdks/mcp" rel="noopener noreferrer"&gt;Claude Desktop, Claude Code, Cursor, and Windsurf&lt;/a&gt; are in the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can ask once it's installed
&lt;/h2&gt;

&lt;p&gt;Real prompts that route through the MCP tools without you specifying which one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Audit &lt;a href="https://stripe.com" rel="noopener noreferrer"&gt;https://stripe.com&lt;/a&gt; and tell me the top 3 things to fix."&lt;/p&gt;

&lt;p&gt;"Compare the AI readability scores of stripe.com, square.com, and adyen.com."&lt;/p&gt;

&lt;p&gt;"What backlinks have we observed pointing at my-startup.com?"&lt;/p&gt;

&lt;p&gt;"Show me the score history for our marketing site over the last 30 days."&lt;/p&gt;

&lt;p&gt;"Are any of my monitored URLs trending down?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI picks the right tool. You don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;backlinks&lt;/code&gt; tool deserves its own paragraph
&lt;/h2&gt;

&lt;p&gt;This is unique to SEO Score API. Every audit our customers run contributes to a growing backlink graph: external links observed on each audited page get persisted into a public dataset. The &lt;code&gt;backlinks&lt;/code&gt; tool surfaces that dataset for any domain, with an explicit &lt;code&gt;data_caveat&lt;/code&gt; in every response.&lt;/p&gt;

&lt;p&gt;It is not a comprehensive backlink index — Ahrefs crawls 8 billion pages a day and we don't, and we say so. It is honest, audit-fed, and grows with usage. For "show me referring pages we have actually seen," it works exactly the way you'd want.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; What backlinks does SEO Score API see pointing to stripe.com?

(via `backlinks` tool)

stripe.com:
  total observations: 142
  referring domains: 38
  top referring: docs.stripe.com (24), github.com (18), ...
  data_caveat: Backlinks observed during SEO Score API audits.
  Not exhaustive — coverage grows over time.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No competitor with our pricing offers anything like this, because no competitor has the audit volume we do feeding the graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we're shipping this now
&lt;/h2&gt;

&lt;p&gt;Two reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One:&lt;/strong&gt; every developer audience we care about is moving into MCP-aware tools. Claude Desktop has it, Claude Code has it, Cursor has it, Windsurf has it, the OpenAI agents framework has it. If our API isn't reachable from the place customers actually do their work in 2026, we're invisible — even when we're the right answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two:&lt;/strong&gt; MCP is the cleanest possible packaging of the API. There's no SDK to learn, no auth dance to script, no batching to build. The user types a question, the AI picks the tool, the API answers. Friction approaches zero. That's the kind of leverage we want.&lt;/p&gt;

&lt;p&gt;We have official Python and Node SDKs. We have a &lt;a href="https://dev.to/blog/github-actions-seo-audit"&gt;GitHub Action for CI&lt;/a&gt;. We have an &lt;a href="https://dev.to/blog/n8n-seo-automation"&gt;n8n community node&lt;/a&gt;. The MCP server is the same idea — meet developers where they already are — pointed at AI clients instead of CI runners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Source + license
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;seoscoreapi-mcp&lt;/code&gt; is MIT-licensed and open source. The whole server is one file you can read in five minutes:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/avansledright/seoscoreapi.com/tree/main/sdks/mcp" rel="noopener noreferrer"&gt;https://github.com/avansledright/seoscoreapi.com/tree/main/sdks/mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you build something on top of it, send us the link. We'll feature interesting integrations on the blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/#signup"&gt;Get a free API key&lt;/a&gt; — 30 seconds, no card.&lt;/li&gt;
&lt;li&gt;Add the MCP config to your AI client of choice.&lt;/li&gt;
&lt;li&gt;Restart the client.&lt;/li&gt;
&lt;li&gt;Ask: "Audit &lt;a href="https://example.com" rel="noopener noreferrer"&gt;https://example.com&lt;/a&gt;."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the entire onboarding.&lt;/p&gt;

</description>
      <category>python</category>
      <category>seo</category>
      <category>api</category>
      <category>ai</category>
    </item>
    <item>
      <title>What We're Seeing After 8,000 SEO Audits</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Thu, 25 Jun 2026 19:00:13 +0000</pubDate>
      <link>https://dev.to/avansledright/what-were-seeing-after-8000-seo-audits-395p</link>
      <guid>https://dev.to/avansledright/what-were-seeing-after-8000-seo-audits-395p</guid>
      <description>&lt;p&gt;When you operate an SEO audit API, you end up with a strange vantage point. You don't see the full web — you see whatever sites people are bothering to check. That sample is biased: agencies running their client roster, developers wiring up CI checks, builders auditing their own pages. It is not a random crawl of the internet.&lt;/p&gt;

&lt;p&gt;But it &lt;em&gt;is&lt;/em&gt; a fairly honest cross-section of the sites that someone, somewhere, cared enough about to audit. And after running roughly 8,000 audits, the patterns are remarkably consistent.&lt;/p&gt;

&lt;p&gt;Here's what we're seeing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most sites land in the same narrow band
&lt;/h2&gt;

&lt;p&gt;The single most surprising finding wasn't about a specific issue. It was about how &lt;em&gt;little&lt;/em&gt; spread there is in scores.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Average score:&lt;/strong&gt; ~73 / 100&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Median:&lt;/strong&gt; ~73&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;~90% of all audits land between 70 and 79&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Roughly 8% score 80 or higher. Almost nothing scores below 60. The web isn't bimodal between "great" and "terrible" sites — it's a giant middle. Most sites are doing the basics, missing the same handful of intermediate things, and getting a soft B as a result.&lt;/p&gt;

&lt;p&gt;That tight clustering matters strategically: if you're operating in this range, the work to move from a 73 to an 85 is mostly &lt;em&gt;the same five fixes&lt;/em&gt; applied to almost every site we see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five issues that show up almost everywhere
&lt;/h2&gt;

&lt;p&gt;When we tally the top-five priority issues flagged on each audit, five items dominate — each appearing on roughly &lt;strong&gt;9 out of 10&lt;/strong&gt; audits we've run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;th&gt;Share of audits flagging it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No structured data (schema.org)&lt;/td&gt;
&lt;td&gt;~94%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No sitemap.xml&lt;/td&gt;
&lt;td&gt;~93%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing meta description&lt;/td&gt;
&lt;td&gt;~92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No canonical tag&lt;/td&gt;
&lt;td&gt;~92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thin content on the audited page&lt;/td&gt;
&lt;td&gt;~88%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These aren't exotic problems. They're not algorithm-update edge cases. They're the items every SEO checklist has had on it for a decade. And yet they're nearly universal.&lt;/p&gt;

&lt;p&gt;A few notes on what's going on under each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured data&lt;/strong&gt; isn't optional anymore. Google's AI Overviews, ChatGPT search citations, and Perplexity's source picker all lean on schema to disambiguate what a page is &lt;em&gt;about&lt;/em&gt;. A site without &lt;code&gt;Article&lt;/code&gt;, &lt;code&gt;Organization&lt;/code&gt;, or &lt;code&gt;Product&lt;/code&gt; schema is asking AI search to guess.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sitemap.xml&lt;/strong&gt; is the cheapest crawl-budget signal you can give a search engine, and most sites still don't have one, or have one that isn't referenced from &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta description&lt;/strong&gt; isn't a ranking factor — but it's a click-through factor, which is a downstream ranking factor. Letting Google auto-generate it from page content is leaving CTR on the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canonical tags&lt;/strong&gt; are the single most reliable way to prevent self-inflicted duplicate-content problems. Most sites still don't set them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thin content&lt;/strong&gt; here usually means the audited URL itself was light on text — landing pages, footer-linked utility pages, "about" stubs. Worth deciding whether those pages should be indexed at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance is &lt;em&gt;not&lt;/em&gt; where most sites are losing
&lt;/h2&gt;

&lt;p&gt;This one cut against my expectations. The performance category — request timing, response size, a basic look at render readiness — averages around &lt;strong&gt;97 / 100&lt;/strong&gt; across all audits.&lt;/p&gt;

&lt;p&gt;The "your site is slow" narrative is mostly outdated for the modern web. Cloudflare, Vercel, Netlify, and standard CDN setups have made fast-by-default the norm. The sites failing on performance are now the exception, not the rule.&lt;/p&gt;

&lt;p&gt;Where they still exist, the failure mode is almost always the same: a heavy WordPress install with a page-builder theme stacking 30+ render-blocking scripts. The fix isn't tuning — it's a different stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The social / AI surface area is broken almost everywhere
&lt;/h2&gt;

&lt;p&gt;Inversely, the social score average sits at around &lt;strong&gt;41 / 100&lt;/strong&gt;. This is the metadata that controls how a page renders when shared in Slack, LinkedIn, iMessage, X, Facebook — and increasingly, how AI search engines build their preview cards.&lt;/p&gt;

&lt;p&gt;The pattern: sites have &lt;em&gt;some&lt;/em&gt; OpenGraph tags (often whatever the CMS auto-generates), but they're missing &lt;code&gt;og:image&lt;/code&gt;, &lt;code&gt;twitter:card&lt;/code&gt;, structured author data, and consistent canonical URLs. The result is that when a link gets shared — which is increasingly the dominant traffic source for content sites — the unfurl looks like a 2012 Reddit post.&lt;/p&gt;

&lt;p&gt;This is the highest ROI fix in the whole list, because it's the one your traffic source actually sees first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the numbers don't tell you
&lt;/h2&gt;

&lt;p&gt;A few honest caveats, because the data has limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The sample skews toward sites being audited.&lt;/strong&gt; Sites run through an audit API are disproportionately professional/SaaS/agency clients. Genuinely abandoned or amateur sites are underrepresented.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeated audits of the same page count separately.&lt;/strong&gt; If a developer wires up an audit into CI, that one URL gets audited dozens of times. The aggregate is "audits run," not "unique sites."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Top-5 priorities is a per-audit ranking.&lt;/strong&gt; An issue showing up in 92% of audits' top 5 doesn't mean 92% of &lt;em&gt;sites&lt;/em&gt; have it — it means it's almost always severe enough to be flagged in the top 5 &lt;em&gt;when present&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So treat the numbers as &lt;strong&gt;directional&lt;/strong&gt; rather than &lt;strong&gt;demographic&lt;/strong&gt;. They tell you what shows up over and over when professionals look at the sites they care about. That's still useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actionable read
&lt;/h2&gt;

&lt;p&gt;If you're prioritizing SEO work in 2026, the data suggests a clear order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ship structured data first.&lt;/strong&gt; This is your AI search insurance policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix social/OG metadata.&lt;/strong&gt; Highest visibility per hour of effort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit canonical and sitemap setup.&lt;/strong&gt; Cheapest "crawl hygiene" wins available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stop worrying about base performance&lt;/strong&gt; unless you're on WordPress with a heavy theme. You probably already pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide which thin pages should be indexed&lt;/strong&gt; and noindex the rest, rather than trying to bulk them up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these are new. The takeaway from 8,000 audits isn't &lt;em&gt;what to do&lt;/em&gt; — it's &lt;em&gt;how few sites have done it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you want to see where your site sits in this distribution, &lt;a href="https://dev.to/"&gt;run an audit&lt;/a&gt; — it takes about 12 seconds.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
