Website change monitoring with scheduled screenshots: one page, one month
A month of one page, as a calendar you can flip through: pick a day to see what changed and whether it was worth an alert. CurlShot takes the screenshots in this method. It does not schedule them, compare them or send alerts.
Day 5 compared with day 4
NoiseThe promo strip rotated to another message
- What happened
- The line under the navigation swapped one offer text for another. The site does this on its own every few weeks.
- Region, whole page
- 400 x 24 = 9,600 px0.94% of the screenshot
- Pricing cards only
- not seenOutside the element, so not seen.
- Why it is noise
- Nothing about the plans or their prices changed. An alert here trains you to ignore the next one.
- At the current setting
- False alarmAn alert goes out for something that does not matter.
Which days would have sent an alert?
One 1280 x 800 screenshot of the page.
5 alerts this month: 4 real changes, 1 false alarm. Nothing that mattered was missed.
What changed in the month, and what was worth an alert?
The illustrative month has 30 daily captures of one pricing page, each a 1280 x 800 screenshot of 1,024,000 pixels. On 25 days the capture is identical to the day before. On 5 days something changed, and each change stays on the page afterwards. It is a drawing made for this article, not data from a real site.
| Day | What changed | Changed region | Pricing cards only | Verdict |
|---|---|---|---|---|
| Day 5 | The promo strip rotated to another messageNothing about the plans or their prices changed. An alert here trains you to ignore the next one. | Changed region, whole page400 x 24 = 9,600 px0.94% | Pricing cards onlynot seen | VerdictNoise |
| Day 11 | The price of the middle plan went from $29 to $35This is the change the monitor exists for, and it is the smallest one of the month. | Changed region, whole page96 x 40 = 3,840 px0.38% | Pricing cards only1.15% | VerdictWorth an alert |
| Day 16 | The money-back guarantee section was removedA promise to customers disappeared from the page. Someone should know, whether it was planned or not. | Changed region, whole page1280 x 240 = 307,200 px30% | Pricing cards onlynot seen | VerdictWorth an alert |
| Day 22 | The layout broke: the third card dropped under the firstThe page still loads and an uptime check still passes. Only a look at the page shows that it is broken. | Changed region, whole page320 x 320 + 1280 x 260 = 435,200 px42.5% | Pricing cards onlysize changed | VerdictWorth an alert |
| Day 27 | A sale banner was added above the navigationA sale changes what customers pay, even though no number in the pricing cards changed. | Changed region, whole page1280 x 800 = 1,024,000 px100% | Pricing cards onlynot seen | VerdictWorth an alert |
The size of a change here is the area of the region it touches, as a share of the capture. A real comparison counts the pixels that differ, which is a smaller number, because blank areas match. The element is the block of three pricing cards, #plans, 1040 x 320 pixels.
The smallest change is the one that matters most
Which changes send an alert at each sensitivity setting?
| Alert when the changed region is | Alerts sent | False alarms | Missed changes |
|---|---|---|---|
| Any difference | Alerts sentday 5, day 11, day 16, day 22, day 27 | False alarmsday 5 | Missed changesnone |
| More than 0.5% | Alerts sentday 5, day 16, day 22, day 27 | False alarmsday 5 | Missed changesday 11 |
| More than 1% | Alerts sentday 16, day 22, day 27 | False alarmsnone | Missed changesday 11 |
| More than 35% | Alerts sentday 22, day 27 | False alarmsnone | Missed changesday 11, day 16 |
| More than 50% | Alerts sentday 27 | False alarmsnone | Missed changesday 11, day 16, day 22 |
| Alert when the changed region is | Alerts sent | False alarms | Missed changes |
|---|---|---|---|
| Any difference | Alerts sentday 11, day 22 | False alarmsnone | Missed changesday 16, day 27 |
| More than 0.5% | Alerts sentday 11, day 22 | False alarmsnone | Missed changesday 16, day 27 |
| More than 1% | Alerts sentday 11, day 22 | False alarmsnone | Missed changesday 16, day 27 |
| More than 35% | Alerts sentday 22 | False alarmsnone | Missed changesday 11, day 16, day 27 |
| More than 50% | Alerts sentday 22 | False alarmsnone | Missed changesday 11, day 16, day 27 |
The element capture never sends a false alarm in this month, and it catches the price at every sensible setting. It cannot see the removed section or the sale banner. Watch a second element, or add a slower whole-page check, for what happens around the first one.
How does website change monitoring work? The loop
Website change monitoring is one loop with four stations: schedule, capture, compare, decide and notify. Every monitoring tool runs this loop, and each station needs something stored between runs.
- 1
Schedule
A cron job or a scheduled workflow starts a run at a fixed time.
You store: The list of pages, each with its selector and capture options.
- 2
Capture
A browser opens the page with the same options as every other run and takes a fresh screenshot.
You store: The image file, named with the time it was taken.
- 3
Compare
The new capture is compared with the baseline: the last capture you accepted as good.
You store: The baseline, and how large the difference was.
- 4
Decide and notify
A difference over the threshold that is still there on the next run sends a message with both images. The new capture becomes the baseline.
You store: A change seen once and waiting to be confirmed, and a log of alerts sent.
Then back to step 1, at the next scheduled time.
The first run has nothing to compare with, so its capture becomes the baseline. After an alert, the new capture becomes the baseline: otherwise the same change would send a message on every run.
Three ways to detect a change on a web page
A change on a web page can be detected by comparing screenshots, by comparing text, or by comparing one element. They fail in different ways, so the right one depends on what you are watching for.
Compare the screenshots, pixel by pixel
Two images of the whole page.
Right when: Your own pages, where the question is "does it still look right?"
Compare the text or the HTML
The words or the markup of the page, as a line-by-line diff.
Right when: Terms, policies, notices and price lists, where the words are the content.
Compare one element only
A screenshot or the text of one element, picked with a CSS selector.
Right when: One price, one stock badge, one table: the best signal for the least noise.
| Method | Catches | Misses or gets wrong |
|---|---|---|
| Compare the screenshots, pixel by pixel | CatchesAnything a visitor can see: a broken layout, a missing image, a new banner, a changed colour. | Misses or gets wrongNothing visible, which is the problem: ads, animations, dates and font loading all count as changes. |
| Compare the text or the HTML | CatchesPrices, wording, stock notices, dates of effect. The alert can quote the exact words that changed. | Misses or gets wrongLayout, images, text inside images, and anything a script draws after loading unless a real browser renders the page first. |
| Compare one element only | CatchesEvery change to the thing you care about, with almost nothing else in the way. | Misses or gets wrongEverything outside the element. A renamed class makes the selector match nothing, so treat "element not found" as an alert. |
How pixel comparison works, and what a colour threshold and a pixel budget do, is in the visual regression testing guide. How to pick a selector that survives a redesign is in CSS selectors for element screenshots.
How do you stop false alarms?
False alarms in page monitoring come from things that change on their own: banners, ads, animations, dates. Remove them before the capture, and make every capture as alike as you can. Work down this list; each line removes one source of noise.
Capture one element, not the page
Everything outside the element stops mattering: the promo strip, the footer date, a banner that pushes the page down.
In CurlShot:
selectorBlock cookie banners, ads and chat widgets
A consent dialog, an ad slot or a chat bubble is different on every visit and covers what you are watching.
In CurlShot:
block_cookie_banners,block_ads,block_chatsFix the viewport and the device scale
A page lays out differently at every width, and a different pixel density changes every pixel. Send the same numbers on every run.
In CurlShot:
viewport_width,viewport_height,device_scale_factorFreeze animations
A carousel or a fade caught at a different instant looks like a change.
In CurlShot:
reduced_motionHide dates, counters and rotating promos
A "last updated" stamp, a visitor counter or a countdown changes on its own. Hide it before the capture.
In CurlShot:
hide_selectors, orstylesto hide it and keep its spaceWait until the element is there
A capture taken before a script has filled in the price shows a placeholder, and the next run shows the price.
In CurlShot:
wait_for_selectorCapture at the same time of day, in one time zone
Pages that show "today", opening hours or a daily deal differ between morning and night.
In CurlShot:
time_zone, plus a fixed hour in the scheduleRequire a change to persist for two runs
A half-loaded page, a failed image or a test variant shown to one visit in ten is gone on the next run. A real change is still there.
How often should you check a page for changes?
Check a page as rarely as still answers your question. The interval is the longest delay you accept between a change and your alert, and every check is a page load on someone else's server and one screenshot on your side.
| Situation | Sensible interval | Checks a month, one page | Why |
|---|---|---|---|
| Legal or terms page | Sensible intervalOnce a week | Checks a month, one page4 | WhyThese pages change a few times a year, and a week of delay rarely matters. |
| Competitor pricing page | Sensible intervalOnce a day | Checks a month, one page30 | WhyA price change matters within days, not minutes. A daily capture at a fixed hour also makes a tidy calendar. |
| News or regulator page | Sensible intervalEvery 6 hours | Checks a month, one page120 | WhyNotices appear during working hours and you want them the same day. |
| Your own home page, as an "is it broken?" check | Sensible intervalEvery hour | Checks a month, one page720 | WhyAn hour is how long you accept a broken page going unnoticed. A plain uptime check is cheaper for "is it down?"; the screenshot answers "is it up but wrong?" |
| Stock or availability notice | Sensible intervalEvery 30 minutes, while it matters | Checks a month, one page1,440 | WhyThe item can sell out again quickly. Stop the monitor once you have what you wanted. |
The arithmetic
checks per month = pages x checks per day x 30
Once a week is 1/7 of a check per day. The intervals in the table are starting points reasoned from how fast each kind of page moves, not measurements.
- 10 pages, once a day10 x 1 x 30 = 300
- 50 pages, once a week50 x 1/7 x 30 = 214
- 3 pages, every hour3 x 24 x 30 = 2,160
- 1 page, every 30 minutes1 x 48 x 30 = 1,440
For scale: the free plan of CurlShot includes 100 screenshots a month, which is a daily check of 3 pages (100 / 30, rounded down).
A working setup: cron and a short Node.js script
A minimal page monitor is one scheduled command and one script of about 70 lines. The script below fetches a screenshot of one element, compares a hash of its bytes with the last good one, keeps both files, and prints or posts a message when they differ on two runs in a row.
1. The schedule
A crontab line is five time fields and then the command.[1]
| Field | Allowed | Here |
|---|---|---|
| Minute | 0 to 59 | 0 |
| Hour | 0 to 23 | 7 |
| Day of the month | 1 to 31 | * |
| Month | 1 to 12 | * |
| Day of the week | 0 to 6, 0 is Sunday | * |
0 7 * * * cd $HOME/page-watch && . ./.env && node watch.mjs >> watch.log 2>&1This line runs the script every day at 07:00, in the time zone of the machine. The file .env holds one line, export SCREENSHOT_ACCESS_KEY=..., so the key stays out of the crontab.
0 7 * * *Every day at 07:00.0 7 * * 1Every Monday at 07:00.0 * * * *Every hour, on the hour.0,30 * * * *Every 30 minutes, written as a list so it works in any cron.0 0,6,12,18 * * *Every 6 hours.
POSIX defines * for all values, lists with commas and ranges with a hyphen. It does not define step values such as */15.[1] Many cron programs accept them, and GitHub Actions documents them.[2]
2. The script
The script uses only the Node.js standard library. fetch is built in from Node.js 18 and marked stable in Node.js 21.[3] The decision is one small function, decide, with no network and no disk in it.
// watch.mjs: tell me when one element of one page changes.
// Node.js 18 or newer. No packages to install.
import { createHash } from 'node:crypto'
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
import { pathToFileURL } from 'node:url'
const API = 'https://curlshot.com/api/v1'
const DIR = 'captures'
const OPTIONS = {
url: 'https://example.com/pricing',
selector: '#plans',
wait_for_selector: '#plans',
format: 'png',
viewport_width: '1280',
viewport_height: '1024',
device_scale_factor: '1',
block_cookie_banners: 'true',
block_ads: 'true',
block_chats: 'true',
reduced_motion: 'true',
time_zone: 'UTC',
}
export const sha256 = (bytes) => createHash('sha256').update(bytes).digest('hex')
// The whole decision, with no network and no disk in it.
// state.baseline is the hash of the last good capture,
// state.pending the hash of a change that was seen once.
export function decide(state, hash) {
if (!state.baseline) return { event: 'first', state: { baseline: hash, pending: null } }
if (hash === state.baseline) return { event: 'same', state: { baseline: state.baseline, pending: null } }
if (hash !== state.pending) return { event: 'seen-once', state: { baseline: state.baseline, pending: hash } }
return { event: 'changed', state: { baseline: hash, pending: null } }
}
async function capture() {
const response = await fetch(API + '/screenshot?' + new URLSearchParams(OPTIONS), {
headers: { 'X-Access-Key': process.env.SCREENSHOT_ACCESS_KEY ?? '' },
signal: AbortSignal.timeout(90_000),
})
if (!response.ok) {
throw new Error('Capture failed: HTTP ' + response.status + ' ' + (await response.text()).slice(0, 300))
}
return Buffer.from(await response.arrayBuffer())
}
async function notify(text) {
console.log(text)
const hook = process.env.ALERT_WEBHOOK_URL
if (!hook) return
await fetch(hook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
})
}
async function main() {
await mkdir(DIR, { recursive: true })
const stateFile = DIR + '/state.json'
const state = JSON.parse(await readFile(stateFile, 'utf8').catch(() => '{}'))
const png = await capture()
const result = decide(state, sha256(png))
await writeFile(DIR + '/latest.png', png)
if (result.event === 'changed') await rename(DIR + '/baseline.png', DIR + '/previous.png')
if (result.event === 'first' || result.event === 'changed') await writeFile(DIR + '/baseline.png', png)
await writeFile(stateFile, JSON.stringify(result.state, null, 2) + '\n')
if (result.event === 'changed') {
await notify(
'Changed: ' + OPTIONS.url + ' ' + OPTIONS.selector +
'. Compare ' + DIR + '/previous.png with ' + DIR + '/baseline.png.',
)
} else {
const lines = {
first: 'First capture saved as the baseline.',
same: 'No change.',
'seen-once': 'Different from the baseline. Waiting for the next run to confirm.',
}
console.log(lines[result.event])
}
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
main().catch((error) => {
console.error(error.message)
process.exitCode = 1
})
}
| Outcome | When | What the script does |
|---|---|---|
| first | WhenThere is no baseline yet. | What the script doesSaves baseline.png and its hash. No alert. |
| same | WhenThe hash equals the baseline hash. | What the script doesClears any pending change. No alert. |
| seen-once | WhenThe hash is new and was not seen on the run before. | What the script doesRemembers it as pending. No alert yet. |
| changed | WhenThe same new hash arrives on two runs in a row. | What the script doesMoves the old baseline to previous.png, saves the new baseline.png, prints the alert and posts it to ALERT_WEBHOOK_URL if that is set. |
A hash has no tolerance
previous.png and baseline.png with an image-diff library and a threshold, as visual regression tools do.A failed capture exits with an error and leaves the saved state alone. The error selector_not_found means the element is gone or renamed, which deserves a look in its own right. A page that shows a different variant on every visit never repeats a hash, so it never confirms: hide the varying part or watch a steadier element.
3. The same check as a GitHub Actions workflow
name: Watch a page
on:
schedule:
- cron: '17 7 * * *'
workflow_dispatch:
permissions:
contents: write
jobs:
watch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: node watch.mjs
env:
SCREENSHOT_ACCESS_KEY: ${{ secrets.SCREENSHOT_ACCESS_KEY }}
ALERT_WEBHOOK_URL: ${{ secrets.ALERT_WEBHOOK_URL }}
- name: Keep the captures for the next run
run: |
git config user.name "page-watch"
git config user.email "[email protected]"
git add captures
git diff --cached --quiet || git commit -m "Capture of $(date -u +%F)"
git push
Put watch.mjs in a private repository and add the access key as a repository secret. The last step commits the captures folder, so the next run finds the baseline and the repository becomes a dated history of the element.
- GitHub Actions reads the same five fields and adds
/for steps, so*/15is valid there.[2] - The shortest interval GitHub runs a scheduled workflow at is once every 5 minutes.[2]
- Times are in UTC unless you add a
timezonekey with an IANA name next to thecronline.[2] - A scheduled run can be delayed when GitHub is busy, and the start of every hour is a busy time. That is why the sample runs at minute 17.[2]
- In a public repository, scheduled workflows are disabled automatically after 60 days without repository activity.[2]
Website change monitoring tools, and when to use one
Dedicated change-monitoring tools run the whole loop for you: the schedule, the comparison, the history and the alert. These five are in alphabetical order and described from their own sites. Two are open source.
| Tool | Where it runs | What it watches | Alerts |
|---|---|---|---|
| changedetection.io[4][5]Open source (Apache-2.0), with a paid hosted version | Where it runsOn your own machine with Docker or pip, or in the vendor's cloud. | What it watchesText changes with filters and CSS or XPath rules, chosen elements, restock and price changes on product pages. It can attach a screenshot to an alert. | AlertsEmail, Slack, Discord, Telegram and many more, through the Apprise library. |
| Distill[7]Browser extension and hosted service | Where it runsLocal monitors in your browser (Chrome, Firefox, Edge, Opera) or cloud monitors that run around the clock. | What it watchesThe parts of a page you select, plus PDFs and password-protected pages. | AlertsBrowser notification, email, SMS and mobile apps. |
| urlwatch[9][10]Open source (BSD 3-Clause), command line | Where it runsA Python program you add to your own crontab. Its documentation suggests checking no more often than every 30 minutes. | What it watchesA URL, a page loaded in a headless browser, or the output of a shell command. The alert contains a unified diff of the text. | AlertsEmail, the terminal and third-party services. |
| Visualping[6]Hosted service | Where it runsIn the vendor's cloud. Its site says getting started is free. | What it watchesVisual changes (it compares the current and the prior screenshot), text changes and code changes. | AlertsEmail, Slack, Microsoft Teams, webhooks and an API. |
| Wachete[8]Hosted service | Where it runsIn the vendor's cloud, around the clock. | What it watchesA whole page or the part you care about, including pages that need a login. | AlertsEmail, phone notification, Slack, Teams, Discord and Telegram. |
Prices and plan limits change often and are left out on purpose. Each tool's own site has them.
A dedicated tool is the better choice when
- You watch more than a handful of pages and want a list of them with a history, without keeping a server.
- Someone who does not write code has to add pages and read the alerts.
- You need a text diff in the alert: the exact words that changed, not two images.
- You need alerts in several channels with rules, quiet hours or a digest.
Building the loop yourself fits when
- You watch a few pages and already have a place to run a scheduled job.
- The check is part of something larger: your deploy pipeline, a report, an archive of dated captures.
- You want the images themselves, kept in your own storage, more than the alert.
What a scheduled screenshot cannot see, and good manners
A scheduled screenshot shows the page as a signed-out visitor from a data centre sees it. That is not always the page you see, and the site you watch pays for every visit.
- A capture is a signed-out visitor from a data centre
- The page is loaded without your cookies, from a server, often in another country. Prices shown per region, content behind a login and anything personalised differ from what you see at your desk.
- A time zone is not a location
- Setting the time zone changes the clock the page reads. It does not change where the request comes from, so a site that picks content by the visitor's address still sees the server.
- Some pages refuse automated visits
- A site can answer a server with a challenge page or an error. The monitor then sees the challenge, not the page: look at the first capture before you trust the alerts.
- Check no more often than you need
- Every check is a full page load on someone else's server. Read the site's terms of use, and pick the slowest interval that still answers your question.
The documentation of urlwatch, one of the tools above, suggests running it no more often than every 30 minutes.[9] Whether you may monitor someone else's site, and what you may do with the captures, depends on the site's terms and on the law where you are. This article is not legal advice.
What CurlShot does in a monitoring setup
CurlShot is the capture station of the loop and nothing more. It renders the page when your scheduler asks, and it has options that make two captures of an unchanged page come out alike.
It does
- Renders a URL to a PNG, JPEG or WebP screenshot each time you ask.
- Captures one element with
selector, cropped to its edges. - Blocks cookie banners, ads and chat widgets, hides elements you name, and stops animations.
- Renders the page fresh on every request unless you send
cache=true.
It does not
- Run on a schedule. Your cron job or workflow sends each request.
- Keep a baseline or compare two screenshots. Your script does.
- Send an alert when a page changes.
webhook_urlreports that an asynchronous render finished, which is a different thing. - Change where the request comes from.
time_zonesets the clock, not the location.
The options that keep two captures alike
| Option | Value | What it keeps steady |
|---|---|---|
| selector | Valuea CSS selector | What it keeps steadyCaptures the first element that matches, and nothing around it. No match returns the error selector_not_found with status 422, which is not counted. |
| wait_for_selector | Valuea CSS selector | What it keeps steadyWaits until the element is visible, for prices that a script fills in late. |
| viewport_width, viewport_height | Value1280 and 1024 by default | What it keeps steadyThe window the page is laid out in. Send them on every run so the layout never depends on a default. |
| device_scale_factor | Value1 by default | What it keeps steadyThe pixel density. At 2, the same element is twice as wide and every pixel differs. |
| block_cookie_banners, block_ads, block_chats | Valuefalse by default | What it keeps steadyRemoves the three things most likely to differ between two visits. |
| reduced_motion | Valuefalse by default | What it keeps steadyAsks the site for no animation and stops CSS animations and transitions at the moment of capture. |
| hide_selectors | Valueup to 50 selectors | What it keeps steadyHides a date stamp, a counter or a promo strip. A hidden element takes up no space. |
| time_zone | Valuean IANA name such as UTC | What it keeps steadyFixes the clock the page reads. There is no locale option: send an Accept-Language header with headers for the language. |
| format | Valuepng by default | What it keeps steadyPNG is lossless, so equal pixels give equal pixel data. JPEG and WebP are for viewing, not for comparing. |
A cached response is not a fresh look
- The response cache is off unless you send
cache=true. Without it, every request opens the page again, which is what a monitor needs. - With
cache=true, a repeat request insidecache_ttl(14,400 seconds, 4 hours, by default) returns the stored copy with the headerX-Cache: HIT. That is the old look of the page, not a new one. - To force a fresh render while keeping the cache on, change
cache_key, for example to the date of the run. Changingcache_ttldoes not refresh anything. - One successful render uses one screenshot of the monthly allowance. A cache hit and a failed request use none.
The request the script sends
curl "https://curlshot.com/api/v1/screenshot?url=https://example.com/pricing\
&selector=%23plans\
&wait_for_selector=%23plans\
&format=png\
&viewport_width=1280\
&viewport_height=1024\
&device_scale_factor=1\
&block_cookie_banners=true\
&block_ads=true\
&block_chats=true\
&reduced_motion=true\
&time_zone=UTC" \
-H "X-Access-Key: YOUR_ACCESS_KEY" \
--output plans.pngThe result is a PNG of the element #plans alone. More: screenshot one element of a page, capturing an element, caching.
Change monitoring glossary
- Baseline
- The last capture you accepted as correct. Every new capture is compared with it.
- Diff
- The difference between two captures: an image with the changed pixels marked, or a list of added and removed lines of text.
- False positive
- An alert for a change that does not matter, such as a rotated promo or a new date stamp. Also called a false alarm.
- Missed change
- A change that mattered and sent no alert, because it was under the threshold or outside the watched element. Also called a false negative.
- Threshold
- How large a difference must be before it counts as a change, as a number of pixels or a share of the image.
- Cron
- The scheduler on Unix systems. A cron expression is five fields (minute, hour, day of the month, month, day of the week) that say when a command runs.
- Element capture
- A screenshot of one element of a page, picked with a CSS selector and cropped to its edges.
- Debounce
- Waiting for a change to still be there on the next run before acting on it, so that a one-off glitch sends no alert.
Questions people ask
How do I get notified when a website changes?
Capture the page on a schedule, compare each capture with the previous good one, and send yourself a message when they differ. You can build that with a cron job, a screenshot request and a short script, or use a dedicated service such as changedetection.io, Distill, urlwatch, Visualping or Wachete, which do the scheduling, the comparison and the alert for you.
Does CurlShot monitor websites for changes?
No. CurlShot takes a screenshot each time you ask for one. It has no scheduler, keeps no baseline, does not compare two screenshots and sends no change alerts. In a monitoring setup it is the capture step: your cron job sends the request and your script does the comparison.
How often should I check a web page for changes?
As rarely as still answers your question. Once a week suits a terms page, once a day a pricing page, every 6 hours a news or regulator page, every hour your own home page, and every 30 minutes a stock notice while it matters. Multiply pages by checks per day by 30 to get the checks per month.
Why does my page monitor send false alarms?
Because something on the page changes on its own: a cookie banner, an ad, a chat bubble, an animation, a date stamp or a rotating promo. Capture one element instead of the page, block banners, ads and chats, fix the viewport size, stop animations, hide dates and counters, and only alert when a change is still there on the next run.
Is it better to compare screenshots or the text of a page?
Compare text when the words are the content: prices, terms, notices. A text diff quotes exactly what changed and ignores layout. Compare screenshots when the look is the content: a broken layout or a missing image never shows up in a text diff. Comparing one element, as an image or as text, gives the least noise of all.
Can I compare two screenshots by hashing the files?
Only as a strict equal-or-not test. A hash changes when a single pixel changes, so it suits a stable element saved as lossless PNG and nothing else. It cannot tell a small difference from a large one. For a tolerance, decode both images and count the pixels that differ, as visual regression tools do.
Is it legal to monitor a competitor's website?
It depends on the site's terms of use, on what you capture and on the law where you and the site are. Loading a public page at a modest rate is ordinary web traffic, but terms can forbid automated access. This is not legal advice: ask a lawyer when the answer matters to your business.
Sources
- [1]The Open Group, crontab: schedule periodic background work (Base Specifications Issue 7, 2018 edition). Checked .
- [2]GitHub Docs, Events that trigger workflows: schedule. Checked .
- [3]Node.js, Global objects: fetch. Checked .
- [4]changedetection.io, changedetection.io: website change detection and monitoring. Checked .
- [5]GitHub, dgtlmoon/changedetection.io: README and license. Checked .
- [6]Visualping, Visualping: website change detection, monitoring and alerts. Checked .
- [7]Distill, Distill: monitor website changes. Checked .
- [8]Wachete, Wachete: monitor web page changes. Checked .
- [9]urlwatch, urlwatch documentation: introduction. Checked .
- [10]GitHub, thp/urlwatch: README and COPYING. Checked .
Read next
- GuideVisual regression testing, explained in picturesHow screenshot comparison works, why visual tests go flaky and how to fix each cause, the tools, and a set-up checklist with a Playwright example.
- PlaygroundCSS selectors for element screenshotsType a CSS selector, see what it matches on a specimen page and what a capture returns. With a cheat sheet, a robustness ranking and fixes.
- ChecklistCapture a web page as evidence that holds upA checklist for saving a web page as a credible record: what to capture, how to fix the time with a hash and timestamp, and what makes a screenshot weak.