<?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>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 same 55 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 (LCP 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 83 checks specifically about SEO: title structure, meta description presence, canonical tags, schema validity, OpenGraph completeness, internal linking, AI-readability signals, robot directives.&lt;/p&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://seoscoreapi.com/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=seo-checks-in-cicd-pipeline#signup" rel="noopener noreferrer"&gt;Get a free API key&lt;/a&gt; — 2 audits/day, no credit card required&lt;/li&gt;
&lt;li&gt;Add the workflow YAML to your repo&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;SEO_API_KEY&lt;/code&gt; as a pipeline secret&lt;/li&gt;
&lt;li&gt;Push to main and watch the quality gate run&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For teams running audits on every PR across multiple environments, the &lt;a href="https://seoscoreapi.com/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=seo-checks-in-cicd-pipeline#pricing" rel="noopener noreferrer"&gt;Basic plan ($15/mo)&lt;/a&gt; gives you 1,000 audits/month at 30 RPM.&lt;/p&gt;




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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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