This site is not affiliated with or endorsed by Cloudflare, Inc. It showcases deployable experiments across most Cloudflare products.
Network & Monitoring

Website Change Tracker

Scheduled Browser Rendering snapshots with R2 storage and D1 diff history - detect website changes over time

Register URLs for on-demand and Cron-scheduled snapshots. Rendered text is stored in R2, content hashes and diff summaries in D1. View change history per tracked URL.

Features

  • POST /track - register a URL
  • DELETE /track?url= - unregister
  • GET /history?url= - snapshot diff history
  • Scheduled handler every 30 minutes for tracked URLs

API Reference

POST /track

Register a URL for scheduled tracking.

{ "url": "https://example.com" }

DELETE /track?url=

Unregister a tracked URL.

GET /history?url=

Return snapshot history with content hashes and diff summaries.

Error Codes

  • 400 - INVALID_URL, INVALID_BODY
  • 404 - NOT_TRACKED

Use Cases

  • Monitor marketing or legal pages for unexpected content changes
  • Learn R2 + D1 + Cron + Browser Rendering together
  • Prototype change-detection alerts before production hardening

Limitations

  • Requires D1, R2, Browser Rendering, and Cron bindings
  • Diff summary is line-based heuristic, not semantic
  • 30-minute cron interval; not real-time

Use in your project

Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/website-change-tracker.

DependenciesNoneBindingsD1, R2, Browser RenderingPlatformWeb Crypto
import puppeteer from "@cloudflare/puppeteer";import type { Page } from "@cloudflare/puppeteer";import { DEFAULT_VIEWPORT, MAX_TEXT_LENGTH, NAVIGATION_TIMEOUT_MS } from "../constants/defaults";function truncateText(text: string): string {  const normalized = text.replace(/\s+/g, " ").trim();  if (normalized.length <= MAX_TEXT_LENGTH) {    return normalized;  }  return normalized.slice(0, MAX_TEXT_LENGTH);}export async function withBrowserPage<T>(  browserBinding: Fetcher,  url: string,  fn: (page: Page) => Promise<T>): Promise<T> {  let browser: Awaited<ReturnType<typeof puppeteer.launch>> | null = null;  try {    browser = await puppeteer.launch(browserBinding);    const page = await browser.newPage();    await page.setViewport(DEFAULT_VIEWPORT);    await page.goto(url, {      waitUntil: "networkidle0",      timeout: NAVIGATION_TIMEOUT_MS,    });    return await fn(page);  } finally {    if (browser) {      try {        await browser.close();      } catch {        /* ignore close errors */      }    }  }}export async function extractRenderedText(  browserBinding: Fetcher,  url: string): Promise<{ title: string; text: string }> {  const raw = await withBrowserPage(browserBinding, url, async (page) => {    const title = (await page.title()).trim();    const text = await page.$eval("body", (body) => body.innerText ?? "");    return { title, text };  });  const title = raw.title || url;  const text = truncateText(raw.text);  return { title, text: `${title}\n\n${text}` };}

Deployment

Click the deploy button

Deploy to Cloudflare Workers

Configure bindings

D1 DB, R2 SNAPSHOTS, Browser BROWSER, cron */30 * * * *. Run D1 migrations before deploy.

Test your deployment

See the experiment README for curl examples.

Local Development

cd apps/experiments/website-change-tracker
npm install
npm run dev

Configuration

D1 DB, R2 SNAPSHOTS, Browser BROWSER, cron */30 * * * *. Run D1 migrations before deploy.

Cloudflare Features Used

On this page