<?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>SEO Monitor API: Score Tracking with Webhooks and Slack Alerts</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:00:02 +0000</pubDate>
      <link>https://dev.to/avansledright/seo-monitor-api-score-tracking-with-webhooks-and-slack-alerts-4f00</link>
      <guid>https://dev.to/avansledright/seo-monitor-api-score-tracking-with-webhooks-and-slack-alerts-4f00</guid>
      <description>&lt;p&gt;SEO monitoring is the practice of automatically auditing a URL on a recurring schedule and alerting you when the score changes significantly. Without monitoring, a broken meta tag, a removed H1, or a slow server after a deploy can sit undetected for days — long enough for Google to notice and adjust your rankings.&lt;/p&gt;

&lt;p&gt;SEO Score API is a REST API that monitors any URL every 6 hours and emails you when the score drops or rises by 5+ points, giving you a real-time quality gate on your site's SEO health.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does SEO Monitoring Matter?
&lt;/h2&gt;

&lt;p&gt;SEO monitoring matters because SEO problems are almost always introduced by changes — deploys, CMS updates, plugin installs, or CDN configuration changes. Common scenarios that monitoring catches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A developer deploys a new page template that strips the meta description&lt;/li&gt;
&lt;li&gt;A CMS update removes the canonical tag site-wide&lt;/li&gt;
&lt;li&gt;A CDN misconfiguration causes pages to load in 4+ seconds&lt;/li&gt;
&lt;li&gt;An OG image URL breaks after a media library migration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these would lower your SEO score by 10–30 points within hours. With monitoring, you know the same day it happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Does SEO Score Monitoring Work?
&lt;/h2&gt;

&lt;p&gt;SEO Score monitoring works by storing the URLs you want to watch, auditing each one automatically every 6 hours, comparing the new score to the previous score, and sending an email alert if the difference is 5 or more points.&lt;/p&gt;

&lt;p&gt;The full cycle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You register a URL via &lt;code&gt;POST /monitors&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The API audits it immediately to establish a baseline score&lt;/li&gt;
&lt;li&gt;Every 6 hours, the API re-audits the URL&lt;/li&gt;
&lt;li&gt;If the score changes by ±5 points, you receive an email with the delta and a list of what changed&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How Do You Set Up SEO Monitoring?
&lt;/h2&gt;

&lt;p&gt;You set up SEO monitoring by making a POST request to the monitors endpoint with your URL and API key.&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;-X&lt;/span&gt; POST https://seoscoreapi.com/monitors &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_api_key"&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;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url": "https://yoursite.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&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://yoursite.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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_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;Monitoring started. Baseline 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="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;List your active monitors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://seoscoreapi.com/monitors &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_api_key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Triggers a Monitoring Alert?
&lt;/h2&gt;

&lt;p&gt;A monitoring alert is triggered when the score for a monitored URL changes by 5 or more points between audit cycles. Alerts fire for both drops and improvements.&lt;/p&gt;

&lt;p&gt;The alert email includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The URL that changed&lt;/li&gt;
&lt;li&gt;The previous score and new score&lt;/li&gt;
&lt;li&gt;The point delta (e.g., −18 points)&lt;/li&gt;
&lt;li&gt;A list of checks that changed status since the last audit&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How Many URLs Can You Monitor Per Plan?
&lt;/h2&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;Price&lt;/th&gt;
&lt;th&gt;Monitored URLs&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;$5/mo&lt;/td&gt;
&lt;td&gt;5 URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;$15/mo&lt;/td&gt;
&lt;td&gt;10 URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$39/mo&lt;/td&gt;
&lt;td&gt;25 URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultra&lt;/td&gt;
&lt;td&gt;$99/mo&lt;/td&gt;
&lt;td&gt;50 URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Monitoring is not available on the free tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which URLs Should You Monitor?
&lt;/h2&gt;

&lt;p&gt;You should monitor your highest-value URLs first — pages that drive traffic, generate leads, or support paid campaigns. Recommended starting points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Homepage — highest-authority page, most vulnerable to template changes&lt;/li&gt;
&lt;li&gt;Top 5 organic traffic pages — most likely to affect revenue if rankings drop&lt;/li&gt;
&lt;li&gt;Product or service landing pages — especially if you run paid search to them&lt;/li&gt;
&lt;li&gt;Any page recently launched or redesigned&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How Is SEO Monitoring Different from Google Search Console?
&lt;/h2&gt;

&lt;p&gt;SEO monitoring via API is different from Google Search Console in timing and scope. Search Console reports what Google has already indexed — it reflects the past. Score monitoring catches problems in real time, before Google re-crawls the affected page.&lt;/p&gt;

&lt;p&gt;Key differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timing:&lt;/strong&gt; API monitoring alerts within 6 hours; Search Console data lags 1–7 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; API monitoring checks 54+ technical factors; Search Console focuses on crawl errors and search performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action:&lt;/strong&gt; API monitoring tells you exactly which check failed; Search Console tells you traffic dropped&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  How often does SEO monitoring run?
&lt;/h3&gt;

&lt;p&gt;SEO monitoring audits each registered URL every 6 hours. You will receive an alert within 6 hours of a score-changing event, such as a deploy that removes a meta tag or a server slowdown that degrades your performance score.&lt;/p&gt;

&lt;h3&gt;
  
  
  What plan do I need for SEO monitoring?
&lt;/h3&gt;

&lt;p&gt;SEO monitoring requires a paid plan. The Starter plan ($5/month) includes 5 monitored URLs. Basic ($15/month) includes 10, Pro ($39/month) includes 25, and Ultra ($99/month) includes 50. The free tier does not include monitoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I monitor competitor websites?
&lt;/h3&gt;

&lt;p&gt;Yes. The API monitors any publicly accessible URL, including competitor pages. Monitoring a competitor's key landing pages can alert you when they make significant SEO changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if a monitored URL goes down?
&lt;/h3&gt;

&lt;p&gt;If a monitored URL returns a 4xx or 5xx error, the audit records a failed response. A server error or 404 will cause a significant score drop and trigger an alert. The alert email will note the response code so you can distinguish an SEO regression from an availability issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does monitoring count against my audit quota?
&lt;/h3&gt;

&lt;p&gt;Yes, each scheduled monitoring audit counts against your monthly audit quota. On the Starter plan with 5 monitors, the system runs approximately 600 monitoring audits per month. Plan your monitor count relative to your monthly quota.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>What Is a Good SEO Score? 70+, and What Grades A–F Mean</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 19 Oct 2026 14:00:05 +0000</pubDate>
      <link>https://dev.to/avansledright/what-is-a-good-seo-score-70-and-what-grades-a-f-mean-1kfn</link>
      <guid>https://dev.to/avansledright/what-is-a-good-seo-score-70-and-what-grades-a-f-mean-1kfn</guid>
      <description>&lt;p&gt;An SEO score is a number from 0 to 100 that measures how well a single page is set up for search engines: its meta tags, technical setup, performance, social tags, and basic accessibility. A good SEO score is 70 or above, which is a B grade. Across the audits we have run, though, the average page already scores about 73, so 70 is closer to "normal" than "ahead". The score that separates a page from the pack is 80.&lt;/p&gt;

&lt;p&gt;SEO Score API is a REST API that runs 80+ automated checks on a URL and returns one composite score with a letter grade, so developers and agencies can measure and track SEO health in code. The score and the SEO grade bands below are the ones that API returns.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does an SEO Score Measure?
&lt;/h2&gt;

&lt;p&gt;An SEO score measures the technical and on-page health of one URL across five weighted categories. Each category returns its own sub-score, so you can see which area is pulling the total down. Meta and technical count for most of the score; social and accessibility count for the least.&lt;/p&gt;

&lt;p&gt;The five categories are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Meta and content&lt;/strong&gt;: title tag, &lt;a href="https://seoscoreapi.com/tools/meta-checker?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=what-is-a-good-seo-score" rel="noopener noreferrer"&gt;meta description&lt;/a&gt;, H1 and heading order, content length, readability, internal and external links&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical&lt;/strong&gt;: HTTPS and the SSL certificate, status code, response time, canonical tag, viewport, robots meta, &lt;a href="https://seoscoreapi.com/tools/robots-txt-tester?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=what-is-a-good-seo-score" rel="noopener noreferrer"&gt;robots.txt&lt;/a&gt;, sitemap, &lt;a href="https://seoscoreapi.com/tools/schema-validator?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=what-is-a-good-seo-score" rel="noopener noreferrer"&gt;structured data&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: HTML size, DOM size, compression, render-blocking CSS and scripts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social&lt;/strong&gt;: &lt;a href="https://seoscoreapi.com/tools/og-preview?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=what-is-a-good-seo-score" rel="noopener noreferrer"&gt;Open Graph tags&lt;/a&gt;, Twitter Card tags, favicon&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility&lt;/strong&gt;: the &lt;code&gt;lang&lt;/code&gt; attribute, alt text coverage on images, landmark elements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It does not measure backlinks, keyword rankings, domain authority, or whether the content is any good. Those decide a lot of where a page ranks, and no on-page score sees them.&lt;/p&gt;

&lt;p&gt;The accessibility category is a lightweight check within the SEO score. If you need a full legal-grade audit, our dedicated &lt;a href="https://seoscoreapi.com/blog/ada-accessibility-audit?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=what-is-a-good-seo-score" rel="noopener noreferrer"&gt;ADA compliance checker&lt;/a&gt; runs the complete WCAG 2.1 AA ruleset (via axe-core) and returns an accessibility score plus a lawsuit-risk assessment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is a Good SEO Score?
&lt;/h2&gt;

&lt;p&gt;A good SEO score is 70 or above. A score of 80 or higher is great, and 90 or higher is excellent.&lt;/p&gt;

