--- 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
- Establish the audit target: representative URLs, important states and journeys, public versus authenticated access, and mobile/desktop scope.
- If a page can run, read the measurement workflow and collect a minimal live baseline before searching the codebase broadly.
- Use runtime failures to localize source inspection. Keep measured findings separate from hypotheses found only in code.
- Categorize by user impact and confidence, then make or recommend specific fixes.
- 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.
| Need | Preferred route | Fallback |
|---|---|---|
| Performance and Core Web Vitals | Record a browser performance trace and analyze focused insights; with Chrome DevTools MCP, use performance_start_trace then performance_analyze_insight | Lighthouse CLI or PageSpeed Insights lab data |
| Real-user performance | CrUX values included in current DevTools trace summaries | PageSpeed Insights/CrUX Vis; direct CrUX API only when a key is already available or automation is requested |
| Accessibility, SEO, Best Practices, Agentic Browsing | Run a live Lighthouse audit; with Chrome DevTools MCP, use lighthouse_audit | Category-specific Lighthouse CLI audits plus manual checks |
| Rendered semantics and interaction | Inspect the accessibility tree and exercise the UI; with Chrome DevTools MCP, use take_snapshot and focused evaluate_script | Browser/manual testing |
| Source smoke test | scripts/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 meaningfulalttext. Decorative images usealt="". - 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
langattribute 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
| Level | Description | Action |
|---|---|---|
| Critical | Security vulnerabilities, complete failures | Fix immediately |
| High | Core Web Vitals failures, major a11y barriers | Fix before launch |
| Medium | Performance opportunities, SEO improvements | Fix within sprint |
| Low | Minor optimizations, code quality | Fix when convenient |
Audit output format
When performing an audit, structure findings as:
## Audit resultsEvidence
| 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
- For detailed guidelines on specific areas:
- Performance Optimization
- Core Web Vitals
- Accessibility
- SEO
- Best Practices
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:
- Check page-level CrUX p75 data, with a clearly labeled origin fallback when page data is unavailable.
- Record a browser performance trace under stated conditions. With Chrome DevTools MCP, trace summaries can include CrUX alongside the observed lab metrics.
- Analyze only the insights associated with the failing metric, then inspect the implicated code and resources.
- 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
| Metric | Measures | Good | Needs work | Poor |
|---|---|---|---|---|
| LCP | Loading | ≤ 2.5s | 2.5s – 4s | > 4s |
| INP | Interactivity | ≤ 200ms | 200ms – 500ms | > 500ms |
| CLS | Visual Stability | ≤ 0.1 | 0.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.
<script type="speculationrules">
{
"prerender": [{
"where": { "href_matches": "/*" },
"eagerness": "moderate"
}]
}
</script>Current Chrome behavior is specific enough to guide the choice:
eagerness | Trigger |
|---|---|
conservative | Pointer or touch down |
moderate | Desktop: 200ms hover, or earlier pointer down; mobile: viewport heuristics |
eager | Chrome 143+: desktop 10ms hover; mobile 50ms after the anchor enters the viewport |
immediate | As 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
wherecarefully (href_matchespatterns, exclude logout/checkout) and avoidimmediateoutside 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
- [ ] 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.
// 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
| Source | Use |
|---|---|
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 Console | Prioritize aggregated real-user outcomes at p75 |
| Lighthouse CLI or PageSpeed Insights | Controlled lab fallback when DevTools tools are unavailable |
| First-party RUM | Segment current production experience by route, device, release, and attribution |
Raw PerformanceObserver | Inspect 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
- Detailed LCP optimization — read when an LCP trace points to discovery, loading, or render delay
- Detailed INP optimization — read when a trace or field attribution identifies a slow interaction
- Detailed CLS optimization — read when a trace or field attribution identifies unexpected shifts
- web.dev LCP
- web.dev INP
- web.dev CLS
- Performance skill
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
- If a page can run, read the measurement workflow and establish a field-plus-lab baseline before editing.
- Prioritize poor real-user Core Web Vitals. Use a DevTools performance trace and its focused insights to find the cause.
- Inspect and change only the code or assets connected to measured bottlenecks.
- 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.
| Resource | Budget | Rationale |
|---|---|---|
| Total page weight | < 1.5 MB | Bounds transfer time and data cost on constrained target networks; calibrate with representative pages |
| JavaScript (compressed) | < 300 KB | Protect parse and execution cost |
| CSS (compressed) | < 100 KB | Limit render-blocking work |
| Images (above-fold) | < 500 KB | Protect likely LCP resources |
| Fonts | < 100 KB | Limit critical font transfer |
| Third-party | < 200 KB | Bound 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.
<!-- 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.
| Metric | Kind | Interpretation |
|---|---|---|
| LCP, INP, CLS at p75 | Field | User-outcome Core Web Vitals; use for pass/fail prioritization |
| LCP, CLS in a trace | Lab | Reproducible diagnostic values for one navigation |
| TBT | Lab | Main-thread blocking diagnostic and a rough INP proxy, not field INP |
| FCP, Speed Index | Lab | Loading 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:
- 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. - Use failed audit nodes to localize the relevant component or template instead of searching the whole repository for generic patterns.
- 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. - 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
| Principle | Description |
|---|---|
| Perceivable | Content can be perceived through different senses |
| Operable | Interface can be operated by all users |
| Understandable | Content and interface are understandable |
| Robust | Content works with assistive technologies |
Conformance levels
| Level | Requirement | Target |
|---|---|---|
| A | Minimum accessibility | Must pass |
| AA | Standard compliance | Should pass (legal requirement in many jurisdictions) |
| AAA | Enhanced accessibility | Nice 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 Size | AA minimum | AAA enhanced |
|---|---|---|
| Normal text (< 18px / < 14px bold) | 4.5:1 | 7:1 |
| Large text (≥ 18px / ≥ 14px bold) | 3:1 | 4.5:1 |
| UI components & graphics | 3:1 | 3:1 |
/* ❌ 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)
<!-- 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.
<!-- ❌ 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> ```
// ✅ 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)
/* ❌ 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.
/* ✅ 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.
/* ✅ 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)
// 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)
/* 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)
<!-- ❌ 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)
<!-- 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.
<!-- ✅ 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)
<!-- ✅ 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:
# Lighthouse accessibility audit
npx lighthouse https://example.com --only-categories=accessibilityaxe-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
- WCAG 2.2 Quick Reference
- WAI-ARIA Authoring Practices
- Deque axe Rules
- Web Quality Audit
- WCAG criteria reference
- Accessibility code patterns
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:
- 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. - 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. - Separate technical crawl/index findings from content quality and authority. Do not invent ranking-factor weights or promise ranking changes.
- 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.
| Area | What this skill can verify |
|---|---|
| Crawl and index controls | Technical configuration and consistency |
| Rendered metadata and semantics | Presence, validity, and page-template issues |
| Structured data | Syntax and eligibility signals, not guaranteed rich results |
| Core Web Vitals | Link to measured field/lab evidence from the Core Web Vitals skill |
| Content usefulness and authority | Review 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 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
lastmodwhen 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
<!-- 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
<!-- ❌ 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
<!-- ❌ 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
<!-- ❌ 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
<!-- ❌ 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
<!-- ❌ 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.txtfile 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:
| Control | Documented purpose | Effect of blocking |
|---|---|---|
OAI-SearchBot | ChatGPT search discovery | Prevents page content from being included in ChatGPT summaries and snippets; a link and title may still surface through third-party discovery |
PerplexityBot | Perplexity search indexing | Prevents that crawler from indexing the blocked content for search results |
Claude-SearchBot / Claude-User | Claude search indexing / user-directed retrieval | May reduce search visibility / prevents retrieval for user-directed requests |
Google-Extended | Controls certain Gemini training and grounding uses of content Google crawls | Does 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
<!-- ❌ Not mobile-friendly -->
<meta name="viewport" content="width=1024"><!-- ✅ Responsive viewport --> <meta name="viewport" content="width=device-width, initial-scale=1"> ```
Tap targets
/* ❌ 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
/* ❌ Too small on mobile */
body {
font-size: 10px;
}/* ✅ Readable without zooming */ body { font-size: 16px; line-height: 1.5; } ```
International SEO
Hreflang tags
<!-- 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 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
| Tool | Use |
|---|---|
| Google Search Console | Monitor indexing, fix issues |
| Google PageSpeed Insights | Performance + Core Web Vitals |
| Rich Results Test | Validate structured data |
Live Lighthouse audit (Chrome DevTools MCP: lighthouse_audit) | Rendered SEO and Agentic Browsing checks for agents |
| Lighthouse CLI | SEO audit fallback |
| Screaming Frog | Crawl 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:
- 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. - Inspect the listed console and network failures and fetch individual details only when they support a finding.
- Supplement runtime evidence with dependency, header, configuration, and source inspection; Lighthouse is not a complete security assessment.
- 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
<!-- ❌ Missing or invalid doctype -->
<HTML>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"><!-- ✅ HTML5 doctype --> <!DOCTYPE html> <html lang="en"> ```
Character encoding
<!-- ❌ 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
<!-- ❌ 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
// ❌ 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):
<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
// ❌ 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
// ❌ 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
// ❌ 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)
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
// 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
// ❌ 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
// ❌ 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
// ❌ 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
// ❌ 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
<!-- ❌ 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
<!-- ❌ 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
<!-- ❌ 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
// ❌ 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
<!-- 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
| Tool | Purpose |
|---|---|
npm audit | Dependency vulnerabilities |
| SecurityHeaders.com | Header analysis |
| W3C Validator | HTML validation |
Live Lighthouse audit (Chrome DevTools MCP: lighthouse_audit) | Rendered Best Practices checks for agents |
| Lighthouse CLI | Best Practices audit fallback |
| Observatory | Security scan |