The most useful place to run an SEO analyzer API 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.
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.
Why preview URLs and not production
Three reasons preview-deploy auditing beats production-deploy auditing:
- You can block bad changes. A failed check on a preview is a PR you don't merge. A failed check on production is an incident.
- Each PR is a clean diff. The score on
mainis a moving target; the score delta betweenmainandfeature/new-pricingis a number you can argue about in a PR review. - JS-rendered pages need a real URL. 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.
The Vercel side
Vercel comments the preview URL on every PR. The format is stable enough to parse:
This pull request is being deployed to Vercel.
✅ Preview: https://your-app-git-feature-branch-team.vercel.app
We're going to grab that URL via the Vercel API instead of parsing the comment — same data, more reliable.
The GitHub Actions workflow
Save this as .github/workflows/seo-check.yml:
name: SEO Check
on:
pull_request:
types: [opened, synchronize]
jobs:
seo:
runs-on: ubuntu-latest
steps:
- name: Wait for Vercel preview
id: wait
uses: patrickedqvist/wait-for-vercel-preview@v1.3.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
max_timeout: 300
- name: Audit preview URL
env:
KEY: ${{ secrets.SEO_KEY }}
URL: ${{ steps.wait.outputs.url }}
run: |
RESP=$(curl -s -H "X-API-Key: $KEY" \
"https://seoscoreapi.com/audit?url=$URL")
SCORE=$(echo "$RESP" | jq '.score')
GRADE=$(echo "$RESP" | jq -r '.grade')
echo "Score: $SCORE ($GRADE) on $URL"
if [ "$SCORE" -lt 85 ]; then
echo "::error::SEO score $SCORE is below threshold (85)"
echo "$RESP" | jq '.priorities'
exit 1
fi
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 priorities array into the log so the PR author can see what to fix.
Setting the threshold
85 is the default we recommend, but it depends on your baseline. The right way to pick a threshold:
- Run the audit against
mainto get your current baseline. - Set the threshold 2 points below that baseline.
If your main 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.
For sites that haven't been optimized yet (main scores 65), threshold at 65. Hold the line, then ratchet up as you improve. The pre-deploy SEO gates post has more on threshold strategy.
Auditing more than the homepage
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:
URLS=(
"$URL/"
"$URL/pricing"
"$URL/signup"
"$URL/docs"
)
FAIL=0
for u in "${URLS[@]}"; do
S=$(curl -s -H "X-API-Key: $KEY" "https://seoscoreapi.com/audit?url=$u" | jq '.score')
echo " $u → $S"
[ "$S" -lt 85 ] && FAIL=1
done
exit $FAIL
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.
What this catches that Lighthouse doesn't
Lighthouse runs in your CI by default if you use Vercel — but Lighthouse is Web Vitals first, SEO second. SEO Score API runs 82 checks specifically about SEO: title structure, meta description presence, canonical tags, schema validity, OpenGraph completeness, internal linking, AI-readability signals, robot directives.
You can run both. They overlap maybe 10% — there's no good reason not to gate on both.
What to do when a developer's PR fails the gate
Two things help adoption:
- Print the
prioritiesarray in the failure log. 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." - Allow override via PR label. Add a
seo-skiplabel 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.
Both of those keep the gate from becoming the kind of CI check that everyone learns to ignore.