Short answer: to run SEO checks in CI, add the SeoScoreAPI/seo-audit-action step to a GitHub Actions workflow with a URL, an API key and a threshold. It calls the SEO Score API GET /audit endpoint, writes the score to the job summary, and exits non-zero when the score is under the threshold (default 80). A free key covers 2 audits a day, so on the free tier run it on pushes to main, not on every pull request.
I'm Aaron, the solo developer behind SEO Score API and this action. This post is the workflow I'd actually commit, plus the parts that tend to surprise people.
How do I run an SEO audit in GitHub Actions?
Get a key (free, email plus a 6-digit code, no card), add it as a repository secret named SEO_SCORE_API_KEY, and commit this as .github/workflows/seo.yml:
name: SEO check
on:
push:
branches: [main]
workflow_dispatch:
jobs:
seo:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: SEO audit
id: seo
uses: SeoScoreAPI/seo-audit-action@v1
with:
url: "https://your-site.com"
api-key: ${{ secrets.SEO_SCORE_API_KEY }}
threshold: 80
- name: Print result
if: always()
run: echo "Score ${{ steps.seo.outputs.score }} (${{ steps.seo.outputs.grade }}), report ${{ steps.seo.outputs.report-url }}"
That is the whole thing. No checkout step is needed, because the action audits a URL, not your repository.
What does the SEO audit action do?
It is a composite action: a shell step that runs curl and jq. You can read all of it in action.yml. The inputs and outputs for the standard audit:
| Input | Default | Meaning |
|---|---|---|
url |
required | The page to audit |
api-key |
required | Your SEO Score API key |
threshold |
80 |
Minimum score to pass, 0-100 |
fail-on-threshold |
true |
Set to false to report without failing |
Outputs are score, grade, issues (the number of fixes returned) and report-url. The score is a decimal such as 65.567, and the action compares it numerically. There are also deep-audit-* inputs for the slower Deep Site Audit (Pro and Ultra plans), which I'm leaving out here.
The audit itself is the same GET /audit call you can make by hand: 55 checks per URL on the free tier, 83 on paid plans, across meta and content, technical SEO, social tags, performance, accessibility and AI readability.
How do I fail the build on specific checks, not just the score?
A score threshold catches gradual decay. It is weak at catching one fatal change, because a single check is averaged in with dozens of others.
For that, skip the action, call the API once, and gate on both the score and a list of named checks. This is a complete workflow:
name: SEO gate
on:
push:
branches: [main]
workflow_dispatch:
jobs:
seo-gate:
runs-on: ubuntu-latest
timeout-minutes: 5
env:
SITE_URL: "https://your-site.com"
MIN_SCORE: "80"
NO_FAIL: '["status_code","https","title","meta_description","h1"]'
NO_WARN: '["canonical","robots_meta"]'
steps:
- name: Run audit
env:
SEOSCORE_API_KEY: ${{ secrets.SEO_SCORE_API_KEY }}
run: |
curl -sS --fail-with-body --max-time 120 -G \
-H "X-API-Key: $SEOSCORE_API_KEY" \
--data-urlencode "url=$SITE_URL" \
-o audit.json "https://seoscoreapi.com/audit" \
|| { cat audit.json; exit 1; }
- name: Enforce score and required checks
run: |
SCORE=$(jq -r '.score' audit.json)
BROKEN=$(jq -r --argjson nofail "$NO_FAIL" --argjson nowarn "$NO_WARN" '
.audit[].checks[]
| select(
(.status == "fail" and (.name | IN($nofail[], $nowarn[])))
or (.status == "warning" and (.name | IN($nowarn[])))
)
| "- \(.name) (\(.status)): \(.recommendation // "no detail")"' audit.json)
echo "SEO score: $SCORE (minimum $MIN_SCORE)"
if awk -v s="$SCORE" -v t="$MIN_SCORE" 'BEGIN { exit (s + 0 < t + 0) ? 0 : 1 }'; then
echo "::error::SEO score $SCORE is below $MIN_SCORE"
exit 1
fi
if [ -n "$BROKEN" ]; then
echo "::error::Required SEO checks failed"
echo "$BROKEN"
exit 1
fi
The names are real check names from the response (.audit.<category>.checks[].name). Run against a saved audit of https://example.com with MIN_SCORE at 60, the gate step exits 1 and prints:
SEO score: 65.567 (minimum 60)
::error::Required SEO checks failed
- meta_description (fail): Add a meta description (150-160 chars)
- h1 (fail): Add exactly one H1 tag
- canonical (warning): Add a canonical tag to prevent duplicate content issues
Two things to know. First, a check's status is pass, warning, fail or info, and some serious problems are only a warning. A missing canonical tag is one, and so is a noindex robots meta tag. That is why there are two lists: checks in NO_WARN break the build on a warning too. Second, this version needs a paid key to be useful. Free keys get the full score but a shortened checks array per category, so a check you listed may not be in the response at all. The score threshold works on any plan.
How many audits will CI use?
One audit per URL per run. That makes the free tier's limits easy to reason about:
| Plan | Price | Audits | Requests per minute | Rough CI fit |
|---|---|---|---|---|
| Free | $0 | 2 per day | 2 | One URL, on pushes to main
|
| Starter | $5/month | 200 per month | 10 | One URL on every pull request for a small team |
| Basic | $15/month | 1,000 per month | 30 | A handful of URLs per pull request |
| Pro | $39/month | 5,000 per month | 60 | Many URLs, busy repository |
Ultra is $99/month for 25,000 audits.
What goes wrong in practice?
-
It audits what is deployed. The API fetches a public URL. It cannot see
localhoston your runner. On a push tomainthat means the gate runs after the deploy, so make the SEO job depend on your deploy job (needs: deploy) or it will audit the previous version. To block a merge before production, pointurlat a preview deployment. -
Running out of quota fails the build. The action uses
curl -f, so a 429 (daily limit reached, or more than your requests per minute) ends the step with "Failed to reach SEO Score API". On the free tier the third run of the day will be red for that reason, not because your SEO changed. -
Audited domains appear on a public scoreboard unless you opt the key out:
curl -X PUT "https://seoscoreapi.com/scoreboard/opt-out?opt_out=true" -H "X-API-Key: YOUR_API_KEY". Do this before auditing preview URLs. -
Pick a threshold below your current score. Run it once with
fail-on-threshold: false, then set the threshold a few points under what you got. A gate that is red on day one gets deleted. -
Give it time. Most audits return in seconds, but while writing this I saw one take about a minute. Hence
--max-time 120and a 5 minute job timeout.
When is Lighthouse CI or another tool a better choice?
| Tool | Runs where | Better choice when |
|---|---|---|
| SEO Score API action | Hosted API, audits a public URL | You want one score and a fix list covering on-page, technical and AI-readability checks, with no browser to install |
| Lighthouse CI | Your CI runner, against a local build or a URL | You want to gate before anything is deployed, you care most about performance budgets, or you want no third party and no quota |
| PageSpeed Insights API | Google's servers, public URL | You want Core Web Vitals, including real-user data where Google has it |
| Open-source link checkers and crawlers (lychee, Scrapy) | Your CI runner | You need every page crawled for broken links or custom rules |
Lighthouse CI is free and open source, and its SEO category covers the basics (title, meta description, crawlability, valid robots.txt). If that is all you need, use it and skip the API key. Plenty of teams run both.
FAQ
Is there an SEO audit GitHub Action?
Yes: SeoScoreAPI/seo-audit-action@v1. It takes url, api-key and threshold, and fails the job when the score is below the threshold.
Can I run SEO checks in CI for free?
Yes, within 2 audits per day and 2 requests per minute. For a pull-request gate you will want the $5 Starter plan (200 audits a month) or Lighthouse CI, which has no quota.
Does it work with GitLab CI, CircleCI or Jenkins?
Yes. The action is a wrapper around one curl call, and the second workflow above is plain shell that runs anywhere curl and jq are installed.
Can it audit a pull request before it is deployed?
Only if the pull request has a public preview URL. For a build that never leaves the runner, use Lighthouse CI.
Will this improve my rankings?
No tool can promise that. It stops you from shipping regressions such as a missing title or an accidental noindex, which is a narrower claim.
Top comments (0)