Skills/Web Quality Skills

Web Quality Skills

byAddy Osmani·frontend

An (unofficial) measurement-first collection of Agent Skills for optimizing web projects with Google Lighthouse, Chrome DevTools for agents, Core Web Vitals, WCAG, and search guidance. Stack-agnostic, supporting React, Vue, Angular, Svelte, Next.js, Nuxt, Astro, and plain HTML. Covers live browser audits, performance traces, accessibility snapshots, technical SEO, and modern security patterns.

CLAUDE CODECURSORCODEXGEMINI CLIWINDSURF
Loading stats...

Installation

Install directly into your agent CLI.

$npx skills add addyosmani/web-quality-skills
Usage examplePrompt your agent

“Audit this page for web quality: evaluate Core Web Vitals, accessibility barriers, technical SEO, and security best practices.”

Web Quality Skills Subgroups

Master Quality Auditjump ↓

Orchestrates end-to-end evidence-led reviews combining live Chrome DevTools MCP & Lighthouse audits with static code smoke tests.

Core Web Vitals (LCP, INP, CLS)jump ↓

Diagnose Largest Contentful Paint (< 2.5s), Interaction to Next Paint (< 200ms), and Cumulative Layout Shift (< 0.1) using real-user CrUX data and traces.

Web Performance & Loading Strategiesjump ↓

Optimize the critical rendering path, configure JavaScript bundle splitting, compress images with AVIF/WebP, and streamline font loading.

Web Accessibility (WCAG 2.2)jump ↓

Ensure full WCAG 2.2 AA compliance across Perceivable, Operable, Understandable, and Robust standards with screen reader and keyboard checks.

Technical SEO & Structured Datajump ↓

Validate title tags, meta descriptions, canonical URLs, semantic HTML hierarchy, OpenGraph metadata, and JSON-LD structured schemas.

Security & Modern Best Practicesjump ↓

Enforce HTTPS strict transport security, Content Security Policy (CSP), subresource integrity, and deprecate vulnerable JavaScript APIs.

SKILL.md

--- name: web-quality-skills description: Comprehensive collection of agent skills for optimizing web quality based on Google Lighthouse, Chrome DevTools, Core Web Vitals (LCP, INP, CLS), WCAG 2.2, and search guidance. license: MIT metadata: author: Addy Osmani repo: addyosmani/web-quality-skills version: "2.0" ---

Web Quality Skills

An (unofficial) measurement-first collection of Agent Skills for optimizing web projects with Google Lighthouse, Chrome DevTools for agents, Core Web Vitals, WCAG, and search guidance.

Subgroups Included

1. Master Quality Audit — Live Lighthouse audits for agents across Accessibility, SEO, Best Practices, and Agentic Browsing. 2. Core Web Vitals — Performance traces and Insights for diagnosing LCP, INP, CLS, network, and main-thread bottlenecks. 3. Web Performance & Loading Strategies — Field plus lab measurement using CrUX for real-user context. 4. Web Accessibility (WCAG 2.2) — Full WCAG 2.2 AA compliance across Perceivable, Operable, Understandable, and Robust standards. 5. Technical SEO & Structured Data — Technical search optimization, meta tags, and structured data verification. 6. Security & Web Best Practices — Security headers, modern web APIs, and code hygiene.


Subgroup 1: Master Quality Audit

--- name: web-quality-audit description: Run an evidence-led web quality audit covering performance, accessibility, SEO, best practices, and agentic browsing. Use when asked to "audit my site", "review web quality", "run lighthouse audit", "check page quality", or "optimize my website". license: MIT metadata: author: web-quality-skills version: "2.0" ---

Web quality audit

Comprehensive quality review that combines live browser evidence with source inspection. Covers Performance, Accessibility, SEO, Best Practices, and Agentic Browsing without treating an aggregate score as proof of quality.

Lighthouse 13+. The Performance category now uses shared Performance Insights across Lighthouse and the DevTools Performance panel (announcement). Follow current insight names and evidence. Do not require removed audit IDs or automatically recreate their recommendations; some were retired because they were noisy, inactionable, or easy to over-recommend.

How it works

  1. Establish the audit target: representative URLs, important states and journeys, public versus authenticated access, and mobile/desktop scope.
  2. If a page can run, read the measurement workflow and collect a minimal live baseline before searching the codebase broadly.
  3. Use runtime failures to localize source inspection. Keep measured findings separate from hypotheses found only in code.
  4. Categorize by user impact and confidence, then make or recommend specific fixes.
  5. Re-run equivalent automated checks and the affected manual flows. Report what is verified and what still needs field or human validation.

Tool routing

Use the best capability already available; do not block the audit on optional setup.

NeedPreferred routeFallback
Performance and Core Web VitalsRecord a browser performance trace and analyze focused insights; with Chrome DevTools MCP, use performance_start_trace then performance_analyze_insightLighthouse CLI or PageSpeed Insights lab data
Real-user performanceCrUX values included in current DevTools trace summariesPageSpeed Insights/CrUX Vis; direct CrUX API only when a key is already available or automation is requested
Accessibility, SEO, Best Practices, Agentic BrowsingRun a live Lighthouse audit; with Chrome DevTools MCP, use lighthouse_auditCategory-specific Lighthouse CLI audits plus manual checks
Rendered semantics and interactionInspect the accessibility tree and exercise the UI; with Chrome DevTools MCP, use take_snapshot and focused evaluate_scriptBrowser/manual testing
Source smoke testscripts/analyze.sh <path>Direct source inspection

Chrome DevTools MCP's lighthouse_audit intentionally excludes performance. Its navigation mode reloads the page; use snapshot mode when preserving the current authenticated or user-created state matters. The static analyzer is a fast smoke test, not a substitute for a rendered-page audit.

Audit categories

Performance

  • *Core Web Vitals** — Must pass for good page experience:
  • LCP (Largest Contentful Paint) < 2.5s. The largest visible element must render quickly. Optimize images, fonts, and server response time.
  • INP (Interaction to Next Paint) < 200ms. User interactions must feel instant. Reduce JavaScript execution time and break up long tasks.
  • CLS (Cumulative Layout Shift) < 0.1. Content must not jump around. Set explicit dimensions on images, embeds, and ads.
  • *Resource Optimization:**
  • Compress images. Use WebP/AVIF with fallbacks. Serve correctly sized images via srcset.
  • Minimize JavaScript. Remove unused code. Use code splitting. Defer non-critical scripts.
  • Optimize CSS. Extract critical CSS. Remove unused styles. Avoid @import.
  • Efficient fonts. Use font-display: swap. Preload critical fonts. Subset to needed characters.
  • *Loading Strategy:**
  • Preconnect to origins. Add <link rel="preconnect"> for third-party domains.
  • Preload critical assets. LCP images, fonts, and above-fold CSS.
  • Lazy load below-fold content. Images, iframes, and heavy components.
  • Cache effectively. Long cache TTLs for static assets. Immutable caching for hashed files.

Accessibility

  • *Perceivable:**
  • Text alternatives. Every <img> has meaningful alt text. Decorative images use alt="".
  • Color contrast. Minimum 4.5:1 for normal text, 3:1 for large text (WCAG AA).
  • Don't rely on color alone. Use icons, patterns, or text alongside color indicators.
  • Captions and transcripts. Video has captions. Audio has transcripts.
  • *Operable:**
  • Keyboard accessible. All functionality available via keyboard. No keyboard traps.
  • Focus visible. Clear focus indicators on all interactive elements.
  • Skip links. Provide "Skip to main content" for keyboard users.
  • Sufficient time. Users can extend time limits. No auto-advancing content without controls.
  • *Understandable:**
  • Page language. Set lang attribute on <html>.
  • Consistent navigation. Same navigation structure across pages.
  • Error identification. Form errors clearly described and associated with fields.
  • Labels and instructions. All form inputs have associated labels.
  • *Robust:**
  • Valid HTML. No duplicate IDs. Properly nested elements.
  • ARIA used correctly. Prefer native elements. ARIA roles match behavior.
  • Name, role, value. Interactive elements have accessible names and correct roles.

