Local Visual Diff is a PHP 8.3 command-line tool for visually comparing two locally reachable websites.
It is designed for cases such as:
- comparing an old site against a migrated site
- checking a framework upgrade visually
- validating a relaunch or alternate implementation
- reviewing whether a candidate site still looks like the baseline site
The tool is local-first. It uses a locally installed Chromium-based browser to render pages, captures screenshots, compares them visually, and writes an HTML report with links to the baseline page, candidate page, and generated artifacts.
Given two base URLs, the tool:
- opens the baseline homepage
- follows same-origin links recursively on the baseline site
- maps every discovered normalized target to the candidate site
- renders both pages in a local browser
- compares the screenshots visually
- writes a report under
var/results/run-YYYYMMDD-HHMMSS/
The report lists each page as one of:
MatchingDifferentHTTP error ...Runtime error
If baseline and candidate return the same HTTP error status, such as 403 or
404, the page is still visually compared and can be reported as Matching or
Different. The report's HTTP column shows non-2xx/non-3xx response codes,
using a single code for matching statuses and baseline/candidate for
mismatched statuses, such as 500/200.
When pages differ, the report can link to:
- baseline screenshot
- candidate screenshot
- overlay image
- diff image
- context image for very tall pages
- Crawling happens only on the baseline site.
- Candidate pages are never crawled directly. They are derived from the same normalized target path/query.
- Same-origin links are followed recursively by default.
- Query-string variants are treated as separate pages by default.
- Some query parameter names can be ignored during crawl deduplication with a regular expression.
- Relative links are resolved like a browser would:
- redirects change the effective document URL
<base href="...">is honored- absolute URLs remain absolute
- root-relative URLs remain root-relative
- Visual comparison is screenshot-based, not DOM-based.
This tool intentionally does not compare:
- DOM structure
- CSS file names
- JavaScript bundle names
- generated asset names
- PHP
^8.3 - Composer
ext-curlext-domext-imagickext-jsonext-libxmlext-mbstringext-sockets- a locally installed Chromium-based browser
The current Composer dependencies are:
chrome-php/chromesymfony/consoletypo3fluid/fluid
Install dependencies:
composer installRun the tool:
php bin/visual-diff https://baseline.local https://candidate.localCompare one explicit page without crawling the whole site:
php bin/visual-diff --single-page=/en/check-this/page/ https://baseline.local https://candidate.localThe --single-page value is a path/query target, not a full URL. The report is
generated in the normal format, but contains only that one normalized target.
By default, a run writes to:
var/results/run-YYYYMMDD-HHMMSS/
Typical output files:
report.htmlreport.jsonpages/<page-slug>/...
Stored image artifacts use WebP by default to reduce size. The comparison itself still uses temporary lossless PNG screenshots internally so the visual diff remains accurate.
The tool reads configuration in this order:
config/defaults.iniconfig/local.ini- CLI options
That means:
defaults.inicontains shared project defaultslocal.inicontains machine-specific overrides and should not be committed- CLI options override both files for a single run
This file is tracked in Git and contains the shared defaults for the project.
Current sections:
[browser][compare][crawl][report][run]
This file is optional and intended for local machine overrides such as:
- browser executable path
- different viewport settings
- different preferred browser language
- local crawl TLS behavior
- different artifact image format
Example:
[browser]
path = "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
accept_language = "de, en"
scroll_warmup = false
[crawl]
fetch_verify_tls = false
[report]
artifact_image_format = "webp"pathBrowser executable path. If empty or invalid, the tool also tries common browser locations for the current OS.accept_languageLanguage preference used for browser capture. Default:"de, en".viewport_widthBrowser viewport width in pixels.viewport_heightBrowser viewport height in pixels.settle_msExtra wait time after navigation before screenshot capture.timeout_msTimeout for browser operations.scroll_warmupWhether the browser should scroll through the page before capture. Default:false. Enable this only for sites that need scroll-driven lazy content, because scrolling can trigger sticky menus or other JavaScript state that would not be visible in the initial browser view.max_fullpage_heightHeight threshold for large-page fallback behavior.context_paddingExtra surrounding area around detected differences in tall-page context artifacts.
shift_toleranceAllowed local pixel drift before a difference is counted. Default:0.color_thresholdAllowed per-channel color difference. Default:0.overlay_opacityOpacity used for overlay rendering.
crawl_homepage_linksRecursive crawl enable/disable. Default:true.ignored_query_param_name_patternRegular expression matching query parameter names that should be stripped before deduplication.fetch_verify_tlsWhether crawl-time cURL fetching verifies TLS certificates. Default in this project isfalse, which is useful for local DDEV-style environments.max_pagesMaximum number of pages to compare.0means unlimited.
output_dirParent directory for generated runs.artifact_image_formatStored artifact format such aswebporpng. Default:webp. If the requested format is not supported by the local Imagick build, the tool falls back topng.
capture_matching_screenshotsKeep screenshots even for matching pages.overwriteAllow reuse of an existing output directory.
Main command form:
php bin/visual-diff [options] <baseline-base-url> <candidate-base-url>Important options:
--output-dir--browser-path--accept-language--viewport-width--viewport-height--settle-ms--timeout-ms--scroll-warmup--max-fullpage-height--context-padding--shift-tolerance--color-threshold--ignored-query-param-name-pattern--insecure-crawl-fetch--max-pages--single-page--artifact-image-format--capture-matching-screenshots--overwrite--profile
Console verbosity:
- default output shows progress lines such as
Comparing /about -valso shows detailed page diagnostics-qsuppresses normal progress output
Basic run:
php bin/visual-diff https://legacy.local https://candidate.localLimit the run to the homepage only:
php bin/visual-diff --max-pages=1 https://legacy.local https://candidate.localCompare one explicit page only:
php bin/visual-diff --single-page=/en/check-this/page/ https://legacy.local https://candidate.localShow detailed diagnostics while running:
php bin/visual-diff -v https://legacy.local https://candidate.localAllow small layout drift of 3 pixels:
php bin/visual-diff --shift-tolerance=3 https://legacy.local https://candidate.localIgnore selected query parameter namespaces during crawl deduplication:
php bin/visual-diff \
--ignored-query-param-name-pattern='/^(tx_solr\[|tx_my\[)/' \
https://legacy.local \
https://candidate.localStore artifacts as PNG instead of WebP:
php bin/visual-diff \
--artifact-image-format=png \
https://legacy.local \
https://candidate.localUse an explicit output directory:
php bin/visual-diff \
--output-dir=var/results/manual-run \
https://legacy.local \
https://candidate.localUseful for local DDEV-like certificates:
php bin/visual-diff \
--insecure-crawl-fetch \
https://legacy.local \
https://candidate.local- Browser rendering can differ if installed fonts differ between environments.
- Background videos and motion are disabled during capture to reduce false positives.
- If the baseline and candidate homepage fail with different HTTP statuses, the tool reports that failure and does not continue discovery from that page.
- Matching pages do not keep screenshots by default, which helps reduce disk usage.
This tool was created by ChatGPT/Codex after heavy brainstorming and discussions with Dmitry Dulepov.