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.
This post is the production setup for catching SEO regressions on Vercel previews using SEO Score API. It's a sibling of Wiring an SEO analyzer API into a Next.js build but focused on the Vercel-specific gotchas.
The shape of the workflow
Three steps:
- Wait for the Vercel preview to be live (it takes 30s–3min after PR push).
- Audit the preview URL with SEO Score API.
- Fail the PR check if the score is below the threshold.
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 deploy isn't ready for several minutes. Audit too early and you'll audit a 404 or an in-progress build.
The workflow
.github/workflows/seo-preview.yml:
name: SEO Preview Check
on:
pull_request:
types: [opened, synchronize]
jobs:
seo:
runs-on: ubuntu-latest
steps:
- name: Wait for Vercel preview to be ready
uses: patrickedqvist/wait-for-vercel-preview@v1.3.1
id: preview
with:
token: ${{ secrets.GITHUB_TOKEN }}
max_timeout: 600 # 10 min — Next.js builds can be slow
- name: Audit preview
id: audit
env:
KEY: ${{ secrets.SEO_KEY }}
run: |
URL="${{ steps.preview.outputs.url }}"
RESP=$(curl -s -H "X-API-Key: $KEY" \
"https://seoscoreapi.com/audit?url=$URL")
SCORE=$(echo "$RESP" | jq '.score // 0')
echo "score=$SCORE" >> $GITHUB_OUTPUT
echo "## SEO Score: $SCORE" >> $GITHUB_STEP_SUMMARY
echo "$RESP" | jq -r '.priorities[] | "- \(.issue)"' >> $GITHUB_STEP_SUMMARY
- name: Threshold check
run: |
[ "${{ steps.audit.outputs.score }}" -ge 85 ] || exit 1
The wait-for-vercel-preview 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.
What we learned the hard way
A few things that bit us in production on the first version:
1. Don't audit immediately. Even after Vercel reports the preview is live, the CDN propagation takes another 5–10 seconds for global routes. Insert a sleep 15 after wait-for-vercel-preview if you see intermittent 502s from the audit.
2. Branch URLs change on rebase. The Vercel preview URL incorporates the branch name and commit SHA. After a force-push, the URL changes. wait-for-vercel-preview handles this if you call it fresh in every workflow run.
3. Auth-gated previews are invisible to us. 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 protection-bypass-for-automation token and pass it as a header — we accept a ?bypass_token query parameter.
4. The first audit cold-starts the page. 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 curl $URL step before the audit step and the numbers settle.
5. Set the threshold against main, not against the score you wish you had. A gate that fails every PR teaches developers to bypass it. See Pre-Deploy SEO Gates for how to pick the right threshold.
Multi-page auditing
The example above audits one URL. Most teams want to audit a small list of routes — the ones that matter for SEO, like /, /pricing, /signup, and the top 3–5 product pages.
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 // 0')
echo " $u → $S"
[ "$S" -lt 85 ] && FAIL=1
done
exit $FAIL
Four URLs × ~10 PRs/day × 22 working days = ~880 audits/month. That fits in the Basic plan ($15/mo, 1,000 audits) with margin.
Posting the report back to the PR
GitHub's STEP_SUMMARY works, but a sticky PR comment is better — same content, doesn't get buried. The marocchino/sticky-pull-request-comment action handles the dedup:
- uses: marocchino/sticky-pull-request-comment@v2
with:
header: seo-score
message: |
### SEO Preview Score: ${{ steps.audit.outputs.score }}
Top issues:
${{ steps.audit.outputs.priorities }}
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.
When to skip the gate
A seo-skip 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.
What this gives you
Three months in, the win isn't the headline number going up — it's the absence of regressions. The gate catches the accidentally-deleted <title> 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.
That's the whole case for the Vercel preview audit.