Skip to content
ExponaBLOG
TECHNICAL SEO FOR DEVELOPERS · DAY 076 / 100

SEO monitoring and alerting for Technical SEO developers

Catch SEO regressions before Google does.

Expona Admin10 min read

SEO monitoring and alerting: Catch regressions before Google does

I've watched production deployments silently break SEO for hours. A misconfigured robots.txt here, a missing canonical there, and suddenly your indexation plummets. SEO monitoring and alerting is what separates teams that discover issues in Search Console two weeks later from teams that know within minutes. This post covers how to build practical monitoring that ties SEO health to your release cycle.

Why this matters in production

Unlike application errors that throw 500s, SEO regressions are silent killers. Google's crawlers see the broken state, index it, and you lose rankings over weeks. Meanwhile, your alerts stayed quiet. I've seen:

  • A deploy that accidentally set noindex on the homepage—caught 8 hours later
  • Canonical tags pointing to wrong URLs for 12 hours after a CDN migration
  • Robots.txt blocking all crawlers due to a typo in the build process
  • 404s on paginated content after a URL structure refactor

The cost isn't just traffic loss. It's emergency fixes under pressure, search visibility taking weeks to recover, and eroded trust in your deployment process. Monitoring catches these fast.

Monitor key pages—Status, title, canonical and robots meta for top pages, every day

Start by monitoring what Google sees on your most important pages. Your homepage, key landing pages, and top revenue drivers should have daily health checks.

What to track

For each page, monitor:

  • HTTP status code — Should be 2xx, not 404 or 5xx
  • Title tag — Should exist, have reasonable length (30–60 characters), and not be empty or auto-generated
  • Meta description — Present and not duplicated across pages
  • Canonical tag — Points to itself or the correct canonical URL, not to 404s
  • Robots meta — Should not have unexpected noindex, nofollow, or nocache directives
  • Open Graph tags — For social crawlers, if applicable

Practical implementation

Here's a Node.js script that crawls key pages and checks these signals:

html
const axios = require('axios');
const cheerio = require('cheerio');

const KEY_PAGES = [
  'https://example.com/',
  'https://example.com/pricing',
  'https://example.com/blog/getting-started'
];

async function checkSeoSignals(url) {
  try {
    const response = await axios.get(url, {
      headers: { 'User-Agent': 'SEO-Monitor/1.0' }
    });

    const $ = cheerio.load(response.data);
    const statusCode = response.status;
    const title = $('title').text();
    const description = $('meta[name="description"]').attr('content');
    const canonical = $('link[rel="canonical"]').attr('href');
    const robotsMeta = $('meta[name="robots"]').attr('content') || '';

    return {
      url,
      timestamp: new Date().toISOString(),
      statusCode,
      title,
      description,
      canonical,
      robotsMeta,
      hasNoindex: robotsMeta.includes('noindex'),
      hasNofollow: robotsMeta.includes('nofollow')
    };
  } catch (error) {
    return {
      url,
      timestamp: new Date().toISOString(),
      error: error.message,
      statusCode: error.response?.status
    };
  }
}

async function monitorPages() {
  const results = await Promise.all(
    KEY_PAGES.map(url => checkSeoSignals(url))
  );

  results.forEach(result => {
    if (result.error) {
      console.error(`ERROR: ${result.url} - ${result.statusCode} ${result.error}`);
    } else if (result.statusCode !== 200) {
      console.warn(`WARNING: ${result.url} returned ${result.statusCode}`);
    } else if (result.hasNoindex) {
      console.error(`CRITICAL: ${result.url} has noindex meta tag`);
    } else if (!result.title) {
      console.warn(`WARNING: ${result.url} has empty title tag`);
    } else {
      console.log(`OK: ${result.url} - Status ${result.statusCode}`);
    }
  });

  return results;
}

monitorPages();

Run this daily via a cron job or CI pipeline. Store results in a database so you can compare day-over-day.

Real example

A SaaS platform I worked on monitors 50 key pages every 6 hours. When one deployment accidentally changed the canonical from https://example.com/page to https://example.com/page/ (added trailing slash), the monitoring caught it in the next run. The alert fired, we caught it before crawl budget was wasted, and a quick fix deployed within 30 minutes.

