You already gate merge requests on tests and linting. SEO deserves the same treatment: a page that ships with a missing canonical tag or a broken meta description is a regression, and the cheapest time to catch it is before it merges. This post adds an SEO quality gate to GitLab CI — a job that audits your review app, fails the pipeline if the SEO Score drops below a number you choose, and leaves the result on the merge request.

It's the GitLab counterpart to the pre-deploy gates pattern, using nothing but curl, jq, and a CI variable.

Step 1: store your API key as a CI variable

In your project, go to Settings → CI/CD → Variables and add:

  • Key: SEOSCORE_API_KEY
  • Value: your key from seoscoreapi.com/#signup
  • Flags: mask it, and protect it if the gate only runs on protected branches.

Never hard-code the key in .gitlab-ci.yml — the masked variable keeps it out of logs and out of your repo history.

Step 2: the quality-gate job

Add this job to .gitlab-ci.yml. It audits a URL, extracts the score with jq, and exits non-zero — failing the pipeline — when the score falls below the threshold:

seo-audit:
  stage: test
  image: alpine:latest
  variables:
    AUDIT_URL: "https://your-review-app.example.com"
    MIN_SCORE: "85"
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      echo "Auditing $AUDIT_URL (minimum score: $MIN_SCORE)"
      RESPONSE=$(curl -s -H "X-API-Key: $SEOSCORE_API_KEY" \
        "https://seoscoreapi.com/audit?url=$AUDIT_URL")

      SCORE=$(echo "$RESPONSE" | jq -r '.score // empty')
      GRADE=$(echo "$RESPONSE" | jq -r '.grade // "?"')

      if [ -z "$SCORE" ]; then
        echo "Audit failed:"; echo "$RESPONSE" | jq .
        exit 1
      fi

      echo "Score: $SCORE ($GRADE)"
      echo "Top issues:"
      echo "$RESPONSE" | jq -r '.priorities[:3][]? | "  - " + .issue'

      if [ "$SCORE" -lt "$MIN_SCORE" ]; then
        echo "❌ Score $SCORE is below the $MIN_SCORE threshold — failing."
        exit 1
      fi
      echo "✅ Score $SCORE meets the bar."

Set AUDIT_URL to whatever URL represents this change — a GitLab review app, a Netlify/Vercel preview, or a staging deploy. The job prints the top three fixes on every run so a failure tells you why, not just that it failed.

Step 3: gate the right pipelines

You usually want this on merge requests, not on every commit. Scope it with rules:

  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

Pair it with a needs: on your deploy-preview job so the audit only runs once the review app is actually live. Now every MR gets a green or red SEO check next to its tests, and a red one blocks the merge exactly like a failing unit test would.

How do I audit a page that isn't publicly reachable?

The API fetches the URL from the public internet, so a review app behind Basic Auth or a private network won't be reachable. Two options: expose the review app at a public preview URL (most PaaS hosts do this by default), or run the gate in a deploy-to-staging → audit staging sequence where staging is public. If neither is possible, move the gate to run right after deploy to your public staging environment instead of at MR time — you still catch the regression before production, just one stage later. The Vercel preview approach covers the public-preview pattern in depth.

Should a low score fail the build or just warn?

Start with a warning, then tighten. On day one, set MIN_SCORE low enough that only genuine breakage trips it — a missing title, a 500 — so the team trusts the gate. Once scores are stable, raise the threshold toward your real target (85–90 is a sensible bar for a marketing site). If you want a soft gate that reports without blocking, add allow_failure: true to the job — the pipeline stays green but the SEO check still shows its result on the MR. Graduating from allow_failure: true to a hard gate is the natural adoption path, and it mirrors the CI/CD quality-gate philosophy of never shipping a metric you refuse to measure.

Won't the gate eat my audit quota?

Only lightly. One audit per merge request, on the branches you choose, is a handful of audits a day even on an active repo — well within Starter ($5/mo). It's when you expand the gate to audit every changed page in a large site that volume climbs; at that point the batch endpoint audits up to 10 URLs per call and the ROI breakdown shows which plan the volume lands in. For a single-page gate, the cost is a rounding error against the value of not shipping a regression.

Why gate on SEO at all?

Because SEO regressions are silent. A broken build screams; a dropped meta description just quietly costs you rankings for weeks until someone notices traffic sliding. Putting the check in CI turns an invisible, slow-bleeding problem into a loud, immediate one that blocks the merge — the same reason you gate on tests. The score becomes a shared, objective bar that a reviewer can point at instead of a subjective "did anyone check the SEO?" that everyone assumes someone else did.

Where to take it next

Extend the job to audit multiple critical paths by looping the curl over an array of URLs, or post the score into the MR using GitLab's API to add a note from CI. For teams running the same check across GitHub and GitLab, the GitHub Actions version is a near-identical drop-in — one API, two pipelines, the same gate.

Tests keep your code correct. This job keeps your SEO correct — and it's one YAML block away.