@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,350 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: image-processing
|
|
3
|
+
description: Image processing patterns with Sharp for resizing, format conversion, CDN optimization, responsive images, and lazy loading.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Image Processing Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
Apply image processing when your application handles user uploads, serves responsive images, or needs to optimize assets for web delivery. Sharp is the go-to library for server-side processing due to its speed (libvips-based). These patterns cover resizing, format conversion (WebP/AVIF), CDN integration, responsive srcset generation, and lazy loading strategies. Implement these patterns at upload time and at build time to minimize runtime processing.
|
|
10
|
+
|
|
11
|
+
## How It Works
|
|
12
|
+
|
|
13
|
+
### Sharp Processing Pipeline
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// src/images/processor.ts
|
|
17
|
+
import sharp from 'sharp';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
|
|
20
|
+
interface ProcessOptions {
|
|
21
|
+
width?: number;
|
|
22
|
+
height?: number;
|
|
23
|
+
quality?: number;
|
|
24
|
+
format?: 'webp' | 'avif' | 'jpeg' | 'png';
|
|
25
|
+
fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function processImage(input: Buffer | string, options: ProcessOptions): Promise<Buffer> {
|
|
29
|
+
let pipeline = sharp(input);
|
|
30
|
+
|
|
31
|
+
// Auto-rotate based on EXIF
|
|
32
|
+
pipeline = pipeline.rotate();
|
|
33
|
+
|
|
34
|
+
// Resize
|
|
35
|
+
if (options.width || options.height) {
|
|
36
|
+
pipeline = pipeline.resize({
|
|
37
|
+
width: options.width,
|
|
38
|
+
height: options.height,
|
|
39
|
+
fit: options.fit ?? 'cover',
|
|
40
|
+
withoutEnlargement: true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Format conversion
|
|
45
|
+
const quality = options.quality ?? 80;
|
|
46
|
+
switch (options.format ?? 'webp') {
|
|
47
|
+
case 'webp':
|
|
48
|
+
pipeline = pipeline.webp({ quality, effort: 4 });
|
|
49
|
+
break;
|
|
50
|
+
case 'avif':
|
|
51
|
+
pipeline = pipeline.avif({ quality, effort: 4 });
|
|
52
|
+
break;
|
|
53
|
+
case 'jpeg':
|
|
54
|
+
pipeline = pipeline.jpeg({ quality, progressive: true, mozjpeg: true });
|
|
55
|
+
break;
|
|
56
|
+
case 'png':
|
|
57
|
+
pipeline = pipeline.png({ quality, compressionLevel: 9, palette: true });
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return pipeline.toBuffer();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function getImageMetadata(input: Buffer | string) {
|
|
65
|
+
const metadata = await sharp(input).metadata();
|
|
66
|
+
return {
|
|
67
|
+
width: metadata.width ?? 0,
|
|
68
|
+
height: metadata.height ?? 0,
|
|
69
|
+
format: metadata.format ?? 'unknown',
|
|
70
|
+
size: metadata.size ?? 0,
|
|
71
|
+
hasAlpha: metadata.hasAlpha ?? false,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Responsive Image Generation
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// src/images/responsive.ts
|
|
80
|
+
import sharp from 'sharp';
|
|
81
|
+
import path from 'path';
|
|
82
|
+
import fs from 'fs/promises';
|
|
83
|
+
|
|
84
|
+
interface ResponsiveConfig {
|
|
85
|
+
widths: number[];
|
|
86
|
+
formats: Array<'webp' | 'avif' | 'jpeg'>;
|
|
87
|
+
quality: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const defaultConfig: ResponsiveConfig = {
|
|
91
|
+
widths: [320, 640, 768, 1024, 1280, 1920],
|
|
92
|
+
formats: ['avif', 'webp', 'jpeg'],
|
|
93
|
+
quality: 80,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export async function generateResponsiveSet(
|
|
97
|
+
inputPath: string,
|
|
98
|
+
outputDir: string,
|
|
99
|
+
config: ResponsiveConfig = defaultConfig
|
|
100
|
+
): Promise<Array<{ width: number; format: string; path: string; size: number }>> {
|
|
101
|
+
const results: Array<{ width: number; format: string; path: string; size: number }> = [];
|
|
102
|
+
const baseName = path.basename(inputPath, path.extname(inputPath));
|
|
103
|
+
const input = sharp(inputPath).rotate();
|
|
104
|
+
const metadata = await input.metadata();
|
|
105
|
+
|
|
106
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
for (const width of config.widths) {
|
|
109
|
+
if (width > (metadata.width ?? 0)) continue; // skip upscaling
|
|
110
|
+
|
|
111
|
+
for (const format of config.formats) {
|
|
112
|
+
const fileName = `${baseName}-${width}w.${format}`;
|
|
113
|
+
const outputPath = path.join(outputDir, fileName);
|
|
114
|
+
|
|
115
|
+
const buffer = await sharp(inputPath)
|
|
116
|
+
.rotate()
|
|
117
|
+
.resize({ width, withoutEnlargement: true })
|
|
118
|
+
[format]({ quality: config.quality })
|
|
119
|
+
.toBuffer();
|
|
120
|
+
|
|
121
|
+
await fs.writeFile(outputPath, buffer);
|
|
122
|
+
|
|
123
|
+
results.push({
|
|
124
|
+
width,
|
|
125
|
+
format,
|
|
126
|
+
path: outputPath,
|
|
127
|
+
size: buffer.length,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return results;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Generate HTML picture element
|
|
136
|
+
export function generatePictureElement(
|
|
137
|
+
images: Array<{ width: number; format: string; path: string }>,
|
|
138
|
+
alt: string,
|
|
139
|
+
sizes: string = '(max-width: 768px) 100vw, 50vw'
|
|
140
|
+
): string {
|
|
141
|
+
const byFormat = new Map<string, typeof images>();
|
|
142
|
+
for (const img of images) {
|
|
143
|
+
const list = byFormat.get(img.format) ?? [];
|
|
144
|
+
list.push(img);
|
|
145
|
+
byFormat.set(img.format, list);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const sources = ['avif', 'webp'].map((format) => {
|
|
149
|
+
const formatImages = byFormat.get(format);
|
|
150
|
+
if (!formatImages) return '';
|
|
151
|
+
const srcset = formatImages.map((i) => `${i.path} ${i.width}w`).join(', ');
|
|
152
|
+
return `<source type="image/${format}" srcset="${srcset}" sizes="${sizes}" />`;
|
|
153
|
+
}).filter(Boolean).join('\n ');
|
|
154
|
+
|
|
155
|
+
const fallback = byFormat.get('jpeg')?.[0];
|
|
156
|
+
|
|
157
|
+
return `<picture>
|
|
158
|
+
${sources}
|
|
159
|
+
<img src="${fallback?.path}" alt="${alt}" loading="lazy" decoding="async" sizes="${sizes}" />
|
|
160
|
+
</picture>`;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Upload Processing Pipeline
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// src/images/upload-handler.ts
|
|
168
|
+
import { Router } from 'express';
|
|
169
|
+
import multer from 'multer';
|
|
170
|
+
import { processImage, getImageMetadata } from './processor';
|
|
171
|
+
|
|
172
|
+
const upload = multer({
|
|
173
|
+
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB max
|
|
174
|
+
fileFilter: (_req, file, cb) => {
|
|
175
|
+
const allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif'];
|
|
176
|
+
cb(null, allowed.includes(file.mimetype));
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const router = Router();
|
|
181
|
+
|
|
182
|
+
router.post('/api/images/upload', upload.single('image'), async (req, res) => {
|
|
183
|
+
if (!req.file) return res.status(400).json({ error: 'No image file provided' });
|
|
184
|
+
|
|
185
|
+
const metadata = await getImageMetadata(req.file.buffer);
|
|
186
|
+
|
|
187
|
+
// Generate multiple sizes
|
|
188
|
+
const variants = await Promise.all([
|
|
189
|
+
processImage(req.file.buffer, { width: 150, height: 150, format: 'webp', fit: 'cover' })
|
|
190
|
+
.then((buf) => ({ name: 'thumbnail', buffer: buf })),
|
|
191
|
+
processImage(req.file.buffer, { width: 800, format: 'webp' })
|
|
192
|
+
.then((buf) => ({ name: 'medium', buffer: buf })),
|
|
193
|
+
processImage(req.file.buffer, { width: 1920, format: 'webp' })
|
|
194
|
+
.then((buf) => ({ name: 'large', buffer: buf })),
|
|
195
|
+
processImage(req.file.buffer, { width: 1920, format: 'avif' })
|
|
196
|
+
.then((buf) => ({ name: 'large-avif', buffer: buf })),
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
// Upload all variants to storage
|
|
200
|
+
const urls: Record<string, string> = {};
|
|
201
|
+
for (const variant of variants) {
|
|
202
|
+
const key = `images/${req.userId}/${Date.now()}-${variant.name}`;
|
|
203
|
+
urls[variant.name] = await uploadToStorage(key, variant.buffer);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Generate blur placeholder
|
|
207
|
+
const placeholder = await sharp(req.file.buffer)
|
|
208
|
+
.resize(20, 20, { fit: 'inside' })
|
|
209
|
+
.blur()
|
|
210
|
+
.toBuffer();
|
|
211
|
+
const blurDataURL = `data:image/jpeg;base64,${placeholder.toString('base64')}`;
|
|
212
|
+
|
|
213
|
+
res.json({
|
|
214
|
+
urls,
|
|
215
|
+
metadata: { width: metadata.width, height: metadata.height, format: metadata.format },
|
|
216
|
+
placeholder: blurDataURL,
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
export default router;
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### CDN-Optimized Image Serving
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
// src/images/cdn-handler.ts
|
|
227
|
+
import { Router } from 'express';
|
|
228
|
+
import sharp from 'sharp';
|
|
229
|
+
|
|
230
|
+
const router = Router();
|
|
231
|
+
|
|
232
|
+
// On-demand image transformation: /images/:key?w=800&f=webp&q=80
|
|
233
|
+
router.get('/images/:key(*)', async (req, res) => {
|
|
234
|
+
const { w, h, f, q, fit } = req.query;
|
|
235
|
+
const width = w ? parseInt(w as string) : undefined;
|
|
236
|
+
const height = h ? parseInt(h as string) : undefined;
|
|
237
|
+
const format = (f as 'webp' | 'avif' | 'jpeg') ?? 'webp';
|
|
238
|
+
const quality = q ? parseInt(q as string) : 80;
|
|
239
|
+
|
|
240
|
+
// Check cache first
|
|
241
|
+
const cacheKey = `img:${req.params.key}:${width}:${height}:${format}:${quality}`;
|
|
242
|
+
const cached = await cache.get(cacheKey);
|
|
243
|
+
if (cached) {
|
|
244
|
+
res.set({
|
|
245
|
+
'Content-Type': `image/${format}`,
|
|
246
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
247
|
+
});
|
|
248
|
+
return res.send(cached);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Fetch original
|
|
252
|
+
const original = await fetchFromStorage(req.params.key);
|
|
253
|
+
if (!original) return res.status(404).send('Image not found');
|
|
254
|
+
|
|
255
|
+
// Transform
|
|
256
|
+
const result = await processImage(original, {
|
|
257
|
+
width, height, format, quality,
|
|
258
|
+
fit: (fit as any) ?? 'cover',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Cache transformed result
|
|
262
|
+
await cache.set(cacheKey, result, 86400);
|
|
263
|
+
|
|
264
|
+
res.set({
|
|
265
|
+
'Content-Type': `image/${format}`,
|
|
266
|
+
'Cache-Control': 'public, max-age=31536000, immutable',
|
|
267
|
+
'Vary': 'Accept',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
res.send(result);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
export default router;
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### React Lazy Loading Component
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
// src/components/OptimizedImage.tsx
|
|
280
|
+
import { useState, useRef, useEffect } from 'react';
|
|
281
|
+
|
|
282
|
+
interface OptimizedImageProps {
|
|
283
|
+
src: string;
|
|
284
|
+
alt: string;
|
|
285
|
+
width: number;
|
|
286
|
+
height: number;
|
|
287
|
+
placeholder?: string; // blur data URL
|
|
288
|
+
sizes?: string;
|
|
289
|
+
className?: string;
|
|
290
|
+
priority?: boolean;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function OptimizedImage({
|
|
294
|
+
src, alt, width, height, placeholder, sizes, className, priority,
|
|
295
|
+
}: OptimizedImageProps) {
|
|
296
|
+
const [loaded, setLoaded] = useState(false);
|
|
297
|
+
const imgRef = useRef<HTMLImageElement>(null);
|
|
298
|
+
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (imgRef.current?.complete) setLoaded(true);
|
|
301
|
+
}, []);
|
|
302
|
+
|
|
303
|
+
const srcSet = [320, 640, 1024, 1920]
|
|
304
|
+
.map((w) => `${src}?w=${w}&f=webp ${w}w`)
|
|
305
|
+
.join(', ');
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div className="relative overflow-hidden" style={{ aspectRatio: `${width}/${height}` }}>
|
|
309
|
+
{placeholder && !loaded && (
|
|
310
|
+
<img src={placeholder} alt="" aria-hidden className="absolute inset-0 w-full h-full object-cover blur-lg scale-110" />
|
|
311
|
+
)}
|
|
312
|
+
<picture>
|
|
313
|
+
<source type="image/avif" srcSet={srcSet.replace(/f=webp/g, 'f=avif')} sizes={sizes} />
|
|
314
|
+
<source type="image/webp" srcSet={srcSet} sizes={sizes} />
|
|
315
|
+
<img
|
|
316
|
+
ref={imgRef}
|
|
317
|
+
src={`${src}?w=1024&f=jpeg`}
|
|
318
|
+
alt={alt}
|
|
319
|
+
width={width}
|
|
320
|
+
height={height}
|
|
321
|
+
sizes={sizes ?? '100vw'}
|
|
322
|
+
loading={priority ? 'eager' : 'lazy'}
|
|
323
|
+
decoding={priority ? 'sync' : 'async'}
|
|
324
|
+
onLoad={() => setLoaded(true)}
|
|
325
|
+
className={`${className ?? ''} transition-opacity duration-300 ${loaded ? 'opacity-100' : 'opacity-0'}`}
|
|
326
|
+
/>
|
|
327
|
+
</picture>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Examples
|
|
334
|
+
|
|
335
|
+
| Format | Quality 80 Size | Browser Support | Use Case |
|
|
336
|
+
|--------|----------------|-----------------|----------|
|
|
337
|
+
| AVIF | ~40% of JPEG | Chrome, Firefox | Best compression, modern browsers |
|
|
338
|
+
| WebP | ~60% of JPEG | All modern | Good balance of quality and support |
|
|
339
|
+
| JPEG | Baseline | Universal | Fallback for all browsers |
|
|
340
|
+
| PNG | Lossless | Universal | Screenshots, graphics with transparency |
|
|
341
|
+
|
|
342
|
+
## Checklist
|
|
343
|
+
- [ ] Images auto-rotated based on EXIF orientation before processing
|
|
344
|
+
- [ ] Multiple sizes generated at upload time (thumbnail, medium, large)
|
|
345
|
+
- [ ] AVIF and WebP variants served with JPEG fallback via `<picture>`
|
|
346
|
+
- [ ] Blur placeholder generated for progressive loading experience
|
|
347
|
+
- [ ] `loading="lazy"` and `decoding="async"` on below-fold images
|
|
348
|
+
- [ ] Width and height attributes set to prevent layout shift (CLS)
|
|
349
|
+
- [ ] CDN Cache-Control headers set with long TTL and immutable
|
|
350
|
+
- [ ] Upload handler validates file type and enforces size limits
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: java-springboot
|
|
3
|
+
description: Spring Boot patterns for dependency injection, JPA, security, actuator, profiles, and testing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Spring Boot Patterns
|
|
7
|
+
|
|
8
|
+
## When to Use
|
|
9
|
+
|
|
10
|
+
Apply these patterns when building Spring Boot 3.x applications with Java 17+.
|
|
11
|
+
Use this skill for structuring services with dependency injection, mapping JPA
|
|
12
|
+
entities, configuring Spring Security, exposing health checks via Actuator, and
|
|
13
|
+
writing integration tests.
|
|
14
|
+
|
|
15
|
+
## How It Works
|
|
16
|
+
|
|
17
|
+
### Dependency Injection
|
|
18
|
+
|
|
19
|
+
Use constructor injection exclusively. Avoid `@Autowired` on fields. Spring
|
|
20
|
+
auto-detects single-constructor beans. Use `@Qualifier` only when multiple
|
|
21
|
+
implementations exist.
|
|
22
|
+
|
|
23
|
+
```java
|
|
24
|
+
@Service
|
|
25
|
+
public class OrderService {
|
|
26
|
+
private final OrderRepository orderRepo;
|
|
27
|
+
private final PaymentGateway paymentGateway;
|
|
28
|
+
|
|
29
|
+
// No @Autowired needed with single constructor
|
|
30
|
+
public OrderService(OrderRepository orderRepo, PaymentGateway paymentGateway) {
|
|
31
|
+
this.orderRepo = orderRepo;
|
|
32
|
+
this.paymentGateway = paymentGateway;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public Order place(CreateOrderRequest req) {
|
|
36
|
+
Order order = Order.from(req);
|
|
37
|
+
paymentGateway.charge(order.total());
|
|
38
|
+
return orderRepo.save(order);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### JPA Entities
|
|
44
|
+
|
|
45
|
+
Use `@Entity` with explicit table and column names. Prefer `IDENTITY` generation
|
|
46
|
+
for PostgreSQL. Always override `equals`/`hashCode` based on the business key,
|
|
47
|
+
not the generated ID.
|
|
48
|
+
|
|
49
|
+
```java
|
|
50
|
+
@Entity
|
|
51
|
+
@Table(name = "orders")
|
|
52
|
+
public class Order {
|
|
53
|
+
@Id
|
|
54
|
+
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
55
|
+
private Long id;
|
|
56
|
+
|
|
57
|
+
@Column(nullable = false, unique = true, length = 36)
|
|
58
|
+
private String orderNumber;
|
|
59
|
+
|
|
60
|
+
@Enumerated(EnumType.STRING)
|
|
61
|
+
@Column(nullable = false)
|
|
62
|
+
private OrderStatus status;
|
|
63
|
+
|
|
64
|
+
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
65
|
+
private List<OrderItem> items = new ArrayList<>();
|
|
66
|
+
|
|
67
|
+
@Override
|
|
68
|
+
public boolean equals(Object o) {
|
|
69
|
+
if (this == o) return true;
|
|
70
|
+
if (!(o instanceof Order other)) return false;
|
|
71
|
+
return orderNumber != null && orderNumber.equals(other.orderNumber);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Override
|
|
75
|
+
public int hashCode() {
|
|
76
|
+
return Objects.hash(orderNumber);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Repository Layer
|
|
82
|
+
|
|
83
|
+
Use Spring Data JPA interfaces. Add custom queries with `@Query` for complex
|
|
84
|
+
lookups. Use projections for read-only DTOs.
|
|
85
|
+
|
|
86
|
+
```java
|
|
87
|
+
public interface OrderRepository extends JpaRepository<Order, Long> {
|
|
88
|
+
Optional<Order> findByOrderNumber(String orderNumber);
|
|
89
|
+
|
|
90
|
+
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status")
|
|
91
|
+
List<Order> findByStatusWithItems(@Param("status") OrderStatus status);
|
|
92
|
+
|
|
93
|
+
@Query("SELECT new com.example.dto.OrderSummary(o.orderNumber, o.status, o.createdAt) FROM Order o")
|
|
94
|
+
Page<OrderSummary> findSummaries(Pageable pageable);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Spring Security
|
|
99
|
+
|
|
100
|
+
Use `SecurityFilterChain` bean configuration (not `WebSecurityConfigurerAdapter`).
|
|
101
|
+
Extract JWT claims into a custom `UserDetails`.
|
|
102
|
+
|
|
103
|
+
```java
|
|
104
|
+
@Configuration
|
|
105
|
+
@EnableMethodSecurity
|
|
106
|
+
public class SecurityConfig {
|
|
107
|
+
@Bean
|
|
108
|
+
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
109
|
+
return http
|
|
110
|
+
.csrf(csrf -> csrf.disable())
|
|
111
|
+
.sessionManagement(sm -> sm.sessionCreationPolicy(STATELESS))
|
|
112
|
+
.authorizeHttpRequests(auth -> auth
|
|
113
|
+
.requestMatchers("/api/public/**").permitAll()
|
|
114
|
+
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
|
115
|
+
.anyRequest().authenticated()
|
|
116
|
+
)
|
|
117
|
+
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))
|
|
118
|
+
.build();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Actuator and Health Checks
|
|
124
|
+
|
|
125
|
+
Expose `/actuator/health` for load balancers. Add custom health indicators for
|
|
126
|
+
downstream dependencies.
|
|
127
|
+
|
|
128
|
+
```java
|
|
129
|
+
@Component
|
|
130
|
+
public class DatabaseHealthIndicator implements HealthIndicator {
|
|
131
|
+
private final DataSource dataSource;
|
|
132
|
+
|
|
133
|
+
public DatabaseHealthIndicator(DataSource dataSource) {
|
|
134
|
+
this.dataSource = dataSource;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Override
|
|
138
|
+
public Health health() {
|
|
139
|
+
try (Connection conn = dataSource.getConnection()) {
|
|
140
|
+
if (conn.isValid(2)) {
|
|
141
|
+
return Health.up().withDetail("database", "reachable").build();
|
|
142
|
+
}
|
|
143
|
+
} catch (SQLException e) {
|
|
144
|
+
return Health.down(e).build();
|
|
145
|
+
}
|
|
146
|
+
return Health.down().build();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Profiles
|
|
152
|
+
|
|
153
|
+
Use `application-{profile}.yml` for environment-specific config. Activate with
|
|
154
|
+
`SPRING_PROFILES_ACTIVE=prod`. Keep `application.yml` for defaults.
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
# application.yml (defaults)
|
|
158
|
+
spring:
|
|
159
|
+
jpa:
|
|
160
|
+
open-in-view: false
|
|
161
|
+
|
|
162
|
+
# application-prod.yml
|
|
163
|
+
spring:
|
|
164
|
+
datasource:
|
|
165
|
+
url: ${DATABASE_URL}
|
|
166
|
+
hikari:
|
|
167
|
+
maximum-pool-size: 20
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Testing
|
|
171
|
+
|
|
172
|
+
Use `@SpringBootTest` sparingly (slow). Prefer `@WebMvcTest` for controllers,
|
|
173
|
+
`@DataJpaTest` for repositories. Use `@MockBean` to replace dependencies.
|
|
174
|
+
|
|
175
|
+
```java
|
|
176
|
+
@WebMvcTest(OrderController.class)
|
|
177
|
+
class OrderControllerTest {
|
|
178
|
+
@Autowired MockMvc mockMvc;
|
|
179
|
+
@MockBean OrderService orderService;
|
|
180
|
+
|
|
181
|
+
@Test
|
|
182
|
+
void getOrder_returnsOrder() throws Exception {
|
|
183
|
+
when(orderService.findByNumber("ORD-001"))
|
|
184
|
+
.thenReturn(Optional.of(testOrder()));
|
|
185
|
+
|
|
186
|
+
mockMvc.perform(get("/api/orders/ORD-001"))
|
|
187
|
+
.andExpect(status().isOk())
|
|
188
|
+
.andExpect(jsonPath("$.orderNumber").value("ORD-001"));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
@Test
|
|
192
|
+
void getOrder_notFound_returns404() throws Exception {
|
|
193
|
+
when(orderService.findByNumber("MISSING"))
|
|
194
|
+
.thenReturn(Optional.empty());
|
|
195
|
+
|
|
196
|
+
mockMvc.perform(get("/api/orders/MISSING"))
|
|
197
|
+
.andExpect(status().isNotFound());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Examples
|
|
203
|
+
|
|
204
|
+
**Pattern: Global exception handler**
|
|
205
|
+
```java
|
|
206
|
+
@RestControllerAdvice
|
|
207
|
+
public class GlobalExceptionHandler {
|
|
208
|
+
@ExceptionHandler(EntityNotFoundException.class)
|
|
209
|
+
ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException ex) {
|
|
210
|
+
return ResponseEntity.status(404).body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Checklist
|
|
216
|
+
|
|
217
|
+
- [ ] Constructor injection only (no `@Autowired` on fields)
|
|
218
|
+
- [ ] JPA entities have `equals`/`hashCode` on business key, not generated ID
|
|
219
|
+
- [ ] `@Query` with `JOIN FETCH` to prevent N+1 queries
|
|
220
|
+
- [ ] `open-in-view: false` in application config
|
|
221
|
+
- [ ] Security config uses `SecurityFilterChain` bean, not deprecated adapter
|
|
222
|
+
- [ ] Actuator health endpoint exposed; custom indicators for critical dependencies
|
|
223
|
+
- [ ] Profile-specific configs in `application-{profile}.yml`
|
|
224
|
+
- [ ] `@WebMvcTest` / `@DataJpaTest` over `@SpringBootTest` where possible
|
|
225
|
+
- [ ] `@Transactional` on service methods that do multiple writes
|
|
226
|
+
- [ ] `@RestControllerAdvice` for centralized exception handling
|