SEO

  • *Crawlability:**
  • Valid robots.txt. Doesn't block important resources.
  • XML sitemap. Lists all important pages. Submitted to Search Console.
  • Canonical URLs. Prevent duplicate content issues.
  • No noindex on important pages. Check meta robots and headers.
  • *On-Page SEO:**
  • Unique title tags. Make each title descriptive and concise; display truncation varies by device and result type.
  • Meta descriptions. Write useful, page-specific summaries; search engines may choose a different snippet.
  • Heading hierarchy. The primary heading is descriptive and the structure is logical; do not fail valid HTML solely for using more than one <h1>.
  • Descriptive link text. Not "click here" or "read more".
  • *Technical SEO:**
  • Mobile-friendly. Responsive design. Tap targets ≥ 48px.
  • HTTPS. Secure connection required.
  • Page experience signals. Use field Core Web Vitals as evidence, without promising a ranking change.
  • Structured data. JSON-LD for rich snippets (Article, Product, FAQ, etc.).

Best practices

  • *Security:**
  • HTTPS everywhere. No mixed content. HSTS enabled.
  • No vulnerable libraries. Keep dependencies updated.
  • CSP headers. Content Security Policy to prevent XSS.
  • No exposed source maps. In production builds.
  • *Modern Standards:**
  • No deprecated APIs. Replace document.write, synchronous XHR, etc.
  • Valid doctype. Use <!DOCTYPE html>.
  • Charset declared. <meta charset="UTF-8"> as first element in <head>.
  • No browser errors. Clean console. No CORS issues.
  • *UX Patterns:**
  • No intrusive interstitials. Especially on mobile.
  • Clear permission requests. Only ask when needed, with context.
  • No misleading buttons. Buttons do what they say.

Agentic browsing

Use the Lighthouse Agentic Browsing results as technical signals for how well assistants can understand and interact with the rendered page.

  • Accessible interaction surface. Semantic HTML, labels, names, roles, and states must expose meaningful controls in the accessibility tree.
  • WebMCP integrations are valid when present. Review registered tools, schemas, and form coverage; do not add WebMCP solely to raise an audit score.
  • `llms.txt` is optional. A valid file may help compatible tools discover curated content, but a Lighthouse pass does not prove that search or AI products will ingest, rank, or cite it.
  • Keep this category separate from SEO claims. Agentic browsability is not evidence of search ranking or AI visibility.

Severity levels

LevelDescriptionAction
CriticalSecurity vulnerabilities, complete failuresFix immediately
HighCore Web Vitals failures, major a11y barriersFix before launch
MediumPerformance opportunities, SEO improvementsFix within sprint
LowMinor optimizations, code qualityFix when convenient

Audit output format

When performing an audit, structure findings as:

markdown
## Audit results

Evidence

| Signal | Scope/conditions | Result | Source | |--------|------------------|--------|--------| | LCP | URL, phone, p75/28 days | 3.1s (needs improvement) | CrUX | | Accessibility | URL, mobile navigation | 92 | Lighthouse |

Critical issues (X found)

- [Category] Issue description. File: path/to/file.js:123 - Impact: Why this matters - Evidence: Measured failure, runtime observation, or source hypothesis - Fix: Specific code change or recommendation

High priority (X found)

...

Summary

- Performance: measured status and X findings - Accessibility: automated status, X findings, manual checks pending/passed - SEO: X findings - Best Practices: X findings - Agentic Browsing: X findings or not available

Recommended priority

1. First fix this because... 2. Then address... 3. Finally optimize...

Verification

- Re-run results under the same conditions - Manual checks completed - Field validation still pending ```

Quick checklist

Before every deploy

- [ ] Core Web Vitals passing - [ ] No accessibility errors (axe/Lighthouse) - [ ] No console errors - [ ] HTTPS working - [ ] Meta tags present

Weekly review

- [ ] Check Search Console for issues - [ ] Review Core Web Vitals trends - [ ] Update dependencies - [ ] Test with screen reader

Monthly deep dive

- [ ] Full Lighthouse audit - [ ] Performance profiling - [ ] Accessibility audit with real users - [ ] SEO keyword review

References


Subgroup 2: Core Web Vitals Optimization

--- name: core-web-vitals description: Optimize Core Web Vitals (LCP, INP, CLS) for better page experience using field and lab evidence. Use when asked to "improve Core Web Vitals", "fix LCP", "reduce CLS", "optimize INP", "page experience optimization", or "fix layout shifts". license: MIT metadata: author: web-quality-skills version: "2.0" ---

Core Web Vitals optimization

Targeted optimization for the three Core Web Vitals using field data to identify user impact and browser traces to diagnose causes.

Measure before optimizing

When a runnable URL is available, read the performance measurement workflow. Prefer this sequence:

  1. Check page-level CrUX p75 data, with a clearly labeled origin fallback when page data is unavailable.
  2. Record a browser performance trace under stated conditions. With Chrome DevTools MCP, trace summaries can include CrUX alongside the observed lab metrics.
  3. Analyze only the insights associated with the failing metric, then inspect the implicated code and resources.
  4. Re-run equivalent lab measurements after the fix. Do not claim an immediate field improvement; CrUX and first-party RUM need new user visits.

If only source code is available, identify likely causes but do not claim that LCP, INP, or CLS is failing without runtime evidence.

The three metrics

MetricMeasuresGoodNeeds workPoor
LCPLoading≤ 2.5s2.5s – 4s> 4s
INPInteractivity≤ 200ms200ms – 500ms> 500ms
CLSVisual Stability≤ 0.10.1 – 0.25> 0.25

Google measures at the 75th percentile — 75% of page visits must meet "Good" thresholds.


LCP: Largest Contentful Paint

  • LCP measures when the largest visible content element renders. Usually this is:
  • Hero image or video
  • Large text block
  • Background image
  • <svg> element

Common LCP issues

1. Slow server response (TTFB > 800ms) `` Fix: CDN, caching, optimized backend, edge rendering

2. Render-blocking resources ```html <!-- ❌ Blocks rendering --> <link rel="stylesheet" href="/all-styles.css">

<!-- ✅ Critical CSS inlined, rest deferred --> <style>/* Critical above-fold CSS */</style> <link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> ```

3. Slow resource load times ```html <!-- ❌ LCP image is discovered only after a stylesheet loads --> <div class="hero"></div>

<!-- ✅ Discoverable in initial HTML and prioritized --> <link rel="preload" href="/hero.webp" as="image" fetchpriority="high"> <img src="/hero.webp" alt="Hero" fetchpriority="high"> ```

Prefer a discoverable <img> with fetchpriority="high". Add the preload only when the trace shows that the resource would otherwise be discovered late; duplicate or speculative preloads can compete for bandwidth.

4. Client-side rendering delays ```javascript // ❌ Content loads after JavaScript useEffect(() => { fetch('/api/hero-text').then(r => r.json()).then(setHeroText); }, []);

// ✅ Server-side or static rendering // Use SSR, SSG, or streaming to send HTML with content export async function getServerSideProps() { const heroText = await fetchHeroText(); return { props: { heroText } }; } ```

5. Make navigations instant with the Speculation Rules API

For sites with predictable same-origin journeys, prerendering a likely next page can make a successful subsequent navigation much faster. Treat this as a measured navigation optimization, not a substitute for fixing the current page's LCP.

html
<script type="speculationrules">
{
  "prerender": [{
    "where": { "href_matches": "/*" },
    "eagerness": "moderate"
  }]
}
</script>

Current Chrome behavior is specific enough to guide the choice:

eagernessTrigger
conservativePointer or touch down
moderateDesktop: 200ms hover, or earlier pointer down; mobile: viewport heuristics
eagerChrome 143+: desktop 10ms hover; mobile 50ms after the anchor enters the viewport
immediateAs soon as the rules are observed

Start conservatively and measure prediction hit rate, transferred bytes, server load, and navigation improvement before expanding the rules. Recheck Chrome's maintained eagerness documentation before hardcoding timing-sensitive behavior.

  • Caveats:
  • Bandwidth/CPU cost. Each prerender is roughly a full page load. Scope where carefully (href_matches patterns, exclude logout/checkout) and avoid immediate outside small sites.
  • Side effects fire early. Analytics, ads, and any code that runs on load will fire when the prerender starts, not when the user navigates. Gate side effects on the `prerenderingchange` event or document.prerendering.
  • Chromium-only. Safari and Firefox ignore the script — it's a progressive enhancement, never a regression.

