Webskan for developers · reference
Finding ids
Everything audit_website, recheck_website and the PDF audit can report, by id. Use this page to decide what a CI job fails on, to tell an agent which findings are its job and which are the hosting provider's, and to look up what a fixed / new line in a re-check actually means.
How ids work
- Shape.
area.rule, optionally followed by@target:sec.header.hstsis site-wide,seo.h1.missing@/pricing/is about one page,links.broken@https://…/file.zipabout one link,a11y.color-contrast@/#darkabout one axe rule on one page in one colour scheme. The part before@is listed below; the target is whatever the finding is about. - Stable. The same problem produces the same id in every run, which is what makes the re-check diff (
fixed/open/new/unverified) and the weekly Monitor comparison possible. Titles and details are human text and may change; ids do not. - Severity is one of
high,medium,low,info. Some rules have a fixed severity, others pick it from a threshold (listed below).infois context, never a defect. - Areas:
secsecurity ·perfperformance ·seotechnical SEO ·a11yaccessibility ·uxusability ·journeycustomer journeys in a real mobile browser ·linksbroken links. - Unverified in a re-check means the target could not be checked this time (link blocked or timed out, page returned an error), so it is reported neither as fixed nor as new.
- Performance ids (
perf.*) move with network conditions. Gate on them with a threshold you accept, not on their mere presence; the Monitor's weekly diff excludes them for the same reason.
Security – sec.*
| Id | Severity | Fires when | Usual fix |
|---|---|---|---|
sec.tls.invalid | high | The TLS handshake fails or the certificate does not match the host. | Issue or renew the certificate for every name the site answers to (apex and www); check the chain. |
sec.tls.expiring | high < 7 days, else medium (fires under 21 days) | The certificate expires soon. | Fix automatic renewal (certbot timer, host panel, CDN) – do not just renew by hand. |
sec.https.no-redirect | high | http:// does not redirect to https://. | 301 from port 80 to https at the server or CDN level. |
sec.www.no-redirect | medium | The www or non-www variant serves the site instead of redirecting. | Pick one canonical host, 301 the other. |
sec.exposed-file@/path | high for .git, .env, SQL dumps, backup archives, wp-config copies; medium for phpinfo, logs, Adminer; low for .DS_Store | A well-known sensitive path returns 200 and the body matches the expected content signature (a “200 for everything” site does not trigger it). | Delete the file from the web root, keep backups outside it, and deny these paths in the server or CDN config. Rotate any credential that was inside. |
sec.header.hsts | medium | No Strict-Transport-Security on the https response. | Strict-Transport-Security: max-age=31536000; includeSubDomains once https is stable everywhere. |
sec.header.xfo | medium | Neither X-Frame-Options nor CSP frame-ancestors is set. | X-Frame-Options: SAMEORIGIN or Content-Security-Policy: frame-ancestors 'self'. |
sec.header.xcto | low | No X-Content-Type-Options: nosniff. | Add the header; one line, no side effects. |
sec.header.referrer | low | No Referrer-Policy. | Referrer-Policy: strict-origin-when-cross-origin. |
sec.header.csp | low | No Content-Security-Policy. | Start in Content-Security-Policy-Report-Only, tighten, then enforce. Matters most with logins, payments and user content. |
sec.header.permissions | low | No Permissions-Policy. | Permissions-Policy: camera=(), microphone=(), geolocation=() unless you use them. |
sec.leak.<header> | low | Server, X-Powered-By or similar reveals a software version. | server_tokens off (nginx), ServerTokens Prod (Apache), expose_php = Off, strip X-Powered-By. |
sec.mixed-content@/page | high | An https page loads scripts, styles, images or frames over http://. | Use protocol-relative or https URLs; in a CMS, search-and-replace the old http origin in content and theme settings. |
sec.form.insecure-action@/page | high | A form's action points to an http:// URL. | Change the action to https (or a relative path). |
Performance – perf.*
Measured with Lighthouse on an emulated mid-range phone over simulated 4G, from the audit location. Values vary between runs; gate with a margin.
| Id | Severity | Fires when | Usual fix |
|---|---|---|---|
perf.lighthouse.incomplete | info | The page did not finish loading within 45 s. No other perf.* finding is produced from that run, because its metrics would be inflated. | Find the resource that never resolves from outside your network (third-party script, blocked geo); check the page in PageSpeed Insights. |
perf.score.mobile | high < 50, medium 50–89 | Lighthouse mobile performance score below 90. | Work the specific perf.* findings below; the score follows. |
perf.lcp.mobile | high > 4 s, medium > 2.5 s | Largest Contentful Paint above the Google threshold. | Preload the hero image or font, serve it in a modern format and the right size, cut render-blocking CSS/JS, fix slow TTFB. |
perf.cls.mobile | high > 0.25, medium > 0.1 | Cumulative Layout Shift above threshold. | Width/height on images and embeds, reserve space for banners and ads, font-display: optional or matching fallback fonts. |
perf.tbt.mobile | high > 600 ms, medium > 300 ms | Total Blocking Time above threshold (the lab proxy for INP). | Defer or remove heavy scripts, split long tasks, load tag managers and chat widgets after interaction. |
perf.weight.mobile | medium | Total transferred bytes above 3 MB. | Compress and resize images, drop unused scripts and fonts. |
perf.heavy-script@file.js | info | TBT is above 300 ms; names the single script with the most main-thread time. | Profile that file; it is usually the first thing to defer or remove. |
perf.blocking-scripts | medium ≥ 3 scripts, else low | Synchronous scripts in <head> without defer/async. | Add defer, or move the script to the end of <body>. |
perf.opp.<lighthouse-audit> | medium ≥ 1.5 s saving, else low | A Lighthouse opportunity estimates ≥ 0.5 s or ≥ 50 KB of savings (e.g. perf.opp.render-blocking-resources, perf.opp.uses-responsive-images). | Follow the Lighthouse audit of that name. |
perf.images.oversized@/page | low | Images have several times more pixels than their rendered size on a phone. | srcset/sizes or an image CDN; export at the displayed size. |
perf.images.no-dims@/page | low | Three or more images without width/height on a page whose CLS is above 0.1. | Add the attributes or aspect-ratio in CSS. |
perf.slow-response@/page | medium > 5 s, low > 2.5 s | Time to first byte of the HTML above threshold during the crawl. | Page cache, object cache, fewer plugins, better hosting, a CDN in front. |
perf.compression.none | medium | The HTML response is not gzip/brotli compressed. | Enable compression at the server or CDN. |
Technical SEO – seo.*
| Id | Severity | Fires when | Usual fix |
|---|---|---|---|
seo.title.missing@/page | high on the home page, medium elsewhere | Empty or missing <title>. | A unique title per page; in a CMS fix the template. |
seo.title.long | low | Titles longer than 70 characters on one or more pages (one finding listing them all). | Shorten the title template; stop appending the site name to long post titles. |
seo.title.duplicate@<title> | medium | Several crawled pages share one title. | Unique titles; check paginated and filtered pages. |
seo.desc.missing@/page | medium on the home page, low elsewhere (blog/tag/category/pagination pages are skipped) | No meta description. | Write one per commercial page; let Google pick for the rest. |
seo.desc.long | info | Descriptions longer than 170 characters (one finding listing them all). | Cosmetic – trim if convenient. |
seo.desc.duplicate@<text> | low | Several pages share one description. | Per-page descriptions. |
seo.h1.missing@/page | low | No <h1>. | One H1 per page naming what the page is. |
seo.h1.multiple@/page | info | More than one <h1>. | Demote the extra ones (logo in H1 is the usual culprit). |
seo.noindex@/page | medium | A crawled page carries noindex. | Fine if intentional (thank-you pages). After a launch, check it is not a staging leftover. |
seo.robots.blocks-all | high | robots.txt has Disallow: / for all agents. | Remove it; this is the classic staging-to-production leftover. |
seo.robots.missing | info | No robots.txt. | Optional; add one pointing to the sitemap. |
seo.sitemap.missing | low (medium when more than 10 pages were crawled) | No sitemap found at the usual locations or in robots.txt. | Generate one and reference it in robots.txt. |
seo.sitemap.error | medium | The declared sitemap returns an error or no URLs. | Fix the generator or the path; check it is not blocked by a bot rule. |
seo.canonical.missing | low | The home page has no rel="canonical". | Self-referencing canonical on every page. |
seo.canonical.mismatch@/page | medium | A page's canonical points to a different URL. | Intentional for duplicates; otherwise a staging domain or http canonical left in the template. |
seo.og.missing | low | Fewer than two Open Graph tags on the home page. | og:title, og:description, og:image (1200×630). |
seo.sd.missing | info | No JSON-LD structured data on the home page. | Optional: Organization/LocalBusiness, Product, Article. |
seo.sd.invalid@/page | low | A ld+json block is not valid JSON. | Fix the JSON; validate with the Rich Results Test. |
seo.lang.missing | low | No lang attribute on <html>. | <html lang="en">. |
seo.viewport.missing | high | No meta viewport. | <meta name="viewport" content="width=device-width, initial-scale=1">. |
seo.viewport.no-zoom | medium | user-scalable=no or maximum-scale=1. | Remove them; zoom is a WCAG requirement. |
seo.img.alt@/page | low | Images without an alt attribute. | Describe meaningful images; alt="" for decorative ones. |
seo.page.error@/page | high for 5xx or no response, medium for 4xx | An internal link leads to a page that returns an error. | Fix the page or the link; add a redirect if the URL changed. |
seo.lighthouse.<audit> | low | A Lighthouse SEO audit fails that is not already covered above (e.g. seo.lighthouse.link-text, seo.lighthouse.crawlable-anchors). | Follow the Lighthouse audit of that name. |
Accessibility – a11y.*
| Id | Severity | Fires when | Usual fix |
|---|---|---|---|
a11y.<axe-rule>@/page (the home page is tested in both colour schemes, so its ids end in #light or #dark) | high for axe impact critical, medium for serious, low for moderate and minor | axe-core reports a WCAG 2.1 AA violation on a crawled page, e.g. a11y.color-contrast@/#dark, a11y.label@/contact/, a11y.link-name@/, a11y.image-alt@/about/. The finding carries the axe helpUrl and sample nodes. | Follow the axe rule page linked in helpUrl; contrast, form labels and link names cover most of the volume. |
a11y.score.mobile | high < 70, medium 70–89 | Lighthouse accessibility score below 90. | Work the a11y.* rule findings; automated tests find roughly a third of barriers, so a 100 is not “accessible”. |
Usability – ux.*
Measured in a real mobile browser (390 px) on the crawled pages.
| Id | Severity | Fires when | Usual fix |
|---|---|---|---|
ux.overflow@/page | medium | The page is more than 8 px wider than the phone screen (horizontal scrollbar). | Find the element with a fixed width or a non-wrapping table/code block; max-width: 100%, overflow-wrap. |
ux.small-text@/page | low | Visible paragraph text under 13 px, or more than 30 % of the paragraphs are under 14 px. Text that is off-screen, clipped or in a closed widget is not counted. | Body text ≥ 16 px on mobile. |
ux.js-errors@/page | medium ≥ 3 errors, else low | Uncaught errors in the console while loading. | Open the console on a phone-sized window; the first error usually explains the rest. |
ux.failed-requests@/page | medium ≥ 3, else low | Resources fail to load (404 images, blocked scripts, missing fonts). | Fix or remove the references. |
ux.cookie.blocking | medium | A consent dialog covers most of the first screen on mobile. | A bottom bar instead of a full-screen modal. |
ux.cookie.no-reject | medium | The consent dialog has no reject option at the same level as accept. | “Reject” or “Necessary only” next to “Accept” (EDPB guidance). |
ux.phone.not-clickable@/page | medium | A phone number appears as text and the page has no tel: link. | <a href="tel:+48…">. |
ux.email.not-clickable@/page | medium on the contact page, low elsewhere | An e-mail address appears as text without a mailto: link. | <a href="mailto:…"> or a form. |
ux.no-contact | high | No contact form and no clickable phone or e-mail on the key pages. | A contact page with a short form and a tel:/mailto: link. |
ux.contact.form-missing | low | Contact is only an e-mail address written as text. | A mailto: link or a three-field form. |
ux.form.unlabelled_fields@/page | medium | Visible form fields without an associated <label>, aria-label or title (placeholder only). Hidden fields and honeypot traps (off-screen, aria-hidden, tabindex=-1) are ignored; the field names are listed in the finding. | <label for> or aria-label on every field. |
ux.form.too_many_fields@/page | medium | The contact form has more than 6 fields. | Name, e-mail or phone, message; everything else optional or removed. |
ux.form.no_required@/page | low | No field is marked required. | required on the fields you need. |
ux.form.no_spam_protection@/page | low | No CAPTCHA, Turnstile, hCaptcha or honeypot field (a hidden text input such as website/url) detected in the contact form. | A honeypot field first; Turnstile if spam continues. |
ux.form.no_consent@/page | low | A form collecting e-mail has no privacy notice or consent checkbox. | One sentence with a link to the privacy policy under the submit button. |
Customer journeys – journey.*
The audit walks the site like a customer on a phone: opens the menu, searches for a word from the page, finds the contact form, adds a product to the cart and reaches checkout (nothing is submitted or paid), and requests a non-existent address. Each finding carries a screenshot.
| Id | Severity | Fires when | Usual fix |
|---|---|---|---|
journey.nav.fail | high | Tapping the mobile menu button reveals no links. | Usually a JavaScript error or a CSS breakpoint mismatch; see ux.js-errors. |
journey.nav.not_found | medium | No menu or menu button is visible on the first mobile screen. | A visible hamburger button or inline links above the fold. |
journey.search.fail | medium | Site search ends on an error page. | Fix the search endpoint; test with a plain word. |
journey.search.warn | low | Searching for a word taken from the page's own heading returns nothing. | Reindex, or check the search ignores accents and word forms. |
journey.404.soft404 | medium | A random address returns 200. | Return a real 404 status with the error page. |
journey.404.blank | high | The error page is empty. | A 404 page with menu, search and a link home. |
journey.404.no_way_out | medium | The error page has no navigation. | Same as above. |
journey.404.server_error | high | A non-existent address returns 5xx. | Fix the router or rewrite rule that crashes on unknown paths. |
journey.shop.fail | high | Product → cart → checkout does not reach the checkout page; the reason is in the title. | Reproduce on a phone with a fresh session. |
journey.shop.login_required | high | Checkout demands an account. | Enable guest checkout. |
journey.shop.no_payment_methods_visible | medium | The checkout page shows no payment methods. | Show accepted methods before the customer types anything. |
journey.shop.many_fields | medium | Checkout has more than 14 fields. | Hide optional fields, enable autofill, drop the account-creation block. |
journey.shop.add_button_disabled_(out_of_stock_or_variant) | low | The tested product could not be added (out of stock or variant required). | Check the customer gets a clear message; not a defect by itself. |
Links – links.*
| Id | Severity | Fires when | Usual fix |
|---|---|---|---|
links.broken@<url> | medium | A link found on the crawled pages (internal or external, up to 500 checked, up to 40 reported) returns 4xx/5xx or no response after a retry. The finding lists the pages it was found on. Links that answer 401/403/429/503 to automated clients are reported as blocked, not broken, and count as unverified in a re-check. | Fix or remove the link; redirect if the target moved. |
What to gate on
- Always fail: any
high. They are deterministic (redirects, exposed files, mixed content, robots, viewport, broken journeys) or far past a threshold. - Fail on
newmedium findings in a re-check, not on all medium findings – legacy debt should not block today's deploy, regressions should. - Do not gate on
info, onperf.*without a margin, or onlinks.broken@for external hosts you do not control (watch them in the Monitor instead). - The CI script and the
CLAUDE.mdblock that implement this are in Make your coding agent verify every deploy.
Run it on your site
claude mcp add --transport http webskan https://webskan.com/mcp
Then ask the agent to audit https://your-site and fix every high finding. Free: 2 whole-site audits per IP per day. Tools, limits and client configs · Developer plan for 30 audits a day and re-checks without the per-host window.