@girardmedia/bootspring 3.3.2 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/accessibility-auditor.md +39 -0
- package/assets/agents/api-designer.md +40 -0
- package/assets/agents/auth-implementer.md +64 -0
- package/assets/agents/bug-hunter.md +42 -0
- package/assets/agents/bundle-analyzer.md +40 -0
- package/assets/agents/cache-optimizer.md +55 -0
- package/assets/agents/changelog-writer.md +55 -0
- package/assets/agents/ci-cd-builder.md +40 -0
- package/assets/agents/code-explainer.md +39 -0
- package/assets/agents/code-reviewer.md +39 -0
- package/assets/agents/cost-optimizer.md +57 -0
- package/assets/agents/cron-scheduler.md +51 -0
- package/assets/agents/data-seeder.md +56 -0
- package/assets/agents/database-architect.md +40 -0
- package/assets/agents/dependency-updater.md +40 -0
- package/assets/agents/deploy-checker.md +40 -0
- package/assets/agents/docker-optimizer.md +40 -0
- package/assets/agents/documentation-writer.md +40 -0
- package/assets/agents/email-builder.md +55 -0
- package/assets/agents/env-setup.md +40 -0
- package/assets/agents/error-handler.md +40 -0
- package/assets/agents/eslint-fixer.md +46 -0
- package/assets/agents/feature-flagger.md +69 -0
- package/assets/agents/git-detective.md +39 -0
- package/assets/agents/graphql-builder.md +60 -0
- package/assets/agents/incident-responder.md +59 -0
- package/assets/agents/log-analyzer.md +39 -0
- package/assets/agents/migration-planner.md +41 -0
- package/assets/agents/monorepo-navigator.md +39 -0
- package/assets/agents/nextjs-expert.md +57 -0
- package/assets/agents/notification-builder.md +56 -0
- package/assets/agents/onboarding-guide.md +39 -0
- package/assets/agents/performance-profiler.md +40 -0
- package/assets/agents/prisma-expert.md +57 -0
- package/assets/agents/rate-limiter.md +58 -0
- package/assets/agents/react-expert.md +58 -0
- package/assets/agents/refactorer.md +42 -0
- package/assets/agents/regex-builder.md +46 -0
- package/assets/agents/release-manager.md +40 -0
- package/assets/agents/s3-manager.md +58 -0
- package/assets/agents/schema-validator.md +40 -0
- package/assets/agents/search-builder.md +62 -0
- package/assets/agents/security-auditor.md +39 -0
- package/assets/agents/sitemap-generator.md +53 -0
- package/assets/agents/stripe-integrator.md +59 -0
- package/assets/agents/tailwind-expert.md +55 -0
- package/assets/agents/tech-debt-tracker.md +39 -0
- package/assets/agents/test-writer.md +42 -0
- package/assets/agents/type-fixer.md +45 -0
- package/assets/agents/webhook-builder.md +54 -0
- package/assets/rules/cpp.md +53 -0
- package/assets/rules/css.md +52 -0
- package/assets/rules/go.md +50 -0
- package/assets/rules/html.md +52 -0
- package/assets/rules/java.md +51 -0
- package/assets/rules/kotlin.md +50 -0
- package/assets/rules/php.md +51 -0
- package/assets/rules/python.md +51 -0
- package/assets/rules/ruby.md +51 -0
- package/assets/rules/rust.md +49 -0
- package/assets/rules/shell.md +52 -0
- package/assets/rules/sql.md +49 -0
- package/assets/rules/swift.md +50 -0
- package/assets/rules/typescript.md +52 -0
- package/assets/rules/yaml-json.md +51 -0
- package/assets/skills/accessibility.md +210 -0
- package/assets/skills/agent-patterns.md +387 -0
- package/assets/skills/ai-integration.md +263 -0
- package/assets/skills/animation-patterns.md +224 -0
- package/assets/skills/api-design.md +218 -0
- package/assets/skills/api-gateway.md +341 -0
- package/assets/skills/api-versioning.md +226 -0
- package/assets/skills/astro-patterns.md +233 -0
- package/assets/skills/auth-patterns.md +248 -0
- package/assets/skills/aws-patterns.md +171 -0
- package/assets/skills/background-jobs.md +162 -0
- package/assets/skills/browser-extensions.md +309 -0
- package/assets/skills/caching-patterns.md +253 -0
- package/assets/skills/ci-cd.md +251 -0
- package/assets/skills/cli-development.md +296 -0
- package/assets/skills/code-review.md +185 -0
- package/assets/skills/cron-patterns.md +327 -0
- package/assets/skills/data-fetching.md +231 -0
- package/assets/skills/database-migrations.md +346 -0
- package/assets/skills/database-patterns.md +219 -0
- package/assets/skills/debugging.md +281 -0
- package/assets/skills/design-system.md +289 -0
- package/assets/skills/django-patterns.md +182 -0
- package/assets/skills/docker-patterns.md +235 -0
- package/assets/skills/e2e-testing.md +287 -0
- package/assets/skills/edge-computing.md +268 -0
- package/assets/skills/electron-patterns.md +266 -0
- package/assets/skills/email-templates.md +206 -0
- package/assets/skills/error-handling.md +265 -0
- package/assets/skills/event-driven.md +232 -0
- package/assets/skills/express-patterns.md +239 -0
- package/assets/skills/fastapi-patterns.md +198 -0
- package/assets/skills/feature-flags.md +212 -0
- package/assets/skills/figma-to-code.md +298 -0
- package/assets/skills/file-upload.md +228 -0
- package/assets/skills/forms-patterns.md +264 -0
- package/assets/skills/gcp-patterns.md +189 -0
- package/assets/skills/git-workflow.md +187 -0
- package/assets/skills/golang-patterns.md +185 -0
- package/assets/skills/graphql-patterns.md +244 -0
- package/assets/skills/i18n-patterns.md +172 -0
- package/assets/skills/image-processing.md +350 -0
- package/assets/skills/java-springboot.md +226 -0
- package/assets/skills/kotlin-patterns.md +207 -0
- package/assets/skills/kubernetes-patterns.md +326 -0
- package/assets/skills/laravel-patterns.md +261 -0
- package/assets/skills/llm-fine-tuning.md +335 -0
- package/assets/skills/load-testing.md +303 -0
- package/assets/skills/logging-observability.md +228 -0
- package/assets/skills/markdown-processing.md +318 -0
- package/assets/skills/mcp-server-patterns.md +292 -0
- package/assets/skills/microservices.md +272 -0
- package/assets/skills/migration-patterns.md +239 -0
- package/assets/skills/mongodb-patterns.md +189 -0
- package/assets/skills/monorepo-patterns.md +287 -0
- package/assets/skills/nextjs-app-router.md +237 -0
- package/assets/skills/notification-patterns.md +348 -0
- package/assets/skills/oauth-patterns.md +246 -0
- package/assets/skills/payment-integration.md +222 -0
- package/assets/skills/pdf-generation.md +307 -0
- package/assets/skills/performance-optimization.md +277 -0
- package/assets/skills/php-patterns.md +210 -0
- package/assets/skills/prisma-patterns.md +241 -0
- package/assets/skills/prompt-engineering.md +193 -0
- package/assets/skills/pwa-patterns.md +247 -0
- package/assets/skills/python-patterns.md +158 -0
- package/assets/skills/python-testing.md +172 -0
- package/assets/skills/queue-patterns.md +295 -0
- package/assets/skills/rag-patterns.md +159 -0
- package/assets/skills/rate-limiting.md +319 -0
- package/assets/skills/react-components.md +201 -0
- package/assets/skills/react-native-patterns.md +299 -0
- package/assets/skills/real-time-patterns.md +181 -0
- package/assets/skills/redis-patterns.md +188 -0
- package/assets/skills/refactoring.md +218 -0
- package/assets/skills/regex-patterns.md +191 -0
- package/assets/skills/remix-patterns.md +262 -0
- package/assets/skills/responsive-design.md +199 -0
- package/assets/skills/ruby-rails-patterns.md +178 -0
- package/assets/skills/rust-patterns.md +211 -0
- package/assets/skills/search-patterns.md +227 -0
- package/assets/skills/security-hardening.md +237 -0
- package/assets/skills/seo-patterns.md +179 -0
- package/assets/skills/serverless-patterns.md +223 -0
- package/assets/skills/sql-optimization.md +154 -0
- package/assets/skills/state-management.md +254 -0
- package/assets/skills/storybook-patterns.md +330 -0
- package/assets/skills/svelte-patterns.md +258 -0
- package/assets/skills/swift-patterns.md +227 -0
- package/assets/skills/tailwind-patterns.md +272 -0
- package/assets/skills/tdd-workflow.md +199 -0
- package/assets/skills/terraform-patterns.md +270 -0
- package/assets/skills/testing-react.md +240 -0
- package/assets/skills/testing-vitest.md +232 -0
- package/assets/skills/typescript-strict.md +159 -0
- package/assets/skills/video-processing.md +340 -0
- package/assets/skills/vue-patterns.md +247 -0
- package/assets/skills/web-workers.md +327 -0
- package/assets/skills/webhooks-patterns.md +283 -0
- package/assets/skills/websocket-patterns.md +306 -0
- package/dist/cli/index.js +941 -958
- package/dist/core/index.d.ts +341 -11
- package/dist/core.js +58 -95
- package/dist/mcp/index.d.ts +33 -1
- package/dist/mcp-server.js +177 -255
- package/package.json +4 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: search-patterns
|
|
3
|
+
description: Search implementation patterns with full-text search, Elasticsearch, Meilisearch, faceted search, and autocomplete.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Search Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply when users need to find content in your application -- products, articles, users, or documents. Good search is the difference between "I cannot find anything" and "it reads my mind." Start with Postgres full-text search, graduate to Elasticsearch or Meilisearch when you need fuzzy matching, facets, or scale beyond 1M documents.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Postgres Full-Text Search -- Start Here
|
|
14
|
+
|
|
15
|
+
Built into Postgres, no extra infrastructure:
|
|
16
|
+
|
|
17
|
+
```sql
|
|
18
|
+
-- Add a tsvector column for search
|
|
19
|
+
ALTER TABLE products ADD COLUMN search_vector tsvector;
|
|
20
|
+
|
|
21
|
+
-- Populate from name and description with weighted fields
|
|
22
|
+
UPDATE products SET search_vector =
|
|
23
|
+
setweight(to_tsvector('english', coalesce(name, '')), 'A') ||
|
|
24
|
+
setweight(to_tsvector('english', coalesce(description, '')), 'B') ||
|
|
25
|
+
setweight(to_tsvector('english', coalesce(category, '')), 'C');
|
|
26
|
+
|
|
27
|
+
-- GIN index for fast lookups
|
|
28
|
+
CREATE INDEX idx_products_search ON products USING GIN (search_vector);
|
|
29
|
+
|
|
30
|
+
-- Auto-update with trigger
|
|
31
|
+
CREATE FUNCTION products_search_trigger() RETURNS trigger AS $$
|
|
32
|
+
BEGIN
|
|
33
|
+
NEW.search_vector :=
|
|
34
|
+
setweight(to_tsvector('english', coalesce(NEW.name, '')), 'A') ||
|
|
35
|
+
setweight(to_tsvector('english', coalesce(NEW.description, '')), 'B') ||
|
|
36
|
+
setweight(to_tsvector('english', coalesce(NEW.category, '')), 'C');
|
|
37
|
+
RETURN NEW;
|
|
38
|
+
END $$ LANGUAGE plpgsql;
|
|
39
|
+
|
|
40
|
+
CREATE TRIGGER products_search_update
|
|
41
|
+
BEFORE INSERT OR UPDATE ON products
|
|
42
|
+
FOR EACH ROW EXECUTE FUNCTION products_search_trigger();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
async function searchProducts(query: string, limit = 20) {
|
|
47
|
+
const result = await db.query(`
|
|
48
|
+
SELECT id, name, description,
|
|
49
|
+
ts_rank(search_vector, websearch_to_tsquery('english', $1)) AS rank
|
|
50
|
+
FROM products
|
|
51
|
+
WHERE search_vector @@ websearch_to_tsquery('english', $1)
|
|
52
|
+
ORDER BY rank DESC
|
|
53
|
+
LIMIT $2
|
|
54
|
+
`, [query, limit]);
|
|
55
|
+
return result.rows;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Meilisearch -- Fast and Simple
|
|
60
|
+
|
|
61
|
+
Easier to run than Elasticsearch, great for most use cases:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { MeiliSearch } from "meilisearch";
|
|
65
|
+
|
|
66
|
+
const client = new MeiliSearch({ host: "http://localhost:7700", apiKey: "masterKey" });
|
|
67
|
+
const index = client.index("products");
|
|
68
|
+
|
|
69
|
+
// Index documents
|
|
70
|
+
await index.addDocuments(products);
|
|
71
|
+
|
|
72
|
+
// Configure searchable and filterable attributes
|
|
73
|
+
await index.updateSettings({
|
|
74
|
+
searchableAttributes: ["name", "description", "category"],
|
|
75
|
+
filterableAttributes: ["category", "price", "inStock"],
|
|
76
|
+
sortableAttributes: ["price", "createdAt"],
|
|
77
|
+
typoTolerance: { enabled: true, minWordSizeForTypos: { oneTypo: 4, twoTypos: 8 } },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Search with filters and facets
|
|
81
|
+
const results = await index.search("wireless keyboard", {
|
|
82
|
+
filter: ["category = electronics", "price < 100", "inStock = true"],
|
|
83
|
+
facets: ["category"],
|
|
84
|
+
sort: ["price:asc"],
|
|
85
|
+
limit: 20,
|
|
86
|
+
});
|
|
87
|
+
// results.hits, results.facetDistribution, results.estimatedTotalHits
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Elasticsearch -- Full-Featured
|
|
91
|
+
|
|
92
|
+
Use when you need complex aggregations or operate at > 10M documents:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { Client } from "@elastic/elasticsearch";
|
|
96
|
+
|
|
97
|
+
const elastic = new Client({ node: "http://localhost:9200" });
|
|
98
|
+
|
|
99
|
+
// Search with boosting, fuzzy matching, and highlighting
|
|
100
|
+
async function search(query: string, filters: SearchFilters) {
|
|
101
|
+
const result = await elastic.search({
|
|
102
|
+
index: "products",
|
|
103
|
+
body: {
|
|
104
|
+
query: {
|
|
105
|
+
bool: {
|
|
106
|
+
must: {
|
|
107
|
+
multi_match: {
|
|
108
|
+
query,
|
|
109
|
+
fields: ["name^3", "description", "tags^2"],
|
|
110
|
+
fuzziness: "AUTO",
|
|
111
|
+
type: "best_fields",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
filter: [
|
|
115
|
+
...(filters.category ? [{ term: { category: filters.category } }] : []),
|
|
116
|
+
...(filters.maxPrice ? [{ range: { price: { lte: filters.maxPrice } } }] : []),
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
highlight: { fields: { name: {}, description: {} } },
|
|
121
|
+
aggs: {
|
|
122
|
+
categories: { terms: { field: "category.keyword", size: 20 } },
|
|
123
|
+
price_ranges: {
|
|
124
|
+
range: {
|
|
125
|
+
field: "price",
|
|
126
|
+
ranges: [
|
|
127
|
+
{ key: "under-25", to: 25 },
|
|
128
|
+
{ key: "25-50", from: 25, to: 50 },
|
|
129
|
+
{ key: "50-100", from: 50, to: 100 },
|
|
130
|
+
{ key: "over-100", from: 100 },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Autocomplete / Typeahead
|
|
142
|
+
|
|
143
|
+
Show suggestions as the user types. Must respond in under 100ms:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// Simple prefix matching in Postgres (good for < 100K items)
|
|
147
|
+
async function autocomplete(prefix: string, limit = 8) {
|
|
148
|
+
return db.query(
|
|
149
|
+
"SELECT DISTINCT name FROM products WHERE name ILIKE $1 ORDER BY popularity DESC LIMIT $2",
|
|
150
|
+
[`${prefix}%`, limit]
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Frontend -- debounce input, abort previous request
|
|
155
|
+
function useAutocomplete(query: string) {
|
|
156
|
+
const [suggestions, setSuggestions] = useState<string[]>([]);
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (query.length < 2) { setSuggestions([]); return; }
|
|
160
|
+
const controller = new AbortController();
|
|
161
|
+
const timer = setTimeout(async () => {
|
|
162
|
+
const res = await fetch(`/api/autocomplete?q=${encodeURIComponent(query)}`, {
|
|
163
|
+
signal: controller.signal,
|
|
164
|
+
});
|
|
165
|
+
setSuggestions(await res.json());
|
|
166
|
+
}, 150);
|
|
167
|
+
return () => { clearTimeout(timer); controller.abort(); };
|
|
168
|
+
}, [query]);
|
|
169
|
+
|
|
170
|
+
return suggestions;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Fuzzy Matching
|
|
175
|
+
|
|
176
|
+
Handle typos and misspellings:
|
|
177
|
+
|
|
178
|
+
```sql
|
|
179
|
+
-- Postgres pg_trgm extension for trigram similarity
|
|
180
|
+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
|
181
|
+
CREATE INDEX idx_products_name_trgm ON products USING GIN (name gin_trgm_ops);
|
|
182
|
+
|
|
183
|
+
-- Find "laptop" even if user types "laptpo"
|
|
184
|
+
SELECT name, similarity(name, 'laptpo') AS sim
|
|
185
|
+
FROM products
|
|
186
|
+
WHERE name % 'laptpo'
|
|
187
|
+
ORDER BY sim DESC
|
|
188
|
+
LIMIT 10;
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Faceted Search
|
|
192
|
+
|
|
193
|
+
Let users filter results by categories, price ranges, and attributes:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// Response shape for faceted search
|
|
197
|
+
interface SearchResponse {
|
|
198
|
+
hits: Product[];
|
|
199
|
+
total: number;
|
|
200
|
+
facets: {
|
|
201
|
+
categories: { value: string; count: number }[];
|
|
202
|
+
priceRanges: { label: string; min: number; max: number; count: number }[];
|
|
203
|
+
};
|
|
204
|
+
query: string;
|
|
205
|
+
took: number;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Examples
|
|
210
|
+
|
|
211
|
+
| Scale | Tool | When |
|
|
212
|
+
|-------|------|------|
|
|
213
|
+
| < 100K docs | Postgres `tsvector` + GIN | Already have Postgres |
|
|
214
|
+
| 100K-10M, easy ops | Meilisearch | Need fuzzy + facets, simple setup |
|
|
215
|
+
| 10M+, complex queries | Elasticsearch | Need aggregations, custom scoring |
|
|
216
|
+
| Semantic search | pgvector, Pinecone | Meaning-based retrieval |
|
|
217
|
+
| Code search | `pg_trgm` | Substring and regex matching |
|
|
218
|
+
|
|
219
|
+
## Checklist
|
|
220
|
+
- [ ] Search query debounced on frontend (150-300ms)
|
|
221
|
+
- [ ] Autocomplete returns results in under 100ms
|
|
222
|
+
- [ ] Fuzzy matching handles common typos
|
|
223
|
+
- [ ] Search fields weighted by importance (name > description)
|
|
224
|
+
- [ ] Results include highlighted matching snippets
|
|
225
|
+
- [ ] Faceted filters show counts and update dynamically
|
|
226
|
+
- [ ] Empty and no-results states show helpful suggestions
|
|
227
|
+
- [ ] Search index kept in sync with source database
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: security-hardening
|
|
3
|
+
description: Harden web applications — CSP, CORS, rate limiting, input validation, injection prevention, and secrets management.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Security Hardening
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns to every web application and API from the start. Security
|
|
11
|
+
is not a feature you add later — it's a set of constraints that prevent
|
|
12
|
+
catastrophic failures. Each pattern here blocks a specific, common attack vector.
|
|
13
|
+
|
|
14
|
+
## How It Works
|
|
15
|
+
|
|
16
|
+
### 1. Content Security Policy (CSP)
|
|
17
|
+
|
|
18
|
+
Prevent XSS by controlling which resources the browser can load.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import helmet from 'helmet';
|
|
22
|
+
|
|
23
|
+
app.use(helmet({
|
|
24
|
+
contentSecurityPolicy: {
|
|
25
|
+
directives: {
|
|
26
|
+
defaultSrc: ["'self'"],
|
|
27
|
+
scriptSrc: ["'self'", "'nonce-{{NONCE}}'"], // no 'unsafe-inline'
|
|
28
|
+
styleSrc: ["'self'", "'unsafe-inline'"], // inline styles often needed
|
|
29
|
+
imgSrc: ["'self'", "data:", "https://cdn.example.com"],
|
|
30
|
+
connectSrc: ["'self'", "https://api.example.com"],
|
|
31
|
+
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
|
32
|
+
objectSrc: ["'none'"],
|
|
33
|
+
frameAncestors: ["'none'"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Generate a unique nonce per request for inline scripts:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
app.use((req, res, next) => {
|
|
43
|
+
res.locals.nonce = crypto.randomBytes(16).toString('base64');
|
|
44
|
+
next();
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. CORS Configuration
|
|
49
|
+
|
|
50
|
+
Only allow origins you explicitly trust.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import cors from 'cors';
|
|
54
|
+
|
|
55
|
+
app.use(cors({
|
|
56
|
+
origin: (origin, callback) => {
|
|
57
|
+
const allowed = ['https://app.example.com', 'https://admin.example.com'];
|
|
58
|
+
if (!origin || allowed.includes(origin)) {
|
|
59
|
+
callback(null, true);
|
|
60
|
+
} else {
|
|
61
|
+
callback(new Error('Not allowed by CORS'));
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
|
65
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
66
|
+
credentials: true,
|
|
67
|
+
maxAge: 86400, // preflight cache: 24 hours
|
|
68
|
+
}));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Never use `origin: '*'` with `credentials: true` — browsers block it anyway,
|
|
72
|
+
and trying to work around it opens you to credential theft.
|
|
73
|
+
|
|
74
|
+
### 3. Rate Limiting
|
|
75
|
+
|
|
76
|
+
Prevent brute force, DDoS, and abuse.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import rateLimit from 'express-rate-limit';
|
|
80
|
+
|
|
81
|
+
// Global: 100 requests per minute per IP
|
|
82
|
+
app.use(rateLimit({
|
|
83
|
+
windowMs: 60 * 1000,
|
|
84
|
+
max: 100,
|
|
85
|
+
standardHeaders: true,
|
|
86
|
+
legacyHeaders: false,
|
|
87
|
+
message: { error: { code: 'RATE_LIMITED', message: 'Too many requests' } },
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
// Strict: 5 login attempts per 15 minutes
|
|
91
|
+
app.use('/api/auth/login', rateLimit({
|
|
92
|
+
windowMs: 15 * 60 * 1000,
|
|
93
|
+
max: 5,
|
|
94
|
+
skipSuccessfulRequests: true,
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
// API keys: higher limits
|
|
98
|
+
app.use('/api/v1', rateLimit({
|
|
99
|
+
windowMs: 60 * 1000,
|
|
100
|
+
max: 1000,
|
|
101
|
+
keyGenerator: (req) => req.headers['x-api-key'] as string || req.ip,
|
|
102
|
+
}));
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
For production, use a Redis-backed store so limits work across server instances.
|
|
106
|
+
|
|
107
|
+
### 4. Input Validation
|
|
108
|
+
|
|
109
|
+
Validate and sanitize every input at the boundary. Use Zod for schema
|
|
110
|
+
validation.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { z } from 'zod';
|
|
114
|
+
|
|
115
|
+
const CreateUserSchema = z.object({
|
|
116
|
+
name: z.string().min(1).max(100).trim(),
|
|
117
|
+
email: z.string().email().max(255).toLowerCase(),
|
|
118
|
+
age: z.number().int().min(13).max(150),
|
|
119
|
+
bio: z.string().max(2000).optional(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function validateBody<T>(schema: z.ZodSchema<T>) {
|
|
123
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
124
|
+
const result = schema.safeParse(req.body);
|
|
125
|
+
if (!result.success) {
|
|
126
|
+
return res.status(400).json({
|
|
127
|
+
error: {
|
|
128
|
+
code: 'VALIDATION_ERROR',
|
|
129
|
+
details: result.error.flatten().fieldErrors,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
req.body = result.data; // replace with validated data
|
|
134
|
+
next();
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
router.post('/users', validateBody(CreateUserSchema), createUser);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 5. SQL Injection Prevention
|
|
142
|
+
|
|
143
|
+
Use parameterized queries. Never concatenate user input into SQL.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// DANGEROUS — SQL injection
|
|
147
|
+
const result = await db.query(`SELECT * FROM users WHERE id = '${userId}'`);
|
|
148
|
+
|
|
149
|
+
// SAFE — parameterized query
|
|
150
|
+
const result = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
151
|
+
|
|
152
|
+
// SAFE — Prisma (parameterized by default)
|
|
153
|
+
const user = await prisma.user.findUnique({ where: { id: userId } });
|
|
154
|
+
|
|
155
|
+
// SAFE — tagged template (Prisma raw)
|
|
156
|
+
const result = await prisma.$queryRaw`SELECT * FROM users WHERE id = ${userId}`;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 6. Security Headers
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
app.use(helmet({
|
|
163
|
+
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
|
|
164
|
+
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
165
|
+
noSniff: true, // X-Content-Type-Options: nosniff
|
|
166
|
+
frameguard: { action: 'deny' }, // X-Frame-Options: DENY
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
// Permissions Policy — disable unused browser features
|
|
170
|
+
app.use((req, res, next) => {
|
|
171
|
+
res.setHeader('Permissions-Policy',
|
|
172
|
+
'camera=(), microphone=(), geolocation=(), payment=()');
|
|
173
|
+
next();
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 7. Secrets Management
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Load from environment, never from code
|
|
181
|
+
const config = {
|
|
182
|
+
dbUrl: requireEnv('DATABASE_URL'),
|
|
183
|
+
jwtSecret: requireEnv('JWT_SECRET'),
|
|
184
|
+
apiKey: requireEnv('STRIPE_API_KEY'),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
function requireEnv(name: string): string {
|
|
188
|
+
const value = process.env[name];
|
|
189
|
+
if (!value) throw new Error(`Missing required env var: ${name}`);
|
|
190
|
+
return value;
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Rules:
|
|
195
|
+
- `.env` is in `.gitignore` — always
|
|
196
|
+
- Use `.env.example` with placeholder values for documentation
|
|
197
|
+
- Production secrets come from the deployment platform (Vercel, AWS SSM, Vault)
|
|
198
|
+
- Rotate secrets quarterly; immediately after any exposure
|
|
199
|
+
- Never log secrets — redact in structured logging
|
|
200
|
+
|
|
201
|
+
### 8. Request Body Limits
|
|
202
|
+
|
|
203
|
+
Prevent denial of service via large payloads.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
app.use(express.json({ limit: '1mb' }));
|
|
207
|
+
app.use(express.urlencoded({ extended: true, limit: '1mb' }));
|
|
208
|
+
|
|
209
|
+
// Stricter limits on specific routes
|
|
210
|
+
app.post('/api/auth/login', express.json({ limit: '10kb' }), loginHandler);
|
|
211
|
+
|
|
212
|
+
// File upload routes get larger limits
|
|
213
|
+
app.post('/api/uploads', express.raw({ limit: '50mb' }), uploadHandler);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Examples
|
|
217
|
+
|
|
218
|
+
| Attack | Defense | Implementation |
|
|
219
|
+
|--------|---------|---------------|
|
|
220
|
+
| XSS | CSP + input sanitization | helmet CSP + Zod validation |
|
|
221
|
+
| CSRF | SameSite cookies + CORS | `sameSite: 'lax'` + origin whitelist |
|
|
222
|
+
| Brute force | Rate limiting | express-rate-limit + Redis store |
|
|
223
|
+
| SQL injection | Parameterized queries | Prisma / `$1` placeholders |
|
|
224
|
+
| Credential theft | HTTPS + secure headers | HSTS + httpOnly cookies |
|
|
225
|
+
| Secret exposure | Environment variables | `.env` in `.gitignore` + requireEnv |
|
|
226
|
+
|
|
227
|
+
## Checklist
|
|
228
|
+
|
|
229
|
+
- [ ] CSP is configured with no `unsafe-inline` for scripts
|
|
230
|
+
- [ ] CORS whitelist contains only known origins — no wildcards with credentials
|
|
231
|
+
- [ ] Rate limiting is enabled globally and stricter on auth endpoints
|
|
232
|
+
- [ ] All user input is validated with Zod at the route boundary
|
|
233
|
+
- [ ] All SQL uses parameterized queries or an ORM — zero string concatenation
|
|
234
|
+
- [ ] Helmet sets HSTS, noSniff, frameguard, and referrer policy
|
|
235
|
+
- [ ] `.env` is in `.gitignore` and secrets never appear in logs or responses
|
|
236
|
+
- [ ] Request body size limits are set per route
|
|
237
|
+
- [ ] Dependencies are audited (`npm audit`) and updated monthly
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: seo-patterns
|
|
3
|
+
description: SEO patterns for meta tags, JSON-LD structured data, sitemaps, Core Web Vitals, and SSR/SSG rendering strategies.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SEO Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply these patterns to any public-facing web application. SEO is not an afterthought -- it is a core requirement for content sites, e-commerce, SaaS landing pages, and documentation. This skill covers technical SEO: meta tags, structured data (JSON-LD), sitemaps, Core Web Vitals optimization, and rendering strategy selection (SSR vs. SSG vs. ISR).
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Meta Tags -- The Foundation
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
// Next.js App Router metadata API
|
|
17
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
18
|
+
const product = await getProduct(params.slug);
|
|
19
|
+
return {
|
|
20
|
+
title: `${product.name} | MyStore`,
|
|
21
|
+
description: product.summary.slice(0, 155),
|
|
22
|
+
alternates: { canonical: `https://mystore.com/products/${params.slug}` },
|
|
23
|
+
openGraph: {
|
|
24
|
+
title: product.name,
|
|
25
|
+
description: product.summary,
|
|
26
|
+
images: [{ url: product.image, width: 1200, height: 630, alt: product.name }],
|
|
27
|
+
type: "website",
|
|
28
|
+
},
|
|
29
|
+
twitter: { card: "summary_large_image", title: product.name, images: [product.image] },
|
|
30
|
+
robots: { index: product.isPublished, follow: true },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Every page needs a unique `<title>` (50-60 chars) and `<meta description>` (120-155 chars). Set canonical URL to prevent duplicate content. Use `noindex` for admin pages and search results.
|
|
36
|
+
|
|
37
|
+
### Structured Data (JSON-LD)
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
function ProductJsonLd({ product }: { product: Product }) {
|
|
41
|
+
const jsonLd = {
|
|
42
|
+
"@context": "https://schema.org",
|
|
43
|
+
"@type": "Product",
|
|
44
|
+
name: product.name,
|
|
45
|
+
image: product.images,
|
|
46
|
+
description: product.description,
|
|
47
|
+
sku: product.sku,
|
|
48
|
+
brand: { "@type": "Brand", name: product.brand },
|
|
49
|
+
offers: {
|
|
50
|
+
"@type": "Offer",
|
|
51
|
+
priceCurrency: "USD",
|
|
52
|
+
price: product.price,
|
|
53
|
+
availability: product.inStock
|
|
54
|
+
? "https://schema.org/InStock"
|
|
55
|
+
: "https://schema.org/OutOfStock",
|
|
56
|
+
},
|
|
57
|
+
aggregateRating: product.rating ? {
|
|
58
|
+
"@type": "AggregateRating",
|
|
59
|
+
ratingValue: product.rating.average,
|
|
60
|
+
reviewCount: product.rating.count,
|
|
61
|
+
} : undefined,
|
|
62
|
+
};
|
|
63
|
+
return (
|
|
64
|
+
<script
|
|
65
|
+
type="application/ld+json"
|
|
66
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Common schema types: `Article`, `Product`, `FAQPage`, `BreadcrumbList`, `Organization`, `LocalBusiness`.
|
|
73
|
+
|
|
74
|
+
### Sitemap Generation
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// Next.js App Router sitemap.ts
|
|
78
|
+
import { MetadataRoute } from "next";
|
|
79
|
+
|
|
80
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
81
|
+
const products = await db.products.findMany({
|
|
82
|
+
where: { isPublished: true },
|
|
83
|
+
select: { slug: true, updatedAt: true },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return [
|
|
87
|
+
{ url: "https://mystore.com", lastModified: new Date(), changeFrequency: "daily", priority: 1 },
|
|
88
|
+
{ url: "https://mystore.com/about", lastModified: new Date(), changeFrequency: "monthly", priority: 0.5 },
|
|
89
|
+
...products.map((p) => ({
|
|
90
|
+
url: `https://mystore.com/products/${p.slug}`,
|
|
91
|
+
lastModified: p.updatedAt,
|
|
92
|
+
changeFrequency: "weekly" as const,
|
|
93
|
+
priority: 0.8,
|
|
94
|
+
})),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
For large sites (>50K URLs), split into sitemap index files.
|
|
100
|
+
|
|
101
|
+
### Core Web Vitals
|
|
102
|
+
|
|
103
|
+
| Metric | Target | Optimization |
|
|
104
|
+
|--------|--------|-------------|
|
|
105
|
+
| LCP | < 2.5s | Preload hero image, use `<Image priority>`, optimize server response |
|
|
106
|
+
| INP | < 200ms | Avoid long tasks, use `startTransition`, debounce inputs |
|
|
107
|
+
| CLS | < 0.1 | Set explicit width/height on images, avoid injecting content above fold |
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
// Preload critical hero image
|
|
111
|
+
<link rel="preload" as="image" href="/hero.webp" fetchPriority="high" />
|
|
112
|
+
|
|
113
|
+
// Next.js Image with priority
|
|
114
|
+
<Image src={product.image} alt={product.name} width={800} height={600}
|
|
115
|
+
priority sizes="(max-width: 768px) 100vw, 50vw" />
|
|
116
|
+
|
|
117
|
+
// Font preloading to prevent CLS
|
|
118
|
+
<link rel="preload" href="/fonts/inter-var.woff2" as="font"
|
|
119
|
+
type="font/woff2" crossOrigin="anonymous" />
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Rendering Strategy Selection
|
|
123
|
+
|
|
124
|
+
| Strategy | When to Use | SEO Impact |
|
|
125
|
+
|----------|-------------|------------|
|
|
126
|
+
| SSG | Content changes rarely (blog, docs) | Best -- instant, fully crawlable |
|
|
127
|
+
| ISR | Content changes periodically | Great -- static + fresh |
|
|
128
|
+
| SSR | Content changes per request | Good -- crawlable but slower |
|
|
129
|
+
| CSR | Behind auth, dashboards | Poor -- use SSR for public pages |
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// Next.js ISR -- revalidate every 60 seconds
|
|
133
|
+
export const revalidate = 60;
|
|
134
|
+
|
|
135
|
+
export default async function ProductPage({ params }: Props) {
|
|
136
|
+
const product = await getProduct(params.slug);
|
|
137
|
+
return <ProductView product={product} />;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Heading Hierarchy and Internal Linking
|
|
142
|
+
|
|
143
|
+
```html
|
|
144
|
+
<!-- One H1 per page, logical heading hierarchy -->
|
|
145
|
+
<h1>Running Shoes</h1>
|
|
146
|
+
<h2>Men's Running Shoes</h2>
|
|
147
|
+
<h3>Trail Running</h3>
|
|
148
|
+
<h3>Road Running</h3>
|
|
149
|
+
<h2>Women's Running Shoes</h2>
|
|
150
|
+
|
|
151
|
+
<!-- Breadcrumbs with semantic markup -->
|
|
152
|
+
<nav aria-label="Breadcrumb">
|
|
153
|
+
<ol>
|
|
154
|
+
<li><a href="/">Home</a></li>
|
|
155
|
+
<li><a href="/shoes">Shoes</a></li>
|
|
156
|
+
<li aria-current="page">Running Shoes</li>
|
|
157
|
+
</ol>
|
|
158
|
+
</nav>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Examples
|
|
162
|
+
|
|
163
|
+
| Pattern | Impact | Effort |
|
|
164
|
+
|---------|--------|--------|
|
|
165
|
+
| Unique title + description | High -- click-through rate | Low |
|
|
166
|
+
| JSON-LD structured data | High -- rich snippets | Medium |
|
|
167
|
+
| Sitemap | Medium -- crawl discovery | Low |
|
|
168
|
+
| Core Web Vitals | High -- ranking signal | Medium-High |
|
|
169
|
+
| SSG/ISR | High -- speed + crawlability | Medium |
|
|
170
|
+
|
|
171
|
+
## Checklist
|
|
172
|
+
- [ ] Every public page has a unique title (50-60 chars) and description (120-155 chars)
|
|
173
|
+
- [ ] Canonical URLs set on all pages to prevent duplicate content
|
|
174
|
+
- [ ] JSON-LD structured data on product, article, FAQ, and organization pages
|
|
175
|
+
- [ ] Sitemap generated dynamically and submitted to Google Search Console
|
|
176
|
+
- [ ] robots.txt blocks admin, API, and auth routes
|
|
177
|
+
- [ ] Images have explicit width/height, alt text, and use WebP/AVIF formats
|
|
178
|
+
- [ ] LCP < 2.5s, INP < 200ms, CLS < 0.1 (measured with Lighthouse)
|
|
179
|
+
- [ ] Public content uses SSG or ISR, not client-side rendering
|