LCP optimization checklist

markdown
- [ ] TTFB < 800ms (use CDN, edge caching)
- [ ] LCP resource is discoverable in initial HTML and prioritized; preload only if the trace shows late discovery
- [ ] LCP image optimized (WebP/AVIF, correct size)
- [ ] Critical CSS inlined (< 14KB)
- [ ] No render-blocking JavaScript in <head>
- [ ] Fonts don't block text rendering (font-display: swap)
- [ ] LCP element in initial HTML (not JS-rendered)
- [ ] Speculation Rules added for likely-next navigations (moderate eagerness)

LCP element identification

This snippet diagnoses the current page session. It is not field data.

javascript
// Find your LCP element
new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP element:', lastEntry.element);
  console.log('LCP time:', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });

INP: Interaction to Next Paint

INP measures responsiveness across clicks, taps, and key presses during a visit. Diagnose its input delay, processing time, and presentation delay separately; a slow interaction may involve main-thread contention before the handler, expensive application work, or delayed rendering after it.

When field INP is poor or a trace identifies a slow interaction, read the INP reference for trace interpretation, yielding patterns, third-party and rendering causes, a single-session observer, and first-party attribution.


CLS: Cumulative Layout Shift

CLS measures unexpected layout shifts across a page visit. Use field attribution or a trace to identify the shifted node and the trigger; do not assume the visible victim caused the shift.

When field CLS is poor or a trace reports shifts, read the CLS reference for reserved-space patterns, dynamic content, font and animation fixes, a debugging observer, and a verification checklist.


Measurement sources

SourceUse
Browser performance trace (Chrome DevTools MCP: performance_start_trace)Observe one load or interaction and diagnose focused insights; use included CrUX context when available
CrUX or Search ConsolePrioritize aggregated real-user outcomes at p75
Lighthouse CLI or PageSpeed InsightsControlled lab fallback when DevTools tools are unavailable
First-party RUMSegment current production experience by route, device, release, and attribution
Raw PerformanceObserverInspect one page session during debugging

Do not route performance through Chrome DevTools MCP's lighthouse_audit; that capability intentionally covers non-performance Lighthouse categories. Do not compare a single lab value directly with a field p75 as if they were equivalent samples.

When adding or reviewing production collection, read the first-party RUM reference. Prefer the web-vitals library because raw browser APIs do not by themselves implement every Core Web Vital's lifecycle and reporting rules.


Framework quick fixes

Next.js

```jsx // LCP: Use next/image with priority import Image from 'next/image'; <Image src="/hero.jpg" priority fill alt="Hero" />

// INP: Use dynamic imports const HeavyComponent = dynamic(() => import('./Heavy'), { ssr: false });

// CLS: Image component handles dimensions automatically ```

React

```jsx // LCP: Preload in head <link rel="preload" href="/hero.jpg" as="image" fetchpriority="high" />

// INP: Memoize and useTransition const [isPending, startTransition] = useTransition(); startTransition(() => setExpensiveState(newValue));

// CLS: Always specify dimensions in img tags ```

Vue/Nuxt

```vue <!-- LCP: Use nuxt/image with preload --> <NuxtImg src="/hero.jpg" preload loading="eager" />

<!-- INP: Use async components --> <component :is="() => import('./Heavy.vue')" />

<!-- CLS: Use aspect-ratio CSS --> <img :style="{ aspectRatio: '16/9' }" /> ```

References


Subgroup 3: Web Performance & Loading Strategies

--- name: performance description: Optimize web performance for faster loading and better user experience. Use when asked to "speed up my site", "optimize performance", "reduce load time", "fix slow loading", "improve page speed", or "performance audit". license: MIT metadata: author: web-quality-skills version: "2.0" ---

Performance optimization

Evidence-led performance optimization using real-user signals for prioritization and browser traces for diagnosis. Focuses on loading speed, runtime responsiveness, and resource delivery.

How it works

  1. If a page can run, read the measurement workflow and establish a field-plus-lab baseline before editing.
  2. Prioritize poor real-user Core Web Vitals. Use a DevTools performance trace and its focused insights to find the cause.
  3. Inspect and change only the code or assets connected to measured bottlenecks.
  4. Re-run equivalent lab measurements and report before/after values, conditions, and uncertainty. Field verification remains pending until enough new user data arrives.

When no runnable page exists, perform static inspection but call findings hypotheses, not measured regressions. Include the command or browser workflow that can verify each high-impact hypothesis.

Prefer a browser tool that records a performance trace and exposes focused insights. With Chrome DevTools MCP, use performance_start_trace and performance_analyze_insight; do not route performance through lighthouse_audit, which covers non-performance Lighthouse categories.

Starting performance budget

Budgets must reflect the product's target devices, networks, page types, and user journeys. The values below are initial guardrails for a typical content or commerce page, not universal pass/fail criteria. Preserve an existing project budget when one is already defined.

ResourceBudgetRationale
Total page weight< 1.5 MBBounds transfer time and data cost on constrained target networks; calibrate with representative pages
JavaScript (compressed)< 300 KBProtect parse and execution cost
CSS (compressed)< 100 KBLimit render-blocking work
Images (above-fold)< 500 KBProtect likely LCP resources
Fonts< 100 KBLimit critical font transfer
Third-party< 200 KBBound code outside product control

Critical rendering path

Server response

* TTFB < 800ms. Time to First Byte should be fast. Use CDN, caching, and efficient backends. * Enable compression. Gzip or Brotli for text assets. Brotli preferred (15-20% smaller). * HTTP/2 or HTTP/3. Multiplexing reduces connection overhead. * Edge caching. Cache HTML at CDN edge when possible. * Consider Early Hints (HTTP 103) for measured document latency. If a trace shows slow HTML generation and stable critical subresources, send an interim 103 with Link headers before the normal final response from the same request. Use HTTP/2 or later. A CDN may synthesize the 103 from Link headers on an earlier 200, or the origin/edge handler can emit it directly. Unsupported clients continue to the final response, but confirm current browser and infrastructure support. Limit hints to proven critical preloads or preconnects: inaccurate hints waste bandwidth. Cloudflare reported a 20–30% LCP improvement in an artificial, image-heavy test; treat that as a vendor case study, not an expected saving, and measure your result. See MDN's 103 implementation example and the Cloudflare study.

Resource loading

Preconnect to required origins: ``html <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://cdn.example.com" crossorigin>

Preload critical resources:

Preload only resources whose late discovery is visible in the trace. Each preload competes for bandwidth and an unnecessary high-priority request can delay LCP.

html
<!-- LCP image -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">