&lt;p&gt;That is the grading answer. The practical answer comes from the data. In roughly 8,000 audits run through the API, the average score was about 73, about 90% of audits landed between 70 and 79, and about 8% reached 80 or higher. Almost nothing scored below 60. (The numbers and their caveats are in &lt;a href="https://seoscoreapi.com/blog/what-were-seeing-after-8000-audits?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=what-is-a-good-seo-score" rel="noopener noreferrer"&gt;what we're seeing after 8,000 audits&lt;/a&gt;; the sample is pages somebody chose to audit, not a random crawl of the web.)&lt;/p&gt;

&lt;p&gt;So a 73 means the basics are in place and the page has the same handful of gaps as nearly every other page. An 80 means you have closed them.&lt;/p&gt;

&lt;h2&gt;
  
  
  SEO Grades: What A+ Through F Mean
&lt;/h2&gt;

&lt;p&gt;An SEO grade is the letter attached to the score. The bands are fixed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SEO Grade&lt;/th&gt;
&lt;th&gt;Score Range&lt;/th&gt;
&lt;th&gt;What It Means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A+&lt;/td&gt;
&lt;td&gt;90–100&lt;/td&gt;
&lt;td&gt;Excellent. Every category is in good shape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;80–89&lt;/td&gt;
&lt;td&gt;Great. Ahead of about nine in ten audited pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;70–79&lt;/td&gt;
&lt;td&gt;Good, and the most common grade by far&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;60–69&lt;/td&gt;
&lt;td&gt;Below average. Usually one category is failing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;50–59&lt;/td&gt;
&lt;td&gt;Poor. Several basics are missing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;0–49&lt;/td&gt;
&lt;td&gt;Failing. Something is blocking the page outright&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things are worth knowing about how the grade is worked out. It comes from the weighted total, not from an average of the category grades, so a page can earn an A overall with a weak social score. And a grade says how many known problems the page has, not how it will rank: an A+ page with no links and nothing useful on it still will not rank.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is 75 a Good SEO Score?
&lt;/h3&gt;

&lt;p&gt;Yes, 75 is a good SEO score. It sits in the B band (70–79) and a little above the average of 73 in our audits. It is not holding the page back. The quickest gains from there come from whichever category has the lowest sub-score, which is the social category more often than any other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Good SEO Scores by Number: 60, 70, 80, 85, 90
&lt;/h3&gt;

&lt;p&gt;Here is a quick answer for the exact scores people ask about most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is 60 a good SEO score?&lt;/strong&gt; No. 60 is the bottom of the C band (60–69) and well below the pages we usually see; very few audits score that low. It normally means a whole category is failing, most often missing meta tags or no HTTPS. It is fixable in an afternoon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is 70 a good SEO score?&lt;/strong&gt; Yes. 70 is the start of the B band and the pass mark. It is also slightly below average, so treat it as a floor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is 80 a good SEO score?&lt;/strong&gt; Yes. 80 earns an &lt;strong&gt;A&lt;/strong&gt;, and only about 8% of the audits we run reach it. It is the target we would set for most pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is 85 a good SEO score?&lt;/strong&gt; Yes. 85 is a strong A. Past this point each extra point costs more work and buys less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is 90 a good SEO score?&lt;/strong&gt; Yes. 90 and above is an A+, and it is rare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is 100 a good SEO score?&lt;/strong&gt; It is a perfect one, and it is not worth chasing. See the FAQ below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not sure where your page lands? &lt;a href="https://seoscoreapi.com/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=what-is-a-good-seo-score" rel="noopener noreferrer"&gt;Check any URL's SEO score for free&lt;/a&gt;. No credit card required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Score Do You Need to Rank on Google?
&lt;/h2&gt;

&lt;p&gt;There is no score that gets a page ranked. Google does not use this score or anybody else's; an SEO score is a third-party checklist, and Google has no "SEO score" of its own. Pages with a 72 outrank pages with a 95 every day, because links, relevance, and content quality are not in the number.&lt;/p&gt;

&lt;p&gt;What the score tells you is whether something on the page is getting in the way: a &lt;code&gt;noindex&lt;/code&gt; left on by accident, a missing canonical, no HTTPS, a title Google has to rewrite. Those are worth fixing whatever the page is trying to rank for.&lt;/p&gt;

&lt;p&gt;Our view: get to 80, then stop working on the score and work on the content and the links. Going from 85 to 95 is usually wasted effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Check Your SEO Score?
&lt;/h2&gt;

&lt;p&gt;You can check your SEO score with the SEO Score API by making a single GET request. The API returns a full JSON audit in seconds, including your composite score, letter grade, and per-category breakdowns.&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://yoursite.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_api_key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response includes:&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;"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://yoursite.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;82&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;"A"&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;"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;90&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;"technical"&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;"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;78&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;"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="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;"social"&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;"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;85&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;"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;80&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;Sign up for a free API key at &lt;a href="https://seoscoreapi.com/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=what-is-a-good-seo-score" rel="noopener noreferrer"&gt;seoscoreapi.com&lt;/a&gt;. No credit card is needed. The free tier is 2 audits per day and returns the score, the grade, 2 checks per category, and the top 2 priorities; paid plans return every check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Causes a Low SEO Score?
&lt;/h2&gt;

&lt;p&gt;A low SEO score is caused by missing on-page elements or a broken technical setup. Five issues turn up among the top priorities on roughly nine out of ten audits we run:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No structured data (JSON-LD)&lt;/li&gt;
&lt;li&gt;No sitemap.xml&lt;/li&gt;
&lt;li&gt;No meta description&lt;/li&gt;
&lt;li&gt;No canonical tag&lt;/li&gt;
&lt;li&gt;Thin content on the audited page&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those five are why so many pages sit at 73. The issues that drag a page lower than that are less common and more serious:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Missing or empty title tag&lt;/li&gt;
&lt;li&gt;No H1&lt;/li&gt;
&lt;li&gt;HTTP instead of HTTPS, or a broken SSL certificate&lt;/li&gt;
&lt;li&gt;No viewport tag, so the page is not mobile-friendly&lt;/li&gt;
&lt;li&gt;A server response slower than a second&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Performance is rarely the problem. The performance category averages about 97 out of 100 in our audits, while the social category averages about 41, mostly from missing &lt;code&gt;og:image&lt;/code&gt; and Twitter Card tags.&lt;/p&gt;

&lt;p&gt;The audit response includes a &lt;code&gt;priorities&lt;/code&gt; array listing the failed checks in order, with what to fix for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Improve Your SEO Score?
&lt;/h2&gt;

&lt;p&gt;You improve your SEO score by fixing the specific issues flagged in your audit report, starting with the highest-impact failures. The fastest wins are usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a meta description.&lt;/strong&gt; It is not a ranking factor, but it is what searchers read before they click, and it is missing on most pages we audit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a canonical tag.&lt;/strong&gt; One line in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;, and it stops duplicate URLs competing with each other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add structured data.&lt;/strong&gt; An &lt;code&gt;Organization&lt;/code&gt;, &lt;code&gt;Article&lt;/code&gt;, or &lt;code&gt;Product&lt;/code&gt; JSON-LD block that matches the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish a sitemap.xml&lt;/strong&gt; and reference it from robots.txt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete the Open Graph and Twitter Card tags,&lt;/strong&gt; including &lt;code&gt;og:image&lt;/code&gt;. This is where the social sub-score is usually lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix the title tag.&lt;/strong&gt; Aim for 50–60 characters, unique per page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After fixing them, audit the same URL again to confirm the score moved. Teams that run the API in CI/CD catch regressions the same way: a deploy that drops the meta description fails the check before it reaches production.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Often Should You Audit Your SEO Score?
&lt;/h2&gt;

&lt;p&gt;You should audit your SEO score whenever you deploy changes to a page, and run scheduled audits at least weekly for important URLs. Score drops of 5+ points usually indicate a broken element introduced by a recent change: a template update, CMS migration, or plugin conflict.&lt;/p&gt;

&lt;p&gt;On paid plans, score monitoring re-audits a URL daily or weekly and alerts you by email or webhook when the score drops past a threshold you set.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  What is a good SEO score for a new website?
&lt;/h3&gt;

&lt;p&gt;A good SEO score for a new website is 70 or above, the same as for any other site; the score does not know how old the domain is. New sites tend to lose points on a missing sitemap, no structured data, and thin pages. Fix the title, meta description, H1, canonical, and Open Graph tags first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is an SEO score of 100 possible?
&lt;/h3&gt;

&lt;p&gt;Yes, 100 is possible, and it is rare: only about 8% of the audits we run reach even 80. A 100 means passing every check, including ones that depend on server response time. It is not a sensible goal. The last few points come from details that do not change how a page ranks.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is an SEO grade?
&lt;/h3&gt;

&lt;p&gt;An SEO grade is the letter version of an SEO score: A+ for 90–100, A for 80–89, B for 70–79, C for 60–69, D for 50–59, and F below 50. Different tools draw the bands differently, so a B from one tool is not the same as a B from another. Compare grades only within the same tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a higher SEO score guarantee better Google rankings?
&lt;/h3&gt;

&lt;p&gt;No. An SEO score measures technical and on-page factors. It does not measure backlinks, domain authority, or content quality, and Google does not use it. Fixing the issues behind a low score removes obstacles to indexing and ranking; it does not create the reasons to rank.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between an SEO score and a PageSpeed score?
&lt;/h3&gt;

&lt;p&gt;An SEO score is a composite measure across meta, technical, performance, social, and accessibility checks. A PageSpeed score (Google Lighthouse) measures only performance and Core Web Vitals. Our SEO score includes performance as one of five categories, giving you a broader picture.&lt;/p&gt;

&lt;h3&gt;
  
  
  How accurate are automated SEO scores?
&lt;/h3&gt;

&lt;p&gt;Automated SEO scores are reliable at what they check: whether a tag exists, how long it is, whether the page is served over HTTPS, how large the HTML is. They cannot judge whether the content answers the query or whether the title is persuasive. Treat the score as a defect count for the page's setup, not a verdict on the page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I check my competitor's SEO score?
&lt;/h3&gt;

&lt;p&gt;Yes. The SEO Score API audits any publicly accessible URL, including competitor pages, so you can compare your score with theirs and see which checks they pass that you do not.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Free SEO Score Checker: Audit Any Website</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 12 Oct 2026 14:00:05 +0000</pubDate>
      <link>https://dev.to/avansledright/free-seo-score-checker-audit-any-website-1eab</link>
      <guid>https://dev.to/avansledright/free-seo-score-checker-audit-any-website-1eab</guid>
      <description>&lt;p&gt;SEO Score API is a REST API that scores any URL on 50+ SEO and AI readability checks and returns a score from 0 to 100 with a letter grade — for free. The free tier gives you 2 audits per day with a score for every category: meta tags, technical SEO, performance, social signals, and accessibility. The free response details 2 checks per category and the top 2 fixes; paid plans return every check and fix. No credit card required.&lt;/p&gt;

&lt;p&gt;A free SEO score checker is a tool that analyzes your website and returns an objective measurement of its search optimization health. Unlike manual audits that take hours, an API-based checker usually returns results in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does the Free SEO Checker Include?
&lt;/h2&gt;

&lt;p&gt;The free tier scores your page on the same 50+ core checks across five categories that paid plans use. The free response shows every score, 2 checks per category in detail and the top 2 fixes; paid plans return every check and fix. The free limit is 2 audits per day.&lt;/p&gt;

&lt;p&gt;The five scoring categories are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Meta&lt;/strong&gt; — title tag, meta description, canonical, robots, viewport&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical&lt;/strong&gt; — H1, structured data (JSON-LD), sitemap detection, hreflang, HTTPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt; — server response time, compression, resource loading, redirect count&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Social&lt;/strong&gt; — Open Graph title/description/image, Twitter Card tags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility&lt;/strong&gt; — image alt text, ARIA landmarks, language attribute, form labels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each category returns a sub-score (0–100) and letter grade so you can see exactly where your site excels and where it falls short.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Do You Get a Free SEO Score?
&lt;/h2&gt;

&lt;p&gt;You get a free SEO score by signing up for an API key, then making a single GET request to the audit endpoint. The entire process takes under two minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Sign up for a free key:&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;-X&lt;/span&gt; POST https://seoscoreapi.com/signup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email": "you@example.com"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2 — Run your first audit:&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="s2"&gt;"https://seoscoreapi.com/audit?url=https://yoursite.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;&lt;strong&gt;Step 3 — Read your results:&lt;/strong&gt;&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;"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://yoursite.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;76&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;"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 og:image tag"&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;"medium"&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;The &lt;code&gt;priorities&lt;/code&gt; array lists every individual check failure with a plain-English description of what to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Does the Audit Actually Check?
&lt;/h2&gt;

&lt;p&gt;The audit covers 50+ individual checks on the free tier and 80+ on paid plans. Here are some of the most impactful ones:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meta checks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Title tag present and within 50–60 characters&lt;/li&gt;
&lt;li&gt;Meta description present and within 150–160 characters&lt;/li&gt;
&lt;li&gt;Canonical URL set correctly&lt;/li&gt;
&lt;li&gt;Viewport meta tag present (mobile optimization)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Technical checks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single H1 tag present&lt;/li&gt;
&lt;li&gt;JSON-LD structured data detected&lt;/li&gt;
&lt;li&gt;HTTPS enforced&lt;/li&gt;
&lt;li&gt;XML sitemap detectable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Performance checks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server response time under 600ms&lt;/li&gt;
&lt;li&gt;GZIP or Brotli compression enabled&lt;/li&gt;
&lt;li&gt;No 4xx or 5xx response codes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Social checks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;og:title&lt;/code&gt;, &lt;code&gt;og:description&lt;/code&gt;, &lt;code&gt;og:image&lt;/code&gt; all present&lt;/li&gt;
&lt;li&gt;Twitter Card meta tags present&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Accessibility checks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All images have alt attributes&lt;/li&gt;
&lt;li&gt;Page has a &lt;code&gt;lang&lt;/code&gt; attribute on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Heading hierarchy is logical (H1 → H2 → H3)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Free vs. Paid: What Is the Difference?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Free&lt;/th&gt;
&lt;th&gt;Starter ($5/mo)&lt;/th&gt;
&lt;th&gt;Basic ($15/mo)&lt;/th&gt;
&lt;th&gt;Pro ($39/mo)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Audits&lt;/td&gt;
&lt;td&gt;2/day&lt;/td&gt;
&lt;td&gt;200/mo&lt;/td&gt;
&lt;td&gt;1,000/mo&lt;/td&gt;
&lt;td&gt;5,000/mo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests per minute&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scored on the 50+ core checks&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every check and fix in the response&lt;/td&gt;
&lt;td&gt;2 per category, top 2 fixes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SXO, AEO &amp;amp; AIO scores&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch audits&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;URL monitoring&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;5 URLs&lt;/td&gt;
&lt;td&gt;10 URLs&lt;/td&gt;
&lt;td&gt;25 URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For one-off checks, competitor research, or testing integrations, the free tier is all you need. For agencies running audits across client portfolios, paid plans provide the volume and extended scoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Can I Use the Free Checker in My Projects?
&lt;/h2&gt;

&lt;p&gt;You can use the free SEO checker in any project that can make HTTP requests. Common use cases include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-launch checks&lt;/strong&gt; — audit your site before going live to catch missing meta tags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitor research&lt;/strong&gt; — score competitor URLs to identify gaps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CMS testing&lt;/strong&gt; — verify that theme or plugin changes didn't break SEO elements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio reviews&lt;/strong&gt; — quickly assess the SEO health of sites you're pitching on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD gates&lt;/strong&gt; — fail deployments if the score drops below a threshold&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Python SDK makes integration especially fast:&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;signup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audit&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;# emails a 6-digit code
&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify&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="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Code from the email: &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://yoursite.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;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; (&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;h2&gt;
  
  
  Is the Free SEO Checker Accurate?
&lt;/h2&gt;

&lt;p&gt;Yes. The free tier uses the same analysis engine as all paid plans — headless Chromium via Playwright, which renders JavaScript before scoring. This means the audit sees your page the way Google's crawler does, not just the raw HTML source.&lt;/p&gt;

&lt;p&gt;The score reflects technical and on-page factors. It does not measure backlinks, domain authority, or content quality — those require separate tools.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Is the free SEO checker really free?
&lt;/h3&gt;

&lt;p&gt;Yes, the free tier is genuinely free with no credit card required. You get 2 audits per day, each scored on 50+ checks and returned as JSON. Compared to paid plans, the free response details 2 checks per category and the top 2 fixes, and it leaves out SXO/AEO/AIO extended scoring, history, and batch auditing; audit volume is lower too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to install anything to use the free checker?
&lt;/h3&gt;

&lt;p&gt;No installation is required. The SEO Score API is a REST API — you make HTTP requests from any language, terminal, or tool. There is also a Python SDK (&lt;code&gt;pip install seoscoreapi&lt;/code&gt;) and a Node.js SDK (&lt;code&gt;npm install seoscoreapi&lt;/code&gt;) if you prefer a client library.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does a free SEO audit take?
&lt;/h3&gt;

&lt;p&gt;A free SEO audit usually returns in seconds. Pages with heavy JavaScript or slow server responses take longer. The API uses headless Chromium to render the page before scoring, which accounts for most of the processing time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I check any website's SEO score for free?
&lt;/h3&gt;

&lt;p&gt;Yes. You can audit any publicly accessible URL — your own site, competitor sites, or client sites. URLs behind login walls or on private networks cannot be audited.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between free and paid plans?
&lt;/h3&gt;

&lt;p&gt;The free plan limits you to 2 audits per day and 2 requests per minute. Paid plans start at $5/month for 200 audits and add SXO/AEO/AIO extended scores, batch auditing (multiple URLs in one request), and URL monitoring with score-drop alerts.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I upgrade from the free plan?
&lt;/h3&gt;

&lt;p&gt;Log in to your developer portal at &lt;a href="https://seoscoreapi.com/portal?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=free-seo-score-checker" rel="noopener noreferrer"&gt;seoscoreapi.com/portal&lt;/a&gt; with your registered email. From there you can upgrade to any paid plan via Stripe. Your API key stays the same — limits are updated immediately on upgrade.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Fail the build when SEO breaks: SEO checks in CI with GitHub Actions</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Thu, 08 Oct 2026 12:58:53 +0000</pubDate>
      <link>https://dev.to/avansledright/fail-the-build-when-seo-breaks-seo-checks-in-ci-with-github-actions-532o</link>
      <guid>https://dev.to/avansledright/fail-the-build-when-seo-breaks-seo-checks-in-ci-with-github-actions-532o</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; to run SEO checks in CI, add the &lt;a href="https://github.com/SeoScoreAPI/seo-audit-action" rel="noopener noreferrer"&gt;&lt;code&gt;SeoScoreAPI/seo-audit-action&lt;/code&gt;&lt;/a&gt; step to a GitHub Actions workflow with a URL, an API key and a &lt;code&gt;threshold&lt;/code&gt;. It calls the SEO Score API &lt;code&gt;GET /audit&lt;/code&gt; endpoint, writes the score to the job summary, and exits non-zero when the score is under the threshold (default 80). A free key covers 2 audits a day, so on the free tier run it on pushes to &lt;code&gt;main&lt;/code&gt;, not on every pull request.&lt;/p&gt;

&lt;p&gt;I'm Aaron, the solo developer behind &lt;a href="https://seoscoreapi.com" rel="noopener noreferrer"&gt;SEO Score API&lt;/a&gt; and this action. This post is the workflow I'd actually commit, plus the parts that tend to surprise people.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I run an SEO audit in GitHub Actions?
&lt;/h2&gt;

&lt;p&gt;Get a key (free, email plus a 6-digit code, no card), add it as a repository secret named &lt;code&gt;SEO_SCORE_API_KEY&lt;/code&gt;, and commit this as &lt;code&gt;.github/workflows/seo.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;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;workflow_dispatch&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;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&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;Print result&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;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo "Score ${{ steps.seo.outputs.score }} (${{ steps.seo.outputs.grade }}), report ${{ steps.seo.outputs.report-url }}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole thing. No checkout step is needed, because the action audits a URL, not your repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the SEO audit action do?
&lt;/h2&gt;

&lt;p&gt;It is a composite action: a shell step that runs &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt;. You can read all of it in &lt;a href="https://github.com/SeoScoreAPI/seo-audit-action/blob/v1/action.yml" rel="noopener noreferrer"&gt;&lt;code&gt;action.yml&lt;/code&gt;&lt;/a&gt;. The inputs and outputs for the standard audit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;td&gt;The page to audit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;api-key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;td&gt;Your SEO Score API key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;threshold&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;80&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum score to pass, 0-100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fail-on-threshold&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set to &lt;code&gt;false&lt;/code&gt; to report without failing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Outputs are &lt;code&gt;score&lt;/code&gt;, &lt;code&gt;grade&lt;/code&gt;, &lt;code&gt;issues&lt;/code&gt; (the number of fixes returned) and &lt;code&gt;report-url&lt;/code&gt;. The score is a decimal such as &lt;code&gt;65.567&lt;/code&gt;, and the action compares it numerically. There are also &lt;code&gt;deep-audit-*&lt;/code&gt; inputs for the slower Deep Site Audit (Pro and Ultra plans), which I'm leaving out here.&lt;/p&gt;

&lt;p&gt;The audit itself is the same &lt;code&gt;GET /audit&lt;/code&gt; call you can make by hand: 55 checks per URL on the free tier, 83 on paid plans, across meta and content, technical SEO, social tags, performance, accessibility and AI readability.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I fail the build on specific checks, not just the score?
&lt;/h2&gt;

&lt;p&gt;A score threshold catches gradual decay. It is weak at catching one fatal change, because a single check is averaged in with dozens of others.&lt;/p&gt;

&lt;p&gt;For that, skip the action, call the API once, and gate on both the score and a list of named checks. This is a complete workflow:&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 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;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;workflow_dispatch&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-gate&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;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&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;SITE_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;MIN_SCORE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80"&lt;/span&gt;
      &lt;span class="na"&gt;NO_FAIL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;["status_code","https","title","meta_description","h1"]'&lt;/span&gt;
      &lt;span class="na"&gt;NO_WARN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;["canonical","robots_meta"]'&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;Run 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;SEOSCORE_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;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;curl -sS --fail-with-body --max-time 120 -G \&lt;/span&gt;
            &lt;span class="s"&gt;-H "X-API-Key: $SEOSCORE_API_KEY" \&lt;/span&gt;
            &lt;span class="s"&gt;--data-urlencode "url=$SITE_URL" \&lt;/span&gt;
            &lt;span class="s"&gt;-o audit.json "https://seoscoreapi.com/audit" \&lt;/span&gt;
            &lt;span class="s"&gt;|| { cat audit.json; exit 1; }&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;Enforce score and required checks&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;SCORE=$(jq -r '.score' audit.json)&lt;/span&gt;
          &lt;span class="s"&gt;BROKEN=$(jq -r --argjson nofail "$NO_FAIL" --argjson nowarn "$NO_WARN" '&lt;/span&gt;
            &lt;span class="s"&gt;.audit[].checks[]&lt;/span&gt;
            &lt;span class="s"&gt;| select(&lt;/span&gt;
                &lt;span class="s"&gt;(.status == "fail" and (.name | IN($nofail[], $nowarn[])))&lt;/span&gt;
                &lt;span class="s"&gt;or (.status == "warning" and (.name | IN($nowarn[])))&lt;/span&gt;
              &lt;span class="s"&gt;)&lt;/span&gt;
            &lt;span class="s"&gt;| "- \(.name) (\(.status)): \(.recommendation // "no detail")"' audit.json)&lt;/span&gt;

          &lt;span class="s"&gt;echo "SEO score: $SCORE (minimum $MIN_SCORE)"&lt;/span&gt;
          &lt;span class="s"&gt;if awk -v s="$SCORE" -v t="$MIN_SCORE" 'BEGIN { exit (s + 0 &amp;lt; t + 0) ? 0 : 1 }'; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "::error::SEO score $SCORE is below $MIN_SCORE"&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
          &lt;span class="s"&gt;if [ -n "$BROKEN" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "::error::Required SEO checks failed"&lt;/span&gt;
            &lt;span class="s"&gt;echo "$BROKEN"&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;The names are real check names from the response (&lt;code&gt;.audit.&amp;lt;category&amp;gt;.checks[].name&lt;/code&gt;). Run against a saved audit of &lt;code&gt;https://example.com&lt;/code&gt; with &lt;code&gt;MIN_SCORE&lt;/code&gt; at 60, the gate step exits 1 and prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SEO score: 65.567 (minimum 60)
::error::Required SEO checks failed
- meta_description (fail): Add a meta description (150-160 chars)
- h1 (fail): Add exactly one H1 tag
- canonical (warning): Add a canonical tag to prevent duplicate content issues
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to know. First, a check's status is &lt;code&gt;pass&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;fail&lt;/code&gt; or &lt;code&gt;info&lt;/code&gt;, and some serious problems are only a &lt;code&gt;warning&lt;/code&gt;. A missing canonical tag is one, and so is a &lt;code&gt;noindex&lt;/code&gt; robots meta tag. That is why there are two lists: checks in &lt;code&gt;NO_WARN&lt;/code&gt; break the build on a warning too. Second, this version needs a paid key to be useful. Free keys get the full score but a shortened &lt;code&gt;checks&lt;/code&gt; array per category, so a check you listed may not be in the response at all. The score threshold works on any plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  How many audits will CI use?
&lt;/h2&gt;

&lt;p&gt;One audit per URL per run. That makes the free tier's limits easy to reason about:&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;Price&lt;/th&gt;
&lt;th&gt;Audits&lt;/th&gt;
&lt;th&gt;Requests per minute&lt;/th&gt;
&lt;th&gt;Rough CI fit&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;$0&lt;/td&gt;
&lt;td&gt;2 per day&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;One URL, on pushes to &lt;code&gt;main&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starter&lt;/td&gt;
&lt;td&gt;$5/month&lt;/td&gt;
&lt;td&gt;200 per month&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;One URL on every pull request for a small team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;$15/month&lt;/td&gt;
&lt;td&gt;1,000 per month&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;A handful of URLs per pull request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$39/month&lt;/td&gt;
&lt;td&gt;5,000 per month&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;Many URLs, busy repository&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Ultra is $99/month for 25,000 audits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes wrong in practice?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It audits what is deployed.&lt;/strong&gt; The API fetches a public URL. It cannot see &lt;code&gt;localhost&lt;/code&gt; on your runner. On a push to &lt;code&gt;main&lt;/code&gt; that means the gate runs after the deploy, so make the SEO job depend on your deploy job (&lt;code&gt;needs: deploy&lt;/code&gt;) or it will audit the previous version. To block a merge before production, point &lt;code&gt;url&lt;/code&gt; at a preview deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running out of quota fails the build.&lt;/strong&gt; The action uses &lt;code&gt;curl -f&lt;/code&gt;, so a 429 (daily limit reached, or more than your requests per minute) ends the step with "Failed to reach SEO Score API". On the free tier the third run of the day will be red for that reason, not because your SEO changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audited domains appear on a public scoreboard&lt;/strong&gt; unless you opt the key out: &lt;code&gt;curl -X PUT "https://seoscoreapi.com/scoreboard/opt-out?opt_out=true" -H "X-API-Key: YOUR_API_KEY"&lt;/code&gt;. Do this before auditing preview URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick a threshold below your current score.&lt;/strong&gt; Run it once with &lt;code&gt;fail-on-threshold: false&lt;/code&gt;, then set the threshold a few points under what you got. A gate that is red on day one gets deleted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give it time.&lt;/strong&gt; Most audits return in seconds, but while writing this I saw one take about a minute. Hence &lt;code&gt;--max-time 120&lt;/code&gt; and a 5 minute job timeout.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When is Lighthouse CI or another tool a better choice?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Runs where&lt;/th&gt;
&lt;th&gt;Better choice when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SEO Score API action&lt;/td&gt;
&lt;td&gt;Hosted API, audits a public URL&lt;/td&gt;
&lt;td&gt;You want one score and a fix list covering on-page, technical and AI-readability checks, with no browser to install&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighthouse CI&lt;/td&gt;
&lt;td&gt;Your CI runner, against a local build or a URL&lt;/td&gt;
&lt;td&gt;You want to gate before anything is deployed, you care most about performance budgets, or you want no third party and no quota&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PageSpeed Insights API&lt;/td&gt;
&lt;td&gt;Google's servers, public URL&lt;/td&gt;
&lt;td&gt;You want Core Web Vitals, including real-user data where Google has it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open-source link checkers and crawlers (lychee, Scrapy)&lt;/td&gt;
&lt;td&gt;Your CI runner&lt;/td&gt;
&lt;td&gt;You need every page crawled for broken links or custom rules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Lighthouse CI is free and open source, and its SEO category covers the basics (title, meta description, crawlability, valid &lt;code&gt;robots.txt&lt;/code&gt;). If that is all you need, use it and skip the API key. Plenty of teams run both.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is there an SEO audit GitHub Action?&lt;/strong&gt;&lt;br&gt;
Yes: &lt;code&gt;SeoScoreAPI/seo-audit-action@v1&lt;/code&gt;. It takes &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;api-key&lt;/code&gt; and &lt;code&gt;threshold&lt;/code&gt;, and fails the job when the score is below the threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run SEO checks in CI for free?&lt;/strong&gt;&lt;br&gt;
Yes, within 2 audits per day and 2 requests per minute. For a pull-request gate you will want the $5 Starter plan (200 audits a month) or Lighthouse CI, which has no quota.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it work with GitLab CI, CircleCI or Jenkins?&lt;/strong&gt;&lt;br&gt;
Yes. The action is a wrapper around one &lt;code&gt;curl&lt;/code&gt; call, and the second workflow above is plain shell that runs anywhere &lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;jq&lt;/code&gt; are installed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can it audit a pull request before it is deployed?&lt;/strong&gt;&lt;br&gt;
Only if the pull request has a public preview URL. For a build that never leaves the runner, use Lighthouse CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will this improve my rankings?&lt;/strong&gt;&lt;br&gt;
No tool can promise that. It stops you from shipping regressions such as a missing title or an accidental &lt;code&gt;noindex&lt;/code&gt;, which is a narrower claim.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>githubactions</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>A free SEO audit API for developers: score any URL with one request</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Thu, 08 Oct 2026 12:58:52 +0000</pubDate>
      <link>https://dev.to/avansledright/a-free-seo-audit-api-for-developers-score-any-url-with-one-request-87j</link>
      <guid>https://dev.to/avansledright/a-free-seo-audit-api-for-developers-score-any-url-with-one-request-87j</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; yes, there is a free SEO API you can call from code. &lt;a href="https://seoscoreapi.com" rel="noopener noreferrer"&gt;SEO Score API&lt;/a&gt; has a free tier of 2 audits per day with no credit card: send &lt;code&gt;GET /audit?url=...&lt;/code&gt; with an &lt;code&gt;X-API-Key&lt;/code&gt; header and you get back JSON with a 0-100 score, a letter grade, per-category scores and a list of fixes. The free tier runs 55 checks per URL; paid plans run 83 and start at $5 a month.&lt;/p&gt;

&lt;p&gt;I'm Aaron, the solo developer who built it, so read this as a tutorial from the person with the bias. I've tried to be plain about what the free tier leaves out and when you should use something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I check a website's SEO score programmatically?
&lt;/h2&gt;

&lt;p&gt;Three steps: get a key, make one GET request, read the JSON.&lt;/p&gt;

&lt;p&gt;Sign up on the &lt;a href="https://seoscoreapi.com" rel="noopener noreferrer"&gt;homepage&lt;/a&gt; or do it from the terminal. Signup is two calls because the key is tied to a verified email address:&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="nt"&gt;-X&lt;/span&gt; POST https://seoscoreapi.com/signup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email": "you@example.com"}'&lt;/span&gt;

&lt;span class="c"&gt;# a 6-digit code arrives by email&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://seoscoreapi.com/verify &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email": "you@example.com", "code": "123456"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then audit a URL:&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="nt"&gt;--max-time&lt;/span&gt; 120 &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&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;
  | jq &lt;span class="s1"&gt;'{score, grade, issues: (.priorities | length)}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a paid key, that printed this when I ran it for this post:&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;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;65.567&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;"C"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"issues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&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;Note that &lt;code&gt;score&lt;/code&gt; is a decimal, not an integer. Compare it as a number.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the SEO audit API return?
&lt;/h2&gt;

&lt;p&gt;Here is an excerpt of the real response for &lt;code&gt;https://example.com&lt;/code&gt;, trimmed to one check per category and one fix. I removed items; I did not rename or add any field.&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;"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="mf"&gt;65.567&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;"C"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response_time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.50s"&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;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;52.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"checks"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"status"&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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Domain"&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;50&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;"Title is short (14 chars). Aim for 50-60 characters."&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="nl"&gt;"technical"&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;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;70.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"checks"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"response_time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.50s"&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;100&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;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;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"meta"&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;"fix"&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 meta description (150-160 chars)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"plain_english"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The page has no summary for search results, so Google picks a random snippet that may not make people click."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"who_fixes_it"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"content"&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;"ai_readability"&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;"ai_readability_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;43&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_readability_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;"F"&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;The &lt;code&gt;audit&lt;/code&gt; object has five categories: &lt;code&gt;meta&lt;/code&gt;, &lt;code&gt;technical&lt;/code&gt;, &lt;code&gt;social&lt;/code&gt;, &lt;code&gt;performance&lt;/code&gt; and &lt;code&gt;accessibility&lt;/code&gt;. Each has a &lt;code&gt;score&lt;/code&gt; and a &lt;code&gt;checks&lt;/code&gt; array, and each check has a &lt;code&gt;name&lt;/code&gt;, a &lt;code&gt;status&lt;/code&gt; (&lt;code&gt;pass&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;fail&lt;/code&gt; or &lt;code&gt;info&lt;/code&gt;), the &lt;code&gt;value&lt;/code&gt; found and usually a &lt;code&gt;recommendation&lt;/code&gt;. &lt;code&gt;priorities&lt;/code&gt; is the fix list sorted by severity, and &lt;code&gt;who_fixes_it&lt;/code&gt; is &lt;code&gt;developer&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt; or &lt;code&gt;marketing&lt;/code&gt;. The response also carries a &lt;code&gt;core_web_vitals&lt;/code&gt; block, which can come back with &lt;code&gt;"status": "unavailable"&lt;/code&gt; when the upstream lookup fails, so don't assume it is populated.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I call it from Python?
&lt;/h2&gt;

&lt;p&gt;There is an official client on PyPI (&lt;code&gt;pip install seoscoreapi&lt;/code&gt;). It is a thin wrapper over &lt;code&gt;requests&lt;/code&gt;:&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;import&lt;/span&gt; &lt;span class="n"&gt;sys&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="k"&gt;if&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&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;https://example.com&lt;/span&gt;&lt;span class="sh"&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;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;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="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;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;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;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;category&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;audit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;items&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;name&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;14&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;category&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="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;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;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="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;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="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;who_fixes_it&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;Output from my run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://example.com: 65.567/100 (C)
  meta           52.75
  technical      70.0
  social         50.0
  performance    96.25
  accessibility  66.667
  [high] Missing meta description (content)
  [high] No H1 tag found (content)
  [medium] Thin content (133 words) (content)
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;audit()&lt;/code&gt; raises &lt;code&gt;requests.HTTPError&lt;/code&gt; on a non-2xx response, so a 401 (bad key) or 429 (out of quota, or too many requests per minute) surfaces as an exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do I call it from Node.js?
&lt;/h2&gt;

&lt;p&gt;Same idea with the npm package (&lt;code&gt;npm install seoscoreapi&lt;/code&gt;). It uses the built-in &lt;code&gt;fetch&lt;/code&gt;, so you need Node 18 or later:&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;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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;main&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;url&lt;/span&gt; &lt;span class="o"&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;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;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="k"&gt;try&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;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;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;result&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;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="s2"&gt;/100 (&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="s2"&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;p&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;priorities&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;severity&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fix&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// err.status is the HTTP status: 401 bad key, 429 out of quota or too fast&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Audit failed (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;network&lt;/span&gt;&lt;span class="dl"&gt;"&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;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;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you'd rather not add a dependency, the curl call above translates directly to &lt;code&gt;fetch&lt;/code&gt; or &lt;code&gt;requests.get&lt;/code&gt;. There is nothing else to the protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the free tier not include?
&lt;/h2&gt;

&lt;p&gt;The free tier is real (no trial clock, no card), but it is small, and the response is trimmed. From the pricing table and the API docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2 audits per day and 2 requests per minute.&lt;/strong&gt; Enough to test the API or watch one page. Not enough to audit a sitemap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;55 checks, not 83.&lt;/strong&gt; The SXO, AEO and AIO scoring blocks are paid. A free response has an &lt;code&gt;_extended_gated&lt;/code&gt; placeholder where they would be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A capped fix list and capped check detail.&lt;/strong&gt; The overall score, grade and category scores are complete, but the &lt;code&gt;priorities&lt;/code&gt; list and the per-category &lt;code&gt;checks&lt;/code&gt; arrays are shortened on free keys. A &lt;code&gt;_priorities_gated&lt;/code&gt; or &lt;code&gt;_gated&lt;/code&gt; field tells you how many more there are.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No history, batch or monitoring.&lt;/strong&gt; Score deltas, &lt;code&gt;POST /audit/batch&lt;/code&gt; and scheduled monitoring start on the $5 Starter plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No accessibility (WCAG) audit, GEO audit or AI citation tracking.&lt;/strong&gt; Those are paid endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One URL per call.&lt;/strong&gt; &lt;code&gt;/audit&lt;/code&gt; scores a page. It does not crawl a site.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more things worth knowing before you build on it. Audits add the audited domain to a public &lt;a href="https://seoscoreapi.com/scoreboard" rel="noopener noreferrer"&gt;scoreboard&lt;/a&gt; unless you opt your key out with &lt;code&gt;PUT /scoreboard/opt-out?opt_out=true&lt;/code&gt;. And set a client timeout of a minute or two: the site says most pages finish in about two seconds, but in my runs for this post one call took under 7 seconds and another took about a minute.&lt;/p&gt;

&lt;p&gt;Paid plans, for reference: Starter $5/month (200 audits), Basic $15 (1,000), Pro $39 (5,000), Ultra $99 (25,000).&lt;/p&gt;

&lt;h2&gt;
  
  
  Which free SEO API should I use?
&lt;/h2&gt;

&lt;p&gt;It depends on what you are measuring. I'd pick a different tool in several of these cases.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;Better choice when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SEO Score API (free tier)&lt;/td&gt;
&lt;td&gt;$0, 2 audits/day&lt;/td&gt;
&lt;td&gt;One score plus on-page, technical, social and AI-readability checks as JSON&lt;/td&gt;
&lt;td&gt;You want a single number and a fix list from one HTTP call, with nothing to host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google PageSpeed Insights API&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Lighthouse lab data plus real-user Core Web Vitals where Google has them&lt;/td&gt;
&lt;td&gt;Performance is the question. It is Google's own data and I can't match it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lighthouse CI&lt;/td&gt;
&lt;td&gt;Free, open source&lt;/td&gt;
&lt;td&gt;Lighthouse runs on your own machine or CI runner&lt;/td&gt;
&lt;td&gt;You need to test a local build or a page behind a login, or you want no third party and no quota&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open-source crawlers (Scrapy, advertools)&lt;/td&gt;
&lt;td&gt;Free, you host it&lt;/td&gt;
&lt;td&gt;Whatever you program it to collect, across a whole site&lt;/td&gt;
&lt;td&gt;You need site-wide crawling, broken-link checks or custom rules, and can maintain it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these, mine included, do keyword research or rank tracking. That is what Ahrefs, Semrush and Moz are for.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is there a free SEO API for developers?&lt;/strong&gt;&lt;br&gt;
Yes. SEO Score API's free tier gives 2 audits per day and 2 requests per minute, with no credit card. Google's PageSpeed Insights API is also free and is the better choice for performance data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many checks does the free tier run?&lt;/strong&gt;&lt;br&gt;
55 per URL, across meta and content, technical SEO, social tags, performance, accessibility and AI readability. Paid plans add 28 SXO, AEO and AIO checks for 83.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a credit card?&lt;/strong&gt;&lt;br&gt;
No. Signup is an email address and a 6-digit verification code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I audit a whole site?&lt;/strong&gt;&lt;br&gt;
Not with one free call. &lt;code&gt;/audit&lt;/code&gt; takes one URL. Batch audits (up to 10 URLs per request) and multi-page crawls are on paid plans.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use it in CI?&lt;/strong&gt;&lt;br&gt;
Yes. There is an official GitHub Action, &lt;a href="https://github.com/SeoScoreAPI/seo-audit-action" rel="noopener noreferrer"&gt;SeoScoreAPI/seo-audit-action&lt;/a&gt;, that fails a build when the score drops below a threshold. On the free tier, budget for 2 runs a day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where are the full docs?&lt;/strong&gt;&lt;br&gt;
The OpenAPI reference is at &lt;a href="https://seoscoreapi.com/docs" rel="noopener noreferrer"&gt;seoscoreapi.com/docs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>python</category>
    </item>
    <item>
      <title>One Deep Audit for $12: Full Report, No Subscription</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 05 Oct 2026 17:47:19 +0000</pubDate>
      <link>https://dev.to/avansledright/one-deep-audit-for-12-full-report-no-subscription-57c5</link>
      <guid>https://dev.to/avansledright/one-deep-audit-for-12-full-report-no-subscription-57c5</guid>
      <description>&lt;p&gt;You can now buy a single Deep Audit for $12. There is no plan to sign up for and nothing to cancel. Open a report, look at the free version, and pay only if you want the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you see for free
&lt;/h2&gt;

&lt;p&gt;Every hosted report at &lt;code&gt;seoscoreapi.com/report/yourdomain.com&lt;/code&gt; opens as a free look. It shows the overall score and grade, how many issues were found at each severity, and the title of the most important one. The remaining issues and their fixes are hidden until the report is unlocked.&lt;/p&gt;

&lt;p&gt;That is enough to answer the first question most people have, which is whether the site has a problem worth paying to understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  What $12 unlocks
&lt;/h2&gt;

&lt;p&gt;The single purchase does two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It unlocks the full hosted report for that domain.&lt;/strong&gt; Every issue, every fix, and a plain-English sentence for each one that says what is wrong and who should fix it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It gives you one Deep Audit credit.&lt;/strong&gt; A Deep Audit is the long-form audit: thousands of checks from a public catalog of more than 14,000, scored across nine sections, with an AI analysis pass on top. It takes about 90 seconds and runs in the background.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You also get the exports. The unlocked report downloads as a PDF, as Markdown, or as a CSV, so the fix list can go straight into a ticket tracker or a client email.&lt;/p&gt;

&lt;p&gt;A free API key is created for the email address you pay with. You don't need to use it, but it is there if you later want to run audits from code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A site owner who wants one answer.&lt;/strong&gt; You don't run audits every week. You want to know what is wrong with one site, once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An agency checking a prospect.&lt;/strong&gt; Run the audit before a pitch, export the PDF, and bring real findings to the first call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A developer about to take on a rebuild.&lt;/strong&gt; Get a baseline before you touch anything, so the improvement is measurable afterwards.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When a pack or a plan costs less
&lt;/h2&gt;

&lt;p&gt;The single is the right buy for one report. If you need more, the price per audit drops:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Deep Audits&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;th&gt;Per audit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Single&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;td&gt;$12.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-pack&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$29&lt;/td&gt;
&lt;td&gt;$9.67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-pack&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;$79&lt;/td&gt;
&lt;td&gt;$7.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro plan&lt;/td&gt;
&lt;td&gt;20 a month&lt;/td&gt;
&lt;td&gt;$39/mo&lt;/td&gt;
&lt;td&gt;$1.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ultra plan&lt;/td&gt;
&lt;td&gt;100 a month&lt;/td&gt;
&lt;td&gt;$99/mo&lt;/td&gt;
&lt;td&gt;$0.99&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pack credits are valid for 12 months. Pro and Ultra also include the standard audit API (5,000 and 25,000 audits a month), so anyone auditing regularly is better off on a plan. Details are in &lt;a href="https://seoscoreapi.com/blog/deep-audit-credits?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=single-deep-audit-no-subscription" rel="noopener noreferrer"&gt;Buy Deep Audit Credits&lt;/a&gt; and on the &lt;a href="https://seoscoreapi.com/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=single-deep-audit-no-subscription#pricing" rel="noopener noreferrer"&gt;pricing section&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to buy one
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;https://seoscoreapi.com/report/yourdomain.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Enter your email in the unlock box and pay by card.&lt;/li&gt;
&lt;li&gt;You land back on the full report. A button on the receipt starts the Deep Audit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to know what a Deep Audit scores before you buy, &lt;a href="https://seoscoreapi.com/blog/reading-a-deep-audit-report?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=single-deep-audit-no-subscription" rel="noopener noreferrer"&gt;Reading a Deep Audit Report&lt;/a&gt; walks through each section, and &lt;a href="https://seoscoreapi.com/checks?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=single-deep-audit-no-subscription" rel="noopener noreferrer"&gt;the check catalog&lt;/a&gt; lists every check by name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is the $12 a subscription?
&lt;/h3&gt;

&lt;p&gt;No. It is a one-time charge. Nothing renews.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the unlock cover more than one site?
&lt;/h3&gt;

&lt;p&gt;No. One purchase unlocks the full report for one domain and includes one Deep Audit credit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need an account?
&lt;/h3&gt;

&lt;p&gt;No. You pay with an email address. A free API key is created for that address so the unlock follows you if you sign in later.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Replacing Screaming Frog with an API: A 10,000-URL Agency Workflow</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:00:07 +0000</pubDate>
      <link>https://dev.to/avansledright/replacing-screaming-frog-with-an-api-a-10000-url-agency-workflow-30lm</link>
      <guid>https://dev.to/avansledright/replacing-screaming-frog-with-an-api-a-10000-url-agency-workflow-30lm</guid>
      <description>&lt;p&gt;Screaming Frog SEO Spider is the most-installed SEO tool in agency workflows. Most audits we've seen referenced over the past decade started with a Screaming Frog crawl. It's a good piece of software. It is also a desktop application that has to be running on someone's laptop, that has to be configured per-project, that has to be re-run when anything changes, and that produces a CSV which then has to be opened in Excel and triaged by hand.&lt;/p&gt;

&lt;p&gt;This post is for agencies and in-house teams asking the obvious question: &lt;em&gt;what does the API equivalent look like, and is it actually cheaper?&lt;/em&gt; The short answer is yes, with a different shape of workflow. The longer answer is below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Screaming Frog is good at
&lt;/h2&gt;

&lt;p&gt;Crediting where credit is due. Screaming Frog does several things well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Crawl discovery.&lt;/strong&gt; Point it at a URL and it walks every internal link. You don't need to know your sitemap, your sitemap doesn't have to be correct, you'll find pages the sitemap forgot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-page detail.&lt;/strong&gt; It pulls headings, meta tags, internal/external links, response codes, redirect chains, and a long tail of properties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-off depth.&lt;/strong&gt; Configure once, crawl once, walk away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're doing a single deep audit of a single site and never re-running it, Screaming Frog is fine. Pay the £199/year and move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it isn't good at
&lt;/h2&gt;

&lt;p&gt;Where it breaks down for an agency or in-house team running ongoing audits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It's a desktop app.&lt;/strong&gt; Someone's laptop has to be on, awake, and not crashing. For a 50,000-URL site that's a 6-hour crawl on a tuned machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crawls don't compose.&lt;/strong&gt; Last week's crawl is a CSV on someone's hard drive. There is no built-in "what changed since last week" view that isn't manual diffing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No native scoring or grading.&lt;/strong&gt; It tells you a &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; is 67 characters long. It does not tell you "this page scores 84 and your competitor scores 91 and here's why."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No API surface.&lt;/strong&gt; You cannot trigger it from a CI pipeline, schedule it from a server, or hand a button to a non-technical colleague.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-seat licensing.&lt;/strong&gt; Each analyst needs their own license, their own configuration, their own copy of the CSV.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a team of three doing weekly audits across twelve clients, those constraints stop being theoretical and start eating real hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The API-equivalent workflow
&lt;/h2&gt;

&lt;p&gt;Same outcome — a scored audit of every URL on the site — restructured as a server-side job:&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;requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;xml.etree.ElementTree&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ET&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;BASE&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://api.seoscoreapi.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;crawl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;site&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Pull URLs from the sitemap, recursing into indexes.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;ns&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;sm&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;http://www.sitemaps.org/schemas/sitemap/0.9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromstring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&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="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&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;text&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;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sitemapindex&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.//sm:sitemap/sm:loc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ns&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="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_collect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.//sm:url/sm:loc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_collect&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;site&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/sitemap.xml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;audit_all&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;rows&lt;/span&gt; &lt;span class="o"&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="nf"&gt;len&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="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/audit/batch&lt;/span&gt;&lt;span class="sh"&gt;"&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;X-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;API_KEY&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;json&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;urls&lt;/span&gt;&lt;span class="sh"&gt;"&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;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;

&lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;crawl&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://client.com&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;Discovered &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;urls&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&lt;/span&gt;&lt;span class="sh"&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;audit_all&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="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;Audited &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;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; pages&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;Sixty lines, end to end. Run it on a server. Output a CSV. Hand the CSV to the analyst, or pipe it into a dashboard, or post it to Slack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The crawl-discovery gap (and how to close it)
&lt;/h2&gt;

&lt;p&gt;The honest tradeoff: a sitemap-based crawl is not a &lt;em&gt;true&lt;/em&gt; crawl. If a page exists but isn't in the sitemap, you won't find it. Screaming Frog walks every link and finds those orphans.&lt;/p&gt;

&lt;p&gt;For most sites this matters less than you'd think. Marketing sites publish complete sitemaps. Shopify, WooCommerce, WordPress with Yoast — all generate exhaustive sitemaps automatically. If you've audited the sitemap, you've audited the site that Google can see.&lt;/p&gt;

&lt;p&gt;For sites where it does matter (legacy CMS, fragments of static HTML hand-rolled into Apache, archives that nobody linked into the nav), a one-time Screaming Frog crawl to &lt;em&gt;generate the URL list&lt;/em&gt;, plus the API for ongoing scoring, is a perfectly reasonable hybrid. The API doesn't have to replace every part of the workflow to be a 10x improvement on the parts it does replace.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the API does that the desktop can't
&lt;/h2&gt;

&lt;p&gt;The interesting tradeoff isn't "what we lost" — it's "what we gained that wasn't possible before."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Historical comparison.&lt;/strong&gt; Every audit's score is stored against your key. Run the same audit weekly and you have a per-URL trend line, with deltas surfaced automatically. The &lt;a href="https://seoscoreapi.com/blog/historical-seo-score-tracking?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=replacing-screaming-frog-with-an-api" rel="noopener noreferrer"&gt;historical tracking post&lt;/a&gt; has the full API. There is no equivalent in Screaming Frog without manually saving and diffing CSVs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composability.&lt;/strong&gt; The audit results are JSON. Pipe them into anything. We've seen teams build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slack alerts that fire when any tracked client drops 3+ points.&lt;/li&gt;
&lt;li&gt;HTML dashboards generated from &lt;code&gt;/history/domains&lt;/code&gt; for monthly client reviews.&lt;/li&gt;
&lt;li&gt;Re-audits triggered automatically when a new deploy lands in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Side-by-side comparisons.&lt;/strong&gt; &lt;code&gt;/compare&lt;/code&gt; audits two URLs in parallel and returns a diff. Use it for client-vs-competitor pitches, before/after migration checks, or A/B preview/production validation. The &lt;a href="https://seoscoreapi.com/blog/comparing-saas-landing-pages-with-one-api-call?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=replacing-screaming-frog-with-an-api" rel="noopener noreferrer"&gt;Stripe/Square/Adyen comparison post&lt;/a&gt; shows the format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI readability and GEO scoring.&lt;/strong&gt; SEO Score API runs the kind of on-page SEO checks Screaming Frog covers &lt;em&gt;plus&lt;/em&gt; an additional layer specifically for how LLMs read your content. That's not in Screaming Frog at all and isn't going to be — it's a different category of analysis. The &lt;a href="https://seoscoreapi.com/blog/geo-optimization-guide?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=replacing-screaming-frog-with-an-api" rel="noopener noreferrer"&gt;GEO optimization guide&lt;/a&gt; and &lt;a href="https://seoscoreapi.com/blog/ai-readability-score?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=replacing-screaming-frog-with-an-api" rel="noopener noreferrer"&gt;AI readability post&lt;/a&gt; cover what's in there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shareable reports and badges.&lt;/strong&gt; Every audited URL gets a publicly shareable report at &lt;code&gt;/report/{domain}&lt;/code&gt; and an SVG score badge. Drop the badge in client decks; drop the report URL in emails. There's no Screaming Frog equivalent — its outputs are CSVs that only the analyst sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost comparison, honestly
&lt;/h2&gt;

&lt;p&gt;Screaming Frog SEO Spider: &lt;strong&gt;£199/year per seat&lt;/strong&gt; (~$250/year).&lt;/p&gt;

&lt;p&gt;SEO Score API:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Audits/mo&lt;/th&gt;
&lt;th&gt;Annual cost&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://seoscoreapi.com/upgrade?tier=basic&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=replacing-screaming-frog-with-an-api" rel="noopener noreferrer"&gt;Basic&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;$180/year&lt;/td&gt;
&lt;td&gt;Solo SEO, 3-5 small clients&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://seoscoreapi.com/upgrade?tier=pro&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=replacing-screaming-frog-with-an-api" rel="noopener noreferrer"&gt;Pro&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;$468/year&lt;/td&gt;
&lt;td&gt;Agency with 10-20 clients, weekly audits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://seoscoreapi.com/upgrade?tier=ultra&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=replacing-screaming-frog-with-an-api" rel="noopener noreferrer"&gt;Ultra&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;25,000&lt;/td&gt;
&lt;td&gt;$1,188/year&lt;/td&gt;
&lt;td&gt;Agency with 30+ clients or one large enterprise site&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A two-person agency replacing two Screaming Frog seats (~$500/year) gets the &lt;em&gt;same audit coverage&lt;/em&gt; on Pro for less money, plus everything in the "what the API does that the desktop can't" section. A larger agency replacing 4-5 seats (~$1,200/year) lands on Ultra and gets unlimited history retention, which on its own is worth the difference.&lt;/p&gt;

&lt;p&gt;The break-even is rough but real: &lt;strong&gt;somewhere around 3 seats of Screaming Frog, Ultra is cheaper and strictly more capable.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What we keep Screaming Frog around for
&lt;/h2&gt;

&lt;p&gt;We don't pretend the API replaces 100% of use cases. Where Screaming Frog still wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Crawling sites with broken or missing sitemaps.&lt;/li&gt;
&lt;li&gt;Following redirect chains across external links to figure out what an old domain ended up redirecting to.&lt;/li&gt;
&lt;li&gt;One-shot deep audits where you want every property of every page in a single CSV and you'll never re-run the audit.&lt;/li&gt;
&lt;li&gt;Legacy site migrations where you need to map old URLs → new URLs by content hash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use the right tool for the job. The point isn't to eliminate Screaming Frog — it's to stop using it for the &lt;em&gt;weekly&lt;/em&gt; and &lt;em&gt;per-deploy&lt;/em&gt; and &lt;em&gt;cross-client&lt;/em&gt; work that it was never designed to do, where its constraints turn into real cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration path
&lt;/h2&gt;

&lt;p&gt;If you're an agency moving from Screaming Frog-only to API-first, the order we'd suggest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick your three highest-effort clients.&lt;/strong&gt; Audit their sites with the API. Compare the output to your most recent Screaming Frog audit. Confirm the coverage is equivalent for your purposes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stand up a weekly job.&lt;/strong&gt; Cron + a Python script + Slack webhook. The &lt;a href="https://seoscoreapi.com/blog/agency-seo-monitor-setup?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=replacing-screaming-frog-with-an-api" rel="noopener noreferrer"&gt;agency monitor setup post&lt;/a&gt; has the full walkthrough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move quarterly client reports onto the API output.&lt;/strong&gt; The HTML report at &lt;code&gt;/report/{domain}&lt;/code&gt; is shareable; the badge is droppable; the history endpoint generates trend charts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Screaming Frog only for the deep one-off audits where its discovery wins.&lt;/strong&gt; That's roughly twice a year per client, not weekly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most agencies that do this end up shifting 80–90% of their crawler-hours to the API and keeping Screaming Frog around for the long-tail cases. Total cost drops, total throughput goes up, and the analyst time freed up goes into actual fixes instead of running spider crawls overnight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;If you're a solo or small agency, &lt;a href="https://seoscoreapi.com/upgrade?tier=basic&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=replacing-screaming-frog-with-an-api" rel="noopener noreferrer"&gt;Basic ($15/mo)&lt;/a&gt; covers 3-5 sites at weekly cadence. If you're running an agency at scale and Screaming Frog has become a bottleneck, &lt;a href="https://seoscoreapi.com/upgrade?tier=pro&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=replacing-screaming-frog-with-an-api" rel="noopener noreferrer"&gt;Pro ($39/mo)&lt;/a&gt; is the sweet spot. For the largest agencies with 30+ clients, &lt;a href="https://seoscoreapi.com/upgrade?tier=ultra&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=replacing-screaming-frog-with-an-api" rel="noopener noreferrer"&gt;Ultra ($99/mo)&lt;/a&gt; replaces the seat-licensing math entirely.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>One SEO Tools API, Five Workflows Your Team Probably Already Needs</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 28 Sep 2026 14:00:03 +0000</pubDate>
      <link>https://dev.to/avansledright/one-seo-tools-api-five-workflows-your-team-probably-already-needs-46k4</link>
      <guid>https://dev.to/avansledright/one-seo-tools-api-five-workflows-your-team-probably-already-needs-46k4</guid>
      <description>&lt;p&gt;The phrase "SEO tools API" gets searched a lot, and it usually means something different to whoever's typing it. Some people mean "an API that audits one page." Some mean "an API that pulls multiple SEO tools together." Some mean "an API I can wire into my existing tools."&lt;/p&gt;

&lt;p&gt;SEO Score API is the first of those, but it functions as the second and third — because once you have a structured audit response, it slots into existing tooling without much glue. This post walks through the five workflows engineering and agency teams build on top of the same endpoint, all from one API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow 1: CI gates
&lt;/h2&gt;

&lt;p&gt;The most common first integration. You hit &lt;code&gt;/audit?url=...&lt;/code&gt; against your preview deploy in GitHub Actions and fail the build if the score regresses.&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;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;SCORE=$(curl -s -H "X-API-Key: $KEY" \&lt;/span&gt;
      &lt;span class="s"&gt;"https://seoscoreapi.com/audit?url=$PREVIEW" | jq .score)&lt;/span&gt;
    &lt;span class="s"&gt;[ "$SCORE" -lt 85 ] &amp;amp;&amp;amp; exit 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole gate. Detailed walkthrough in &lt;a href="https://seoscoreapi.com/blog/seo-analyzer-api-nextjs-build?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=one-seo-tools-api-five-workflows" rel="noopener noreferrer"&gt;Wiring an SEO analyzer API into a Next.js build&lt;/a&gt; and the published &lt;a href="https://seoscoreapi.com/blog/github-actions-seo-audit?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=one-seo-tools-api-five-workflows" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow 2: Scheduled monitoring
&lt;/h2&gt;

&lt;p&gt;Same endpoint, different schedule. Instead of one audit per PR, run a list of URLs nightly or weekly and alert on regressions.&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;# nightly_monitor.py
&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="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;requests&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://site.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://site.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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&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;r&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;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;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;KEY&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;r&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="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SLACK_HOOK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;json&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;text&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;⚠️ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; scored &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&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;r&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;Cron it, point it at Slack, ship it. &lt;a href="https://seoscoreapi.com/blog/seo-monitoring-alerting?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=one-seo-tools-api-five-workflows" rel="noopener noreferrer"&gt;SEO Monitor API: score tracking with webhooks and Slack alerts&lt;/a&gt; has the production-grade version with deduplication and SLA windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow 3: AI agents
&lt;/h2&gt;

&lt;p&gt;LLMs are bad at reading screenshots and good at reading JSON. The same &lt;code&gt;/audit&lt;/code&gt; response that powers a CI gate is what you hand an agent when you want it to fix SEO issues autonomously.&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="n"&gt;findings&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;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;KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;agent_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;
You&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re an SEO engineer. Here&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the audit JSON for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.
Fix the top 3 failing checks. Return a unified diff.

&lt;/span&gt;&lt;span class="si"&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="n"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&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;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&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;claude-opus-4-7&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. The structured response is the contract; the agent just acts on it. &lt;a href="https://seoscoreapi.com/blog/ai-seo-agent?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=one-seo-tools-api-five-workflows" rel="noopener noreferrer"&gt;Build an AI SEO Agent&lt;/a&gt; walks through the full loop with Claude, and we ship an &lt;a href="https://seoscoreapi.com/blog/seoscoreapi-mcp-server?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=one-seo-tools-api-five-workflows" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; so Claude and Cursor can call the API natively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow 4: Client and stakeholder reports
&lt;/h2&gt;

&lt;p&gt;The audit response is structured to be rendered as well as acted on. Three rendering options, in order of effort:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lowest-effort:&lt;/strong&gt; link clients to &lt;code&gt;/report/{domain}&lt;/code&gt; — that's our hosted report URL, auto-refreshed when the audit re-runs. Zero code, branded enough for most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Medium-effort:&lt;/strong&gt; wrap the &lt;code&gt;/audit&lt;/code&gt; JSON in a Jinja template, send as HTML email via your SMTP of choice. ~80 lines. Full walkthrough in &lt;a href="https://seoscoreapi.com/blog/automated-weekly-client-seo-reports?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=one-seo-tools-api-five-workflows" rel="noopener noreferrer"&gt;Generating weekly client SEO reports automatically&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Highest-effort:&lt;/strong&gt; build your own white-label dashboard on top of the API. The &lt;a href="https://seoscoreapi.com/blog/white-label-seo-reports?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=one-seo-tools-api-five-workflows" rel="noopener noreferrer"&gt;white-label SEO reports&lt;/a&gt; post covers this.&lt;/p&gt;

&lt;p&gt;Same endpoint, three different report surfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow 5: Status badges
&lt;/h2&gt;

&lt;p&gt;Useful for open-source projects, public marketing pages, and internal dashboards. Embed an SVG that reflects the current score:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;![&lt;/span&gt;&lt;span class="nv"&gt;SEO Score&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;https://seoscoreapi.com/badge/your-domain.com&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The badge re-renders from the most recent audit. Run a weekly cron that audits your domain and the badge stays current. Free tier covers this for one domain forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one endpoint instead of five
&lt;/h2&gt;

&lt;p&gt;Some SEO APIs split their product across endpoints — one for "audit," one for "monitor," one for "report." That's a UX choice with a real cost: every new endpoint is a new response shape your code has to learn, a new error model to handle, a new SDK method to remember.&lt;/p&gt;

&lt;p&gt;We deliberately built around one endpoint (&lt;code&gt;/audit&lt;/code&gt;) returning one response shape. The five workflows above all consume the same JSON. That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Jinja template you write for the weekly client report works on a CI gate's output too.&lt;/li&gt;
&lt;li&gt;The Slack alert format you wrote for nightly monitoring works on PR-blocking failures.&lt;/li&gt;
&lt;li&gt;The agent prompt you tuned against last Tuesday's audit still works against tomorrow's.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The endpoint is the API. Everything else is glue you write once and reuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ties the workflows together
&lt;/h2&gt;

&lt;p&gt;One API key. One endpoint. One response shape.&lt;/p&gt;

&lt;p&gt;If you're evaluating SEO tools APIs and the comparison matrix gets complicated — "this one has 6 endpoints, this one has 12, this one has webhooks but not batches" — that's a signal that the surface is too wide. The narrower the API, the more workflows it actually fits.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://seoscoreapi.com/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=one-seo-tools-api-five-workflows" rel="noopener noreferrer"&gt;Sign up for free&lt;/a&gt; and see if a single &lt;code&gt;/audit&lt;/code&gt; response slots into more places than you'd expect. The free tier (2/day, no card) is enough to prototype any of the five workflows above.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>The SEO Checker API: Pass/Fail SEO Checks for Code, Not Dashboards</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 21 Sep 2026 14:00:05 +0000</pubDate>
      <link>https://dev.to/avansledright/the-seo-checker-api-passfail-seo-checks-for-code-not-dashboards-1dih</link>
      <guid>https://dev.to/avansledright/the-seo-checker-api-passfail-seo-checks-for-code-not-dashboards-1dih</guid>
      <description>&lt;p&gt;An &lt;a href="https://seoscoreapi.com/seo-checker-api?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=seo-checker-api-programmatic-pass-fail" rel="noopener noreferrer"&gt;SEO checker API&lt;/a&gt; is an SEO checker that returns its results as machine-readable JSON instead of a dashboard. Every check returns a structured pass/fail, a numeric score, and the value found — so the next thing in your pipeline can act on it without a human in the loop.&lt;/p&gt;

&lt;p&gt;That distinction is the entire reason to use an API over a tool like Screaming Frog or a SaaS dashboard. The dashboard is for humans clicking through findings. The API is for code that already knows what to do once it has the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "pass/fail per check" actually means
&lt;/h2&gt;

&lt;p&gt;When you hit &lt;code&gt;GET /audit?url=https://example.com&lt;/code&gt; against SEO Score API, you get back a response that looks like this (trimmed):&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;"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;78&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;"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_content"&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;"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;92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"checks"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"title_present"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Domain"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"title_length"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ideal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30-60"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"meta_description_present"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&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;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;"technical_seo"&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;"..."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;code&gt;status&lt;/code&gt; is one of &lt;code&gt;pass&lt;/code&gt;, &lt;code&gt;warn&lt;/code&gt;, &lt;code&gt;fail&lt;/code&gt;. That single field is the whole product surface for code-driven workflows: filter the array, count the failures, threshold on category sub-scores, write a diff against last week's run. None of that requires reading the dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three workflows the pass/fail shape unlocks
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. CI gates.&lt;/strong&gt; Pull the score in a GitHub Action and fail the build if it drops below 85. Twenty lines of YAML and a single API call. We publish a &lt;a href="https://seoscoreapi.com/blog/github-actions-seo-audit?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=seo-checker-api-programmatic-pass-fail" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt; that does exactly this, but you can roll your own with curl.&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;SEO checker&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;SCORE=$(curl -s -H "X-API-Key: ${{ secrets.SEO_KEY }}" \&lt;/span&gt;
      &lt;span class="s"&gt;"https://seoscoreapi.com/audit?url=https://preview.example.com" \&lt;/span&gt;
      &lt;span class="s"&gt;| jq '.score')&lt;/span&gt;
    &lt;span class="s"&gt;if [ "$SCORE" -lt 85 ]; then exit 1; fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Regression diffs.&lt;/strong&gt; Store yesterday's audit JSON, run today's, diff the &lt;code&gt;checks&lt;/code&gt; arrays. Any check that flipped &lt;code&gt;pass → fail&lt;/code&gt; is a regression you can name. We do this in the &lt;a href="https://seoscoreapi.com/blog/historical-seo-score-tracking?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=seo-checker-api-programmatic-pass-fail" rel="noopener noreferrer"&gt;historical tracking endpoint&lt;/a&gt; and it's also trivial to do client-side with two JSON blobs and a dict comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Agent loops.&lt;/strong&gt; An AI agent doesn't want a screenshot, it wants structured findings to act on. Hand the agent the &lt;code&gt;checks&lt;/code&gt; array and let it decide which failures to fix in which order. See &lt;a href="https://seoscoreapi.com/blog/ai-seo-agent?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=seo-checker-api-programmatic-pass-fail" rel="noopener noreferrer"&gt;Build an AI SEO Agent&lt;/a&gt; for a worked example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why structured pass/fail beats a score-only API
&lt;/h2&gt;

&lt;p&gt;A lot of "SEO score" APIs return a number and nothing else. That's not actually useful in code — you can't fix a problem you can't name. The minimum useful response for an SEO checker API is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An overall 0–100 score (for thresholding)&lt;/li&gt;
&lt;li&gt;A category breakdown (for routing fixes to teams)&lt;/li&gt;
&lt;li&gt;Per-check results with stable IDs (for diffs and remediation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SEO Score API returns all three. Every check has a stable string ID (&lt;code&gt;title_length&lt;/code&gt;, &lt;code&gt;canonical_present&lt;/code&gt;, &lt;code&gt;og_image_dimensions&lt;/code&gt;) that you can match against across audits without parsing display text.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it relates to dashboard tools
&lt;/h2&gt;

&lt;p&gt;A dashboard tool — Ahrefs Site Audit, Screaming Frog, Sitebulb — is built to give a human the full picture of a site so they can prioritize. An SEO checker API is built for the moment &lt;em&gt;after&lt;/em&gt; prioritization, when you already know what "good" looks like and want code to enforce it.&lt;/p&gt;

&lt;p&gt;The two are complementary. Big agencies typically run a dashboard for human-driven discovery and an API for the production guardrails (CI gates, weekly client diffs, monitoring). &lt;a href="https://seoscoreapi.com/blog/seo-score-api-alongside-ahrefs-and-moz?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=seo-checker-api-programmatic-pass-fail" rel="noopener noreferrer"&gt;How SEO Score API sits alongside Ahrefs and Moz&lt;/a&gt; goes into more detail on the split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started in five minutes
&lt;/h2&gt;

&lt;p&gt;Sign up for the free tier (2 audits/day, no credit card), grab your key, and hit the endpoint:&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;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: YOUR_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://seoscoreapi.com/audit?url=https://your-site.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the full first interaction. You'll get back the same response shape your CI gates and monitoring will eventually consume — which means the prototype you write today is the production code you ship tomorrow.&lt;/p&gt;

&lt;p&gt;If you're evaluating SEO checker APIs against each other, the questions worth asking are: does it render JavaScript pages (we do, via headless Chromium)? Does it return stable check IDs (yes)? Does it have SDKs in your language (Python, Node, n8n, and a GitHub Action ship today)? And does the free tier let you actually evaluate the response shape before committing? All four of those are table stakes for a checker that's going to live in your pipeline.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Pre-Deploy SEO Gates: Catch Score Regressions Before You Ship</title>
      <dc:creator>Aaron VanSledright</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:00:04 +0000</pubDate>
      <link>https://dev.to/avansledright/pre-deploy-seo-gates-catch-score-regressions-before-you-ship-1pnc</link>
      <guid>https://dev.to/avansledright/pre-deploy-seo-gates-catch-score-regressions-before-you-ship-1pnc</guid>
      <description>&lt;p&gt;The pattern that costs teams the most SEO traffic is the one nobody can post-mortem: a regression that shipped a month ago, that nobody noticed because traffic only dipped 8%, that nobody can attribute to a specific deploy. By the time someone runs an audit, twenty commits have landed and the cause is buried.&lt;/p&gt;

&lt;p&gt;The fix is to audit &lt;em&gt;every PR&lt;/em&gt; before it merges and fail the gate when the score drops materially. This post walks through that gate end-to-end — what to compare, what threshold to use, and the actual GitHub Actions / GitLab CI / generic Python implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a pre-deploy SEO gate actually checks
&lt;/h2&gt;

&lt;p&gt;There are three distinct comparisons worth running on a PR, in order of how strict they should be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PR preview URL vs production.&lt;/strong&gt; Most teams have preview deploys (Vercel, Netlify, Cloudflare Pages). Audit the preview, audit prod, fail if the gap is wider than threshold. This is the strictest, most useful check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preview vs last preview.&lt;/strong&gt; If you don't ship to production from main, or you want a check on a feature branch before it touches prod, audit this branch's preview vs the previous branch's preview.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR preview vs all-time best.&lt;/strong&gt; Useful as a soft warning, not a hard gate — "this PR isn't bad, but the homepage was at 94 in February and you're at 89."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We'll focus on #1, which is what 90% of teams want.&lt;/p&gt;

&lt;h2&gt;
  
  
  The minimum viable gate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .ci/seo-gate.py
&lt;/span&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;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&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;PREVIEW_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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PREVIEW_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# e.g., https://pr-142-foo.vercel.app
&lt;/span&gt;&lt;span class="n"&gt;PROD_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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROD_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;         &lt;span class="c1"&gt;# e.g., https://yourdomain.com
&lt;/span&gt;&lt;span class="n"&gt;BASE&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://api.seoscoreapi.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;

&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/compare&lt;/span&gt;&lt;span class="sh"&gt;"&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;X-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;API_KEY&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&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;urls&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;PREVIEW_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PROD_URL&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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;r&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="c1"&gt;# /compare returns sites[] in submission order
&lt;/span&gt;&lt;span class="n"&gt;preview_score&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sites&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="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;prod_score&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sites&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;preview_score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prod_score&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;Preview: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;preview_score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  Prod: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prod_score&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  Delta: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delta&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;delta&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="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;::error::SEO regression of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delta&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="s"&gt; points (threshold &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;THRESHOLD&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire gate. Twenty lines. One API call (the &lt;code&gt;/compare&lt;/code&gt; endpoint audits both sides in parallel on our side). 30 seconds wall time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick a useful threshold
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;-3.0&lt;/code&gt; is the threshold we've landed on for most teams after watching this run on real PRs for a year. It's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tight enough to catch real regressions.&lt;/strong&gt; A 3-point drop is roughly "you broke a category" — typically performance from an unoptimized asset, or accessibility from a new component shipped without &lt;code&gt;alt&lt;/code&gt; text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loose enough to avoid false positives.&lt;/strong&gt; SEO Score has some run-to-run noise of ~1 point (response timing, network variance). &lt;code&gt;-3.0&lt;/code&gt; is comfortably outside that band.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For high-stakes pages (homepage, top landing page), tighten to &lt;code&gt;-2.0&lt;/code&gt;. For experimental routes, loosen to &lt;code&gt;-5.0&lt;/code&gt;. For &lt;em&gt;categories you care about specifically&lt;/em&gt;, gate per-category:&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="n"&gt;prod_cats&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sites&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categories&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;prev_cats&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sites&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categories&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;cat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;performance&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;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accessibility&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;2&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&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;3&lt;/span&gt;&lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prev_cats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prod_cats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cat&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;d&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="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;::error::&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; regressed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&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="s"&gt; (threshold &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;threshold&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per-category gates are what turn this from "the gate fails sometimes" into "the gate tells me &lt;em&gt;exactly&lt;/em&gt; what's wrong before I open the PR."&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/seo-gate.yml&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 regression 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;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gate&lt;/span&gt;&lt;span class="pi"&gt;:&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'&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;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;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="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&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="pi"&gt;-&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 requests&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 gate&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;SEOSCORE_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SEOSCORE_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;PREVIEW_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.event.deployment_status.target_url }}&lt;/span&gt;
          &lt;span class="na"&gt;PROD_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://yourdomain.com&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;python .ci/seo-gate.py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;deployment_status&lt;/code&gt; trigger waits for Vercel/Netlify/etc. to finish building the preview, then runs the gate against the preview URL it provides. That timing matters — running on &lt;code&gt;pull_request&lt;/code&gt; instead would audit a preview that hasn't deployed yet and either error or audit stale content.&lt;/p&gt;

&lt;p&gt;GitLab CI and CircleCI versions look essentially identical; see the &lt;a href="https://seoscoreapi.com/blog/seo-checks-in-cicd-pipeline?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=pre-deploy-seo-gates" rel="noopener noreferrer"&gt;generic CI/CD post&lt;/a&gt; for those.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about multi-page gates?
&lt;/h2&gt;

&lt;p&gt;A single-URL gate misses regressions on routes that didn't change in this PR but were affected by a global change — a layout shift, a font swap, a navigation rewrite. The fix is to audit a list of "critical pages" in batch:&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="n"&gt;CRITICAL_PAGES&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;/&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;/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;/blog&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;/docs&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;/blog/your-top-post&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;/your-top-product&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;prev_urls&lt;/span&gt; &lt;span class="o"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PREVIEW_URL&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;CRITICAL_PAGES&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;prod_urls&lt;/span&gt; &lt;span class="o"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROD_URL&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;CRITICAL_PAGES&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/audit/batch&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;json&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;urls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prev_urls&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;prod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&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;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/audit/batch&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;json&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;urls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prod_urls&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&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;prev_r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prod_r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prev_r&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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;prod_r&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;if&lt;/span&gt; &lt;span class="n"&gt;delta&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="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;prev_r&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="n"&gt;delta&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;failures&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&lt;/span&gt;&lt;span class="p"&gt;,&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;failures&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;::error::&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; regressed &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;d&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Batch audit is paid-only — this is where the gate stops being a Free-tier experiment and starts being part of your shipping process. For a team shipping 5–10 PRs a day and auditing 8 critical pages each, that's ~2,000 audits a month, which is exactly the &lt;a href="https://seoscoreapi.com/upgrade?tier=pro&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=pre-deploy-seo-gates" rel="noopener noreferrer"&gt;Pro tier ($39/mo)&lt;/a&gt; target.&lt;/p&gt;

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

&lt;p&gt;The failure message in CI is fine. The team will see it. But a comment on the PR with the actual scores is much more useful — it turns the gate from "the build is red" into "the homepage went from 92 to 87 because LCP regressed by 1.2s."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;comment &lt;span class="nv"&gt;$PR_NUMBER&lt;/span&gt; &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"## SEO gate
| URL | Previous | This PR | Delta |
|-----|----------|---------|-------|
| / | 92 | 87 | -5.0 ❌ |
| /pricing | 88 | 88 | 0.0 ✅ |
| /docs | 84 | 84 | 0.0 ✅ |
"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire that up with the GitHub CLI inside the action and you've got the full pattern. The &lt;a href="https://seoscoreapi.com/blog/github-actions-seo-audit?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=pre-deploy-seo-gates" rel="noopener noreferrer"&gt;github-actions-seo-audit post&lt;/a&gt; has a more elaborate version with collapsed details and per-category breakdowns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using &lt;code&gt;/history&lt;/code&gt; instead of &lt;code&gt;/compare&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/compare&lt;/code&gt; is the right call for a PR gate — both sides are audited fresh, no historical data needed, works on any plan with batch access. But for a &lt;em&gt;post-merge&lt;/em&gt; check ("did main just regress from where it was yesterday?"), the &lt;a href="https://seoscoreapi.com/blog/historical-seo-score-tracking?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=pre-deploy-seo-gates" rel="noopener noreferrer"&gt;history endpoint&lt;/a&gt; is cheaper:&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="n"&gt;hist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="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;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/history?url=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROD_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="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;X-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;API_KEY&lt;/span&gt;&lt;span class="p"&gt;},&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="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hist&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;1&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;yesterday&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hist&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;2&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;if&lt;/span&gt; &lt;span class="n"&gt;today&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;yesterday&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;3.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;notify_slack&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;main regressed from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;yesterday&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;today&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;Run that as a scheduled job after deploys merge. It uses one audit (today's) instead of two and gives you the same answer as long as you're already auditing the URL regularly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this catches that humans miss
&lt;/h2&gt;

&lt;p&gt;A non-exhaustive list from real PRs we've seen the gate flag:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A "loading skeleton" component that shipped without an &lt;code&gt;aria-label&lt;/code&gt;, knocking accessibility by 4 points sitewide.&lt;/li&gt;
&lt;li&gt;A font swap from a local woff2 to a Google Fonts CDN, regressing LCP by 800ms and tanking performance.&lt;/li&gt;
&lt;li&gt;A new "trust banner" with three external images, all 1.2MB JPEGs that should have been ~80KB WebP.&lt;/li&gt;
&lt;li&gt;A Markdown→HTML refactor that dropped &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; semantics inside blog posts.&lt;/li&gt;
&lt;li&gt;A new "share to social" widget that injected six tracking scripts before paint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these would have shipped silently and been discovered weeks later in a quarterly audit if anyone ran one. With a PR gate, they fail the build and get fixed before merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost of the gate vs cost of the regression
&lt;/h2&gt;

&lt;p&gt;Two pricing observations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The gate costs ~10 audits per PR (one batch of critical pages × pre/post). For a team shipping 100 PRs a month, that's 1,000 audits. &lt;strong&gt;Basic ($15/mo) covers it.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The regression it catches costs you weeks of organic traffic to a top page. &lt;strong&gt;One catch pays for the next several years of Basic.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We've yet to talk to a team that turned a pre-deploy SEO gate on and then turned it off again. It's the single highest-leverage piece of API tooling for a team that ships frequently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Copy the script, drop it in &lt;code&gt;.ci/&lt;/code&gt;, wire it to your deployment platform's preview hook, set the threshold to &lt;code&gt;-3.0&lt;/code&gt;, and ship a no-op PR to verify it runs. The first time it fails on a real regression — and it will, within a month — you'll know it was worth it.&lt;/p&gt;

&lt;p&gt;For per-category gates and history-based deltas, &lt;a href="https://seoscoreapi.com/upgrade?tier=basic&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=pre-deploy-seo-gates" rel="noopener noreferrer"&gt;grab a Basic key&lt;/a&gt;. For multi-page batch gates on a team shipping daily, the math points at &lt;a href="https://seoscoreapi.com/upgrade?tier=pro&amp;amp;utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=pre-deploy-seo-gates" rel="noopener noreferrer"&gt;Pro&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <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://seoscoreapi.com/blog/seo-analyzer-api-nextjs-build?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=catching-seo-regressions-vercel-preview" rel="noopener noreferrer"&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://seoscoreapi.com/blog/pre-deploy-seo-gates?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=catching-seo-regressions-vercel-preview" rel="noopener noreferrer"&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://seoscoreapi.com/seo-audit-api?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=seo-analyzer-api-nextjs-build" rel="noopener noreferrer"&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://seoscoreapi.com/blog/pre-deploy-seo-gates?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=seo-analyzer-api-nextjs-build" rel="noopener noreferrer"&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 80+ 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>
  </channel>
</rss>
