Skip to main content
5 Min Read

SEO Monitoring with GitHub Actions

SEO regressions are invisible until Google notices them. A developer removes a canonical tag, a CMS update strips title tags. Nobody sees it for weeks. This guide shows how to catch those regressions in CI, the same way you catch type errors.

This guide covers the SEOLint API in GitHub Actions. For a broader setup including Core Web Vitals thresholds and PageSpeed Insights, see How to Check Core Web Vitals in GitHub Actions.

The API flow

The SEOLint API fits into any CI step in three calls:

  1. POST /api/v1/scanStart a scan, get back scanId and pollUrl
  2. GET pollUrlPoll every 3s until status is complete
  3. Read issues[]Each issue has severity: critical | warning | info

Minimal workflow

Add to .github/workflows/seo-check.yml. Set SITE_URL as a repository variable.

name: SEO Audit

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  seo-audit:
    runs-on: ubuntu-latest
    steps:
      - name: Start scan
        id: scan
        run: |
          RESPONSE=$(curl -s -X POST https://seolint.dev/api/v1/scan \
            -H "Content-Type: application/json" \
            -d '{"url": "${{ vars.SITE_URL }}"}')
          echo "scan_id=$(echo $RESPONSE | jq -r '.scanId')" >> $GITHUB_OUTPUT
          echo "poll_url=$(echo $RESPONSE | jq -r '.pollUrl')" >> $GITHUB_OUTPUT

      - name: Wait for results
        id: results
        run: |
          POLL_URL="${{ steps.scan.outputs.poll_url }}"
          for i in $(seq 1 20); do
            STATUS=$(curl -s "$POLL_URL" | jq -r '.status')
            [ "$STATUS" = "complete" ] && break
            sleep 3
          done

      - name: Fail on critical issues
        run: |
          CRITICAL=$(curl -s "${{ steps.scan.outputs.poll_url }}" \
            | jq '[.issues[] | select(.severity == "critical")] | length')
          if [ "$CRITICAL" -gt "0" ]; then
            curl -s "${{ steps.scan.outputs.poll_url }}" \
              | jq -r '.issues[] | select(.severity == "critical") | "❌ " + .title'
            exit 1
          fi
          echo "✅ No critical SEO issues"

What gets checked

CheckSeverity
Missing title tagcritical
Missing H1critical
Missing canonical URLcritical
LCP > 2.5scritical
Images missing alt textwarning
Missing Open Graph tagswarning
Meta description > 160 charswarning
Missing robots metainfo

Auto-open a GitHub issue

Add this step after the failure check. It opens an issue with the full markdown report attached.

      - name: Open issue with report
        if: failure()
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          MARKDOWN=$(curl -s "${{ steps.scan.outputs.poll_url }}/markdown")
          gh issue create \
            --title "SEO regression: $(date +%Y-%m-%d)" \
            --body "$MARKDOWN" \
            --label "seo"

The /markdown endpoint returns structured fix instructions, including JSON-LD suggestions that align with Google's structured data guidelines. Paste the issue body into Claude or Cursor to apply the fixes automatically.

FAQ

Can I run SEO checks in GitHub Actions?

Yes. POST a URL to the SEOLint API, poll for results, and fail the workflow if critical issues are found. All from a standard GitHub Actions step.

What SEO issues does automated monitoring catch?

Missing title tags, missing H1, broken canonical URLs, images without alt text, missing Open Graph tags, and Core Web Vital regressions.

What plan do I need for API access?

The SEOLint plan ($79/month, one site under agent watch) includes REST API access and GitHub Actions integration. 200 AI credits/month bundled in covers a typical CI loop, and you can connect your own Claude key to skip the credit cap entirely. Teams running many sites can email us for the Custom plan with volume pricing.

Add SEO monitoring to your pipeline

Takes 5 minutes. SEOLint ($79/month) required for API access.

DS

Daniel Smidstrup

Building SEOLint and other developer tools at danielsmidstrup.com

← All articles