Page check
How the page check works.
What the scanner loads, what it never does, how the score is computed, and how a site owner opts out.
What it does
- Normalises the address (https:// is added when missing; only http and https on the standard ports) and refuses IP
addresses, local names, genclass.dev itself and hostnames that resolve to private, loopback, link-local, carrier-grade NAT or
cloud metadata addresses. It reads the site's
robots.txtfor aGenClassScangroup. - Starts a fresh browser (Cloudflare Browser Rendering) with no cookies or storage, and loads the page three times in parallel: desktop, 1366 × 768, light; phone, 390 × 844, light; phone, 390 × 844, dark. Each load has 20 seconds, then up to 4 more for the network to go quiet.
- Runs the visual checks in each view for about 6 seconds: the measured checks built for the GenClass runtime (in testing; not in runtime 0.2.1), the runtime's visual chunk from
commit
6ae209f, in an isolated part of the browser the page's scripts cannot reach. It presses Tab a few times on the desktop and scrolls with the mouse wheel, which some checks need (focus under a sticky header, a scroll lock, a control under a fixed bar). With GenClass on, the checks run in guard mode with their fixes turned on, and a second, report-only set of the same checks then measures the page again; the fixes are undone before the screenshot. The page's fetch and XHR requests in flight are counted and handed to the checks, which do not judge a blank page or a page that ignores clicks while a request runs. - Takes a screenshot of the top of the page in each view and records the box of every finding.
- On the desktop page, with GenClass 0.2.1 loaded in observe mode (telemetry off, model from
genclass.dev/model/), typesa,ab,abcinto the first search box, 80 ms apart, and double-clicks the most prominent submit button of a same-origin form that would POST. Forms with a password or card field are skipped, and so are forms and buttons that would submit into another window. GET responses during the probe get 0 to 1,500 ms of extra latency, so they can arrive out of order. - Opens the page once more with GenClass in guard mode and repeats the probe, to measure what guard changes.
- Scores the findings and keeps the report for 6 hours. The whole scan has a hard budget of 45 seconds. When the page sent requests other than GET while loading, findings that may come from the scanner's stand-in replies (template text, broken images, and GenClass diagnoses other than a duplicate) are listed but not scored.
What it never does
- It never sends a POST, PUT, PATCH, DELETE or a beacon to the site. Every request other than GET, while the page loads and
during the probe, is answered inside the browser with a stand-in
200JSON reply (or a204for a form navigation, so the page stays put) and never leaves it. Form data that the page's own script sends with GET does reach the site's own origin. - It never follows a link off the page: after the first load, any navigation of the page is cancelled. It opens no new
windows or tabs:
window.opendoes nothing, and forms and links that would open one stay in the page. - While it clicks, requests for data to other origins are cancelled and WebSocket messages are held.
- It never signs in, never types into anything but a search box, and registers no service workers. The double-click probe submits the form empty, with the browser's own form validation turned off.
- It does not load private addresses. IP addresses in private, loopback, link-local, carrier-grade NAT and metadata ranges,
and local names, are blocked for every request. Every hostname the page asks for is looked up in DNS first, and a name that
points to a private address is not loaded; a redirect also has to pass the opt-out list and
robots.txt. WebSocket connections to IP addresses or local names are refused, and a WebSocket host that resolves to a private address throws the scan away. The browser then looks each name up again itself, so a DNS server that gives a different answer the second time is the one case these checks cannot rule out.
User agent
Every request carries the token GenClassScan/1.0 (+https://genclass.dev/scan) at the end of a normal Chrome user agent:
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 GenClassScan/1.0 (+https://genclass.dev/scan) Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36 GenClassScan/1.0 (+https://genclass.dev/scan)
Requests come from Cloudflare's Browser Rendering network, which identifies them as automated.
Limits
- Five new scans per minute per visitor, and one running at a time (cached results do not count). Visitors on IPv6 count by their /64 network.
- A few new scans per minute per site, whoever asks, and a small number of scans at once across everyone.
- Crawlers and link previews can open a report but cannot start a scan.
- One scan per address at a time; a second request for the same address waits for the first result.
How the score works
scan-score/2The score starts at 100. Each finding takes points by its severity; each category can lose at most its cap, so one noisy category cannot take the page to zero. The same page with the same findings always gets the same score.
points: high 10, medium 5, low 2 · cap: 25 per category · four categories
| Category | Cap |
|---|---|
| ReadabilityText that cannot be read, template or placeholder text, garbled characters, fonts and icon fonts that failed to load | 25 |
| Images and mediaImages, background images, video and audio that failed to load, stretched images | 25 |
| LayoutStylesheets that failed or were blocked, CSS variables defined nowhere, sideways scrolling, no viewport meta tag, layout shifts, an empty page | 25 |
| Interaction and racesA page that ignores clicks or cannot scroll, controls under fixed bars, hidden focus, covered dialogs, a double click that sends a request twice, problems GenClass's model diagnosed | 25 |
| Finding | Category | Severity | Points |
|---|---|---|---|
Text that is almost invisible on its backgroundtext-invisible | Readability | high | 10 |
Template or debug text shows on the pagetext-template-leak | Readability | high | 10 |
Placeholder content shows on the pagetext-placeholder | Readability | medium | 5 |
Characters are garbledtext-encoding | Readability | medium | 5 |
A web font failed to loadfont-failed | Readability | low | 2 |
An icon font failed to loadicon-font-failed | Readability | medium | 5 |
An image failed to loadimage-broken | Images and media | medium | 5 |
A CSS background image failed to loadimage-css-broken | Images and media | medium | 5 |
A video or audio element has nothing it can playmedia-failed | Images and media | medium | 5 |
An image is stretched or squashedimage-distorted | Images and media | medium | 5 |
A stylesheet failed to loadstylesheet-failed | Layout | high | 10 |
The Content-Security-Policy blocked a file the page usescsp-blocked | Layout | medium | 5 |
A CSS variable is used but defined nowherecss-var-undefined | Layout | low | 2 |
The page scrolls sidewayspage-overflow-x | Layout | high | 10 |
Phones show the desktop layout, zoomed outviewport-meta-missing | Layout | high | 10 |
Content jumps while the page loadslayout-shift | Layout | medium | 5 |
The main part of the page shows nothingpage-blank | Layout | high | 10 |
An invisible layer swallows clicksclick-blockedNot run in the page check: it needs a visitor's clicks | Interaction and races | high | 10 |
The whole page ignores clickspointer-events-leak | Interaction and races | high | 10 |
The page cannot scrollscroll-lock-stuck | Interaction and races | high | 10 |
A control is stuck under a fixed barcontrol-unreachable | Interaction and races | high | 10 |
A focused element is hidden under a sticky barfocus-obscured | Interaction and races | medium | 5 |
An open dialog is covered, or its buttons are off-screendialog-covered | Interaction and races | high | 10 |
A double click sent the same request twiceduplicate-submit | Interaction and races | high | 10 |
GenClass flagged a timing problemgenclass-detection | Interaction and races | high | 10 |
The checks are the measured visual checks built for the GenClass runtime (in testing, not in runtime 0.2.1), 23 kinds with fixed
thresholds (Table 2), 22 of which run in the page check (click-blocked needs a visitor's clicks, and the
scanner clicks only the race probe's button): text under 1.5:1
contrast on a background it can measure (text over images, gradients or video is skipped), [object Object],
$NaN or raw {{ }} placeholders in visible text, lorem ipsum, garbled characters, images, background
images, video, stylesheets, fonts and icon fonts that failed, files the Content-Security-Policy blocked, CSS variables defined
nowhere, stretched images, sideways scrolling, a missing viewport meta tag on phones, layout shifts above 0.25, an empty main
region, a page that ignores clicks or cannot scroll, controls under fixed bars, focus hidden under sticky bars, and dialogs that
are covered. Each finding says what was measured and against which threshold.
How often the checks are right, as measured for the runtime on GitHub runners (Chromium 153, 2026-10-13): on its
fixture pages, 35 of 35 planted faults found across the 23 kinds; on its bench, no false reports in 128 page views of
clean local apps, and recall 1.00 on planted faults in real app layouts for 19 of the 23 kinds (bench run 5); on public
home pages (bench run 4: 31 of 40 sites measured, 96 loads), 8 findings, 5 confirmed real, 0 check errors, 3
unconfirmed. The scanner embeds the same checks at a later commit (6ae209f) and runs them in
Cloudflare's browser (Chrome 128 when last measured) for about 6 s per view, with 4 Tab presses and no hover, so these
numbers describe the checks, not this scanner. Details are in the runtime's RESULTS.md, sections 8 and 9.
The with-GenClass score
It is measured, never projected, and its visual half uses code that is not released yet: the visual
fixes are in testing for a release after runtime 0.2.1, and runtime 0.2.1, which the install
line on the page check loads, does not have them. In the same browser, the visual checks run in guard mode with their
fixes on, as the coming option GenClass.init({ mode: "guard", visual: { on: true, fix: true } })
runs them, and a second set of the same checks, report-only, measures the page again with the fixes in place. The visual
fixes change three kinds in the page: text colour for invisible text, a leaked inline pointer-events: none and a
leaked inline scroll lock on html or body. A finding counts as fixed only when the fix was applied and the re-check no
longer found it, in every view where it appeared; for a leaked pointer-events: none or scroll lock the re-check
must also have run long enough (about 4.5 s) to see it again. Every other kind is reported for the developer with the
suggested CSS or code, and the report says so on each finding. The with-GenClass score only subtracts verified fixes: a
problem that shows up only once a fix is in place (text that becomes measurable after a leaked
pointer-events: none is removed, for example) is not added to it, and the report says how many there were.
The race probe runs again in a fresh page with GenClass in guard mode with its default settings, holding requests for up to 900 ms while it decides; a double submit counts as fixed only when guard's own action sent fewer requests, and a late response only when guard's own recorded discard or defer was on that same request. When guard flags a problem but its expected gain does not clear the safety margin, the report says it held back.
Opting out
Site owners can stop the page check from loading their pages in two ways:
- Add a group to
robots.txt. The scanner reads only groups that name it, since a person asked for one page:
User-agent: GenClassScan Disallow: /
- Or email mehar@daybot.ca with the hostname. It goes on a block list the scanner checks before every scan, every redirect, every report view and every screenshot, subdomains included, so cached results and screenshots for it stop showing at once.
What is kept, and for how long
- The report (findings, timings and the address) is cached for 6 hours, so a shared link shows the same result without a new scan.
- Screenshots and reports are deleted 24 hours after the scan by an hourly sweep.
- The visitor's IP address is used only for the per-minute rate limit, as a hash, and is not stored.
- The browser starts empty for every scan and is closed afterwards. No cookies or storage carry over between scans.