NEWWorld's first AI visibility audit tool for Web3 is live.Run free audit →
Tutorial 07 · 30 minutes · Developer-facing

CI-integrated webhooks for automated re-audits.

Once you have shipped the high-impact fixes and you want continuous SEO health monitoring, this tutorial sets up automated re-audits triggered on every production deploy with Slack alerts on score regression. End to end: webhook handler, signature verification, Slack integration, deploy-trigger configuration. By the end, you cannot ship a regression without knowing about it within minutes.

// Why this matters

SEO regressions ship silently.

Most SEO regressions are caused by deploys that change something the team did not realize affected SEO: a CSS change that breaks Core Web Vitals, a robots.txt edit that accidentally blocks a crawler, a template refactor that drops schema. None of these surface until the next manual audit, often weeks later. By then the damage is real and attribution is hard. CI-integrated webhooks fix the problem: every deploy triggers an audit, score regressions surface immediately in Slack, attribution is automatic.

// Before you start

What you need.

  • Crawlux Pro or Team tier. Webhook callbacks require Pro or higher. The free tier produces audit JSON but does not POST callbacks.
  • A webhook endpoint you control. Node, Python, Go, anything that can receive a POST and return 200 in under 5 seconds. The tutorial uses Node + Express examples.
  • A Slack workspace with incoming webhooks. For the alert step. Create one at api.slack.com/messaging/webhooks.

// Step 1 of 5

Configure the Crawlux webhook.

In the Crawlux dashboard under Settings → Webhooks, create a new webhook. URL points to your endpoint. Subscribe to audit.completed and audit.failed events. Crawlux generates the signing secret; store it as CRAWLUX_WEBHOOK_SECRET in your environment.

// Step 2 of 5

Build the webhook handler.

The handler verifies the signature, parses the audit JSON, compares against the previous audit, and posts to Slack if the score regressed.

webhook-handler.js
const express = require("express");
const crypto = require("crypto");
const fetch = require("node-fetch");

const app = express();
app.use(express.json({ verify: (req, _, buf) => { req.rawBody = buf; } }));

function verifySignature(rawBody, sig, secret) {
  const expected = crypto.createHmac("sha256", secret)
    .update(rawBody).digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(sig.replace("sha256=", ""), "hex")
  );
}

app.post("/webhooks/crawlux", async (req, res) => {
  // 1. Verify signature
  const sig = req.headers["x-crawlux-signature"];
  if (!verifySignature(req.rawBody, sig, process.env.CRAWLUX_WEBHOOK_SECRET)) {
    return res.status(401).send("invalid signature");
  }

  // 2. Return 200 immediately
  res.status(200).send("ok");

  // 3. Process asynchronously
  const audit = req.body;
  const previous = await db.lastAudit(audit.domain);
  const delta = audit.score.overall - (previous?.score?.overall ?? audit.score.overall);
  await db.saveAudit(audit);

  // 4. Alert on regression > 5 points
  if (delta <= -5) {
    await postToSlack({
      domain: audit.domain,
      previous_score: previous.score.overall,
      current_score: audit.score.overall,
      delta,
      top_findings: audit.findings.slice(0, 3)
    });
  }
});

app.listen(3000);

// Step 3 of 5

Format the Slack alert.

Slack alerts should surface the score drop, the deploy that probably caused it, and the top 3 findings to investigate. Use Block Kit for readable formatting:

slack-alert.js
async function postToSlack({ domain, previous_score, current_score, delta, top_findings }) {
  const blocks = [
    {
      type: "header",
      text: { type: "plain_text", text: `SEO regression on ${domain}` }
    },
    {
      type: "section",
      fields: [
        { type: "mrkdwn", text: `*Previous score:*\n${previous_score}` },
        { type: "mrkdwn", text: `*Current score:*\n${current_score} (${delta})` }
      ]
    },
    {
      type: "section",
      text: {
        type: "mrkdwn",
        text: "*Top findings to investigate:*\n" +
              top_findings.map(f => `• ${f.title} (analyzer ${f.analyzer})`).join("\n")
      }
    }
  ];

  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ blocks })
  });
}

// Step 4 of 5

Trigger audits from your deploy pipeline.

After each production deploy, call the Crawlux dashboard's manual audit trigger via the dashboard URL (or, once the API ships Q4 2026, via POST /audits). GitHub Actions example:

.github/workflows/post-deploy-audit.yml
name: Post-deploy Crawlux audit

on:
  workflow_run:
    workflows: ["Deploy Production"]
    types: [completed]

jobs:
  trigger-audit:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    steps:
      - name: Trigger Crawlux audit
        run: |
          # Pre-API: trigger via dashboard URL with auth
          curl -X POST "https://www.crawlux.com/api/dashboard/audits" \
            -H "Authorization: Bearer ${{ secrets.CRAWLUX_DASHBOARD_TOKEN }}" \
            -d '{"domain": "yourdomain.io"}'
          # Post-Q4 2026: use the API endpoint
          # curl -X POST https://api.crawlux.com/v1/audits ...

// Step 5 of 5

Test the full loop.

Three-step test before relying on the alerting:

  1. Manual audit trigger. Trigger an audit from the Crawlux dashboard. Confirm your webhook handler receives the POST, verifies the signature, and returns 200.
  2. Force a regression. Deploy a change you know damages SEO (add Disallow: / to robots.txt temporarily). Trigger an audit. Confirm the Slack alert fires with the expected score drop.
  3. Roll back the test. Remove the bad change. Trigger another audit. Score returns to baseline. No alert fires (delta should be small or positive).

// Going further

What else you can wire up.

  • Block deploys on critical regressions. Score drops over 15 points usually indicate a breaking change. Wire the audit into a deploy-gate check.
  • Per-environment audits. Audit staging separately from production. Catch regressions before they ship.
  • Auto-create issues. When alerts fire, auto-create a GitHub issue with the top 3 findings as the description.
  • Dashboard integration. POST the audit JSON to your internal observability dashboard alongside other deploy metrics.

// Related

More reading.

FREE WEB3 AUDIT

Run a free Crawlux audit and apply this tutorial.

Run a free Crawlux audit and follow the tutorial sequence start to finish.

Free first audit · No signup · 60 seconds · Full PDF report

After state (what good looks like)

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "speakable": {
    "@type": "SpeakableSpecification",
    "cssSelector": [".faq-answer", ".quick-answer"]
  },
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Is Aave safe to use?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Aave has been audited 12+ times..."
      }
    }
  ]
}
</script>

How to validate the fix

Common pitfalls

Pitfall

Adding Speakable without FAQPage

Speakable can be added to Article or other types but works best paired with FAQPage. If your page doesn't have FAQPage yet, add it first. Speakable on a bare HTML page does little.

Pitfall

CSS selector that doesn't exist

If cssSelector points to .faq-answer but your DOM uses .accordion-content, Speakable resolves to nothing. Always validate that the selector actually matches DOM elements.

Pitfall

Pointing Speakable at the entire page

Don't use cssSelector: ["body"] or similar broad selectors. Speakable should point to specific answer-bearing elements. Broad selectors get ignored by parsers.

Pitfall

Speakable on pages without good answers

Speakable amplifies what's on the page. If your answers are vague or marketing-driven, Speakable amplifies vagueness. Make sure FAQ answers are concrete before adding Speakable.

Pitfall

Forgetting to update Speakable when content changes

If you redesign your FAQ block and change the class names, update the Speakable cssSelector. Stale selectors become invisible.

If something breaks: rollback

Remove the speakable property from FAQPage schema. Page falls back to regular FAQPage behavior within minutes. Citation rate may regress but no risk to site functionality.

Run a free Crawlux audit

Crawlux validates the schema, technical and AEO fixes from this tutorial automatically. Free tier on one domain.

Run free audit →

FAQ

Does Speakable work outside FAQPage?

Yes. Speakable can be added to Article, BlogPosting, NewsArticle and most schema types. The pattern is the same: SpeakableSpecification with cssSelector or xpath pointing to the most-quotable sections of the page.

Will Speakable affect Google rich results?

Speakable isn't in Google's primary rich result types yet but it's parsed and used for voice answers. The main beneficiary is AI engines (ChatGPT, Perplexity, Claude) which weight Speakable-marked content higher for citations.

Can I use Speakable for marketing copy?

Technically yes but it backfires. AI engines extract Speakable content verbatim. If your marketing copy is promotional, AI engines may extract it but flag it as biased. Use Speakable for factual answers, not marketing claims.

How specific should cssSelector be?

Specific enough to match only the answer-bearing elements. .faq-answer is good. .content is too broad. Use class names dedicated to the answer sections, not generic content classes.

Does Speakable have a length limit?

No formal limit but practical limit is 1-3 sentences per Speakable section. AI engines extract these as direct answers; longer than 3 sentences typically gets truncated. Optimize answers to 1-3 sentences for best extraction.

Related tutorials

Pillar guides

Audit modules

RUN YOUR FIRST AUDIT

Run the tutorial against a real audit.

Get a free Crawlux audit report and use it as the baseline for the work in this tutorial.

Free first audit · No signup · 60 seconds · Full PDF report

Audit this fix → Free audit