{"id":"04f4cf1e-6507-4472-ae86-212cafe94b0b","shortId":"SmPSLC","kind":"skill","title":"best-practices","tagline":"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\".","description":"# Best practices\n\nModern web development standards based on Lighthouse best practices audits. Covers security, browser compatibility, and code quality patterns.\n\n## Security\n\n### HTTPS everywhere\n\n**Enforce HTTPS:**\n```html\n<!-- ❌ Mixed content -->\n<img src=\"http://example.com/image.jpg\">\n<script src=\"http://cdn.example.com/script.js\"></script>\n\n<!-- ✅ HTTPS only -->\n<img src=\"https://example.com/image.jpg\">\n<script src=\"https://cdn.example.com/script.js\"></script>\n```\n\nAvoid protocol-relative URLs (`//example.com/...`) — they're an HTTP-era pattern with no benefit on HTTPS-only sites and hide the actual scheme from reviewers.\n\n**HSTS Header:**\n```\nStrict-Transport-Security: max-age=31536000; includeSubDomains; preload\n```\n\n### Content Security Policy (CSP)\n\n```html\n<!-- Basic CSP via meta tag -->\n<meta http-equiv=\"Content-Security-Policy\" \n      content=\"default-src 'self'; \n               script-src 'self' https://trusted-cdn.com; \n               style-src 'self' 'unsafe-inline';\n               img-src 'self' data: https:;\n               connect-src 'self' https://api.example.com;\">\n\n<!-- Better: HTTP header -->\n```\n\n**CSP Header (recommended):**\n```\nContent-Security-Policy: \n  default-src 'self';\n  script-src 'self' 'nonce-abc123' https://trusted.com;\n  style-src 'self' 'nonce-abc123';\n  img-src 'self' data: https:;\n  connect-src 'self' https://api.example.com;\n  frame-ancestors 'self';\n  base-uri 'self';\n  form-action 'self';\n```\n\n**Using nonces for inline scripts:**\n```html\n<script nonce=\"abc123\">\n  // This inline script is allowed\n</script>\n```\n\n### Trusted Types (modern DOM-XSS defense)\n\nA strict CSP blocks loading untrusted *script files*, but it doesn't stop a string from reaching `innerHTML`, `eval`, or other DOM-XSS sinks. Trusted Types — Baseline across all major browsers since early 2026 — closes that hole by making sinks reject raw strings and accept only typed objects produced by a named policy.\n\n```\nContent-Security-Policy: require-trusted-types-for 'script'; trusted-types default;\n```\n\n```javascript\n// One central policy that does the sanitization\nconst escape = trustedTypes.createPolicy('default', {\n  createHTML: (s) => DOMPurify.sanitize(s, { RETURN_TRUSTED_TYPE: true })\n});\n\n// ❌ This now throws TypeError under enforcement\nelement.innerHTML = userInput;\n\n// ✅ Goes through the policy\nelement.innerHTML = escape.createHTML(userInput);\n```\n\nRoll out with `Content-Security-Policy-Report-Only` first to find every sink usage in your app, then flip to enforcement. Angular has built-in Trusted Types support; React 19+ produces TrustedHTML when Trusted Types are enforced; for everything else, [DOMPurify](https://github.com/cure53/DOMPurify) is the de-facto sanitizer.\n\n### Subresource Integrity (SRI) for third-party scripts\n\nPin every `<script>` and `<link rel=\"stylesheet\">` you load from a CDN you don't control. If the CDN is compromised — as happened to polyfill.io in 2024 — the browser refuses to execute a file whose hash doesn't match.\n\n```html\n<script src=\"https://cdn.example.com/lib@1.2.3/dist/lib.js\"\n        integrity=\"sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC\"\n        crossorigin=\"anonymous\"></script>\n```\n\n`integrity` accepts space-separated hashes; include the next version's hash before rotating to avoid downtime. Generate with `openssl dgst -sha384 -binary file.js | openssl base64 -A`. SRI requires `crossorigin` and an `Access-Control-Allow-Origin` response header from the CDN.\n\n### Security headers\n\n```\n# Prevent clickjacking — prefer CSP `frame-ancestors` (above); X-Frame-Options\n# is the legacy fallback for older browsers.\nX-Frame-Options: DENY\n\n# Prevent MIME type sniffing\nX-Content-Type-Options: nosniff\n\n# Do NOT send X-XSS-Protection. The legacy browser XSS auditor was deprecated\n# and removed (Chrome 78, Edge 17), and in some cases it introduced its own\n# vulnerabilities. Use a strict CSP + Trusted Types (below) instead.\n\n# Control referrer information\nReferrer-Policy: strict-origin-when-cross-origin\n\n# Permissions policy (formerly Feature-Policy)\nPermissions-Policy: geolocation=(), microphone=(), camera=()\n```\n\n### No vulnerable libraries\n\n```bash\n# Check for vulnerabilities\nnpm audit\nyarn audit\n\n# Auto-fix when possible\nnpm audit fix\n\n# Check specific package\nnpm ls lodash\n```\n\n**Keep dependencies updated:**\n```json\n// package.json\n{\n  \"scripts\": {\n    \"audit\": \"npm audit --audit-level=moderate\",\n    \"update\": \"npm update && npm audit fix\"\n  }\n}\n```\n\n**Known vulnerable patterns to avoid:**\n```javascript\n// ❌ Recursive merges of untrusted input can pollute Object.prototype\n//    via __proto__, constructor, or prototype keys.\n_.merge(target, userInput);          // lodash <4.17.20\n$.extend(true, {}, target, userInput); // jQuery deep extend\nObject.assign(target, ...userInputs); // safe by itself (shallow), but unsafe\n                                      // when target IS Object.prototype-derived\n                                      // and userInput contains __proto__\n\n// ✅ For untrusted bags, use a null-prototype object so __proto__ is just a key\nconst safe = Object.create(null);\nObject.assign(safe, userInput); // shallow, no recursion → safe by construction\n\n// ✅ For deep copies, structuredClone drops __proto__ and functions\nconst deepSafe = structuredClone(userInput);\n\n// ✅ For deep merges, use a library that explicitly blocks dangerous keys\n//    (e.g. lodash ≥4.17.21 _.mergeWith with a customizer, or deepmerge-ts).\n```\n\n### Input sanitization\n\n```javascript\n// ❌ XSS vulnerable\nelement.innerHTML = userInput;\ndocument.write(userInput);\n\n// ✅ Safe text content\nelement.textContent = userInput;\n\n// ✅ If HTML needed, sanitize\nimport DOMPurify from 'dompurify';\nelement.innerHTML = DOMPurify.sanitize(userInput);\n```\n\n### Secure cookies\n\n```javascript\n// ❌ Insecure cookie\ndocument.cookie = \"session=abc123\";\n\n// ✅ Secure cookie (server-side)\nSet-Cookie: session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/\n```\n\n---\n\n## Browser compatibility\n\n### Doctype declaration\n\n```html\n<!-- ❌ Missing or invalid doctype -->\n<HTML>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n\n<!-- ✅ HTML5 doctype -->\n<!DOCTYPE html>\n<html lang=\"en\">\n```\n\n### Character encoding\n\n```html\n<!-- ❌ Missing or late charset -->\n<html>\n<head>\n  <title>Page</title>\n  <meta charset=\"UTF-8\">\n</head>\n\n<!-- ✅ Charset as first element in head -->\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Page</title>\n</head>\n```\n\n### Viewport meta tag\n\n```html\n<!-- ❌ Missing viewport -->\n<head>\n  <title>Page</title>\n</head>\n\n<!-- ✅ Responsive viewport -->\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <title>Page</title>\n</head>\n```\n\n### Feature detection\n\n```javascript\n// ❌ Browser detection (brittle)\nif (navigator.userAgent.includes('Chrome')) {\n  // Chrome-specific code\n}\n\n// ✅ Feature detection\nif ('IntersectionObserver' in window) {\n  // Use IntersectionObserver\n} else {\n  // Fallback\n}\n\n// ✅ Using @supports in CSS\n@supports (display: grid) {\n  .container {\n    display: grid;\n  }\n}\n\n@supports not (display: grid) {\n  .container {\n    display: flex;\n  }\n}\n```\n\n### Polyfills (when needed)\n\nPrefer **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.\n\nIf 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):\n\n```html\n<script>\n  if (!('fetch' in window)) {\n    const s = document.createElement('script');\n    s.src = '/polyfills/fetch.js';\n    s.defer = true;\n    document.head.appendChild(s);\n  }\n</script>\n```\n\n**Never load polyfills from a third-party CDN you don't control.** The `polyfill.io` service was [compromised in mid-2024](https://sansec.io/research/polyfill-supply-chain-attack) 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](https://blog.cloudflare.com/polyfill-io-now-available-on-cdnjs-reduce-your-supply-chain-risk/)) — and pin the version with [Subresource Integrity](#subresource-integrity-sri-for-third-party-scripts).\n\n---\n\n## Deprecated APIs\n\n### Avoid these\n\n```javascript\n// ❌ document.write (blocks parsing)\ndocument.write('<script src=\"...\"></script>');\n\n// ✅ Dynamic script loading\nconst script = document.createElement('script');\nscript.src = '...';\ndocument.head.appendChild(script);\n\n// ❌ Synchronous XHR (blocks main thread)\nconst xhr = new XMLHttpRequest();\nxhr.open('GET', url, false); // false = synchronous\n\n// ✅ Async fetch\nconst response = await fetch(url);\n\n// ❌ Application Cache (deprecated)\n<html manifest=\"cache.manifest\">\n\n// ✅ Service Workers\nif ('serviceWorker' in navigator) {\n  navigator.serviceWorker.register('/sw.js');\n}\n```\n\n### Event listener passive\n\n```javascript\n// ❌ Non-passive touch/wheel (may block scrolling)\nelement.addEventListener('touchstart', handler);\nelement.addEventListener('wheel', handler);\n\n// ✅ Passive listeners (allows smooth scrolling)\nelement.addEventListener('touchstart', handler, { passive: true });\nelement.addEventListener('wheel', handler, { passive: true });\n\n// ✅ If you need preventDefault, be explicit\nelement.addEventListener('touchstart', handler, { passive: false });\n```\n\n---\n\n## Console & errors\n\n### No console errors\n\n```javascript\n// ❌ Errors in production\nconsole.log('Debug info'); // Remove in production\nthrow new Error('Unhandled'); // Catch all errors\n\n// ✅ Proper error handling\ntry {\n  riskyOperation();\n} catch (error) {\n  // Log to error tracking service\n  errorTracker.captureException(error);\n  // Show user-friendly message\n  showErrorMessage('Something went wrong. Please try again.');\n}\n```\n\n### Error boundaries (React)\n\n```jsx\nclass ErrorBoundary extends React.Component {\n  state = { hasError: false };\n  \n  static getDerivedStateFromError(error) {\n    return { hasError: true };\n  }\n  \n  componentDidCatch(error, info) {\n    errorTracker.captureException(error, { extra: info });\n  }\n  \n  render() {\n    if (this.state.hasError) {\n      return <FallbackUI />;\n    }\n    return this.props.children;\n  }\n}\n\n// Usage\n<ErrorBoundary>\n  <App />\n</ErrorBoundary>\n```\n\n### Global error handler\n\n```javascript\n// Catch unhandled errors\nwindow.addEventListener('error', (event) => {\n  errorTracker.captureException(event.error);\n});\n\n// Catch unhandled promise rejections\nwindow.addEventListener('unhandledrejection', (event) => {\n  errorTracker.captureException(event.reason);\n});\n```\n\n---\n\n## Source maps\n\n### Production configuration\n\n```javascript\n// ❌ Source maps exposed in production\n// webpack.config.js\nmodule.exports = {\n  devtool: 'source-map', // Exposes source code\n};\n\n// ✅ Hidden source maps (uploaded to error tracker)\nmodule.exports = {\n  devtool: 'hidden-source-map',\n};\n\n// ✅ Or no source maps in production\nmodule.exports = {\n  devtool: process.env.NODE_ENV === 'production' ? false : 'source-map',\n};\n```\n\n**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.\n\nFor Vite, prefer `sourcemap: 'hidden'` over `'true'` so the `//# sourceMappingURL=` comment isn't emitted into the bundle.\n\n---\n\n## Performance best practices\n\n### Avoid blocking patterns\n\n```javascript\n// ❌ Blocking script\n<script src=\"heavy-library.js\"></script>\n\n// ✅ Deferred script\n<script defer src=\"heavy-library.js\"></script>\n\n// ❌ Blocking CSS import\n@import url('other-styles.css');\n\n// ✅ Link tags (parallel loading)\n<link rel=\"stylesheet\" href=\"styles.css\">\n<link rel=\"stylesheet\" href=\"other-styles.css\">\n```\n\n### Efficient event handlers\n\n```javascript\n// ❌ Handler on every element\nitems.forEach(item => {\n  item.addEventListener('click', handleClick);\n});\n\n// ✅ Event delegation\ncontainer.addEventListener('click', (e) => {\n  if (e.target.matches('.item')) {\n    handleClick(e);\n  }\n});\n```\n\n### Memory management\n\n```javascript\n// ❌ Memory leak (never removed)\nconst handler = () => { /* ... */ };\nwindow.addEventListener('resize', handler);\n\n// ✅ Cleanup when done\nconst handler = () => { /* ... */ };\nwindow.addEventListener('resize', handler);\n\n// Later, when component unmounts:\nwindow.removeEventListener('resize', handler);\n\n// ✅ Using AbortController\nconst controller = new AbortController();\nwindow.addEventListener('resize', handler, { signal: controller.signal });\n\n// Cleanup:\ncontroller.abort();\n```\n\n---\n\n## Code quality\n\n### Valid HTML\n\n```html\n<!-- ❌ Invalid HTML -->\n<div id=\"header\">\n<div id=\"header\"> <!-- Duplicate ID -->\n\n<ul>\n  <div>Item</div> <!-- Invalid child -->\n</ul>\n\n<a href=\"/\"><button>Click</button></a> <!-- Invalid nesting -->\n\n<!-- ✅ Valid HTML -->\n<header id=\"site-header\">\n</header>\n\n<ul>\n  <li>Item</li>\n</ul>\n\n<a href=\"/\" class=\"button\">Click</a>\n```\n\n### Semantic HTML\n\n```html\n<!-- ❌ Non-semantic -->\n<div class=\"header\">\n  <div class=\"nav\">\n    <div class=\"nav-item\">Home</div>\n  </div>\n</div>\n<div class=\"main\">\n  <div class=\"article\">\n    <div class=\"title\">Headline</div>\n  </div>\n</div>\n\n<!-- ✅ Semantic HTML5 -->\n<header>\n  <nav>\n    <a href=\"/\">Home</a>\n  </nav>\n</header>\n<main>\n  <article>\n    <h1>Headline</h1>\n  </article>\n</main>\n```\n\n### Image aspect ratios\n\n```html\n<!-- ❌ Distorted images -->\n<img src=\"photo.jpg\" width=\"300\" height=\"100\">\n<!-- If actual ratio is 4:3, this squishes the image -->\n\n<!-- ✅ Preserve aspect ratio -->\n<img src=\"photo.jpg\" width=\"300\" height=\"225\">\n<!-- Actual 4:3 dimensions -->\n\n<!-- ✅ CSS object-fit for flexibility -->\n<img src=\"photo.jpg\" style=\"width: 300px; height: 200px; object-fit: cover;\">\n```\n\n---\n\n## Permissions & privacy\n\n### Request permissions properly\n\n```javascript\n// ❌ Request on page load (bad UX, often denied)\nnavigator.geolocation.getCurrentPosition(success, error);\n\n// ✅ Request in context, after user action\nfindNearbyButton.addEventListener('click', async () => {\n  // Explain why you need it\n  if (await showPermissionExplanation()) {\n    navigator.geolocation.getCurrentPosition(success, error);\n  }\n});\n```\n\n### Permissions policy\n\n```html\n<!-- Restrict powerful features -->\n<meta http-equiv=\"Permissions-Policy\" \n      content=\"geolocation=(), camera=(), microphone=()\">\n\n<!-- Or allow for specific origins -->\n<meta http-equiv=\"Permissions-Policy\" \n      content=\"geolocation=(self 'https://maps.example.com')\">\n```\n\n---\n\n## Audit checklist\n\n### Security (critical)\n- [ ] HTTPS enabled, no mixed content\n- [ ] No vulnerable dependencies (`npm audit`)\n- [ ] CSP headers configured (with `frame-ancestors`, `base-uri`, `form-action`)\n- [ ] `require-trusted-types-for 'script'` enforced (or report-only during rollout)\n- [ ] Third-party `<script>`/`<link rel=\"stylesheet\">` pinned with SRI hashes\n- [ ] Security headers present (HSTS, X-Content-Type-Options, Referrer-Policy)\n- [ ] No exposed source maps (and `sourcesContent` stripped from uploaded ones)\n\n### Compatibility\n- [ ] Valid HTML5 doctype\n- [ ] Charset declared first in head\n- [ ] Viewport meta tag present\n- [ ] No deprecated APIs used\n- [ ] Passive event listeners for scroll/touch\n\n### Code quality\n- [ ] No console errors\n- [ ] Valid HTML (no duplicate IDs)\n- [ ] Semantic HTML elements used\n- [ ] Proper error handling\n- [ ] Memory cleanup in components\n\n### UX\n- [ ] No intrusive interstitials\n- [ ] Permission requests in context\n- [ ] Clear error messages\n- [ ] Appropriate image aspect ratios\n\n## Tools\n\n| Tool | Purpose |\n|------|---------|\n| `npm audit` | Dependency vulnerabilities |\n| [SecurityHeaders.com](https://securityheaders.com) | Header analysis |\n| [W3C Validator](https://validator.w3.org) | HTML validation |\n| Lighthouse | Best practices audit |\n| [Observatory](https://observatory.mozilla.org) | Security scan |\n\n## References\n\n- [MDN Web Security](https://developer.mozilla.org/en-US/docs/Web/Security)\n- [OWASP Top 10](https://owasp.org/www-project-top-ten/)\n- [Web Quality Audit](../web-quality-audit/SKILL.md)","tags":["best","practices","web","quality","skills","addyosmani","accessibility","agent-skills","claude-skills","core-web-vitals","lighthouse","testing"],"capabilities":["skill","source-addyosmani","skill-best-practices","topic-accessibility","topic-agent-skills","topic-claude-skills","topic-core-web-vitals","topic-lighthouse","topic-skills","topic-testing","topic-web-performance"],"categories":["web-quality-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/addyosmani/web-quality-skills/best-practices","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add addyosmani/web-quality-skills","source_repo":"https://github.com/addyosmani/web-quality-skills","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 1959 github stars · SKILL.md body (16,315 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T18:52:54.976Z","embedding":null,"createdAt":"2026-04-18T20:27:12.165Z","updatedAt":"2026-05-18T18:52:54.976Z","lastSeenAt":"2026-05-18T18:52:54.976Z","tsv":"'-2024':813 '/cure53/dompurify)':315 '/example.com':65 '/polyfill-io-now-available-on-cdnjs-reduce-your-supply-chain-risk/))':847 '/research/polyfill-supply-chain-attack)':816 '/sw.js':914 '100k':829 '17':429 '19':301 '2026':201 '31536000':97 '4.17.20':539 '4.17.21':618 '78':427 'abc123':122,130,659,669 'abortcontrol':1249,1253 'accept':212,333 'access':365 'access-control-allow-origin':364 'across':195 'action':152,1303,1347 'actual':84 'age':96 'allow':367,934 'ancestor':144,382,1341 'angular':292 'anyon':1128 'api':864 'api.example.com':141 'app':287 'append':775 'appli':4,20 'applic':904 'ask':18 'aspect':1278 'async':897,1306 'async/deferred':790 'attack':822 'audit':24,45,479,481,488,502,504,506,513,1321,1334 'audit-level':505 'auditor':421 'auto':483 'auto-fix':482 'avoid':60,347,519,760,865,1180 'await':901,1313 'babel/swc':740 'bad':1291 'bag':567 'base':40,147,1343 'base-uri':146,1342 'base64':357 'baselin':194 'bash':474 'benefit':75 'best':2,8,21,34,43,1178 'best-practic':1 'binari':354 'block':170,613,783,869,884,924,1181,1184,1188 'blog.cloudflare.com':846 'blog.cloudflare.com/polyfill-io-now-available-on-cdnjs-reduce-your-supply-chain-risk/))':845 'boundari':1007 'brittl':696 'broken':788 'browser':48,198,394,419,675,694,751,766 'build':738,844 'built':295 'built-in':294 'bundl':735,1176 'bundler':1118,1145 'byte':763 'cach':905 'camera':470 'case':433 'catch':977,985,1041,1049 'cdn':373,801 'cdnjs':842 'central':237 'chain':821 'charact':680 'check':31,475,490,757 'checklist':1322 'chrome':426,699,701 'chrome-specif':700 'class':1010 'cleanup':1233,1259 'cli':1153 'click':1209,1214,1267,1269,1305 'clickjack':377 'close':202 'cloudflar':840 'code':14,26,27,51,703,1076,1142,1261 'comment':1170 'compat':12,49,676 'compon':1243 'componentdidcatch':1023 'compromis':810 'configur':1061,1143,1337 'connect':138 'connect-src':137 'consol':958,961 'console.log':967 'const':243,580,601,875,887,899,1228,1236,1250 'construct':592 'constructor':531 'contain':563,721,728 'container.addeventlistener':1213 'content':100,109,222,274,406,638,1329 'content-security-polici':108,221 'content-security-policy-report-on':273 'context':791,1300 'control':366,447,805,1251 'controller.abort':1260 'controller.signal':1258 'cooki':653,656,661,667 'copi':595 'core':742 'core-j':741 'cover':46 'createhtml':247 'critic':1324 'cross':457 'crossorigin':361 'csp':103,105,169,379,442,1335 'css':717,1189 'custom':622 'danger':614 'data':135 'de':319 'de-facto':318 'debug':968 'declar':678 'deep':545,594,606 'deepmerg':625 'deepmerge-t':624 'deepsaf':602 'default':113,234,246,1117 'default-src':112 'defens':166 'defer':1186 'deleg':1212 'deni':399,1294 'depend':497,1332 'deprec':423,863,906 'deriv':560 'detect':692,695,705 'develop':7,38 'devtool':1070,1085,1097 'dgst':352 'display':719,722,726,729 'doctyp':677 'document.cookie':657 'document.createelement':877 'document.head.appendchild':880 'document.write':634,781,868,871 'doesn':177 'dom':164,189 'dom-xss':163,188 'dompurifi':312,646,648 'dompurify.sanitize':249,650 'done':1235 'downtim':348 'drop':597 'dynam':872 'e':1215,1220 'e.g':616,839 'e.target.matches':1217 'earli':200 'edg':428 'effici':1198 'element':778,1205 'element.addeventlistener':926,929,937,942,953 'element.innerhtml':261,267,632,649 'element.textcontent':639 'elimin':754 'els':311,712 'emb':1119 'emit':1173 'enabl':1326 'encod':681 'enforc':57,260,291,308,1354 'entir':758 'env':1099 'era':71 'error':959,962,964,975,979,981,986,989,993,1006,1019,1024,1027,1038,1043,1045,1082,1114,1297,1317 'errorboundari':1011 'errortracker.captureexception':992,1026,1047,1056 'escap':244 'escape.createhtml':268 'eval':185 'event':915,1046,1055,1199,1211 'event.error':1048 'event.reason':1057 'everi':282,331,1204 'everyth':310 'everywher':56 'explain':1307 'explicit':612,952 'expos':1065,1074 'extend':540,546,1012 'extra':1028 'facto':320 'fallback':391,713 'fals':894,895,957,1016,1101 'featur':463,691,704 'feature-polici':462 'fetch':898,902 'file':174,1127 'file.js':355 'find':281 'findnearbybutton.addeventlistener':1304 'first':279 'fix':484,489,514 'flag':1154 'flex':730 'flip':289 'form':151,1346 'form-act':150,1345 'former':461 'frame':143,381,386,397,1340 'frame-ancestor':142,380,1339 'friend':997 'full':1121 'function':600 'generat':349 'geoloc':468 'get':892,1139 'getderivedstatefromerror':1018 'github.com':314 'github.com/cure53/dompurify)':313 'global':1037 'goe':263 'grid':720,723,727 'handl':982 'handleclick':1210,1219 'handler':928,931,939,944,955,1039,1200,1202,1229,1232,1237,1240,1247,1256 'haserror':1015,1021 'hash':337,343 'header':89,106,370,375,1336 'headlin':1274,1276 'hidden':1077,1087,1164 'hidden-source-map':1086 'hide':82 'hole':204 'home':1273,1275 'host':833 'hsts':88 'html':59,104,159,642,679,682,688,792,1264,1265,1271,1272,1280,1320 'http':70 'http-era':69 'httpon':671 'https':55,58,78,136,1325 'https-on':77 'imag':1277 'img':132 'img-src':131 'import':645,1190,1191 'includ':338,1133 'includesubdomain':98 'info':969,1025,1029 'inform':449 'inlin':157 'innerhtml':184 'input':525,627 'insecur':655 'insid':1124 'instead':446 'integr':323,332,854,857 'intersectionobserv':707,711 'introduc':435 'isn':1171 'item':1207,1218,1266,1268 'item.addeventlistener':1208 'items.foreach':1206 'javascript':235,520,629,654,693,867,918,963,1040,1062,1183,1201,1223,1286 'jqueri':544 'js':743 'json':499 'jsx':1009 'keep':496 'key':534,579,615 'known':515 'later':1241 'leak':1225 'legaci':390,418 'level':507 'librari':473,610 'lighthous':42 'link':1194 'list':752 'listen':916,933 'load':171,770,794,874,1197,1290 'lodash':495,538,617 'log':987 'ls':494 'main':885 'major':197 'make':206 'malwar':827 'manag':1222 'map':1059,1064,1073,1079,1089,1093,1104,1109,1126,1132 'max':95 'max-ag':94 'may':923 'memori':1221,1224 'merg':522,535,607 'mergewith':619 'messag':998 'meta':686 'microphon':469 'mid':812 'mime':401 'mirror':838 'misconfigur':1136 'mix':1328 'moder':508 'modern':5,25,36,162,765 'module.exports':1069,1084,1096 'must':769 'name':219 'navig':912 'navigator.geolocation.getcurrentposition':1295,1315 'navigator.serviceworker.register':913 'navigator.useragent.includes':698 'need':643,733,949,1310 'never':779,793,1226 'new':889,974,1252 'next':340 'non':920 'non-pass':919 'nonc':121,129,155 'nonce-abc123':120,128 'nosniff':409 'npm':478,487,493,503,510,512,1333 'null':571,583 'null-prototyp':570 'object':215,573 'object.assign':547,584 'object.create':582 'object.prototype':528,559 'obtain':1130 'often':1293 'older':393 'omit':1147 'one':236 'openssl':351,356 'option':387,398,408 'origin':368,455,458,1122 'other-styles.css':1193 'packag':492 'package.json':500 'page':683,684,689,690,1289 'parallel':1196 'pars':870 'parser':785 'parti':328,800,861,1363 'passiv':917,921,932,940,945,956 'path':674 'pattern':53,72,517,1182 'perform':1177 'permiss':459,466,1281,1284,1318 'permissions-polici':465 'pin':330,849 'pleas':1003 'polici':102,111,220,224,238,266,276,452,460,464,467,1319 'pollut':527 'polyfil':731,736,762,772,795,843 'polyfill.io':807 'possibl':486 'practic':3,9,22,35,44,1179 'prefer':378,734,1162 'preload':99 'prevent':376,400 'preventdefault':950 'privaci':1282 'process.env.node':1098 'produc':216,302 'product':966,972,1060,1067,1095,1100,1108 'promis':1051 'proper':980,1285 'protect':416 'proto':530,564,575,598 'protocol':62 'protocol-rel':61 'prototyp':533,572 'qualiti':15,28,52,1262 'ratio':1279 'raw':209 're':67 'reach':183 'react':300,1008 'react.component':1013 'recommend':107 'recurs':521,589 'referr':448,451 'referrer-polici':450 'reject':208,1052 'relat':63 'remov':425,970,1227 'render':1030 'report':277,1357 'report-on':1356 'request':1283,1287,1298 'requir':226,360,1349 'require-trusted-types-for':225,1348 'resiz':1231,1239,1246,1255 'respons':369,900 'return':251,1020,1033,1034 'review':29,87 'riskyoper':984 'roll':270 'rollout':1360 'rotat':345 'runtim':756,774 'safe':550,581,585,590,636 'samesit':672 'sanit':242,321,628,644 'sansec.io':815 'sansec.io/research/polyfill-supply-chain-attack)':814 'scheme':85 'script':117,158,173,230,329,501,777,862,873,876,878,881,1185,1187,1353 'script-src':116 'script.src':879 'scroll':925,936 'secur':11,23,47,54,93,101,110,223,275,374,652,660,670,1323 'self':115,119,127,134,140,145,149,153,832 'self-host':831 'semant':1270 'send':412 'sentry/bugsnag':1152 'separ':336 'serv':826 'server':663 'server-sid':662 'servic':808,907,991 'servicework':910 'session':658,668 'set':666 'set-cooki':665 'sha384':353 'shallow':553,587 'ship':761 'show':994 'showerrormessag':999 'showpermissionexplan':1314 'side':664 'signal':1257 'sinc':199 'sink':191,207,283 'site':80,830 'skill' 'skill-best-practices' 'smooth':935 'snif':403 'someth':1000 'sourc':1058,1063,1072,1075,1078,1088,1092,1103,1123 'source-addyosmani' 'source-map':1071,1102 'sourcemap':1163 'sourcemappingurl':1169 'sourcescont':1106,1148 'space':335 'space-separ':334 'specif':491,702 'src':114,118,126,133,139 'sri':324,359,858 'standard':39 'state':1014 'static':1017 'step':1138 'stop':179 'strict':91,168,441,454,673 'strict-origin-when-cross-origin':453 'strict-transport-secur':90 'string':181,210 'strip':1105 'structuredclon':596,603 'style':125 'style-src':124 'subresourc':322,853,856 'subresource-integrity-sri-for-third-party-script':855 'success':1296,1316 'suppli':820 'supply-chain':819 'support':299,715,718,724,750 'supported-brows':749 'synchron':882,896 'tag':687,1195 'target':536,542,548,557,746 'text':637 'third':327,799,860,1362 'third-parti':326,798,1361 'this.props.children':1035 'this.state.haserror':1032 'thread':886 'throw':257,973 'time':739 'topic-accessibility' 'topic-agent-skills' 'topic-claude-skills' 'topic-core-web-vitals' 'topic-lighthouse' 'topic-skills' 'topic-testing' 'topic-web-performance' 'touch/wheel':922 'touchstart':927,938,954 'track':990 'tracker':1083,1115 'transport':92 'tri':983,1004 'true':254,541,941,946,1022,1166 'trust':160,192,227,232,252,297,305,443,1350 'trusted-typ':231 'trusted.com':123 'trustedhtml':303 'trustedtypes.createpolicy':245 'ts':626 'type':161,193,214,228,233,253,298,306,402,407,444,1351 'typeerror':258 'unhandl':976,1042,1050 'unhandledreject':1054 'unminifi':1141 'unmount':1244 'unsaf':555 'untrust':172,524,566 'updat':498,509,511 'upload':1080,1111,1137,1159 'uri':148,1344 'url':64,893,903,1192 'usag':284,1036 'use':16,154,439,568,608,710,714,780,824,835,1150,1248 'user':996,1302 'user-friend':995 'userinput':262,269,537,543,549,562,586,604,633,635,640,651 'ux':1292 'valid':1263 'version':341,851 'vet':837 'via':529,1134 'viewport':685 'vite':1161 'vitejs/plugin-legacy':745 'vulner':33,438,472,477,516,631,1331 'web':6,37 'webpack.config.js':1068 'went':1001 'wheel':930,943 'window':709 'window.addeventlistener':1044,1053,1230,1238,1254 'window.removeeventlistener':1245 'worker':908 'wrong':1002 'x':385,396,405,414 'x-content-type-opt':404 'x-frame-opt':384,395 'x-xss-protect':413 'xhr':883,888 'xhr.open':891 'xmlhttprequest':890 'xss':165,190,415,420,630 'yarn':480","prices":[{"id":"7a252982-d78c-4718-bc55-469363d80c5b","listingId":"04f4cf1e-6507-4472-ae86-212cafe94b0b","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"addyosmani","category":"web-quality-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T20:27:12.165Z"}],"sources":[{"listingId":"04f4cf1e-6507-4472-ae86-212cafe94b0b","source":"github","sourceId":"addyosmani/web-quality-skills/best-practices","sourceUrl":"https://github.com/addyosmani/web-quality-skills/tree/main/skills/best-practices","isPrimary":false,"firstSeenAt":"2026-04-18T21:54:15.607Z","lastSeenAt":"2026-05-18T18:52:54.976Z"},{"listingId":"04f4cf1e-6507-4472-ae86-212cafe94b0b","source":"skills_sh","sourceId":"addyosmani/web-quality-skills/best-practices","sourceUrl":"https://skills.sh/addyosmani/web-quality-skills/best-practices","isPrimary":true,"firstSeenAt":"2026-04-18T20:27:12.165Z","lastSeenAt":"2026-05-07T22:40:21.008Z"}],"details":{"listingId":"04f4cf1e-6507-4472-ae86-212cafe94b0b","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"addyosmani","slug":"best-practices","github":{"repo":"addyosmani/web-quality-skills","stars":1959,"topics":["accessibility","agent-skills","claude-skills","core-web-vitals","lighthouse","skills","testing","web-performance"],"license":"mit","html_url":"https://github.com/addyosmani/web-quality-skills","pushed_at":"2026-05-09T22:25:48Z","description":"Agent Skills for optimizing web quality based on Lighthouse and Core Web Vitals.","skill_md_sha":"818d55e6f855dbf48a2fba5db29a134ff7081f38","skill_md_path":"skills/best-practices/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/addyosmani/web-quality-skills/tree/main/skills/best-practices"},"layout":"multi","source":"github","category":"web-quality-skills","frontmatter":{"name":"best-practices","license":"MIT","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\"."},"skills_sh_url":"https://skills.sh/addyosmani/web-quality-skills/best-practices"},"updatedAt":"2026-05-18T18:52:54.976Z"}}