Alert on changes—A sudden noindex on the homepage should page someone

Monitoring without alerting is just logging. You need immediate notification when SEO signals change unexpectedly.

What warrants an alert

Critical alerts (wake someone up):

  • Homepage returns non-2xx status code
  • Any key page has noindex meta tag
  • Robots.txt becomes unreachable or blocks all crawlers
  • Canonical tag is missing on important pages
  • Title or meta description suddenly empty

Warning alerts (log and digest):

  • Title tag length outside normal range (below 20 or above 80 characters)
  • Duplicate meta descriptions across multiple pages
  • Canonical tag pointing to different URL than yesterday
  • Robots meta changed but noindex not set

Integration with Slack or PagerDuty

Here's how to send alerts when critical changes are detected:

html
const axios = require('axios');

async function sendAlert(severity, message, pageUrl) {
  const slackWebhook = process.env.SLACK_WEBHOOK_URL;

  const payload = {
    text: `SEO Alert [${severity}]`,
    attachments: [
      {
        color: severity === 'CRITICAL' ? 'danger' : 'warning',
        fields: [
          { title: 'Page', value: pageUrl, short: false },
          { title: 'Message', value: message, short: false },
          { title: 'Timestamp', value: new Date().toISOString(), short: true }
        ]
      }
    ]
  };

  try {
    await axios.post(slackWebhook, payload);
  } catch (error) {
    console.error('Failed to send alert:', error.message);
  }
}

async function checkAndAlert(previousState, currentState) {
  // Check for noindex
  if (!previousState.hasNoindex && currentState.hasNoindex) {
    await sendAlert('CRITICAL', 'noindex meta tag detected', currentState.url);
  }

  // Check for status code change to 4xx/5xx
  if (previousState.statusCode === 200 && currentState.statusCode >= 400) {
    await sendAlert('CRITICAL', `Status changed from 200 to ${currentState.statusCode}`, currentState.url);
  }

  // Check for canonical change
  if (previousState.canonical !== currentState.canonical) {
    await sendAlert('WARNING', 
      `Canonical changed from ${previousState.canonical} to ${currentState.canonical}`, 
      currentState.url
    );
  }

  // Check for title change
  if (previousState.title !== currentState.title) {
    await sendAlert('WARNING', 
      `Title changed from "${previousState.title}" to "${currentState.title}"`, 
      currentState.url
    );
  }
}

In production, I recommend sending critical alerts to PagerDuty (which can page on-call engineers) and warnings to Slack (which goes to a monitoring channel).

Watch Search Console—Coverage drops and crawl error spikes

Google Search Console is the single source of truth for how Google sees your site. Monitor it programmatically.

Key metrics to track

  • Indexed pages — Should be stable or growing. A sudden drop is a red flag
  • Crawl errors — Spikes in 404, 5xx, or DNS errors indicate problems
  • Coverage issues — Excluded pages, soft 404s, and duplicate content
  • Mobile usability — Large jumps in mobile issues after a deploy

Fetching data from Search Console API

You'll need to authenticate with Google's API. Use a service account:

html
const { google } = require('googleapis');
const fs = require('fs');

const SCOPES = ['https://www.googleapis.com/auth/webmasters.readonly'];

async function initializeAuth() {
  const keyFile = JSON.parse(fs.readFileSync(process.env.GOOGLE_CREDENTIALS_PATH));
  const auth = new google.auth.GoogleAuth({ keyFile, scopes: SCOPES });
  return google.webmasters({ version: 'v3', auth });
}

async function getIndexingStats(webmastersClient, siteUrl) {
  try {
    const response = await webmastersClient.sites.get({ siteUrl });
    console.log('Site verified:', response.data.siteUrl);
  } catch (error) {
    console.error('Error fetching site stats:', error.message);
  }
}

async function monitorSearchConsole() {
  const webmastersClient = await initializeAuth();
  const siteUrl = 'https://example.com/';

  // Fetch crawl errors from past 7 days
  const response = await webmastersClient.urlcrawlerrorscounts.query({
    siteUrl,
    category: 'serverError',
    latestCountsOnly: true
  });

  const errorCounts = response.data.countPerTypes || [];
  errorCounts.forEach(count => {
    console.log(`${count.platform}: ${count.count} crawl errors`);
    if (count.count > 100) {
      // Alert if threshold exceeded
      console.warn(`ALERT: Crawl error count ${count.count} exceeds threshold`);
    }
  });
}

