Audit and polish a frontend — static HTML/CSS/JS or a built SPA — towards verifiable perfection. The skill exists because production web perf tooling commonly breaks when it pokes at private APIs (Playwright internals, old Lighthouse signatures, unisolated global deps). This skill runs on STABLE apis only, computes exactly (luminance, not eyeballs), and demands a before/after report bound to audit ids.
seo-toolkit (this skill
measures layout, contrast, tokens and a11y; the SEO-layer breadth lives there).frontend-design-taste
(palette/type/signature), then run this skill to verify the result.python3 -m http.server 8377 (or the
project’s dev server). Static files are fine over http://localhost.node scripts/audit.js --url http://localhost:8377/ --mobile --out lh-mobile.json
node scripts/audit.js --url http://localhost:8377/ --desktop --out lh-desktop.json
Default threshold is 100; exit 0 only when every measured category is
at or above it. Add --only <category> to iterate on one category.
python3 scripts/meta_audit.py --html index.html --css main.css css/demo.css --out meta.json
Exit 0 = no violations; exit 1 = violations found.
audit.js color-contrast, meta_audit.py meta:description).--no-headless to watch). No Playwright dependency, no private fields,
no _ws_url transport hacks. CDP port comes from chrome.port (public).require("lighthouse").default automatically.node_modules → NODE_PATH
→ npm root -g. The script explains how to install instead of dying with
Cannot find module:
npm i lighthouse chrome-launcher # in the script's dir
# or globally:
npm i -g lighthouse chrome-launcher && export NODE_PATH=$(npm root -g)
--out) with per-category scores and only the
failed weighted audits (id, title, score, weight) — no 4000-line dump.0 all categories ≥ threshold, 1 below threshold, 2 runner error.Checks and their ids:
| id | what it verifies |
|---|---|
meta:title / meta:title-length |
title present, ≤ 60 chars |
meta:description / meta:description-length |
description present, ≤ 160 chars |
meta:canonical |
canonical link |
meta:og:*, meta:og:size |
OG tags; image must declare 1200×630 (crop-safe) |
meta:twitter:card |
twitter card tag |
meta:json-ld |
any application/ld+json script |
meta:robots |
not blocked with noindex/nofollow |
meta:sitemap-link |
sitemap referenced |
headings:single-h1 |
exactly one h1 |
headings:order |
h1→h6 sequence, no level skips (h1→h3 is a violation) |
tokens:raw-hex |
zero raw hex outside the token block (:root/tokens) |
contrast:wcag-aa |
computed WCAG relative luminance, fg/bg pairs ≥ 4.5:1 |
adaptive:scroll-padding |
fixed header ⇒ scroll-padding-top present |
adaptive:media-queries |
responsive breakpoints exist for tablet widths |
a11y:img-alt |
every <img> has an alt attribute (decorative may be empty) — WCAG 1.1.1 / axe image-alt |
a11y:button-name / a11y:link-name |
buttons and links have an accessible name (text or aria-label) — WCAG 4.1.2 / axe button-name, link-name |
a11y:form-label |
inputs/selects/textareas have a label (<label for>, wrapper, aria-label) — WCAG 4.1.2 / axe label |
a11y:aria-valid |
every aria-* attribute name is in the WAI-ARIA 1.2 set — axe aria-valid-attr |
a11y:landmark-unique |
at most one <main>; multiple <nav> need distinct labels — WCAG 1.3.1 / axe landmark-unique |
html:doctype |
HTML5 doctype on the first line |
html:charset |
charset declared as utf-8 |
html:viewport |
responsive viewport meta present |
html:lang |
<html lang> with a BCP 47 code (a11y + SEO) |
html:dir-rtl |
RTL languages (ar/he/fa/ur/yi) require dir="rtl" |
html:unique-id |
no duplicate id attributes |
html:semantic |
semantic elements used: header, main, footer |
html:favicons |
favicon link present |
html:web-app-manifest |
manifest.json referenced (PWA) |
html:sri |
CDN-hosted scripts carry integrity (SRI); local scripts exempt |
html:defer-async |
external scripts load with defer/async/type=module (no render-blocking) |
html:input-types |
inputs declare a type (not defaulting to text) |
images:dimensions |
img has width/height (CLS prevention) |
images:lazy-loading |
3+ images lazy-load offscreen content |
images:srcset |
responsive srcset used for fixed-size images |
images:modern-format |
raster jpg/png converted to webp/avif |
js:no-inline |
no inline handlers (onclick=…) or inline <script> blocks |
js:no-console |
no console.log/debug/warn in HTML |
css:focus-visible |
visible :focus-visible indicator for keyboard users |
css:print |
@media print stylesheet exists |
css:dark-mode |
prefers-color-scheme dark mode supported |
css:font-display |
webfonts use font-display: swap (no FOIT) |
perf:resource-hints |
preload/preconnect/dns-prefetch hints for critical origins (LCP) |
security:https |
no http:// URLs in src/href |
security:noopener |
target="_blank" links carry rel="noopener" |
privacy:consent |
cookie/consent mention present (GDPR / 152-ФЗ) |
nav:back-to-top |
long pages have a way back to top — logo links to top and/or a floating scroll-to-top button (bottom-right, aria-label, appears after scroll) — WCAG 2.4.1 / UX pattern |
scripts/security_privacy_audit.py extends the skill with the Front-End-Checklist
Security (22), Privacy (5) and Internationalization (5) categories — the
statically verifiable subset. It is pure Python 3 stdlib (no requests/bs4/
PyYAML), mirrors meta_audit.py’s JSON/exit-code conventions, and emits audit ids
prefixed sec: / priv: / i18n:. It does NOT duplicate the dedicated sibling
skills’ a11y/perf/testing checks — only Security/Privacy/i18n.
python3 scripts/security_privacy_audit.py --html index.html --css main.css --js app.js --out sec.json
python3 scripts/security_privacy_audit.py --html a.html b.html --json # stdout JSON
Exit 0 = no violations; 1 = ≥1 violation; 2 = runner error. The report is
machine-readable JSON only on stdout (no human summary) — evidence gate.
| id | severity | what it verifies |
|---|---|---|
sec:https |
high | no http:// URLs in href/src (HTTPS everywhere) |
sec:mixed-content |
high | no http:// in CSS url()/@import/srcset (mixed content) |
sec:csp |
medium | Content-Security-Policy present (meta or server header) — OWASP A05 |
sec:sri |
high | external <script>/<link> carry integrity (SRI) |
sec:security-headers |
low | HSTS/XCTO/XFO/Referrer-Policy/Permissions-Policy (meta; server-side canonical) |
sec:noopener |
high | target="_blank" carries rel="noopener noreferrer" |
sec:secrets-in-url |
critical | no tokens/keys in query strings (token=/api_key=/secret=) |
sec:localstorage-secrets |
medium | no secret keys in localStorage/sessionStorage |
sec:csrf |
medium | state-changing (POST) forms carry a CSRF token |
sec:eval |
high | no eval()/new Function() in JS |
sec:deprecated-crypto |
medium | no md5/sha1 in security context |
sec:external-origins |
low | external script origins enumerated (info) |
sec:internal-leak |
medium | no internal IPs / .env references leaked |
sec:cookie-flags |
low | document.cookie assignments use Secure |
priv:cookie-consent |
medium | cookie/consent banner before non-essential tracking (GDPR / 152-ФЗ) |
priv:tracking-before-consent |
medium | no tracking scripts (GA/GTM/fbq/Metrika) before consent |
priv:privacy-policy |
low | privacy policy link present |
priv:dnt |
low | Do Not Track / Global Privacy Control respected (info/manual) |
priv:third-party-data |
low | no PII sent to third parties without consent (info) |
i18n:lang |
medium | <html lang> present, valid BCP 47 |
i18n:dir |
medium | RTL languages (ar/he/fa/ur/yi) set dir="rtl" |
i18n:intl-api |
low | Intl.NumberFormat/DateTimeFormat used for formatting (info) |
i18n:charset-early |
medium | <meta charset> within first 1024 bytes |
i18n:hardcoded-locale |
low | no hardcoded locale-specific strings (info) |
security_privacy_audit.py covers the static surface. The following require a
live server or manual review and are NOT asserted by the script:
frontend-performance’s perf_headers.py (perf:sec:* / perf:hsts) or
curl -I.Set-Cookie) — inspect response
headers.For depth beyond this skill’s baseline, delegate to the dedicated sibling skills
(same skills/ dir):
frontend-a11y
static + a11y_axe.mjs runtime).frontend-performance (perf_headers.py offline +
audit.js Lighthouse).frontend-testing (scaffolds configs; does not re-audit).Do NOT re-implement those domains here — this skill owns layout/perf-SEO/tokens + the Security/Privacy/i18n static subset above; the siblings own their full categories.
For static projects WITHOUT a build system (plain HTML/CSS/JS), a minimal design system still applies:
:root { --color-*: ...; } layer or a dedicated tokens.css).var(--color-*).meta_audit.py flags any raw hex outside the token block; this is the
enforceable version of “no hardcoded colors”.Social networks crop images differently (WhatsApp square-ish, Facebook center-crop). The skill’s rules, learned the hard way:
og-2026-08-09.png).screenshot() may capture the stale layout. Read
void element.offsetHeight (or getBoundingClientRect()) to force
layout sync, then shoot.og:image:width/og:image:height =
1200/630 so scrapers don’t guess and mis-trim.If position: fixed header exists, anchor jumps hide content underneath.
Fix: html { scroll-padding-top: <header-height> } (+ scroll-margin-top on
the anchors if needed). meta_audit.py reports adaptive:scroll-padding
when this is missing.
lighthouse
headless-shell in the default call.!important spray or target 100 by hiding audits: raise
the underlying metrics (real fix at the root).--out
files you name.localhost vs production differ (no CDN, no real
TLS); state this in the report when it matters.Full loop on a static site:
cd ~/projects/lovii_demo
python3 -m http.server 8377 &
node .../audit.js --url http://localhost:8377/ --mobile --out lh-mobile.json
node .../audit.js --url http://localhost:8377/ --desktop --out lh-desktop.json
python3 .../meta_audit.py --html index.html --css main.css css/demo.css --out meta.json
Iterate on one category until green:
node .../audit.js --url http://localhost:8377/ --mobile --only accessibility
See references/canonical-patterns.md for the canonical sources this skill
is aligned with, and the showcase in docs/showcase/showcase-frontend-perfection-lovii.md.
frontend-a11y or frontend-performance.