<!-- Critical font --> <link rel="preload" href="/font.woff2" as="font" type="font/woff2" crossorigin> ```

Prerender likely-next navigations with the Speculation Rules API: ``html <script type="speculationrules"> { "prerender": [{ "where": { "href_matches": "/*" }, "eagerness": "moderate" }] } </script> ` moderate waits for a stronger intent signal than eager modes. Measure prediction hit rate, transferred bytes, and server cost; a wrong prerender is roughly an unused navigation. See [core-web-vitals → LCP](../core-web-vitals/SKILL.md#lcp-largest-contentful-paint) for the tradeoffs and the prerenderingchange` gating needed for analytics.

Defer non-critical CSS: ```html <!-- Critical CSS inlined --> <style>/* Above-fold styles */</style>

<!-- Non-critical CSS --> <link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="/styles.css"></noscript> ```

JavaScript optimization

Defer non-essential scripts: ```html <!-- Parser-blocking (avoid) --> <script src="/critical.js"></script>

<!-- Deferred (preferred) --> <script defer src="/app.js"></script>

<!-- Async (for independent scripts) --> <script async src="/analytics.js"></script>

<!-- Module (deferred by default) --> <script type="module" src="/app.mjs"></script> ```

Code splitting patterns: ```javascript // Route-based splitting const Dashboard = lazy(() => import('./Dashboard'));

// Component-based splitting const HeavyChart = lazy(() => import('./HeavyChart'));

// Feature-based splitting if (user.isPremium) { const PremiumFeatures = await import('./PremiumFeatures'); } ```

Tree shaking best practices: ```javascript // ❌ Imports entire library import _ from 'lodash'; _.debounce(fn, 300);

// ✅ Imports only what's needed import debounce from 'lodash/debounce'; debounce(fn, 300); ```

Image optimization

Format selection

| Format | Use case | Browser support | |--------|----------|-----------------| | AVIF | Photos, best compression | 92%+ | | WebP | Photos, good fallback | 97%+ | | PNG | Graphics with transparency | Universal | | SVG | Icons, logos, illustrations | Universal |

Responsive images

html <picture> <!-- AVIF for modern browsers --> <source type="image/avif" srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w" sizes="(max-width: 600px) 100vw, 50vw"> <!-- WebP fallback --> <source type="image/webp" srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w" sizes="(max-width: 600px) 100vw, 50vw"> <!-- JPEG fallback --> <img src="hero-800.jpg" srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w" sizes="(max-width: 600px) 100vw, 50vw" width="1200" height="600" alt="Hero image" loading="lazy" decoding="async"> </picture>

LCP image priority

```html <!-- Above-fold LCP image: eager loading, high priority --> <img src="hero.webp" fetchpriority="high" loading="eager" decoding="sync" alt="Hero">

<!-- Below-fold images: lazy loading --> <img src="product.webp" loading="lazy" decoding="async" alt="Product"> ```

Font optimization

Loading strategy

```css /* System font stack as fallback */ body { font-family: 'Custom Font', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }

/* Prevent invisible text */ @font-face { font-family: 'Custom Font'; src: url('/fonts/custom.woff2') format('woff2'); font-display: swap; /* or optional for non-critical */ font-weight: 400; font-style: normal; unicode-range: U+0000-00FF; /* Subset to Latin */ } ```

Preloading critical fonts

html <link rel="preload" href="/fonts/heading.woff2" as="font" type="font/woff2" crossorigin>

Variable fonts

css /* One file instead of multiple weights */ @font-face { font-family: 'Inter'; src: url('/fonts/Inter-Variable.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

Caching strategy

Cache-Control headers

``` # HTML (short or no cache) Cache-Control: no-cache, must-revalidate

Static assets with hash (immutable)

Cache-Control: public, max-age=31536000, immutable

Static assets without hash

Cache-Control: public, max-age=86400, stale-while-revalidate=604800

API responses

Cache-Control: private, max-age=0, must-revalidate ```

Service worker caching

javascript // Cache-first for static assets self.addEventListener('fetch', (event) => { if (event.request.destination === 'image' || event.request.destination === 'style' || event.request.destination === 'script') { event.respondWith( caches.match(event.request).then((cached) => { return cached || fetch(event.request).then((response) => { const clone = response.clone(); caches.open('static-v1').then((cache) => cache.put(event.request, clone)); return response; }); }) ); } });

Runtime performance

Avoid layout thrashing

```javascript // ❌ Forces multiple reflows elements.forEach(el => { const height = el.offsetHeight; // Read el.style.height = height + 10 + 'px'; // Write });

// ✅ Batch reads, then batch writes const heights = elements.map(el => el.offsetHeight); // All reads elements.forEach((el, i) => { el.style.height = heights[i] + 10 + 'px'; // All writes }); ```

Debounce expensive operations

```javascript function debounce(fn, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => fn(...args), delay); }; }

// Debounce scroll/resize handlers window.addEventListener('scroll', debounce(handleScroll, 100)); ```

Use requestAnimationFrame

```javascript // ❌ May cause jank setInterval(animate, 16);

// ✅ Synced with display refresh function animate() { // Animation logic requestAnimationFrame(animate); } requestAnimationFrame(animate); ```

Virtualize long lists

javascript // For lists > 100 items, render only visible items // Use libraries like react-window, vue-virtual-scroller, or native CSS: .virtual-list { content-visibility: auto; contain-intrinsic-size: 0 50px; /* Estimated item height */ }

Smooth navigations with View Transitions

The View Transitions API lets the browser cross-fade (or custom-animate) between two DOM states using a single GPU-composited snapshot — no double-render, no layout thrash, and the snapshot doesn't count toward CLS.

Same-document (SPA-style) — Baseline 2026: ``javascript // Wrap the DOM mutation that swaps the view function navigate(newView) { if (!document.startViewTransition) return swapDOM(newView); document.startViewTransition(() => swapDOM(newView)); }

Cross-document (MPA-style) — Chromium-stable, progressive enhancement elsewhere: ``css /* On both source and destination pages */ @view-transition { navigation: auto; } ` That's the entire integration — same-origin navigations now fade automatically. To opt specific elements into shared-element transitions (e.g. a thumbnail expanding into a hero), give them a matching view-transition-name: `css .product-thumb[data-id="42"], .product-hero { view-transition-name: product-42; }

Pair this with Speculation Rules (above) for instant + animated navigations.

Third-party scripts

Load strategies

```javascript // ❌ Blocks main thread <script src="https://analytics.example.com/script.js"></script>

// ✅ Async loading <script async src="https://analytics.example.com/script.js"></script>

// ✅ Delay until interaction <script> document.addEventListener('DOMContentLoaded', () => { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { const script = document.createElement('script'); script.src = 'https://widget.example.com/embed.js'; document.body.appendChild(script); observer.disconnect(); } }); observer.observe(document.querySelector('#widget-container')); }); </script> ```

Facade pattern

html <!-- Show static placeholder until interaction --> <div class="youtube-facade" data-video-id="abc123" onclick="loadYouTube(this)"> <img src="/thumbnails/abc123.jpg" alt="Video title"> <button aria-label="Play video">▶</button> </div>

Measurement

Use the measurement workflow whenever a URL is runnable. It defines Chrome DevTools MCP routing, CrUX and fallback sources, repeatable lab conditions, and a compact evidence format.

MetricKindInterpretation
LCP, INP, CLS at p75FieldUser-outcome Core Web Vitals; use for pass/fail prioritization
LCP, CLS in a traceLabReproducible diagnostic values for one navigation
TBTLabMain-thread blocking diagnostic and a rough INP proxy, not field INP
FCP, Speed IndexLabLoading diagnostics, not Core Web Vitals

Raw PerformanceObserver snippets are useful for the current browser session but are not real-user data by themselves. When the user wants production telemetry, read the first-party RUM reference and prefer web-vitals over a hand-rolled metric implementation.

References

For Core Web Vitals specific optimizations, see Core Web Vitals.


Subgroup 4: Accessibility Standards & WCAG 2.2

--- name: accessibility description: Audit and improve web accessibility following WCAG 2.2 guidelines. Use when asked to "improve accessibility", "a11y audit", "WCAG compliance", "screen reader support", "keyboard navigation", or "make accessible". license: MIT metadata: author: web-quality-skills version: "2.0" ---

Accessibility (a11y)

Comprehensive accessibility guidelines based on WCAG 2.2 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities.

Evidence-led audit workflow

When a rendered page is available:

  1. Run a live Lighthouse Accessibility audit when that capability is available; with Chrome DevTools MCP, use lighthouse_audit. Use mobile navigation mode for a general public page or snapshot mode when reloading would lose authenticated or user-created state.
  2. Use failed audit nodes to localize the relevant component or template instead of searching the whole repository for generic patterns.
  3. Inspect a rendered accessibility-tree snapshot for names, roles, states, landmarks, and heading structure; with Chrome DevTools MCP, use take_snapshot. Exercise the affected flow with the keyboard.
  4. Fix the source, then re-run the same audit and manual interaction.

If the live tools are unavailable, use Lighthouse CLI or axe for automated coverage and complete the same manual checks. Automated tools detect only a subset of accessibility barriers: a score of 100 is not WCAG conformance, and a low score does not replace issue-level evidence.

WCAG Principles: POUR

PrincipleDescription
PerceivableContent can be perceived through different senses
OperableInterface can be operated by all users
UnderstandableContent and interface are understandable
RobustContent works with assistive technologies

Conformance levels

LevelRequirementTarget
AMinimum accessibilityMust pass
AAStandard complianceShould pass (legal requirement in many jurisdictions)
AAAEnhanced accessibilityNice to have

Perceivable

Text alternatives (1.1)

Images require alt text: ```html <!-- ❌ Missing alt --> <img src="chart.png">

<!-- ✅ Descriptive alt --> <img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales">

<!-- ✅ Decorative image (empty alt) --> <img src="decorative-border.png" alt="" role="presentation">

<!-- ✅ Complex image with longer description --> <figure> <img src="infographic.png" alt="2024 market trends infographic" aria-describedby="infographic-desc"> <figcaption id="infographic-desc"> <!-- Detailed description --> </figcaption> </figure> ```

Icon buttons need accessible names: ```html <!-- ❌ No accessible name --> <button><svg><!-- menu icon --></svg></button>

<!-- ✅ Using aria-label --> <button aria-label="Open menu"> <svg aria-hidden="true"><!-- menu icon --></svg> </button>

<!-- ✅ Using visually hidden text --> <button> <svg aria-hidden="true"><!-- menu icon --></svg> <span class="visually-hidden">Open menu</span> </button> ```

Visually hidden class: ``css .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }

Color contrast (1.4.3, 1.4.6)

Text SizeAA minimumAAA enhanced
Normal text (< 18px / < 14px bold)4.5:17:1
Large text (≥ 18px / ≥ 14px bold)3:14.5:1
UI components & graphics3:13:1
css
/* ❌ Low contrast (2.5:1) */
.low-contrast {
  color: #999;
  background: #fff;
}

/* ✅ Sufficient contrast (7:1) */ .high-contrast { color: #333; background: #fff; }

/* ✅ Focus states need contrast too (3:1 against background, WCAG 1.4.11) */ :focus-visible { outline: 2px solid currentColor; outline-offset: 2px; } ```

Don't rely on color alone: ```html <!-- ❌ Only color indicates error --> <input class="error-border"> <style>.error-border { border-color: red; }</style>

<!-- ✅ Color + icon + text --> <div class="field-error"> <input aria-invalid="true" aria-describedby="email-error"> <span id="email-error" class="error-message"> <svg aria-hidden="true"><!-- error icon --></svg> Please enter a valid email address </span> </div> ```

Media alternatives (1.2)

html
<!-- Video with captions -->
<video controls>
  <source src="video.mp4" type="video/mp4">
  <track kind="captions" src="captions.vtt" srclang="en" label="English" default>
  <track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descriptions">
</video>

<!-- Audio with transcript --> <audio controls> <source src="podcast.mp3" type="audio/mp3"> </audio> <details> <summary>Transcript</summary> <p>Full transcript text...</p> </details> ```


Operable

Keyboard accessible (2.1)

All functionality must be keyboard accessible. Prefer native interactive elements — <button>, <a href>, and form controls handle Enter/Space activation, focus, and assistive-tech semantics for free. Only add manual keyboard handling when you cannot use a native element.

html
<!-- ❌ Non-interactive element with click only: not focusable, no keyboard activation -->
<div class="card" onclick="handleAction()">Open</div>

<!-- ✅ Best: use a native button --> <button type="button" onclick="handleAction()">Open</button> ```

javascript
// ✅ When you MUST use a non-interactive element (e.g. div with role="button"),
// make it focusable AND handle keyboard activation. Do NOT add this to a native
// <button> — Enter/Space already fire click, so you'd double-trigger.
element.setAttribute('role', 'button');
element.setAttribute('tabindex', '0');
element.addEventListener('click', handleAction);
element.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    handleAction();
  }
});

No keyboard traps. Users must be able to Tab into and out of every component. Use the modal focus trap pattern for dialogs—the native <dialog> element handles this automatically.

Focus visible (2.4.7)

css
/* ❌ Never remove focus outlines */
*:focus { outline: none; }

/* ✅ Use :focus-visible for keyboard-only focus */ :focus { outline: none; }

:focus-visible { outline: 2px solid currentColor; /* inherits text color → already contrast-checked */ outline-offset: 2px; }

/* ✅ Or pick a brand color and verify ≥3:1 contrast against every background it lands on */ button:focus-visible { box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5); } ```

Focus not obscured (2.4.11) — new in 2.2

When an element receives keyboard focus, it must not be entirely hidden by other author-created content such as sticky headers, footers, or overlapping panels. At Level AAA (2.4.12), no part of the focused element may be hidden.

css
/* ✅ Account for sticky headers when scrolling to focused elements */
:target {
  scroll-margin-top: 80px;
}

/* ✅ Ensure focused items clear fixed/sticky bars */ :focus { scroll-margin-top: 80px; scroll-margin-bottom: 60px; } ```

Skip links (2.4.1)

Provide a skip link so keyboard users can bypass repetitive navigation. See the skip link pattern for full markup and styles.

Target size (2.5.8) — new in 2.2

Interactive targets must be at least 24 × 24 CSS pixels (AA). Exceptions: inline text links, elements where the browser controls the size, and targets where a 24px circle centered on the bounding box does not overlap another target.

css
/* ✅ Minimum target size */
button,
[role="button"],
input[type="checkbox"] + label,
input[type="radio"] + label {
  min-width: 24px;
  min-height: 24px;
}

/* ✅ Comfortable target size (recommended 44×44) */ .touch-target { min-width: 44px; min-height: 44px; display: inline-flex; align-items: center; justify-content: center; } ```

Dragging movements (2.5.7) — new in 2.2

Any action that requires dragging must have a single-pointer alternative (e.g., buttons, inputs). See the dragging movements pattern for a sortable-list example.

Timing (2.2)

javascript
// Allow users to extend time limits
function showSessionWarning() {
  const modal = createModal({
    title: 'Session Expiring',
    content: 'Your session will expire in 2 minutes.',
    actions: [
      { label: 'Extend session', action: extendSession },
      { label: 'Log out', action: logout }
    ],
    timeout: 120000
  });
}

Motion (2.3)

css
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Understandable

Page language (3.1.1)

html
<!-- ❌ No language specified -->
<html>

<!-- ✅ Language specified --> <html lang="en">

<!-- ✅ Language changes within page --> <p>The French word for hello is <span lang="fr">bonjour</span>.</p> ```

Consistent navigation (3.2.3)

html
<!-- Navigation should be consistent across pages -->
<nav aria-label="Main">
  <ul>
    <li><a href="/" aria-current="page">Home</a></li>
    <li><a href="/products">Products</a></li>
    <li><a href="/about">About</a></li>
  </ul>
</nav>

Consistent help (3.2.6) — new in 2.2

If a help mechanism (contact info, chat widget, FAQ link, self-help option) is repeated across multiple pages, it must appear in the same relative order each time. Users who rely on consistent placement shouldn't have to hunt for help on every page.

Form labels (3.3.2)

Every input needs a programmatically associated label. See the form labels pattern for explicit, implicit, and instructional examples.

Error handling (3.3.1, 3.3.3)

Announce errors to screen readers with role="alert" or aria-live, set aria-invalid="true" on invalid fields, and focus the first error on submit. See the error handling pattern for full markup and JS.

Redundant entry (3.3.7) — new in 2.2

Don't force users to re-enter information they already provided in the same session. Auto-populate from earlier steps, or let users select from previously entered values. Exceptions: security re-confirmation and content that has expired.

html
<!-- ✅ Auto-fill shipping address from billing -->
<fieldset>
  <legend>Shipping address</legend>
  <label>
    <input type="checkbox" id="same-as-billing" checked>
    Same as billing address
  </label>
  <!-- Fields auto-populated when checked -->
</fieldset>

Accessible authentication (3.3.8) — new in 2.2

  • Login flows must not rely on cognitive function tests (e.g., remembering a password, solving a puzzle) unless at least one of:
  • A copy-paste or autofill mechanism is available
  • An alternative method exists (e.g., passkey, SSO, email link)
  • The test uses object recognition or personal content (AA only; AAA removes this exception)
html
<!-- ✅ Allow paste in password fields -->
<input type="password" id="password" autocomplete="current-password">

<!-- ✅ Offer passwordless alternatives --> <button type="button">Sign in with passkey</button> <button type="button">Email me a login link</button> ```


Robust

ARIA usage (4.1.2)

Prefer native elements: ```html <!-- ❌ ARIA role on div --> <div role="button" tabindex="0">Click me</div>

<!-- ✅ Native button --> <button>Click me</button>

<!-- ❌ ARIA checkbox --> <div role="checkbox" aria-checked="false">Option</div>

<!-- ✅ Native checkbox --> <label><input type="checkbox"> Option</label> ```

When ARIA is needed, use the correct roles and states. See the ARIA tabs pattern for a complete tablist example.

Live regions (4.1.3)

Use aria-live regions to announce dynamic content changes without moving focus. See the live regions pattern for markup and a showNotification() helper.


Testing checklist

Automated testing

Prefer a live Lighthouse audit that returns failing rendered nodes directly to the agent. With Chrome DevTools MCP, this is lighthouse_audit. Otherwise:

bash
# Lighthouse accessibility audit
npx lighthouse https://example.com --only-categories=accessibility

axe-core

npm install @axe-core/cli -g axe https://example.com ```

Manual testing

  • Keyboard navigation: Tab through entire page, use Enter/Space to activate
  • Screen reader: Test with VoiceOver (Mac), NVDA (Windows), or TalkBack (Android)
  • Zoom: Content usable at 200% zoom
  • High contrast: Test with Windows High Contrast Mode
  • Reduced motion: Test with prefers-reduced-motion: reduce
  • Focus order: Logical and follows visual order
  • Target size: Interactive elements meet 24×24px minimum

See the screen reader commands reference for VoiceOver and NVDA shortcuts.


Common issues by impact

Critical (fix immediately)

1. Missing form labels 2. Missing image alt text 3. Insufficient color contrast 4. Keyboard traps 5. No focus indicators

Serious (fix before launch)

1. Missing page language 2. Missing heading structure 3. Non-descriptive link text 4. Auto-playing media 5. Missing skip links

Moderate (fix soon)

1. Missing ARIA labels on icons 2. Inconsistent navigation 3. Missing error identification 4. Timing without controls 5. Missing landmark regions

References


Subgroup 5: Technical SEO & Structured Data

--- name: seo description: Optimize for search engine visibility and ranking. Use when asked to "improve SEO", "optimize for search", "fix meta tags", "add structured data", "sitemap optimization", or "search engine optimization". license: MIT metadata: author: web-quality-skills version: "2.0" ---

SEO optimization

Search engine optimization based on Lighthouse SEO audits and Google Search guidelines. Focus on technical SEO, on-page optimization, and structured data.

Evidence-led audit workflow

When a rendered page is available:

  1. Run live Lighthouse SEO and Agentic Browsing checks when that capability is available; with Chrome DevTools MCP, use lighthouse_audit. Use the results to localize rendered-page failures.
  2. Inspect signals Lighthouse cannot establish on its own: response headers, redirects, robots.txt, sitemap coverage, canonical consistency across page templates, structured-data eligibility, and Search Console evidence when the user provides access.
  3. Separate technical crawl/index findings from content quality and authority. Do not invent ranking-factor weights or promise ranking changes.
  4. Fix the source and re-run the same checks. For indexation or ranking outcomes, report that search-engine validation remains pending.

If live tools are unavailable, use category-specific Lighthouse CLI output plus direct source and HTTP inspection. A Lighthouse SEO score covers a useful subset of technical checks; it is not a prediction of rankings.

AreaWhat this skill can verify
Crawl and index controlsTechnical configuration and consistency
Rendered metadata and semanticsPresence, validity, and page-template issues
Structured dataSyntax and eligibility signals, not guaranteed rich results
Core Web VitalsLink to measured field/lab evidence from the Core Web Vitals skill
Content usefulness and authorityReview quality, but do not assign synthetic ranking percentages

Technical SEO

Crawlability

robots.txt: ```text # /robots.txt User-agent: * Allow: /

Block admin/private areas

Disallow: /admin/ Disallow: /api/ Disallow: /private/

Don't block resources needed for rendering

# ❌ Disallow: /static/

Sitemap: https://example.com/sitemap.xml ```

Meta robots: ```html <!-- Default: indexable, followable --> <meta name="robots" content="index, follow">

<!-- Noindex specific pages --> <meta name="robots" content="noindex, nofollow">

<!-- Indexable but don't follow links --> <meta name="robots" content="index, nofollow">

<!-- Control snippets --> <meta name="robots" content="max-snippet:150, max-image-preview:large"> ```

Canonical URLs: ```html <!-- Prevent duplicate content issues --> <link rel="canonical" href="https://example.com/page">

<!-- Self-referencing canonical (recommended) --> <link rel="canonical" href="https://example.com/current-page">

<!-- For paginated content --> <link rel="canonical" href="https://example.com/products"> <!-- Or use rel="prev" / rel="next" for explicit pagination --> ```

XML sitemap

xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/</loc>
    <lastmod>2024-01-15</lastmod>
    <changefreq>daily</changefreq>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://example.com/products</loc>
    <lastmod>2024-01-14</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>
  • *Sitemap best practices:**
  • Maximum 50,000 URLs or 50MB per sitemap
  • Use sitemap index for larger sites
  • Include only canonical, indexable URLs
  • Update lastmod when content changes
  • Submit to Google Search Console

URL structure

✅ Good URLs:
https://example.com/products/blue-widget
https://example.com/blog/how-to-use-widgets

❌ Poor URLs: https://example.com/p?id=12345 https://example.com/products/item/category/subcategory/blue-widget-2024-sale-discount ```

  • *URL guidelines:**
  • Use hyphens, not underscores
  • Lowercase only
  • Keep short (< 75 characters)
  • Include target keywords naturally
  • Avoid parameters when possible
  • Use HTTPS always

HTTPS & security

html
<!-- Ensure all resources use HTTPS -->
<img src="https://example.com/image.jpg">

<!-- Not: --> <img src="http://example.com/image.jpg"> ```

Security headers for SEO trust signals: `` Strict-Transport-Security: max-age=31536000; includeSubDomains X-Content-Type-Options: nosniff X-Frame-Options: DENY


On-page SEO

Title tags

html
<!-- ❌ Missing or generic -->
<title>Page</title>
<title>Home</title>

<!-- ✅ Descriptive with primary keyword --> <title>Blue Widgets for Sale | Premium Quality | Example Store</title> ```

  • *Title tag guidelines:**
  • Use 50–60 characters only as a rough linting proxy, not a pass/fail limit. Google truncates title links to fit the rendered device width, so preview width when tooling supports it.
  • Describe the page topic naturally near the beginning
  • Unique for every page
  • Add the brand when it helps users distinguish the result
  • Action-oriented when appropriate

Treat title-link rewriting separately from truncation. Google may build a different title link from the visible page title, headings, anchor text, and other sources even when the <title> is short; investigate accuracy and consistency rather than shortening it automatically. See Google's title-link guidance.

Meta descriptions

html
<!-- ❌ Missing or duplicate -->
<meta name="description" content="">

<!-- ✅ Compelling and unique --> <meta name="description" content="Shop premium blue widgets with free shipping. 30-day returns. Rated 4.9/5 by 10,000+ customers. Order today and save 20%."> ```

  • *Meta description guidelines:**
  • Use roughly 150–160 characters only as a linting proxy. Snippets are query- and device-dependent, and Google may select page content instead of the meta description.
  • Use the page topic naturally
  • Compelling call-to-action
  • Unique for every page
  • Matches page content

Heading structure

html
<!-- ❌ Poor structure -->
<h2>Welcome to Our Store</h2>
<h4>Products</h4>
<h1>Contact Us</h1>

<!-- ✅ Proper hierarchy --> <h1>Blue Widgets - Premium Quality</h1> <h2>Product Features</h2> <h3>Durability</h3> <h3>Design</h3> <h2>Customer Reviews</h2> <h2>Pricing</h2> ```

  • *Heading guidelines:**
  • Make the primary page heading descriptive and the hierarchy unambiguous; do not fail a page solely because valid HTML contains more than one <h1>
  • Logical hierarchy (don't skip levels)
  • Include keywords naturally
  • Descriptive, not generic

Image SEO

html
<!-- ❌ Poor image SEO -->
<img src="IMG_12345.jpg">

<!-- ✅ Optimized image --> <img src="blue-widget-product-photo.webp" alt="Blue widget with chrome finish, side view showing control panel" width="800" height="600" loading="lazy"> ```

  • *Image guidelines:**
  • Descriptive filenames with keywords
  • Alt text describes the image content
  • Compressed and properly sized
  • WebP/AVIF with fallbacks
  • Lazy load below-fold images

Internal linking

html
<!-- ❌ Non-descriptive -->
<a href="/products">Click here</a>
<a href="/widgets">Read more</a>

<!-- ✅ Descriptive anchor text --> <a href="/products/blue-widgets">Browse our blue widget collection</a> <a href="/guides/widget-maintenance">Learn how to maintain your widgets</a> ```

  • *Linking guidelines:**
  • Descriptive anchor text with keywords
  • Link to relevant internal pages
  • Reasonable number of links per page
  • Fix broken links promptly
  • Use breadcrumbs for hierarchy

Structured data (JSON-LD)

Read the structured data reference when the user requests schema markup or an audit surfaces a structured-data issue. It contains Organization, Article, Product, FAQ, and Breadcrumb examples plus validation links.

  • Describe visible, accurate content. Do not add a type or claim solely to obtain a rich result.
  • Use the most specific applicable type. Keep identifiers and absolute URLs stable across renders.
  • Validate rendered output. Passing syntax does not guarantee search-engine eligibility or display.

Agentic browsing and AI discoverability

Keep these concepts separate:

  • Lighthouse Agentic Browsing measures technical signals that help an assistant understand and interact with the rendered page. Current checks include the agent-facing accessibility tree, optional llms.txt, and WebMCP registrations, schemas, and form coverage when present.
  • Search indexing and ranking depend on search-engine systems and cannot be inferred from the Agentic Browsing score.
  • AI ingestion or citation is product-specific. A technically browsable page or valid llms.txt file does not prove that an AI product will ingest, rank, or cite it.

Prioritize semantic HTML, descriptive labels, crawlable content, accurate metadata, and clear page structure because they benefit people, search engines, and agents. Add WebMCP tools only when the application has useful actions to expose and the user wants that integration; validate tool names, descriptions, schemas, and form annotations with Lighthouse.

Crawler controls are product-specific

Audit each documented user agent separately instead of applying a blanket "AI bot" rule:

ControlDocumented purposeEffect of blocking
OAI-SearchBotChatGPT search discoveryPrevents page content from being included in ChatGPT summaries and snippets; a link and title may still surface through third-party discovery
PerplexityBotPerplexity search indexingPrevents that crawler from indexing the blocked content for search results
Claude-SearchBot / Claude-UserClaude search indexing / user-directed retrievalMay reduce search visibility / prevents retrieval for user-directed requests
Google-ExtendedControls certain Gemini training and grounding uses of content Google crawlsDoes not affect Google Search inclusion or ranking

Training controls such as GPTBot and ClaudeBot are distinct from search and user-fetch controls. GoogleOther is a generic crawler, not an AI-search visibility switch. Verify current names and consequences in the vendors' maintained documentation: OpenAI, Perplexity, Anthropic, and Google.

`llms.txt` is optional

llms.txt is an experimental proposal, not a cross-vendor discovery standard. Lighthouse can validate the availability and shape of /llms.txt, but that does not show that a target product reads it. Add one only when the user requests it or a documented consumer supports it; do not recommend it ahead of crawlability, semantic HTML, accurate metadata, and useful content. Never treat it as a ranking or citation factor, duplicate the sitemap, or reorganize content solely to raise this audit.


Mobile SEO

Responsive design

html
<!-- ❌ Not mobile-friendly -->
<meta name="viewport" content="width=1024">

<!-- ✅ Responsive viewport --> <meta name="viewport" content="width=device-width, initial-scale=1"> ```

Tap targets

css
/* ❌ Too small for mobile */
.small-link {
  padding: 4px;
  font-size: 12px;
}

/* ✅ Adequate tap target */ .mobile-friendly-link { padding: 12px; font-size: 16px; min-height: 48px; min-width: 48px; } ```

Font sizes

css
/* ❌ Too small on mobile */
body {
  font-size: 10px;
}

/* ✅ Readable without zooming */ body { font-size: 16px; line-height: 1.5; } ```


International SEO

Hreflang tags

html
<!-- For multi-language sites -->
<link rel="alternate" hreflang="en" href="https://example.com/page">
<link rel="alternate" hreflang="es" href="https://example.com/es/page">
<link rel="alternate" hreflang="fr" href="https://example.com/fr/page">
<link rel="alternate" hreflang="x-default" href="https://example.com/page">

Language declaration

html
<html lang="en">
<!-- or -->
<html lang="es-MX">

SEO audit checklist

Critical

- [ ] HTTPS enabled - [ ] robots.txt allows crawling - [ ] No noindex on important pages - [ ] Title tags present and unique - [ ] Primary page heading is descriptive and the hierarchy is logical

High priority

- [ ] Meta descriptions present - [ ] Sitemap submitted - [ ] Canonical URLs set - [ ] Mobile-responsive - [ ] Core Web Vitals passing

Medium priority

- [ ] Structured data implemented - [ ] Internal linking strategy - [ ] Image alt text - [ ] Descriptive URLs - [ ] Breadcrumb navigation - [ ] Agentic Browsing failures reviewed when agent access matters

Ongoing

- [ ] Fix crawl errors in Search Console - [ ] Update sitemap when content changes - [ ] Monitor ranking changes - [ ] Check for broken links - [ ] Review Search Console insights


Tools

ToolUse
Google Search ConsoleMonitor indexing, fix issues
Google PageSpeed InsightsPerformance + Core Web Vitals
Rich Results TestValidate structured data
Live Lighthouse audit (Chrome DevTools MCP: lighthouse_audit)Rendered SEO and Agentic Browsing checks for agents
Lighthouse CLISEO audit fallback
Screaming FrogCrawl analysis

References


Subgroup 6: Security & Web Best Practices

--- name: best-practices description: Apply modern web development best practices for security, compatibility, and code quality. Use when asked to "apply best practices", "security audit", "modernize code", "code quality review", or "check for vulnerabilities". license: MIT metadata: author: web-quality-skills version: "2.0" ---

Best practices

Modern web development standards based on Lighthouse best practices audits. Covers security, browser compatibility, and code quality patterns.

Evidence-led audit workflow

When a rendered page is available:

  1. Run a live Lighthouse Best Practices audit when that capability is available; with Chrome DevTools MCP, use lighthouse_audit. Use navigation mode for a normal page load or snapshot mode when the current state must be preserved.
  2. Inspect the listed console and network failures and fetch individual details only when they support a finding.
  3. Supplement runtime evidence with dependency, header, configuration, and source inspection; Lighthouse is not a complete security assessment.
  4. Fix the implicated code, re-run the same audit, and keep security findings separate from style preferences.

If live tools are unavailable, use the Lighthouse CLI plus focused dependency and header checks. Never report a high Lighthouse score as proof that the application is secure.

Security

Read the security reference when security is in scope or a live audit surfaces a related failure. It covers HTTPS/HSTS, CSP and Trusted Types, Subresource Integrity, headers, dependencies, sanitization, and cookies.

At minimum:

  • Use HTTPS without mixed content. Add HSTS only after confirming every relevant subdomain supports HTTPS.
  • Treat a strict CSP as defense in depth. Prefer nonces or hashes and test with report-only before enforcement.
  • Sanitize untrusted HTML and protect DOM XSS sinks. Prefer text APIs when markup is not required.
  • Pin and review third-party code. Use SRI where the delivery model supports it and keep dependencies patched.
  • Verify response headers at runtime. Source configuration alone does not prove what the deployed page sends.

Browser compatibility

Doctype declaration

html
<!-- ❌ Missing or invalid doctype -->
<HTML>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">

<!-- ✅ HTML5 doctype --> <!DOCTYPE html> <html lang="en"> ```

Character encoding

html
<!-- ❌ Missing or late charset -->
<html>
<head>
  <title>Page</title>
  <meta charset="UTF-8">
</head>

<!-- ✅ Charset as first element in head --> <html> <head> <meta charset="UTF-8"> <title>Page</title> </head> ```

Viewport meta tag

html
<!-- ❌ Missing viewport -->
<head>
  <title>Page</title>
</head>

<!-- ✅ Responsive viewport --> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Page</title> </head> ```

Feature detection

javascript
// ❌ Browser detection (brittle)
if (navigator.userAgent.includes('Chrome')) {
  // Chrome-specific code
}

// ✅ Feature detection if ('IntersectionObserver' in window) { // Use IntersectionObserver } else { // Fallback }

// ✅ Using @supports in CSS @supports (display: grid) { .container { display: grid; } }

@supports not (display: grid) { .container { display: flex; } } ```

Polyfills (when needed)

Prefer bundling polyfills at build time (Babel/SWC + core-js, or @vitejs/plugin-legacy) targeted by your supported-browsers list. This eliminates the runtime check entirely and avoids shipping polyfill bytes to modern browsers.

If you must load a polyfill at runtime, append a script element — never use document.write (it blocks the parser and is broken in async/deferred contexts):

html
<script>
  if (!('fetch' in window)) {
    const s = document.createElement('script');
    s.src = '/polyfills/fetch.js';
    s.defer = true;
    document.head.appendChild(s);
  }
</script>

Never load polyfills from a third-party CDN you don't control. The polyfill.io service was compromised in mid-2024 in a supply-chain attack and used to serve malware to ~100k sites. Self-host, or use a vetted mirror (e.g. Cloudflare's `cdnjs` polyfill build) — and pin the version with Subresource Integrity.


Deprecated APIs

Avoid these

javascript
// ❌ document.write (blocks parsing)
document.write('<script src="..."></script>');

// ✅ Dynamic script loading const script = document.createElement('script'); script.src = '...'; document.head.appendChild(script);

// ❌ Synchronous XHR (blocks main thread) const xhr = new XMLHttpRequest(); xhr.open('GET', url, false); // false = synchronous

// ✅ Async fetch const response = await fetch(url);

// ❌ Application Cache (deprecated) <html manifest="cache.manifest">

// ✅ Service Workers if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js'); } ```

Event listener passive

javascript
// ❌ Non-passive touch/wheel (may block scrolling)
element.addEventListener('touchstart', handler);
element.addEventListener('wheel', handler);

// ✅ Passive listeners (allows smooth scrolling) element.addEventListener('touchstart', handler, { passive: true }); element.addEventListener('wheel', handler, { passive: true });

// ✅ If you need preventDefault, be explicit element.addEventListener('touchstart', handler, { passive: false }); ```


Console & errors

No console errors

javascript
// ❌ Errors in production
console.log('Debug info'); // Remove in production
throw new Error('Unhandled'); // Catch all errors

// ✅ Proper error handling try { riskyOperation(); } catch (error) { // Log to error tracking service errorTracker.captureException(error); // Show user-friendly message showErrorMessage('Something went wrong. Please try again.'); } ```

Error boundaries (React)

jsx
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, info) {
    errorTracker.captureException(error, { extra: info });
  }
  
  render() {
    if (this.state.hasError) {
      return <FallbackUI />;
    }
    return this.props.children;
  }
}

// Usage <ErrorBoundary> <App /> </ErrorBoundary> ```

Global error handler

javascript
// Catch unhandled errors
window.addEventListener('error', (event) => {
  errorTracker.captureException(event.error);
});

// Catch unhandled promise rejections window.addEventListener('unhandledrejection', (event) => { errorTracker.captureException(event.reason); }); ```


Source maps

Production configuration

javascript
// ❌ Source maps exposed in production
// webpack.config.js
module.exports = {
  devtool: 'source-map', // Exposes source code
};

// ✅ Hidden source maps (uploaded to error tracker) module.exports = { devtool: 'hidden-source-map', };

// ✅ Or no source maps in production module.exports = { devtool: process.env.NODE_ENV === 'production' ? false : 'source-map', }; ```

Strip `sourcesContent` from production maps when uploading to your error tracker. By default, bundlers embed the full original source inside the .map file — anyone who obtains the map (including via a misconfigured upload step) gets your unminified code. Configure your bundler to omit sourcesContent, or use a Sentry/Bugsnag CLI flag that does so when uploading.

For Vite, prefer sourcemap: 'hidden' over 'true' so the //# sourceMappingURL= comment isn't emitted into the bundle.


Performance best practices

Avoid blocking patterns

javascript
// ❌ Blocking script
<script src="heavy-library.js"></script>

// ✅ Deferred script <script defer src="heavy-library.js"></script>

// ❌ Blocking CSS import @import url('other-styles.css');

// ✅ Link tags (parallel loading) <link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="other-styles.css"> ```

Efficient event handlers

javascript
// ❌ Handler on every element
items.forEach(item => {
  item.addEventListener('click', handleClick);
});

// ✅ Event delegation container.addEventListener('click', (e) => { if (e.target.matches('.item')) { handleClick(e); } }); ```

Memory management

javascript
// ❌ Memory leak (never removed)
const handler = () => { /* ... */ };
window.addEventListener('resize', handler);

// ✅ Cleanup when done const handler = () => { /* ... */ }; window.addEventListener('resize', handler);

// Later, when component unmounts: window.removeEventListener('resize', handler);

// ✅ Using AbortController const controller = new AbortController(); window.addEventListener('resize', handler, { signal: controller.signal });

// Cleanup: controller.abort(); ```


Code quality

Valid HTML

html
<!-- ❌ Invalid HTML -->
<div id="header">
<div id="header"> <!-- Duplicate ID -->

<ul> <div>Item</div> <!-- Invalid child --> </ul>

<a href="/"><button>Click</button></a> <!-- Invalid nesting -->

<!-- ✅ Valid HTML --> <header id="site-header"> </header>

<ul> <li>Item</li> </ul>

<a href="/" class="button">Click</a> ```

Semantic HTML

html
<!-- ❌ Non-semantic -->
<div class="header">
  <div class="nav">
    <div class="nav-item">Home</div>
  </div>
</div>
<div class="main">
  <div class="article">
    <div class="title">Headline</div>
  </div>
</div>

<!-- ✅ Semantic HTML5 --> <header> <nav> <a href="/">Home</a> </nav> </header> <main> <article> <h1>Headline</h1> </article> </main> ```

Image aspect ratios

html
<!-- ❌ Distorted images -->
<img src="photo.jpg" width="300" height="100">
<!-- If actual ratio is 4:3, this squishes the image -->

<!-- ✅ Preserve aspect ratio --> <img src="photo.jpg" width="300" height="225"> <!-- Actual 4:3 dimensions -->

<!-- ✅ CSS object-fit for flexibility --> <img src="photo.jpg" style="width: 300px; height: 200px; object-fit: cover;"> ```


Permissions & privacy

Request permissions properly

javascript
// ❌ Request on page load (bad UX, often denied)
navigator.geolocation.getCurrentPosition(success, error);

// ✅ Request in context, after user action findNearbyButton.addEventListener('click', async () => { // Explain why you need it if (await showPermissionExplanation()) { navigator.geolocation.getCurrentPosition(success, error); } }); ```

Permissions policy

html
<!-- Restrict powerful features -->
<meta http-equiv="Permissions-Policy" 
      content="geolocation=(), camera=(), microphone=()">

<!-- Or allow for specific origins --> <meta http-equiv="Permissions-Policy" content="geolocation=(self 'https://maps.example.com')"> ```


Audit checklist

Security (critical)

- [ ] HTTPS enabled, no mixed content - [ ] No vulnerable dependencies (npm audit) - [ ] CSP headers configured (with frame-ancestors, base-uri, form-action) - [ ] require-trusted-types-for 'script' enforced (or report-only during rollout) - [ ] Third-party <script>/<link rel="stylesheet"> pinned with SRI hashes - [ ] Security headers present (HSTS, X-Content-Type-Options, Referrer-Policy) - [ ] No exposed source maps (and sourcesContent stripped from uploaded ones)

Compatibility

- [ ] Valid HTML5 doctype - [ ] Charset declared first in head - [ ] Viewport meta tag present - [ ] No deprecated APIs used - [ ] Passive event listeners for scroll/touch

Code quality

- [ ] No console errors - [ ] Valid HTML (no duplicate IDs) - [ ] Semantic HTML elements used - [ ] Proper error handling - [ ] Memory cleanup in components

UX

- [ ] No intrusive interstitials - [ ] Permission requests in context - [ ] Clear error messages - [ ] Appropriate image aspect ratios

Tools

ToolPurpose
npm auditDependency vulnerabilities
SecurityHeaders.comHeader analysis
W3C ValidatorHTML validation
Live Lighthouse audit (Chrome DevTools MCP: lighthouse_audit)Rendered Best Practices checks for agents
Lighthouse CLIBest Practices audit fallback
ObservatorySecurity scan

References