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_BODY404-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.
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
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 devConfiguration
D1 DB, R2 SNAPSHOTS, Browser BROWSER, cron */30 * * * *. Run D1 migrations before deploy.