This commit is contained in:
440
.agents/skills/accessibility/SKILL.md
Normal file
440
.agents/skills/accessibility/SKILL.md
Normal file
@@ -0,0 +1,440 @@
|
||||
---
|
||||
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: "1.1"
|
||||
---
|
||||
|
||||
# Accessibility (a11y)
|
||||
|
||||
Comprehensive accessibility guidelines based on WCAG 2.2 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities.
|
||||
|
||||
## WCAG Principles: POUR
|
||||
|
||||
| Principle | Description |
|
||||
|-----------|-------------|
|
||||
| **P**erceivable | Content can be perceived through different senses |
|
||||
| **O**perable | Interface can be operated by all users |
|
||||
| **U**nderstandable | Content and interface are understandable |
|
||||
| **R**obust | 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 |
|
||||
|
||||
```css
|
||||
/* ❌ Low contrast (2.5:1) */
|
||||
.low-contrast {
|
||||
color: #999;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ✅ Sufficient contrast (7:1) */
|
||||
.high-contrast {
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* ✅ Focus states need contrast too */
|
||||
:focus-visible {
|
||||
outline: 2px solid #005fcc;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
**Don't rely on color alone:**
|
||||
```html
|
||||
<!-- ❌ Only color indicates error -->
|
||||
<input class="error-border">
|
||||
<style>.error-border { border-color: red; }</style>
|
||||
|
||||
<!-- ✅ Color + icon + text -->
|
||||
<div class="field-error">
|
||||
<input aria-invalid="true" aria-describedby="email-error">
|
||||
<span id="email-error" class="error-message">
|
||||
<svg aria-hidden="true"><!-- error icon --></svg>
|
||||
Please enter a valid email address
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Media alternatives (1.2)
|
||||
|
||||
```html
|
||||
<!-- Video with captions -->
|
||||
<video controls>
|
||||
<source src="video.mp4" type="video/mp4">
|
||||
<track kind="captions" src="captions.vtt" srclang="en" label="English" default>
|
||||
<track kind="descriptions" src="descriptions.vtt" srclang="en" label="Descriptions">
|
||||
</video>
|
||||
|
||||
<!-- Audio with transcript -->
|
||||
<audio controls>
|
||||
<source src="podcast.mp3" type="audio/mp3">
|
||||
</audio>
|
||||
<details>
|
||||
<summary>Transcript</summary>
|
||||
<p>Full transcript text...</p>
|
||||
</details>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Operable
|
||||
|
||||
### Keyboard accessible (2.1)
|
||||
|
||||
**All functionality must be keyboard accessible:**
|
||||
```javascript
|
||||
// ❌ Only handles click
|
||||
element.addEventListener('click', handleAction);
|
||||
|
||||
// ✅ Handles both click and keyboard
|
||||
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](references/A11Y-PATTERNS.md#modal-focus-trap) for dialogs—the native `<dialog>` element handles this automatically.
|
||||
|
||||
### Focus visible (2.4.7)
|
||||
|
||||
```css
|
||||
/* ❌ Never remove focus outlines */
|
||||
*:focus { outline: none; }
|
||||
|
||||
/* ✅ Use :focus-visible for keyboard-only focus */
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid #005fcc;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* ✅ Or custom focus styles */
|
||||
button:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5);
|
||||
}
|
||||
```
|
||||
|
||||
### Focus not obscured (2.4.11) — new in 2.2
|
||||
|
||||
When an element receives keyboard focus, it must not be entirely hidden by other author-created content such as sticky headers, footers, or overlapping panels. At Level AAA (2.4.12), no part of the focused element may be hidden.
|
||||
|
||||
```css
|
||||
/* ✅ Account for sticky headers when scrolling to focused elements */
|
||||
:target {
|
||||
scroll-margin-top: 80px;
|
||||
}
|
||||
|
||||
/* ✅ Ensure focused items clear fixed/sticky bars */
|
||||
:focus {
|
||||
scroll-margin-top: 80px;
|
||||
scroll-margin-bottom: 60px;
|
||||
}
|
||||
```
|
||||
|
||||
### Skip links (2.4.1)
|
||||
|
||||
Provide a skip link so keyboard users can bypass repetitive navigation. See the [skip link pattern](references/A11Y-PATTERNS.md#skip-link) for full markup and styles.
|
||||
|
||||
### Target size (2.5.8) — new in 2.2
|
||||
|
||||
Interactive targets must be at least **24 × 24 CSS pixels** (AA). Exceptions: inline text links, elements where the browser controls the size, and targets where a 24px circle centered on the bounding box does not overlap another target.
|
||||
|
||||
```css
|
||||
/* ✅ Minimum target size */
|
||||
button,
|
||||
[role="button"],
|
||||
input[type="checkbox"] + label,
|
||||
input[type="radio"] + label {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
/* ✅ Comfortable target size (recommended 44×44) */
|
||||
.touch-target {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
```
|
||||
|
||||
### Dragging movements (2.5.7) — new in 2.2
|
||||
|
||||
Any action that requires dragging must have a single-pointer alternative (e.g., buttons, inputs). See the [dragging movements pattern](references/A11Y-PATTERNS.md#dragging-movements) for a sortable-list example.
|
||||
|
||||
### Timing (2.2)
|
||||
|
||||
```javascript
|
||||
// Allow users to extend time limits
|
||||
function showSessionWarning() {
|
||||
const modal = createModal({
|
||||
title: 'Session Expiring',
|
||||
content: 'Your session will expire in 2 minutes.',
|
||||
actions: [
|
||||
{ label: 'Extend session', action: extendSession },
|
||||
{ label: 'Log out', action: logout }
|
||||
],
|
||||
timeout: 120000
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Motion (2.3)
|
||||
|
||||
```css
|
||||
/* Respect reduced motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Understandable
|
||||
|
||||
### Page language (3.1.1)
|
||||
|
||||
```html
|
||||
<!-- ❌ No language specified -->
|
||||
<html>
|
||||
|
||||
<!-- ✅ Language specified -->
|
||||
<html lang="en">
|
||||
|
||||
<!-- ✅ Language changes within page -->
|
||||
<p>The French word for hello is <span lang="fr">bonjour</span>.</p>
|
||||
```
|
||||
|
||||
### Consistent navigation (3.2.3)
|
||||
|
||||
```html
|
||||
<!-- Navigation should be consistent across pages -->
|
||||
<nav aria-label="Main">
|
||||
<ul>
|
||||
<li><a href="/" aria-current="page">Home</a></li>
|
||||
<li><a href="/products">Products</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Consistent help (3.2.6) — new in 2.2
|
||||
|
||||
If a help mechanism (contact info, chat widget, FAQ link, self-help option) is repeated across multiple pages, it must appear in the **same relative order** each time. Users who rely on consistent placement shouldn't have to hunt for help on every page.
|
||||
|
||||
### Form labels (3.3.2)
|
||||
|
||||
Every input needs a programmatically associated label. See the [form labels pattern](references/A11Y-PATTERNS.md#form-labels) 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](references/A11Y-PATTERNS.md#error-handling) for full markup and JS.
|
||||
|
||||
### Redundant entry (3.3.7) — new in 2.2
|
||||
|
||||
Don't force users to re-enter information they already provided in the same session. Auto-populate from earlier steps, or let users select from previously entered values. Exceptions: security re-confirmation and content that has expired.
|
||||
|
||||
```html
|
||||
<!-- ✅ Auto-fill shipping address from billing -->
|
||||
<fieldset>
|
||||
<legend>Shipping address</legend>
|
||||
<label>
|
||||
<input type="checkbox" id="same-as-billing" checked>
|
||||
Same as billing address
|
||||
</label>
|
||||
<!-- Fields auto-populated when checked -->
|
||||
</fieldset>
|
||||
```
|
||||
|
||||
### Accessible authentication (3.3.8) — new in 2.2
|
||||
|
||||
Login flows must not rely on cognitive function tests (e.g., remembering a password, solving a puzzle) unless at least one of:
|
||||
- A copy-paste or autofill mechanism is available
|
||||
- An alternative method exists (e.g., passkey, SSO, email link)
|
||||
- The test uses object recognition or personal content (AA only; AAA removes this exception)
|
||||
|
||||
```html
|
||||
<!-- ✅ Allow paste in password fields -->
|
||||
<input type="password" id="password" autocomplete="current-password">
|
||||
|
||||
<!-- ✅ Offer passwordless alternatives -->
|
||||
<button type="button">Sign in with passkey</button>
|
||||
<button type="button">Email me a login link</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Robust
|
||||
|
||||
### ARIA usage (4.1.2)
|
||||
|
||||
**Prefer native elements:**
|
||||
```html
|
||||
<!-- ❌ ARIA role on div -->
|
||||
<div role="button" tabindex="0">Click me</div>
|
||||
|
||||
<!-- ✅ Native button -->
|
||||
<button>Click me</button>
|
||||
|
||||
<!-- ❌ ARIA checkbox -->
|
||||
<div role="checkbox" aria-checked="false">Option</div>
|
||||
|
||||
<!-- ✅ Native checkbox -->
|
||||
<label><input type="checkbox"> Option</label>
|
||||
```
|
||||
|
||||
**When ARIA is needed,** use the correct roles and states. See the [ARIA tabs pattern](references/A11Y-PATTERNS.md#aria-tabs) 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](references/A11Y-PATTERNS.md#live-regions-and-notifications) for markup and a `showNotification()` helper.
|
||||
|
||||
---
|
||||
|
||||
## Testing checklist
|
||||
|
||||
### Automated testing
|
||||
```bash
|
||||
# Lighthouse accessibility audit
|
||||
npx lighthouse https://example.com --only-categories=accessibility
|
||||
|
||||
# axe-core
|
||||
npm install @axe-core/cli -g
|
||||
axe https://example.com
|
||||
```
|
||||
|
||||
### Manual testing
|
||||
|
||||
- [ ] **Keyboard navigation:** Tab through entire page, use Enter/Space to activate
|
||||
- [ ] **Screen reader:** Test with VoiceOver (Mac), NVDA (Windows), or TalkBack (Android)
|
||||
- [ ] **Zoom:** Content usable at 200% zoom
|
||||
- [ ] **High contrast:** Test with Windows High Contrast Mode
|
||||
- [ ] **Reduced motion:** Test with `prefers-reduced-motion: reduce`
|
||||
- [ ] **Focus order:** Logical and follows visual order
|
||||
- [ ] **Target size:** Interactive elements meet 24×24px minimum
|
||||
|
||||
See the [screen reader commands reference](references/A11Y-PATTERNS.md#screen-reader-commands) 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](https://www.w3.org/WAI/WCAG22/quickref/)
|
||||
- [WAI-ARIA Authoring Practices](https://www.w3.org/WAI/ARIA/apg/)
|
||||
- [Deque axe Rules](https://dequeuniversity.com/rules/axe/)
|
||||
- [Web Quality Audit](../web-quality-audit/SKILL.md)
|
||||
- [WCAG criteria reference](references/WCAG.md)
|
||||
- [Accessibility code patterns](references/A11Y-PATTERNS.md)
|
||||
233
.agents/skills/accessibility/references/A11Y-PATTERNS.md
Normal file
233
.agents/skills/accessibility/references/A11Y-PATTERNS.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# Accessibility Code Patterns
|
||||
|
||||
Practical, copy-paste-ready patterns for common accessibility requirements. Each pattern is self-contained and linked from the main [SKILL.md](../SKILL.md).
|
||||
|
||||
---
|
||||
|
||||
## Modal focus trap
|
||||
|
||||
Trap keyboard focus inside a modal dialog so Tab/Shift+Tab cycle through its focusable elements and Escape closes it.
|
||||
|
||||
```javascript
|
||||
function openModal(modal) {
|
||||
const focusableElements = modal.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
modal.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Tab') {
|
||||
if (e.shiftKey && document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
firstElement.focus();
|
||||
}
|
||||
```
|
||||
|
||||
The native `<dialog>` element handles focus trapping automatically—prefer it when browser support allows.
|
||||
|
||||
---
|
||||
|
||||
## Skip link
|
||||
|
||||
Allows keyboard users to bypass repetitive navigation and jump straight to main content.
|
||||
|
||||
```html
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Skip to main content</a>
|
||||
<header><!-- navigation --></header>
|
||||
<main id="main-content" tabindex="-1">
|
||||
<!-- main content -->
|
||||
</main>
|
||||
</body>
|
||||
```
|
||||
|
||||
```css
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 0;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 8px 16px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error handling
|
||||
|
||||
Announce errors to screen readers and focus the first invalid field on submit.
|
||||
|
||||
```html
|
||||
<form novalidate>
|
||||
<div class="field" aria-live="polite">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email"
|
||||
aria-invalid="true"
|
||||
aria-describedby="email-error">
|
||||
<p id="email-error" class="error" role="alert">
|
||||
Please enter a valid email address (e.g., name@example.com)
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
```javascript
|
||||
form.addEventListener('submit', (e) => {
|
||||
const firstError = form.querySelector('[aria-invalid="true"]');
|
||||
if (firstError) {
|
||||
e.preventDefault();
|
||||
firstError.focus();
|
||||
|
||||
const errorSummary = document.getElementById('error-summary');
|
||||
errorSummary.textContent =
|
||||
`${errors.length} errors found. Please fix them and try again.`;
|
||||
errorSummary.focus();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Form labels
|
||||
|
||||
Every input needs an associated label—either explicit (`for`/`id`) or implicit (wrapping `<label>`).
|
||||
|
||||
```html
|
||||
<!-- ❌ No label association -->
|
||||
<input type="email" placeholder="Email">
|
||||
|
||||
<!-- ✅ Explicit label -->
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email"
|
||||
autocomplete="email" required>
|
||||
|
||||
<!-- ✅ Implicit label -->
|
||||
<label>
|
||||
Email address
|
||||
<input type="email" name="email" autocomplete="email" required>
|
||||
</label>
|
||||
|
||||
<!-- ✅ With instructions -->
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password"
|
||||
aria-describedby="password-requirements">
|
||||
<p id="password-requirements">
|
||||
Must be at least 8 characters with one number.
|
||||
</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dragging movements
|
||||
|
||||
Any action triggered by dragging must offer a single-pointer alternative (WCAG 2.5.7).
|
||||
|
||||
```html
|
||||
<!-- ❌ Drag-only reorder -->
|
||||
<ul class="sortable-list" draggable="true">
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
</ul>
|
||||
|
||||
<!-- ✅ Drag + button alternatives -->
|
||||
<ul class="sortable-list">
|
||||
<li>
|
||||
<span>Item 1</span>
|
||||
<button aria-label="Move Item 1 up">↑</button>
|
||||
<button aria-label="Move Item 1 down">↓</button>
|
||||
</li>
|
||||
<li>
|
||||
<span>Item 2</span>
|
||||
<button aria-label="Move Item 2 up">↑</button>
|
||||
<button aria-label="Move Item 2 down">↓</button>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Also applies to sliders, map panning, colour pickers, and similar drag-based widgets—always provide an equivalent click/tap or keyboard path.
|
||||
|
||||
---
|
||||
|
||||
## ARIA tabs
|
||||
|
||||
Tabs require `role="tablist"`, `role="tab"`, and `role="tabpanel"` with proper `aria-selected`, `aria-controls`, and keyboard support.
|
||||
|
||||
```html
|
||||
<div role="tablist" aria-label="Product information">
|
||||
<button role="tab" id="tab-1" aria-selected="true"
|
||||
aria-controls="panel-1">Description</button>
|
||||
<button role="tab" id="tab-2" aria-selected="false"
|
||||
aria-controls="panel-2" tabindex="-1">Reviews</button>
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
|
||||
<!-- Panel content -->
|
||||
</div>
|
||||
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
|
||||
<!-- Panel content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
Arrow keys should move focus between tabs; the active tab receives `tabindex="0"` while inactive tabs use `tabindex="-1"`.
|
||||
|
||||
---
|
||||
|
||||
## Live regions and notifications
|
||||
|
||||
Use `aria-live` to announce dynamic content changes to screen readers without moving focus.
|
||||
|
||||
```html
|
||||
<!-- Status updates (polite — waits for pause in speech) -->
|
||||
<div aria-live="polite" aria-atomic="true" class="status">
|
||||
<!-- Content updates announced to screen readers -->
|
||||
</div>
|
||||
|
||||
<!-- Urgent alerts (assertive — interrupts) -->
|
||||
<div role="alert" aria-live="assertive">
|
||||
<!-- Interrupts current announcement -->
|
||||
</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
function showNotification(message, type = 'polite') {
|
||||
const container = document.getElementById(`${type}-announcer`);
|
||||
container.textContent = '';
|
||||
requestAnimationFrame(() => {
|
||||
container.textContent = message;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Clear the container before writing to ensure the same message triggers a new announcement.
|
||||
|
||||
---
|
||||
|
||||
## Screen reader commands
|
||||
|
||||
Quick reference for the most common screen reader shortcuts.
|
||||
|
||||
| Action | VoiceOver (Mac) | NVDA (Windows) |
|
||||
|--------|-----------------|----------------|
|
||||
| Start/Stop | ⌘ + F5 | Ctrl + Alt + N |
|
||||
| Next item | VO + → | ↓ |
|
||||
| Previous item | VO + ← | ↑ |
|
||||
| Activate | VO + Space | Enter |
|
||||
| Headings list | VO + U, then arrows | H / Shift + H |
|
||||
| Links list | VO + U | K / Shift + K |
|
||||
191
.agents/skills/accessibility/references/WCAG.md
Normal file
191
.agents/skills/accessibility/references/WCAG.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# WCAG 2.2 Quick Reference
|
||||
|
||||
## Success criteria by level
|
||||
|
||||
### Level A (minimum)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.1.1** Non-text Content | All images, icons have text alternatives |
|
||||
| **1.2.1** Audio-only/Video-only | Provide transcript or audio description |
|
||||
| **1.2.2** Captions | Video with audio has captions |
|
||||
| **1.2.3** Audio Description | Video has audio description |
|
||||
| **1.3.1** Info and Relationships | Information conveyed through presentation is available programmatically |
|
||||
| **1.3.2** Meaningful Sequence | Reading order is logical |
|
||||
| **1.3.3** Sensory Characteristics | Instructions don't rely solely on shape, color, size, location, orientation, or sound |
|
||||
| **1.4.1** Use of Color | Color is not the only visual means of conveying information |
|
||||
| **1.4.2** Audio Control | Audio playing automatically can be paused/stopped |
|
||||
| **2.1.1** Keyboard | All functionality available via keyboard |
|
||||
| **2.1.2** No Keyboard Trap | Keyboard focus can be moved away from any component |
|
||||
| **2.1.4** Character Key Shortcuts | Single-key shortcuts can be turned off or remapped |
|
||||
| **2.2.1** Timing Adjustable | Time limits can be extended |
|
||||
| **2.2.2** Pause, Stop, Hide | Moving/blinking content can be paused |
|
||||
| **2.3.1** Three Flashes | Nothing flashes more than 3 times per second |
|
||||
| **2.4.1** Bypass Blocks | Skip link or landmark navigation available |
|
||||
| **2.4.2** Page Titled | Pages have descriptive titles |
|
||||
| **2.4.3** Focus Order | Focus order preserves meaning |
|
||||
| **2.4.4** Link Purpose | Link purpose clear from link text or context |
|
||||
| **2.5.1** Pointer Gestures | Multi-point gestures have single-pointer alternatives |
|
||||
| **2.5.2** Pointer Cancellation | Down-event doesn't trigger action (use up-event or click) |
|
||||
| **2.5.3** Label in Name | Accessible name contains visible label text |
|
||||
| **2.5.4** Motion Actuation | Motion-triggered functions have alternatives |
|
||||
| **3.1.1** Language of Page | Default language specified in HTML |
|
||||
| **3.2.1** On Focus | Focus doesn't trigger unexpected changes |
|
||||
| **3.2.2** On Input | Input doesn't trigger unexpected changes |
|
||||
| **3.2.6** Consistent Help | Help mechanisms appear in the same relative order across pages |
|
||||
| **3.3.1** Error Identification | Input errors clearly described |
|
||||
| **3.3.2** Labels or Instructions | Form inputs have labels or instructions |
|
||||
| **3.3.7** Redundant Entry | Information previously entered is auto-populated or available to select |
|
||||
| **4.1.2** Name, Role, Value | UI components have accessible names and correct roles |
|
||||
|
||||
### Level AA (standard)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.2.4** Captions (Live) | Live audio has captions |
|
||||
| **1.2.5** Audio Description | Pre-recorded video has audio description |
|
||||
| **1.3.4** Orientation | Content doesn't restrict orientation |
|
||||
| **1.3.5** Identify Input Purpose | Input purpose can be programmatically determined |
|
||||
| **1.4.3** Contrast (Minimum) | 4.5:1 for normal text, 3:1 for large text |
|
||||
| **1.4.4** Resize Text | Text can be resized to 200% without loss of functionality |
|
||||
| **1.4.5** Images of Text | Text used instead of images of text |
|
||||
| **1.4.10** Reflow | Content reflows at 320px width without horizontal scroll |
|
||||
| **1.4.11** Non-text Contrast | UI components have 3:1 contrast |
|
||||
| **1.4.12** Text Spacing | Content adapts to text spacing changes |
|
||||
| **1.4.13** Content on Hover/Focus | Additional content is dismissible, hoverable, persistent |
|
||||
| **2.4.5** Multiple Ways | Multiple ways to find pages |
|
||||
| **2.4.6** Headings and Labels | Headings and labels are descriptive |
|
||||
| **2.4.7** Focus Visible | Focus indicator is visible |
|
||||
| **2.4.11** Focus Not Obscured (Minimum) | Focused element is not entirely hidden by author-created content |
|
||||
| **2.5.7** Dragging Movements | Dragging actions have single-pointer alternatives |
|
||||
| **2.5.8** Target Size (Minimum) | Interactive targets are at least 24×24 CSS pixels (with exceptions) |
|
||||
| **3.1.2** Language of Parts | Language changes are marked |
|
||||
| **3.2.3** Consistent Navigation | Navigation is consistent across pages |
|
||||
| **3.2.4** Consistent Identification | Same functionality uses same labels |
|
||||
| **3.3.3** Error Suggestion | Error corrections suggested when known |
|
||||
| **3.3.4** Error Prevention (Legal) | Actions can be reversed or confirmed |
|
||||
| **3.3.8** Accessible Authentication (Minimum) | No cognitive function test for login unless an alternative or assistance is provided |
|
||||
| **4.1.3** Status Messages | Status messages announced to screen readers |
|
||||
|
||||
### Level AAA (enhanced)
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **1.4.6** Contrast (Enhanced) | 7:1 for normal text, 4.5:1 for large text |
|
||||
| **1.4.8** Visual Presentation | Foreground/background colors can be selected |
|
||||
| **1.4.9** Images of Text (No Exception) | No images of text |
|
||||
| **2.1.3** Keyboard (No Exception) | All functionality keyboard accessible |
|
||||
| **2.2.3** No Timing | No time limits |
|
||||
| **2.2.4** Interruptions | Interruptions can be postponed |
|
||||
| **2.2.5** Re-authenticating | Data preserved on re-authentication |
|
||||
| **2.2.6** Timeouts | Users warned about data loss from inactivity |
|
||||
| **2.3.2** Three Flashes | No content flashes more than 3 times |
|
||||
| **2.3.3** Animation from Interactions | Motion animation can be disabled |
|
||||
| **2.4.8** Location | User location within site is available |
|
||||
| **2.4.9** Link Purpose (Link Only) | Link purpose clear from link text alone |
|
||||
| **2.4.10** Section Headings | Sections have headings |
|
||||
| **2.4.12** Focus Not Obscured (Enhanced) | No part of the focused element is hidden by author-created content |
|
||||
| **2.4.13** Focus Appearance | Focus indicator has sufficient area, contrast, and is not obscured |
|
||||
| **3.1.3** Unusual Words | Definitions available for unusual words |
|
||||
| **3.1.4** Abbreviations | Abbreviations expanded |
|
||||
| **3.1.5** Reading Level | Alternative content for complex text |
|
||||
| **3.1.6** Pronunciation | Pronunciation available where needed |
|
||||
| **3.2.5** Change on Request | Changes initiated only by user |
|
||||
| **3.3.5** Help | Context-sensitive help available |
|
||||
| **3.3.6** Error Prevention (All) | All form submissions can be reviewed |
|
||||
| **3.3.9** Accessible Authentication (Enhanced) | No cognitive function test for login (no object or personal content recognition exceptions) |
|
||||
|
||||
## Common ARIA patterns
|
||||
|
||||
### Buttons
|
||||
```html
|
||||
<button>Label</button>
|
||||
<!-- or -->
|
||||
<button aria-label="Close dialog">×</button>
|
||||
```
|
||||
|
||||
### Links
|
||||
```html
|
||||
<a href="/page">Descriptive link text</a>
|
||||
<!-- External links -->
|
||||
<a href="https://external.com" target="_blank" rel="noopener">
|
||||
External site
|
||||
<span class="visually-hidden">(opens in new tab)</span>
|
||||
</a>
|
||||
```
|
||||
|
||||
### Form fields
|
||||
```html
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" aria-describedby="email-hint">
|
||||
<p id="email-hint">We'll never share your email.</p>
|
||||
```
|
||||
|
||||
### Error states
|
||||
```html
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" aria-invalid="true" aria-describedby="email-error">
|
||||
<p id="email-error" role="alert">Please enter a valid email address.</p>
|
||||
```
|
||||
|
||||
### Navigation
|
||||
```html
|
||||
<nav aria-label="Main">
|
||||
<ul>
|
||||
<li><a href="/" aria-current="page">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Modals
|
||||
```html
|
||||
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
|
||||
<h2 id="dialog-title">Confirm Action</h2>
|
||||
<!-- content -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Live regions
|
||||
```html
|
||||
<!-- Polite (waits for pause in speech) -->
|
||||
<div aria-live="polite">Status update here</div>
|
||||
|
||||
<!-- Assertive (interrupts immediately) -->
|
||||
<div aria-live="assertive" role="alert">Error message here</div>
|
||||
|
||||
<!-- Status (polite, implicit) -->
|
||||
<div role="status">Loading complete</div>
|
||||
```
|
||||
|
||||
## What changed from 2.1 to 2.2
|
||||
|
||||
| Change | Criterion | Level |
|
||||
|--------|-----------|-------|
|
||||
| **Removed** | 4.1.1 Parsing | A |
|
||||
| **Added** | 2.4.11 Focus Not Obscured (Minimum) | AA |
|
||||
| **Added** | 2.4.12 Focus Not Obscured (Enhanced) | AAA |
|
||||
| **Added** | 2.4.13 Focus Appearance | AAA |
|
||||
| **Added** | 2.5.7 Dragging Movements | AA |
|
||||
| **Added** | 2.5.8 Target Size (Minimum) | AA |
|
||||
| **Added** | 3.2.6 Consistent Help | A |
|
||||
| **Added** | 3.3.7 Redundant Entry | A |
|
||||
| **Added** | 3.3.8 Accessible Authentication (Minimum) | AA |
|
||||
| **Added** | 3.3.9 Accessible Authentication (Enhanced) | AAA |
|
||||
|
||||
## Testing tools
|
||||
|
||||
| Tool | Type | URL |
|
||||
|------|------|-----|
|
||||
| axe DevTools | Browser extension | [deque.com/axe](https://www.deque.com/axe/) |
|
||||
| WAVE | Browser extension | [wave.webaim.org](https://wave.webaim.org/) |
|
||||
| Lighthouse | Built into Chrome | DevTools → Lighthouse |
|
||||
| NVDA | Screen reader (Windows) | [nvaccess.org](https://www.nvaccess.org/) |
|
||||
| VoiceOver | Screen reader (Mac) | Built into macOS |
|
||||
| Colour Contrast Analyser | Desktop app | [tpgi.com](https://www.tpgi.com/color-contrast-checker/) |
|
||||
|
||||
## Sources
|
||||
|
||||
- [WCAG 2.2 W3C Recommendation](https://www.w3.org/TR/WCAG22/)
|
||||
- [WCAG 2.2 Quick Reference](https://www.w3.org/WAI/WCAG22/quickref/)
|
||||
- [What's New in WCAG 2.2](https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/)
|
||||
177
.agents/skills/frontend-design/LICENSE.txt
Normal file
177
.agents/skills/frontend-design/LICENSE.txt
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
42
.agents/skills/frontend-design/SKILL.md
Normal file
42
.agents/skills/frontend-design/SKILL.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
639
.agents/skills/nodejs-backend-patterns/SKILL.md
Normal file
639
.agents/skills/nodejs-backend-patterns/SKILL.md
Normal file
@@ -0,0 +1,639 @@
|
||||
---
|
||||
name: nodejs-backend-patterns
|
||||
description: Build production-ready Node.js backend services with Express/Fastify, implementing middleware patterns, error handling, authentication, database integration, and API design best practices. Use when creating Node.js servers, REST APIs, GraphQL backends, or microservices architectures.
|
||||
---
|
||||
|
||||
# Node.js Backend Patterns
|
||||
|
||||
Comprehensive guidance for building scalable, maintainable, and production-ready Node.js backend applications with modern frameworks, architectural patterns, and best practices.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Building REST APIs or GraphQL servers
|
||||
- Creating microservices with Node.js
|
||||
- Implementing authentication and authorization
|
||||
- Designing scalable backend architectures
|
||||
- Setting up middleware and error handling
|
||||
- Integrating databases (SQL and NoSQL)
|
||||
- Building real-time applications with WebSockets
|
||||
- Implementing background job processing
|
||||
|
||||
## Core Frameworks
|
||||
|
||||
### Express.js - Minimalist Framework
|
||||
|
||||
**Basic Setup:**
|
||||
|
||||
```typescript
|
||||
import express, { Request, Response, NextFunction } from "express";
|
||||
import helmet from "helmet";
|
||||
import cors from "cors";
|
||||
import compression from "compression";
|
||||
|
||||
const app = express();
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(",") }));
|
||||
app.use(compression());
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
||||
|
||||
// Request logging
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
console.log(`${req.method} ${req.path}`);
|
||||
next();
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Fastify - High Performance Framework
|
||||
|
||||
**Basic Setup:**
|
||||
|
||||
```typescript
|
||||
import Fastify from "fastify";
|
||||
import helmet from "@fastify/helmet";
|
||||
import cors from "@fastify/cors";
|
||||
import compress from "@fastify/compress";
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: {
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: { colorize: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Plugins
|
||||
await fastify.register(helmet);
|
||||
await fastify.register(cors, { origin: true });
|
||||
await fastify.register(compress);
|
||||
|
||||
// Type-safe routes with schema validation
|
||||
fastify.post<{
|
||||
Body: { name: string; email: string };
|
||||
Reply: { id: string; name: string };
|
||||
}>(
|
||||
"/users",
|
||||
{
|
||||
schema: {
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["name", "email"],
|
||||
properties: {
|
||||
name: { type: "string", minLength: 1 },
|
||||
email: { type: "string", format: "email" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { name, email } = request.body;
|
||||
return { id: "123", name };
|
||||
},
|
||||
);
|
||||
|
||||
await fastify.listen({ port: 3000, host: "0.0.0.0" });
|
||||
```
|
||||
|
||||
## Architectural Patterns
|
||||
|
||||
### Pattern 1: Layered Architecture
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
src/
|
||||
├── controllers/ # Handle HTTP requests/responses
|
||||
├── services/ # Business logic
|
||||
├── repositories/ # Data access layer
|
||||
├── models/ # Data models
|
||||
├── middleware/ # Express/Fastify middleware
|
||||
├── routes/ # Route definitions
|
||||
├── utils/ # Helper functions
|
||||
├── config/ # Configuration
|
||||
└── types/ # TypeScript types
|
||||
```
|
||||
|
||||
**Controller Layer:**
|
||||
|
||||
```typescript
|
||||
// controllers/user.controller.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { UserService } from "../services/user.service";
|
||||
import { CreateUserDTO, UpdateUserDTO } from "../types/user.types";
|
||||
|
||||
export class UserController {
|
||||
constructor(private userService: UserService) {}
|
||||
|
||||
async createUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const userData: CreateUserDTO = req.body;
|
||||
const user = await this.userService.createUser(userData);
|
||||
res.status(201).json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const user = await this.userService.getUserById(id);
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updates: UpdateUserDTO = req.body;
|
||||
const user = await this.userService.updateUser(id, updates);
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await this.userService.deleteUser(id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Service Layer:**
|
||||
|
||||
```typescript
|
||||
// services/user.service.ts
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
import { CreateUserDTO, UpdateUserDTO, User } from "../types/user.types";
|
||||
import { NotFoundError, ValidationError } from "../utils/errors";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
export class UserService {
|
||||
constructor(private userRepository: UserRepository) {}
|
||||
|
||||
async createUser(userData: CreateUserDTO): Promise<User> {
|
||||
// Validation
|
||||
const existingUser = await this.userRepository.findByEmail(userData.email);
|
||||
if (existingUser) {
|
||||
throw new ValidationError("Email already exists");
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(userData.password, 10);
|
||||
|
||||
// Create user
|
||||
const user = await this.userRepository.create({
|
||||
...userData,
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
// Remove password from response
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword as User;
|
||||
}
|
||||
|
||||
async getUserById(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findById(id);
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword as User;
|
||||
}
|
||||
|
||||
async updateUser(id: string, updates: UpdateUserDTO): Promise<User> {
|
||||
const user = await this.userRepository.update(id, updates);
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword as User;
|
||||
}
|
||||
|
||||
async deleteUser(id: string): Promise<void> {
|
||||
const deleted = await this.userRepository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Repository Layer:**
|
||||
|
||||
```typescript
|
||||
// repositories/user.repository.ts
|
||||
import { Pool } from "pg";
|
||||
import { CreateUserDTO, UpdateUserDTO, UserEntity } from "../types/user.types";
|
||||
|
||||
export class UserRepository {
|
||||
constructor(private db: Pool) {}
|
||||
|
||||
async create(
|
||||
userData: CreateUserDTO & { password: string },
|
||||
): Promise<UserEntity> {
|
||||
const query = `
|
||||
INSERT INTO users (name, email, password)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, name, email, password, created_at, updated_at
|
||||
`;
|
||||
const { rows } = await this.db.query(query, [
|
||||
userData.name,
|
||||
userData.email,
|
||||
userData.password,
|
||||
]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<UserEntity | null> {
|
||||
const query = "SELECT * FROM users WHERE id = $1";
|
||||
const { rows } = await this.db.query(query, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<UserEntity | null> {
|
||||
const query = "SELECT * FROM users WHERE email = $1";
|
||||
const { rows } = await this.db.query(query, [email]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async update(id: string, updates: UpdateUserDTO): Promise<UserEntity | null> {
|
||||
const fields = Object.keys(updates);
|
||||
const values = Object.values(updates);
|
||||
|
||||
const setClause = fields
|
||||
.map((field, idx) => `${field} = $${idx + 2}`)
|
||||
.join(", ");
|
||||
|
||||
const query = `
|
||||
UPDATE users
|
||||
SET ${setClause}, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const { rows } = await this.db.query(query, [id, ...values]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<boolean> {
|
||||
const query = "DELETE FROM users WHERE id = $1";
|
||||
const { rowCount } = await this.db.query(query, [id]);
|
||||
return rowCount > 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Dependency Injection
|
||||
|
||||
Use a DI container to wire up repositories, services, and controllers. For a full container implementation, see [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
|
||||
## Middleware Patterns
|
||||
|
||||
### Authentication Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/auth.middleware.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { UnauthorizedError } from "../utils/errors";
|
||||
|
||||
interface JWTPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: JWTPayload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const authenticate = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace("Bearer ", "");
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedError("No token provided");
|
||||
}
|
||||
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
|
||||
|
||||
req.user = payload;
|
||||
next();
|
||||
} catch (error) {
|
||||
next(new UnauthorizedError("Invalid token"));
|
||||
}
|
||||
};
|
||||
|
||||
export const authorize = (...roles: string[]) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return next(new UnauthorizedError("Not authenticated"));
|
||||
}
|
||||
|
||||
// Check if user has required role
|
||||
const hasRole = roles.some((role) => req.user?.roles?.includes(role));
|
||||
|
||||
if (!hasRole) {
|
||||
return next(new UnauthorizedError("Insufficient permissions"));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Validation Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/validation.middleware.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { AnyZodObject, ZodError } from "zod";
|
||||
import { ValidationError } from "../utils/errors";
|
||||
|
||||
export const validate = (schema: AnyZodObject) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await schema.parseAsync({
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
});
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const errors = error.errors.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
next(new ValidationError("Validation failed", errors));
|
||||
} else {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Usage with Zod
|
||||
import { z } from "zod";
|
||||
|
||||
const createUserSchema = z.object({
|
||||
body: z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
}),
|
||||
});
|
||||
|
||||
router.post("/users", validate(createUserSchema), userController.createUser);
|
||||
```
|
||||
|
||||
### Rate Limiting Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/rate-limit.middleware.ts
|
||||
import rateLimit from "express-rate-limit";
|
||||
import RedisStore from "rate-limit-redis";
|
||||
import Redis from "ioredis";
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT || "6379"),
|
||||
});
|
||||
|
||||
export const apiLimiter = rateLimit({
|
||||
store: new RedisStore({
|
||||
client: redis,
|
||||
prefix: "rl:",
|
||||
}),
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // Limit each IP to 100 requests per windowMs
|
||||
message: "Too many requests from this IP, please try again later",
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
export const authLimiter = rateLimit({
|
||||
store: new RedisStore({
|
||||
client: redis,
|
||||
prefix: "rl:auth:",
|
||||
}),
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5, // Stricter limit for auth endpoints
|
||||
skipSuccessfulRequests: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Request Logging Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/logger.middleware.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import pino from "pino";
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: { colorize: true },
|
||||
},
|
||||
});
|
||||
|
||||
export const requestLogger = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
const start = Date.now();
|
||||
|
||||
// Log response when finished
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
logger.info({
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
status: res.statusCode,
|
||||
duration: `${duration}ms`,
|
||||
userAgent: req.headers["user-agent"],
|
||||
ip: req.ip,
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
export { logger };
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Custom Error Classes
|
||||
|
||||
```typescript
|
||||
// utils/errors.ts
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
public message: string,
|
||||
public statusCode: number = 500,
|
||||
public isOperational: boolean = true,
|
||||
) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, AppError.prototype);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends AppError {
|
||||
constructor(
|
||||
message: string,
|
||||
public errors?: any[],
|
||||
) {
|
||||
super(message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends AppError {
|
||||
constructor(message: string = "Resource not found") {
|
||||
super(message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends AppError {
|
||||
constructor(message: string = "Unauthorized") {
|
||||
super(message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError extends AppError {
|
||||
constructor(message: string = "Forbidden") {
|
||||
super(message, 403);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError extends AppError {
|
||||
constructor(message: string) {
|
||||
super(message, 409);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Global Error Handler
|
||||
|
||||
```typescript
|
||||
// middleware/error-handler.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { AppError } from "../utils/errors";
|
||||
import { logger } from "./logger.middleware";
|
||||
|
||||
export const errorHandler = (
|
||||
err: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
if (err instanceof AppError) {
|
||||
return res.status(err.statusCode).json({
|
||||
status: "error",
|
||||
message: err.message,
|
||||
...(err instanceof ValidationError && { errors: err.errors }),
|
||||
});
|
||||
}
|
||||
|
||||
// Log unexpected errors
|
||||
logger.error({
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
});
|
||||
|
||||
// Don't leak error details in production
|
||||
const message =
|
||||
process.env.NODE_ENV === "production"
|
||||
? "Internal server error"
|
||||
: err.message;
|
||||
|
||||
res.status(500).json({
|
||||
status: "error",
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
// Async error wrapper
|
||||
export const asyncHandler = (
|
||||
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>,
|
||||
) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Database Patterns
|
||||
|
||||
Node.js supports both SQL and NoSQL databases. Use connection pooling for all production databases.
|
||||
|
||||
Key patterns covered in [references/advanced-patterns.md](references/advanced-patterns.md):
|
||||
- **PostgreSQL with connection pool** — `pg` Pool configuration and graceful shutdown
|
||||
- **MongoDB with Mongoose** — connection management and schema definition
|
||||
- **Transaction pattern** — `BEGIN`/`COMMIT`/`ROLLBACK` with `pg` client
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
JWT-based auth with access tokens (short-lived, 15m) and refresh tokens (7d). Full `AuthService` implementation with `bcrypt` password comparison in [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
Redis-backed `CacheService` with get/set/delete/invalidatePattern, plus a `@Cacheable` decorator for method-level caching. See [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
|
||||
## API Response Format
|
||||
|
||||
Standardized `ApiResponse` helper with `success`, `error`, and `paginated` static methods. See [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use TypeScript**: Type safety prevents runtime errors
|
||||
2. **Implement proper error handling**: Use custom error classes
|
||||
3. **Validate input**: Use libraries like Zod or Joi
|
||||
4. **Use environment variables**: Never hardcode secrets
|
||||
5. **Implement logging**: Use structured logging (Pino, Winston)
|
||||
6. **Add rate limiting**: Prevent abuse
|
||||
7. **Use HTTPS**: Always in production
|
||||
8. **Implement CORS properly**: Don't use `*` in production
|
||||
9. **Use dependency injection**: Easier testing and maintenance
|
||||
10. **Write tests**: Unit, integration, and E2E tests
|
||||
11. **Handle graceful shutdown**: Clean up resources
|
||||
12. **Use connection pooling**: For databases
|
||||
13. **Implement health checks**: For monitoring
|
||||
14. **Use compression**: Reduce response size
|
||||
15. **Monitor performance**: Use APM tools
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
See `javascript-testing-patterns` skill for comprehensive testing guidance.
|
||||
@@ -0,0 +1,430 @@
|
||||
# Node.js Advanced Patterns
|
||||
|
||||
Advanced patterns for dependency injection, database integration, authentication, caching, and API response formatting.
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
### DI Container
|
||||
|
||||
```typescript
|
||||
// di-container.ts
|
||||
import { Pool } from "pg";
|
||||
import { UserRepository } from "./repositories/user.repository";
|
||||
import { UserService } from "./services/user.service";
|
||||
import { UserController } from "./controllers/user.controller";
|
||||
import { AuthService } from "./services/auth.service";
|
||||
|
||||
class Container {
|
||||
private instances = new Map<string, any>();
|
||||
|
||||
register<T>(key: string, factory: () => T): void {
|
||||
this.instances.set(key, factory);
|
||||
}
|
||||
|
||||
resolve<T>(key: string): T {
|
||||
const factory = this.instances.get(key);
|
||||
if (!factory) {
|
||||
throw new Error(`No factory registered for ${key}`);
|
||||
}
|
||||
return factory();
|
||||
}
|
||||
|
||||
singleton<T>(key: string, factory: () => T): void {
|
||||
let instance: T;
|
||||
this.instances.set(key, () => {
|
||||
if (!instance) {
|
||||
instance = factory();
|
||||
}
|
||||
return instance;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const container = new Container();
|
||||
|
||||
// Register dependencies
|
||||
container.singleton(
|
||||
"db",
|
||||
() =>
|
||||
new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT || "5432"),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
}),
|
||||
);
|
||||
|
||||
container.singleton(
|
||||
"userRepository",
|
||||
() => new UserRepository(container.resolve("db")),
|
||||
);
|
||||
|
||||
container.singleton(
|
||||
"userService",
|
||||
() => new UserService(container.resolve("userRepository")),
|
||||
);
|
||||
|
||||
container.register(
|
||||
"userController",
|
||||
() => new UserController(container.resolve("userService")),
|
||||
);
|
||||
|
||||
container.singleton(
|
||||
"authService",
|
||||
() => new AuthService(container.resolve("userRepository")),
|
||||
);
|
||||
```
|
||||
|
||||
## Database Patterns
|
||||
|
||||
### PostgreSQL with Connection Pool
|
||||
|
||||
```typescript
|
||||
// config/database.ts
|
||||
import { Pool, PoolConfig } from "pg";
|
||||
|
||||
const poolConfig: PoolConfig = {
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT || "5432"),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
};
|
||||
|
||||
export const pool = new Pool(poolConfig);
|
||||
|
||||
// Test connection
|
||||
pool.on("connect", () => {
|
||||
console.log("Database connected");
|
||||
});
|
||||
|
||||
pool.on("error", (err) => {
|
||||
console.error("Unexpected database error", err);
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
export const closeDatabase = async () => {
|
||||
await pool.end();
|
||||
console.log("Database connection closed");
|
||||
};
|
||||
```
|
||||
|
||||
### MongoDB with Mongoose
|
||||
|
||||
```typescript
|
||||
// config/mongoose.ts
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
await mongoose.connect(process.env.MONGODB_URI!, {
|
||||
maxPoolSize: 10,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 45000,
|
||||
});
|
||||
|
||||
console.log("MongoDB connected");
|
||||
} catch (error) {
|
||||
console.error("MongoDB connection error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
mongoose.connection.on("disconnected", () => {
|
||||
console.log("MongoDB disconnected");
|
||||
});
|
||||
|
||||
mongoose.connection.on("error", (err) => {
|
||||
console.error("MongoDB error:", err);
|
||||
});
|
||||
|
||||
export { connectDB };
|
||||
|
||||
// Model example
|
||||
import { Schema, model, Document } from "mongoose";
|
||||
|
||||
interface IUser extends Document {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUser>(
|
||||
{
|
||||
name: { type: String, required: true },
|
||||
email: { type: String, required: true, unique: true },
|
||||
password: { type: String, required: true },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Indexes
|
||||
userSchema.index({ email: 1 });
|
||||
|
||||
export const User = model<IUser>("User", userSchema);
|
||||
```
|
||||
|
||||
### Transaction Pattern
|
||||
|
||||
```typescript
|
||||
// services/order.service.ts
|
||||
import { Pool } from "pg";
|
||||
|
||||
export class OrderService {
|
||||
constructor(private db: Pool) {}
|
||||
|
||||
async createOrder(userId: string, items: any[]) {
|
||||
const client = await this.db.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// Create order
|
||||
const orderResult = await client.query(
|
||||
"INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id",
|
||||
[userId, calculateTotal(items)],
|
||||
);
|
||||
const orderId = orderResult.rows[0].id;
|
||||
|
||||
// Create order items
|
||||
for (const item of items) {
|
||||
await client.query(
|
||||
"INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)",
|
||||
[orderId, item.productId, item.quantity, item.price],
|
||||
);
|
||||
|
||||
// Update inventory
|
||||
await client.query(
|
||||
"UPDATE products SET stock = stock - $1 WHERE id = $2",
|
||||
[item.quantity, item.productId],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return orderId;
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### JWT Authentication
|
||||
|
||||
```typescript
|
||||
// services/auth.service.ts
|
||||
import jwt from "jsonwebtoken";
|
||||
import bcrypt from "bcrypt";
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
import { UnauthorizedError } from "../utils/errors";
|
||||
|
||||
export class AuthService {
|
||||
constructor(private userRepository: UserRepository) {}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
const user = await this.userRepository.findByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedError("Invalid credentials");
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedError("Invalid credentials");
|
||||
}
|
||||
|
||||
const token = this.generateToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
const refreshToken = this.generateRefreshToken({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
refreshToken,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string) {
|
||||
try {
|
||||
const payload = jwt.verify(
|
||||
refreshToken,
|
||||
process.env.REFRESH_TOKEN_SECRET!,
|
||||
) as { userId: string };
|
||||
|
||||
const user = await this.userRepository.findById(payload.userId);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedError("User not found");
|
||||
}
|
||||
|
||||
const token = this.generateToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
return { token };
|
||||
} catch (error) {
|
||||
throw new UnauthorizedError("Invalid refresh token");
|
||||
}
|
||||
}
|
||||
|
||||
private generateToken(payload: any): string {
|
||||
return jwt.sign(payload, process.env.JWT_SECRET!, {
|
||||
expiresIn: "15m",
|
||||
});
|
||||
}
|
||||
|
||||
private generateRefreshToken(payload: any): string {
|
||||
return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
```typescript
|
||||
// utils/cache.ts
|
||||
import Redis from "ioredis";
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT || "6379"),
|
||||
retryStrategy: (times) => {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
return delay;
|
||||
},
|
||||
});
|
||||
|
||||
export class CacheService {
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const data = await redis.get(key);
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
async set(key: string, value: any, ttl?: number): Promise<void> {
|
||||
const serialized = JSON.stringify(value);
|
||||
if (ttl) {
|
||||
await redis.setex(key, ttl, serialized);
|
||||
} else {
|
||||
await redis.set(key, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
await redis.del(key);
|
||||
}
|
||||
|
||||
async invalidatePattern(pattern: string): Promise<void> {
|
||||
const keys = await redis.keys(pattern);
|
||||
if (keys.length > 0) {
|
||||
await redis.del(...keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache decorator
|
||||
export function Cacheable(ttl: number = 300) {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor,
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const cache = new CacheService();
|
||||
const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
|
||||
|
||||
const cached = await cache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const result = await originalMethod.apply(this, args);
|
||||
await cache.set(cacheKey, result, ttl);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Response Format
|
||||
|
||||
```typescript
|
||||
// utils/response.ts
|
||||
import { Response } from "express";
|
||||
|
||||
export class ApiResponse {
|
||||
static success<T>(
|
||||
res: Response,
|
||||
data: T,
|
||||
message?: string,
|
||||
statusCode = 200,
|
||||
) {
|
||||
return res.status(statusCode).json({
|
||||
status: "success",
|
||||
message,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
static error(res: Response, message: string, statusCode = 500, errors?: any) {
|
||||
return res.status(statusCode).json({
|
||||
status: "error",
|
||||
message,
|
||||
...(errors && { errors }),
|
||||
});
|
||||
}
|
||||
|
||||
static paginated<T>(
|
||||
res: Response,
|
||||
data: T[],
|
||||
page: number,
|
||||
limit: number,
|
||||
total: number,
|
||||
) {
|
||||
return res.json({
|
||||
status: "success",
|
||||
data,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
343
.agents/skills/nodejs-best-practices/SKILL.md
Normal file
343
.agents/skills/nodejs-best-practices/SKILL.md
Normal file
@@ -0,0 +1,343 @@
|
||||
---
|
||||
name: nodejs-best-practices
|
||||
description: "Node.js development principles and decision-making. Framework selection, async patterns, security, and architecture. Teaches thinking, not copying."
|
||||
risk: unknown
|
||||
source: community
|
||||
date_added: "2026-02-27"
|
||||
---
|
||||
|
||||
# Node.js Best Practices
|
||||
|
||||
> Principles and decision-making for Node.js development in 2025.
|
||||
> **Learn to THINK, not memorize code patterns.**
|
||||
|
||||
## When to Use
|
||||
Use this skill when making Node.js architecture decisions, choosing frameworks, designing async patterns, or applying security and deployment best practices.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ How to Use This Skill
|
||||
|
||||
This skill teaches **decision-making principles**, not fixed code to copy.
|
||||
|
||||
- ASK user for preferences when unclear
|
||||
- Choose framework/pattern based on CONTEXT
|
||||
- Don't default to same solution every time
|
||||
|
||||
---
|
||||
|
||||
## 1. Framework Selection (2025)
|
||||
|
||||
### Decision Tree
|
||||
|
||||
```
|
||||
What are you building?
|
||||
│
|
||||
├── Edge/Serverless (Cloudflare, Vercel)
|
||||
│ └── Hono (zero-dependency, ultra-fast cold starts)
|
||||
│
|
||||
├── High Performance API
|
||||
│ └── Fastify (2-3x faster than Express)
|
||||
│
|
||||
├── Enterprise/Team familiarity
|
||||
│ └── NestJS (structured, DI, decorators)
|
||||
│
|
||||
├── Legacy/Stable/Maximum ecosystem
|
||||
│ └── Express (mature, most middleware)
|
||||
│
|
||||
└── Full-stack with frontend
|
||||
└── Next.js API Routes or tRPC
|
||||
```
|
||||
|
||||
### Comparison Principles
|
||||
|
||||
| Factor | Hono | Fastify | Express |
|
||||
|--------|------|---------|---------|
|
||||
| **Best for** | Edge, serverless | Performance | Legacy, learning |
|
||||
| **Cold start** | Fastest | Fast | Moderate |
|
||||
| **Ecosystem** | Growing | Good | Largest |
|
||||
| **TypeScript** | Native | Excellent | Good |
|
||||
| **Learning curve** | Low | Medium | Low |
|
||||
|
||||
### Selection Questions to Ask:
|
||||
1. What's the deployment target?
|
||||
2. Is cold start time critical?
|
||||
3. Does team have existing experience?
|
||||
4. Is there legacy code to maintain?
|
||||
|
||||
---
|
||||
|
||||
## 2. Runtime Considerations (2025)
|
||||
|
||||
### Native TypeScript
|
||||
|
||||
```
|
||||
Node.js 22+: --experimental-strip-types
|
||||
├── Run .ts files directly
|
||||
├── No build step needed for simple projects
|
||||
└── Consider for: scripts, simple APIs
|
||||
```
|
||||
|
||||
### Module System Decision
|
||||
|
||||
```
|
||||
ESM (import/export)
|
||||
├── Modern standard
|
||||
├── Better tree-shaking
|
||||
├── Async module loading
|
||||
└── Use for: new projects
|
||||
|
||||
CommonJS (require)
|
||||
├── Legacy compatibility
|
||||
├── More npm packages support
|
||||
└── Use for: existing codebases, some edge cases
|
||||
```
|
||||
|
||||
### Runtime Selection
|
||||
|
||||
| Runtime | Best For |
|
||||
|---------|----------|
|
||||
| **Node.js** | General purpose, largest ecosystem |
|
||||
| **Bun** | Performance, built-in bundler |
|
||||
| **Deno** | Security-first, built-in TypeScript |
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture Principles
|
||||
|
||||
### Layered Structure Concept
|
||||
|
||||
```
|
||||
Request Flow:
|
||||
│
|
||||
├── Controller/Route Layer
|
||||
│ ├── Handles HTTP specifics
|
||||
│ ├── Input validation at boundary
|
||||
│ └── Calls service layer
|
||||
│
|
||||
├── Service Layer
|
||||
│ ├── Business logic
|
||||
│ ├── Framework-agnostic
|
||||
│ └── Calls repository layer
|
||||
│
|
||||
└── Repository Layer
|
||||
├── Data access only
|
||||
├── Database queries
|
||||
└── ORM interactions
|
||||
```
|
||||
|
||||
### Why This Matters:
|
||||
- **Testability**: Mock layers independently
|
||||
- **Flexibility**: Swap database without touching business logic
|
||||
- **Clarity**: Each layer has single responsibility
|
||||
|
||||
### When to Simplify:
|
||||
- Small scripts → Single file OK
|
||||
- Prototypes → Less structure acceptable
|
||||
- Always ask: "Will this grow?"
|
||||
|
||||
---
|
||||
|
||||
## 4. Error Handling Principles
|
||||
|
||||
### Centralized Error Handling
|
||||
|
||||
```
|
||||
Pattern:
|
||||
├── Create custom error classes
|
||||
├── Throw from any layer
|
||||
├── Catch at top level (middleware)
|
||||
└── Format consistent response
|
||||
```
|
||||
|
||||
### Error Response Philosophy
|
||||
|
||||
```
|
||||
Client gets:
|
||||
├── Appropriate HTTP status
|
||||
├── Error code for programmatic handling
|
||||
├── User-friendly message
|
||||
└── NO internal details (security!)
|
||||
|
||||
Logs get:
|
||||
├── Full stack trace
|
||||
├── Request context
|
||||
├── User ID (if applicable)
|
||||
└── Timestamp
|
||||
```
|
||||
|
||||
### Status Code Selection
|
||||
|
||||
| Situation | Status | When |
|
||||
|-----------|--------|------|
|
||||
| Bad input | 400 | Client sent invalid data |
|
||||
| No auth | 401 | Missing or invalid credentials |
|
||||
| No permission | 403 | Valid auth, but not allowed |
|
||||
| Not found | 404 | Resource doesn't exist |
|
||||
| Conflict | 409 | Duplicate or state conflict |
|
||||
| Validation | 422 | Schema valid but business rules fail |
|
||||
| Server error | 500 | Our fault, log everything |
|
||||
|
||||
---
|
||||
|
||||
## 5. Async Patterns Principles
|
||||
|
||||
### When to Use Each
|
||||
|
||||
| Pattern | Use When |
|
||||
|---------|----------|
|
||||
| `async/await` | Sequential async operations |
|
||||
| `Promise.all` | Parallel independent operations |
|
||||
| `Promise.allSettled` | Parallel where some can fail |
|
||||
| `Promise.race` | Timeout or first response wins |
|
||||
|
||||
### Event Loop Awareness
|
||||
|
||||
```
|
||||
I/O-bound (async helps):
|
||||
├── Database queries
|
||||
├── HTTP requests
|
||||
├── File system
|
||||
└── Network operations
|
||||
|
||||
CPU-bound (async doesn't help):
|
||||
├── Crypto operations
|
||||
├── Image processing
|
||||
├── Complex calculations
|
||||
└── → Use worker threads or offload
|
||||
```
|
||||
|
||||
### Avoiding Event Loop Blocking
|
||||
|
||||
- Never use sync methods in production (fs.readFileSync, etc.)
|
||||
- Offload CPU-intensive work
|
||||
- Use streaming for large data
|
||||
|
||||
---
|
||||
|
||||
## 6. Validation Principles
|
||||
|
||||
### Validate at Boundaries
|
||||
|
||||
```
|
||||
Where to validate:
|
||||
├── API entry point (request body/params)
|
||||
├── Before database operations
|
||||
├── External data (API responses, file uploads)
|
||||
└── Environment variables (startup)
|
||||
```
|
||||
|
||||
### Validation Library Selection
|
||||
|
||||
| Library | Best For |
|
||||
|---------|----------|
|
||||
| **Zod** | TypeScript first, inference |
|
||||
| **Valibot** | Smaller bundle (tree-shakeable) |
|
||||
| **ArkType** | Performance critical |
|
||||
| **Yup** | Existing React Form usage |
|
||||
|
||||
### Validation Philosophy
|
||||
|
||||
- Fail fast: Validate early
|
||||
- Be specific: Clear error messages
|
||||
- Don't trust: Even "internal" data
|
||||
|
||||
---
|
||||
|
||||
## 7. Security Principles
|
||||
|
||||
### Security Checklist (Not Code)
|
||||
|
||||
- [ ] **Input validation**: All inputs validated
|
||||
- [ ] **Parameterized queries**: No string concatenation for SQL
|
||||
- [ ] **Password hashing**: bcrypt or argon2
|
||||
- [ ] **JWT verification**: Always verify signature and expiry
|
||||
- [ ] **Rate limiting**: Protect from abuse
|
||||
- [ ] **Security headers**: Helmet.js or equivalent
|
||||
- [ ] **HTTPS**: Everywhere in production
|
||||
- [ ] **CORS**: Properly configured
|
||||
- [ ] **Secrets**: Environment variables only
|
||||
- [ ] **Dependencies**: Regularly audited
|
||||
|
||||
### Security Mindset
|
||||
|
||||
```
|
||||
Trust nothing:
|
||||
├── Query params → validate
|
||||
├── Request body → validate
|
||||
├── Headers → verify
|
||||
├── Cookies → validate
|
||||
├── File uploads → scan
|
||||
└── External APIs → validate response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Testing Principles
|
||||
|
||||
### Test Strategy Selection
|
||||
|
||||
| Type | Purpose | Tools |
|
||||
|------|---------|-------|
|
||||
| **Unit** | Business logic | node:test, Vitest |
|
||||
| **Integration** | API endpoints | Supertest |
|
||||
| **E2E** | Full flows | Playwright |
|
||||
|
||||
### What to Test (Priorities)
|
||||
|
||||
1. **Critical paths**: Auth, payments, core business
|
||||
2. **Edge cases**: Empty inputs, boundaries
|
||||
3. **Error handling**: What happens when things fail?
|
||||
4. **Not worth testing**: Framework code, trivial getters
|
||||
|
||||
### Built-in Test Runner (Node.js 22+)
|
||||
|
||||
```
|
||||
node --test src/**/*.test.ts
|
||||
├── No external dependency
|
||||
├── Good coverage reporting
|
||||
└── Watch mode available
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Anti-Patterns to Avoid
|
||||
|
||||
### ❌ DON'T:
|
||||
- Use Express for new edge projects (use Hono)
|
||||
- Use sync methods in production code
|
||||
- Put business logic in controllers
|
||||
- Skip input validation
|
||||
- Hardcode secrets
|
||||
- Trust external data without validation
|
||||
- Block event loop with CPU work
|
||||
|
||||
### ✅ DO:
|
||||
- Choose framework based on context
|
||||
- Ask user for preferences when unclear
|
||||
- Use layered architecture for growing projects
|
||||
- Validate all inputs
|
||||
- Use environment variables for secrets
|
||||
- Profile before optimizing
|
||||
|
||||
---
|
||||
|
||||
## 10. Decision Checklist
|
||||
|
||||
Before implementing:
|
||||
|
||||
- [ ] **Asked user about stack preference?**
|
||||
- [ ] **Chosen framework for THIS context?** (not just default)
|
||||
- [ ] **Considered deployment target?**
|
||||
- [ ] **Planned error handling strategy?**
|
||||
- [ ] **Identified validation points?**
|
||||
- [ ] **Considered security requirements?**
|
||||
|
||||
---
|
||||
|
||||
> **Remember**: Node.js best practices are about decision-making, not memorizing patterns. Every project deserves fresh consideration based on its requirements.
|
||||
|
||||
## Limitations
|
||||
- Use this skill only when the task clearly matches the scope described above.
|
||||
- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
|
||||
- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
|
||||
513
.agents/skills/seo/SKILL.md
Normal file
513
.agents/skills/seo/SKILL.md
Normal file
@@ -0,0 +1,513 @@
|
||||
---
|
||||
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: "1.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.
|
||||
|
||||
## SEO fundamentals
|
||||
|
||||
Search ranking factors (approximate influence):
|
||||
|
||||
| Factor | Influence | This Skill |
|
||||
|--------|-----------|------------|
|
||||
| Content quality & relevance | ~40% | Partial (structure) |
|
||||
| Backlinks & authority | ~25% | ✗ |
|
||||
| Technical SEO | ~15% | ✓ |
|
||||
| Page experience (Core Web Vitals) | ~10% | See [Core Web Vitals](../core-web-vitals/SKILL.md) |
|
||||
| On-page SEO | ~10% | ✓ |
|
||||
|
||||
---
|
||||
|
||||
## Technical SEO
|
||||
|
||||
### Crawlability
|
||||
|
||||
**robots.txt:**
|
||||
```text
|
||||
# /robots.txt
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Block admin/private areas
|
||||
Disallow: /admin/
|
||||
Disallow: /api/
|
||||
Disallow: /private/
|
||||
|
||||
# Don't block resources needed for rendering
|
||||
# ❌ Disallow: /static/
|
||||
|
||||
Sitemap: https://example.com/sitemap.xml
|
||||
```
|
||||
|
||||
**Meta robots:**
|
||||
```html
|
||||
<!-- Default: indexable, followable -->
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<!-- Noindex specific pages -->
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<!-- Indexable but don't follow links -->
|
||||
<meta name="robots" content="index, nofollow">
|
||||
|
||||
<!-- Control snippets -->
|
||||
<meta name="robots" content="max-snippet:150, max-image-preview:large">
|
||||
```
|
||||
|
||||
**Canonical URLs:**
|
||||
```html
|
||||
<!-- Prevent duplicate content issues -->
|
||||
<link rel="canonical" href="https://example.com/page">
|
||||
|
||||
<!-- Self-referencing canonical (recommended) -->
|
||||
<link rel="canonical" href="https://example.com/current-page">
|
||||
|
||||
<!-- For paginated content -->
|
||||
<link rel="canonical" href="https://example.com/products">
|
||||
<!-- Or use rel="prev" / rel="next" for explicit pagination -->
|
||||
```
|
||||
|
||||
### XML sitemap
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://example.com/</loc>
|
||||
<lastmod>2024-01-15</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://example.com/products</loc>
|
||||
<lastmod>2024-01-14</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
```
|
||||
|
||||
**Sitemap best practices:**
|
||||
- Maximum 50,000 URLs or 50MB per sitemap
|
||||
- Use sitemap index for larger sites
|
||||
- Include only canonical, indexable URLs
|
||||
- Update `lastmod` when content changes
|
||||
- Submit to Google Search Console
|
||||
|
||||
### URL structure
|
||||
|
||||
```
|
||||
✅ Good URLs:
|
||||
https://example.com/products/blue-widget
|
||||
https://example.com/blog/how-to-use-widgets
|
||||
|
||||
❌ Poor URLs:
|
||||
https://example.com/p?id=12345
|
||||
https://example.com/products/item/category/subcategory/blue-widget-2024-sale-discount
|
||||
```
|
||||
|
||||
**URL guidelines:**
|
||||
- Use hyphens, not underscores
|
||||
- Lowercase only
|
||||
- Keep short (< 75 characters)
|
||||
- Include target keywords naturally
|
||||
- Avoid parameters when possible
|
||||
- Use HTTPS always
|
||||
|
||||
### HTTPS & security
|
||||
|
||||
```html
|
||||
<!-- Ensure all resources use HTTPS -->
|
||||
<img src="https://example.com/image.jpg">
|
||||
|
||||
<!-- Not: -->
|
||||
<img src="http://example.com/image.jpg">
|
||||
```
|
||||
|
||||
**Security headers for SEO trust signals:**
|
||||
```
|
||||
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
||||
X-Content-Type-Options: nosniff
|
||||
X-Frame-Options: DENY
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## On-page SEO
|
||||
|
||||
### Title tags
|
||||
|
||||
```html
|
||||
<!-- ❌ Missing or generic -->
|
||||
<title>Page</title>
|
||||
<title>Home</title>
|
||||
|
||||
<!-- ✅ Descriptive with primary keyword -->
|
||||
<title>Blue Widgets for Sale | Premium Quality | Example Store</title>
|
||||
```
|
||||
|
||||
**Title tag guidelines:**
|
||||
- 50-60 characters (Google truncates ~60)
|
||||
- Primary keyword near the beginning
|
||||
- Unique for every page
|
||||
- Brand name at end (unless homepage)
|
||||
- Action-oriented when appropriate
|
||||
|
||||
### Meta descriptions
|
||||
|
||||
```html
|
||||
<!-- ❌ Missing or duplicate -->
|
||||
<meta name="description" content="">
|
||||
|
||||
<!-- ✅ Compelling and unique -->
|
||||
<meta name="description" content="Shop premium blue widgets with free shipping. 30-day returns. Rated 4.9/5 by 10,000+ customers. Order today and save 20%.">
|
||||
```
|
||||
|
||||
**Meta description guidelines:**
|
||||
- 150-160 characters
|
||||
- Include primary keyword naturally
|
||||
- Compelling call-to-action
|
||||
- Unique for every page
|
||||
- Matches page content
|
||||
|
||||
### Heading structure
|
||||
|
||||
```html
|
||||
<!-- ❌ Poor structure -->
|
||||
<h2>Welcome to Our Store</h2>
|
||||
<h4>Products</h4>
|
||||
<h1>Contact Us</h1>
|
||||
|
||||
<!-- ✅ Proper hierarchy -->
|
||||
<h1>Blue Widgets - Premium Quality</h1>
|
||||
<h2>Product Features</h2>
|
||||
<h3>Durability</h3>
|
||||
<h3>Design</h3>
|
||||
<h2>Customer Reviews</h2>
|
||||
<h2>Pricing</h2>
|
||||
```
|
||||
|
||||
**Heading guidelines:**
|
||||
- Single `<h1>` per page (the main topic)
|
||||
- Logical hierarchy (don't skip levels)
|
||||
- Include keywords naturally
|
||||
- Descriptive, not generic
|
||||
|
||||
### Image SEO
|
||||
|
||||
```html
|
||||
<!-- ❌ Poor image SEO -->
|
||||
<img src="IMG_12345.jpg">
|
||||
|
||||
<!-- ✅ Optimized image -->
|
||||
<img src="blue-widget-product-photo.webp"
|
||||
alt="Blue widget with chrome finish, side view showing control panel"
|
||||
width="800"
|
||||
height="600"
|
||||
loading="lazy">
|
||||
```
|
||||
|
||||
**Image guidelines:**
|
||||
- Descriptive filenames with keywords
|
||||
- Alt text describes the image content
|
||||
- Compressed and properly sized
|
||||
- WebP/AVIF with fallbacks
|
||||
- Lazy load below-fold images
|
||||
|
||||
### Internal linking
|
||||
|
||||
```html
|
||||
<!-- ❌ Non-descriptive -->
|
||||
<a href="/products">Click here</a>
|
||||
<a href="/widgets">Read more</a>
|
||||
|
||||
<!-- ✅ Descriptive anchor text -->
|
||||
<a href="/products/blue-widgets">Browse our blue widget collection</a>
|
||||
<a href="/guides/widget-maintenance">Learn how to maintain your widgets</a>
|
||||
```
|
||||
|
||||
**Linking guidelines:**
|
||||
- Descriptive anchor text with keywords
|
||||
- Link to relevant internal pages
|
||||
- Reasonable number of links per page
|
||||
- Fix broken links promptly
|
||||
- Use breadcrumbs for hierarchy
|
||||
|
||||
---
|
||||
|
||||
## Structured data (JSON-LD)
|
||||
|
||||
### Organization
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": "Example Company",
|
||||
"url": "https://example.com",
|
||||
"logo": "https://example.com/logo.png",
|
||||
"sameAs": [
|
||||
"https://twitter.com/example",
|
||||
"https://linkedin.com/company/example"
|
||||
],
|
||||
"contactPoint": {
|
||||
"@type": "ContactPoint",
|
||||
"telephone": "+1-555-123-4567",
|
||||
"contactType": "customer service"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Article
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": "How to Choose the Right Widget",
|
||||
"description": "Complete guide to selecting widgets for your needs.",
|
||||
"image": "https://example.com/article-image.jpg",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Jane Smith",
|
||||
"url": "https://example.com/authors/jane-smith"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Example Blog",
|
||||
"logo": {
|
||||
"@type": "ImageObject",
|
||||
"url": "https://example.com/logo.png"
|
||||
}
|
||||
},
|
||||
"datePublished": "2024-01-15",
|
||||
"dateModified": "2024-01-20"
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Product
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Product",
|
||||
"name": "Blue Widget Pro",
|
||||
"image": "https://example.com/blue-widget.jpg",
|
||||
"description": "Premium blue widget with advanced features.",
|
||||
"brand": {
|
||||
"@type": "Brand",
|
||||
"name": "WidgetCo"
|
||||
},
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"price": "49.99",
|
||||
"priceCurrency": "USD",
|
||||
"availability": "https://schema.org/InStock",
|
||||
"url": "https://example.com/products/blue-widget"
|
||||
},
|
||||
"aggregateRating": {
|
||||
"@type": "AggregateRating",
|
||||
"ratingValue": "4.8",
|
||||
"reviewCount": "1250"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### FAQ
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What colors are available?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Our widgets come in blue, red, and green."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "What is the warranty?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "All widgets include a 2-year warranty."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Breadcrumbs
|
||||
|
||||
```html
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BreadcrumbList",
|
||||
"itemListElement": [
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 1,
|
||||
"name": "Home",
|
||||
"item": "https://example.com"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 2,
|
||||
"name": "Products",
|
||||
"item": "https://example.com/products"
|
||||
},
|
||||
{
|
||||
"@type": "ListItem",
|
||||
"position": 3,
|
||||
"name": "Blue Widgets",
|
||||
"item": "https://example.com/products/blue-widgets"
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
Test structured data at:
|
||||
- [Google Rich Results Test](https://search.google.com/test/rich-results)
|
||||
- [Schema.org Validator](https://validator.schema.org/)
|
||||
|
||||
---
|
||||
|
||||
## Mobile SEO
|
||||
|
||||
### Responsive design
|
||||
|
||||
```html
|
||||
<!-- ❌ Not mobile-friendly -->
|
||||
<meta name="viewport" content="width=1024">
|
||||
|
||||
<!-- ✅ Responsive viewport -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
```
|
||||
|
||||
### Tap targets
|
||||
|
||||
```css
|
||||
/* ❌ Too small for mobile */
|
||||
.small-link {
|
||||
padding: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ✅ Adequate tap target */
|
||||
.mobile-friendly-link {
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
}
|
||||
```
|
||||
|
||||
### Font sizes
|
||||
|
||||
```css
|
||||
/* ❌ Too small on mobile */
|
||||
body {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* ✅ Readable without zooming */
|
||||
body {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## International SEO
|
||||
|
||||
### Hreflang tags
|
||||
|
||||
```html
|
||||
<!-- For multi-language sites -->
|
||||
<link rel="alternate" hreflang="en" href="https://example.com/page">
|
||||
<link rel="alternate" hreflang="es" href="https://example.com/es/page">
|
||||
<link rel="alternate" hreflang="fr" href="https://example.com/fr/page">
|
||||
<link rel="alternate" hreflang="x-default" href="https://example.com/page">
|
||||
```
|
||||
|
||||
### Language declaration
|
||||
|
||||
```html
|
||||
<html lang="en">
|
||||
<!-- or -->
|
||||
<html lang="es-MX">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SEO audit checklist
|
||||
|
||||
### Critical
|
||||
- [ ] HTTPS enabled
|
||||
- [ ] robots.txt allows crawling
|
||||
- [ ] No `noindex` on important pages
|
||||
- [ ] Title tags present and unique
|
||||
- [ ] Single `<h1>` per page
|
||||
|
||||
### 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
|
||||
|
||||
### 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 |
|
||||
| Lighthouse | Full SEO audit |
|
||||
| Screaming Frog | Crawl analysis |
|
||||
|
||||
## References
|
||||
|
||||
- [Google Search Central](https://developers.google.com/search)
|
||||
- [Schema.org](https://schema.org/)
|
||||
- [Core Web Vitals](../core-web-vitals/SKILL.md)
|
||||
- [Web Quality Audit](../web-quality-audit/SKILL.md)
|
||||
Reference in New Issue
Block a user