monitorSearchConsole();

Run this daily. Compare against historical baselines—a jump from 5 crawl errors to 500 overnight means something broke during a deploy.

Tie to deploys—Most SEO disasters arrive with a release

The strongest SEO monitoring integrates with your deployment pipeline. When you deploy, you should automatically run SEO checks and block bad deploys.

Pre-deploy checks

In your CI/CD pipeline, before deployment, verify:

  • Robots.txt is syntactically valid and doesn't block all crawlers
  • Canonical URLs are correctly configured in code
  • No environment-specific SEO tags (like noindex) leaking to production
  • Redirects are configured correctly (301s, not 302s for permanent moves)

Here's a GitHub Actions example:

html
name: SEO Pre-Deploy Check
on:
  pull_request:
    branches: [main]

jobs:
  seo-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Check robots.txt
        run: |
          if grep -q "Disallow: /$" robots.txt; then
            echo "ERROR: robots.txt blocks all crawlers"
            exit 1
          fi
      
      - name: Check for noindex in production config
        run: |
          if grep -r "noindex" src/ --include="*.ts" --include="*.tsx" | grep -v "test\|spec"; then
            echo "WARNING: noindex found in production code"
            exit 1
          fi
      
      - name: Validate canonical URLs
        run: npm run validate:canonical
      
      - name: Check redirect chains
        run: npm run validate:redirects

Post-deploy verification

After deployment to staging or production, immediately run the page monitoring script from earlier. If critical alerts fire, trigger a rollback or notification:

html
async function postDeployCheck(deploymentInfo) {
  console.log(`Running post-deploy SEO check for ${deploymentInfo.version}`);
  
  const results = await monitorPages();
  const criticalIssues = results.filter(r => 
    r.statusCode >= 400 || r.hasNoindex || !r.title
  );

  if (criticalIssues.length > 0) {
    await sendAlert('CRITICAL', 
      `Post-deploy SEO issues detected: ${criticalIssues.length} pages affected`, 
      deploymentInfo.url
    );
    
    // Optionally trigger rollback
    if (process.env.AUTO_ROLLBACK_ON_SEO_FAILURE === 'true') {
      console.log('Triggering automatic rollback...');
      await triggerRollback(deploymentInfo.previousVersion);
    }
  } else {
    console.log('Post-deploy SEO check passed');
  }
}

I've seen this catch mistakes within seconds of deployment. A developer accidentally committed a staging-environment robots.txt that blocked all crawlers. The post-deploy check caught it and paged the on-call engineer before any real damage occurred.

Common mistakes

Monitoring without baseline data. You can't detect a change if you don't know what normal looks like. Track at least 7 days of baseline metrics before alerting on anomalies.

Alerting on everything. If every canonical change triggers an alert, on-call engineers ignore alerts. Set thresholds—a title tag being 2 characters longer isn't critical.

Ignoring staging environments. Many teams only monitor production. But SEO regressions often appear in staging first. Monitor both.

Forgetting about edge cases. AMP pages, mobile-specific canonical tags, and internationalized sites (hreflang) need specialized monitoring. Don't assume a single check fits all.

Not correlating with deploys. You might see a crawl error spike and have no idea when it started. Always log deployment timestamps alongside monitoring data.

Key takeaways

  • Monitor your 50–100 most important pages daily: status codes, titles, canonicals, and robots meta
  • Set up alerts for critical changes (noindex, 4xx/5xx, missing canonicals) and warnings for non-critical ones
  • Integrate Search Console monitoring to catch indexation drops and crawl error spikes
  • Tie monitoring to your deployment pipeline with pre-deploy validation and post-deploy verification
  • Use baseline data to detect anomalies, not just absolute values
  • Alert critical SEO issues to PagerDuty or on-call systems; log warnings to Slack digests
  • Track everything with deployment timestamps so you can correlate issues to specific releases
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles