DEV Community

Bob James
Bob James

Posted on

How to Build a Reliable Website Change Monitor: Headless Rendering, DOM Diffing, and Webhooks

How to Build a Reliable Website Change Monitor: Headless Rendering, DOM Diffing, and Webhooks

Tracking changes on external websites is a common requirement in modern web development and DevOps. Whether you are monitoring upstream API documentation for breaking changes, watching competitor pricing, keeping track of government tender portals without RSS feeds, or ensuring your own production landing pages haven't broken, automated website monitoring is essential.

However, writing a simple while True loop with Python's requests or BeautifulSoup quickly fails in production due to:

  1. Client-Side Rendering (CSR): Single Page Applications (React, Vue, Svelte) rendering empty HTML shells.
  2. Noise and False Positives: Dynamic timestamps, ads, CSRF tokens, or session IDs triggering false alerts.
  3. Anti-Bot Blocking: WAFs and CDNs blocking frequent requests with 403 Forbidden errors.

In this guide, we'll walk through the architectural blueprint and code for building a low-noise, production-ready website change monitoring pipeline.


🛠️ System Architecture

A robust website change monitor consists of four decoupled layers:

+-------------------+      +-------------------+      +-------------------+
| 1. Cloud Renderer | ---> | 2. DOM Cleaner    | ---> | 3. Diff Engine    |
| (Playwright/Chrome)|     | (CSS Selectors)   |      | (Text & Visual)   |
+-------------------+      +-------------------+      +-------------------+
                                                                |
                                                                v
                                                      +-------------------+
                                                      | 4. Webhook Alerts |
                                                      | (Slack/Discord/API|
                                                      +-------------------+
Enter fullscreen mode Exit fullscreen mode

Step 1: Headless Rendering & Resource Optimization

To capture fully hydrated SPA pages, we use Playwright. To reduce CPU/memory overhead and speed up page loads, we block unnecessary static assets like images, fonts, and tracking scripts.

import asyncio
from playwright.async_api import async_playwright

async def render_page(url: str, selector: str = None) -> str:
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            headless=True,
            args=['--no-sandbox', '--disable-setuid-sandbox']
        )
        context = await browser.new_context(
            user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
        )
        page = await context.new_page()

        # Block non-essential network requests to save bandwidth and execution time
        async def intercept_route(route):
            if route.request.resource_type in ["image", "media", "font"]:
                await route.abort()
            else:
                await route.continue_()

        await page.route("**/*", intercept_route)
        await page.goto(url, wait_until="networkidle", timeout=30000)

        # Extract only target selector if specified, otherwise fall back to body
        if selector:
            element = await page.query_selector(selector)
            content = await element.inner_text() if element else ""
        else:
            content = await page.inner_text("body")

        await browser.close()
        return content.strip()
Enter fullscreen mode Exit fullscreen mode

Step 2: DOM Cleaning and Noise Filtering

Comparing full HTML snapshots leads to constant false positives due to dynamic timestamps, session IDs, and rotating ads.

To solve this, we apply two cleaning strategies:

  1. Targeted Selector Scoping: Target only critical containers (e.g., #pricing-table or .api-version-info).
  2. Regex Normalization: Strip dynamic date/time patterns and whitespace.
import re

def sanitize_content(raw_text: str) -> str:
    if not raw_text:
        return ""

    # Normalize whitespaces and tabs
    clean = re.sub(r'\s+', ' ', raw_text)

    # Mask dynamic date/time strings (e.g. YYYY-MM-DD, HH:MM:SS)
    clean = re.sub(r'\d{4}[-/]\d{2}[-/]\d{2}( \d{2}:\d{2}:\d{2})?', '[TIMESTAMP]', clean)
    clean = re.sub(r'\b\d+\s*(mins|hours|days)\s*ago\b', '[TIME_AGO]', clean, flags=re.IGNORECASE)

    return clean.strip()
Enter fullscreen mode Exit fullscreen mode

Step 3: Computing Differences (Text & Visual Diffing)

For textual comparison, Myers Diff algorithm (difflib in Python) allows us to compute precise additions and deletions.

import difflib

def compute_diff(old_text: str, new_text: str) -> str:
    differ = difflib.UnifiedDiff(
        old_text.splitlines(),
        new_text.splitlines(),
        fromfile='previous_version',
        tofile='current_version',
        lineterm=''
    )

    diff_lines = []
    for line in differ:
        if line.startswith('+') and not line.startswith('+++'):
            diff_lines.append(f"ADDED: {line[1:]}")
        elif line.startswith('-') and not line.startswith('---'):
            diff_lines.append(f"REMOVED: {line[1:]}")

    return "\n".join(diff_lines)
Enter fullscreen mode Exit fullscreen mode

Step 4: Webhook Event Dispatching

Once a diff is detected, dispatching a structured JSON payload to a Webhook allows for seamless integration with Slack, Discord, or custom backend services.

from fastapi import FastAPI, Request
import requests

app = FastAPI()

DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/your-channel-id/token"

@app.post("/webhook/site-change")
async def handle_change_alert(request: Request):
    payload = await request.json()

    site_url = payload.get("url")
    diff_summary = payload.get("diff", "Page updated.")

    # Dispatch Discord alert
    message = {
        "content": f"🔔 **Website Change Detected!**\nURL: {site_url}\n```
{% endraw %}
diff\n{diff_summary}\n
{% raw %}
```"
    }
    requests.post(DISCORD_WEBHOOK_URL, json=message)

    return {"status": "success"}
Enter fullscreen mode Exit fullscreen mode

⚡ Self-Hosting vs Managed Monitoring Tools

Building your own change monitor gives you full control, but managing headless Chrome instances, proxy rotation, and database state can become a heavy maintenance burden.

If you prefer an out-of-the-box, no-code solution, you can use PageWatch.tech. It manages the cloud headless browser cluster, visual CSS selector picking, text/snapshot diffing, and Webhook dispatches automatically, saving hours of infrastructure setup.


💡 Summary

To build an efficient website change monitor:

  • Use Headless Chromium (Playwright/Puppeteer) to handle JavaScript-rendered SPA pages.
  • Target specific CSS Selectors and sanitize dynamic timestamps to eliminate false alarms.
  • Use Webhooks to trigger downstream automation scripts or chat notifications.

Happy coding!

Top comments (0)