@rankcli/agent-runtime 0.0.9 → 0.0.11

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.
Files changed (47) hide show
  1. package/README.md +90 -196
  2. package/dist/analyzer-GMURJADU.mjs +7 -0
  3. package/dist/chunk-2JADKV3Z.mjs +244 -0
  4. package/dist/chunk-3ZSCLNTW.mjs +557 -0
  5. package/dist/chunk-4E4MQOSP.mjs +374 -0
  6. package/dist/chunk-6BWS3CLP.mjs +16 -0
  7. package/dist/chunk-AK2IC22C.mjs +206 -0
  8. package/dist/chunk-K6VSXDD6.mjs +293 -0
  9. package/dist/chunk-M27NQCWW.mjs +303 -0
  10. package/dist/{chunk-YNZYHEYM.mjs → chunk-PJLNXOLN.mjs} +0 -14
  11. package/dist/chunk-VSQD74I7.mjs +474 -0
  12. package/dist/core-web-vitals-analyzer-TE6LQJMS.mjs +7 -0
  13. package/dist/geo-analyzer-D47LTMMA.mjs +25 -0
  14. package/dist/image-optimization-analyzer-XP4OQGRP.mjs +9 -0
  15. package/dist/index.d.mts +612 -17
  16. package/dist/index.d.ts +612 -17
  17. package/dist/index.js +9020 -2686
  18. package/dist/index.mjs +4177 -328
  19. package/dist/internal-linking-analyzer-MRMBV7NM.mjs +9 -0
  20. package/dist/mobile-seo-analyzer-67HNQ7IO.mjs +7 -0
  21. package/dist/security-headers-analyzer-3ZUQARS5.mjs +9 -0
  22. package/dist/structured-data-analyzer-2I4NQAUP.mjs +9 -0
  23. package/package.json +2 -2
  24. package/src/analyzers/core-web-vitals-analyzer.test.ts +236 -0
  25. package/src/analyzers/core-web-vitals-analyzer.ts +557 -0
  26. package/src/analyzers/geo-analyzer.test.ts +310 -0
  27. package/src/analyzers/geo-analyzer.ts +814 -0
  28. package/src/analyzers/image-optimization-analyzer.test.ts +145 -0
  29. package/src/analyzers/image-optimization-analyzer.ts +348 -0
  30. package/src/analyzers/index.ts +233 -0
  31. package/src/analyzers/internal-linking-analyzer.test.ts +141 -0
  32. package/src/analyzers/internal-linking-analyzer.ts +419 -0
  33. package/src/analyzers/mobile-seo-analyzer.test.ts +140 -0
  34. package/src/analyzers/mobile-seo-analyzer.ts +455 -0
  35. package/src/analyzers/security-headers-analyzer.test.ts +115 -0
  36. package/src/analyzers/security-headers-analyzer.ts +318 -0
  37. package/src/analyzers/structured-data-analyzer.test.ts +210 -0
  38. package/src/analyzers/structured-data-analyzer.ts +590 -0
  39. package/src/audit/engine.ts +3 -3
  40. package/src/audit/types.ts +3 -2
  41. package/src/fixer/framework-fixes.test.ts +489 -0
  42. package/src/fixer/framework-fixes.ts +3418 -0
  43. package/src/frameworks/detector.ts +642 -114
  44. package/src/frameworks/suggestion-engine.ts +38 -1
  45. package/src/index.ts +3 -0
  46. package/src/types.ts +15 -1
  47. package/dist/analyzer-2CSWIQGD.mjs +0 -6
package/README.md CHANGED
@@ -1,241 +1,135 @@
1
1
  # @rankcli/agent-runtime
2
2
 
3
- Core audit engine for RankCLI. Runs 280+ SEO checks and powers both the CLI and SaaS edge functions.
3
+ Core SEO engine for RankCLI. 280+ checks, 7 deep analyzers, framework-specific fix generation.
4
4
 
5
- ## Architecture
6
-
7
- This package is **isomorphic** - it works in both Node.js and Deno environments:
8
-
9
- - **Node.js**: Used by the CLI (`packages/cli`)
10
- - **Deno**: Used by Supabase Edge Functions (`packages/saas/supabase/functions`)
11
-
12
- The isomorphic design uses native `fetch` API instead of Node-specific modules like `axios` or `https`.
5
+ ## Features
13
6
 
14
- ## Directory Structure
7
+ ### 🤖 GEO (Generative Engine Optimization)
8
+ First SEO engine with AI search optimization:
9
+ - AI crawler detection (GPTBot, ClaudeBot, PerplexityBot, etc.)
10
+ - JS rendering analysis for AI crawlers
11
+ - LLM-friendliness scoring
12
+ - Citation readiness analysis
15
13
 
16
- ```
17
- src/
18
- ├── audit/
19
- │ ├── engine.ts # Main audit orchestrator
20
- │ ├── types.ts # Issue definitions & types
21
- │ ├── deno-entry.ts # Deno-specific entry point
22
- │ └── checks/ # Individual check modules
23
- │ ├── crawlability.ts
24
- │ ├── on-page.ts
25
- │ ├── performance.ts
26
- │ ├── security.ts
27
- │ ├── ai-readiness.ts
28
- │ └── ... (40+ check modules)
29
- ├── utils/
30
- │ └── http.ts # Isomorphic HTTP utilities
31
- ├── content/ # Content generation
32
- ├── geo/ # GEO tracking
33
- ├── git/ # Git/PR helpers
34
- └── index.ts # Main entry point
14
+ ### 📊 7 Deep Analyzers
15
+ ```typescript
16
+ import { analyzers } from '@rankcli/agent-runtime';
17
+
18
+ // Comprehensive analysis
19
+ const result = await analyzers.analyzeComprehensive(html, url);
20
+
21
+ // Individual analyzers
22
+ const geo = await analyzers.analyzeGEO(html, url, robotsTxt);
23
+ const cwv = analyzers.analyzeCoreWebVitals(html, url);
24
+ const security = analyzers.analyzeSecurityHeaders(headers, url);
25
+ const schema = analyzers.analyzeStructuredData(html, url);
26
+ const images = analyzers.analyzeImages(html, url);
27
+ const links = analyzers.analyzeInternalLinking(html, url);
28
+ const mobile = analyzers.analyzeMobileSEO(html, url);
35
29
  ```
36
30
 
37
- ## Development
38
-
39
- ### Prerequisites
40
-
41
- - Node.js 18+
42
- - pnpm
31
+ | Analyzer | Checks |
32
+ |----------|--------|
33
+ | **GEO** | AI crawler access, robots.txt, SSR detection, LLM signals |
34
+ | **Core Web Vitals** | LCP, CLS, INP, TTFB estimation |
35
+ | **Security** | HTTPS, HSTS, CSP, grades A+ to F |
36
+ | **Structured Data** | JSON-LD validation, rich results |
37
+ | **Images** | Alt text, dimensions, formats, lazy loading |
38
+ | **Internal Links** | Anchor text, orphan detection |
39
+ | **Mobile** | Viewport, touch targets, PWA |
43
40
 
44
- ### Commands
45
-
46
- ```bash
47
- # Install dependencies
48
- pnpm install
41
+ ### 🔧 Framework-Specific Fixes
49
42
 
50
- # Development mode (watches both Node and Deno bundles)
51
- pnpm dev
43
+ Auto-generate SEO fixes for 25+ frameworks:
52
44
 
53
- # Run tests
54
- pnpm test # Watch mode
55
- pnpm test:run # Single run
56
-
57
- # Build for production
58
- pnpm build # Builds Node.js + Deno bundles
45
+ ```typescript
46
+ import { generateFrameworkFix } from '@rankcli/agent-runtime';
59
47
 
60
- # Build only Deno bundle
61
- pnpm build:deno
48
+ const fix = generateFrameworkFix({
49
+ framework: 'nextjs-app',
50
+ issue: { code: 'MISSING_TITLE', ... },
51
+ siteMeta: { title: 'My Site', description: '...' }
52
+ });
62
53
  ```
63
54
 
64
- ### Development Workflow
65
-
66
- When you run `pnpm dev`, it:
67
-
68
- 1. Builds the Deno bundle first
69
- 2. Starts tsup in watch mode for Node.js
70
- 3. Rebuilds the Deno bundle on every successful Node.js build
71
-
72
- This ensures both the CLI (Node.js) and edge functions (Deno) stay in sync during development.
55
+ **Supported:**
56
+ - React, Next.js, Vue, Nuxt, Angular, Svelte, SvelteKit, Astro, Remix, Gatsby, Solid.js, Qwik
57
+ - Rails, Django, Laravel, Spring Boot, ASP.NET Core, Phoenix, Go
58
+ - Hugo, Jekyll, Eleventy, Pelican
59
+ - HTMX, Hotwire/Turbo
73
60
 
74
- ### Available Scripts
75
-
76
- | Script | Description |
77
- |--------|-------------|
78
- | `pnpm dev` | Watch mode - rebuilds Node + Deno on changes |
79
- | `pnpm dev:node` | Watch mode - Node.js only |
80
- | `pnpm dev:deno` | Watch mode - Deno bundle only |
81
- | `pnpm build` | Production build (Node.js + Deno) |
82
- | `pnpm build:deno` | Build Deno bundle only |
83
- | `pnpm test` | Run tests in watch mode |
84
- | `pnpm test:run` | Run tests once |
61
+ ## Architecture
85
62
 
86
- ## Deno Bundle
63
+ Isomorphic engine - works in Node.js, Deno, and WASM:
87
64
 
88
- The Deno bundle is generated at:
89
- ```
90
- packages/saas/supabase/functions/_shared/audit/
91
- ├── engine.bundle.js # Bundled audit engine
92
- └── index.ts # Wrapper with cheerio import
93
65
  ```
94
-
95
- Edge functions import from the shared bundle:
96
- ```typescript
97
- import { runFullAudit } from '../_shared/audit/index.ts';
66
+ src/
67
+ ├── analyzers/ # 7 deep analyzers (GEO, CWV, etc.)
68
+ ├── audit/ # 280+ individual checks
69
+ ├── fixer/ # Framework-specific fix generation
70
+ ├── frameworks/ # Framework detection
71
+ ├── content/ # Content analysis
72
+ ├── geo/ # GEO tracking
73
+ ├── git/ # PR/commit helpers
74
+ └── index.ts
98
75
  ```
99
76
 
100
- ### How It Works
77
+ ## Installation
101
78
 
102
- 1. `scripts/build-deno.ts` uses esbuild to bundle `src/audit/deno-entry.ts`
103
- 2. The bundle excludes `cheerio` (imported from esm.sh at runtime)
104
- 3. Node-specific APIs are polyfilled or replaced with web-compatible alternatives
105
-
106
- ### Isomorphic Considerations
107
-
108
- When adding new features to the audit engine:
109
-
110
- - Use `fetch` instead of `axios` or Node's `http`/`https`
111
- - Use the helpers in `src/utils/http.ts` for HTTP requests
112
- - Avoid Node-specific modules (`fs`, `path`, `dns`, `crypto`, etc.)
113
- - For DNS lookups, use DNS-over-HTTPS (see `additional-checks.ts`)
114
- - Test in both Node.js (`pnpm test`) and edge functions
115
-
116
- ## VS Code Tasks
117
-
118
- If using VS Code, these tasks are available (Cmd/Ctrl+Shift+P → "Tasks: Run Task"):
119
-
120
- | Task | Description |
121
- |------|-------------|
122
- | Dev: Agent Runtime (with Deno watch) | Watches and rebuilds both bundles |
123
- | Dev: SaaS Frontend | Runs the Vite dev server |
124
- | Dev: Full Stack | Runs both in parallel |
125
- | Build: Deno Bundle | One-time Deno bundle build |
126
- | Deploy: Edge Functions | Deploys to Supabase |
127
-
128
- ## DevContainer
129
-
130
- When using the devcontainer:
131
-
132
- - The Deno bundle is built automatically on container creation
133
- - Run `pnpm dev` in `packages/agent-runtime` to start watching for changes
134
- - The bundle is rebuilt automatically when you modify audit code
135
-
136
- ## API
79
+ ```bash
80
+ pnpm add @rankcli/agent-runtime
81
+ ```
137
82
 
138
- ### runFullAudit
83
+ ## Usage
139
84
 
140
- Main entry point for running audits:
85
+ ### Full Audit
141
86
 
142
87
  ```typescript
143
- import { runFullAudit } from '@rankcli/agent-runtime';
88
+ import { runAudit, generateFixes } from '@rankcli/agent-runtime';
144
89
 
145
- const report = await runFullAudit('https://example.com', {
146
- maxPages: 10, // Max pages to crawl
147
- includeAdvanced: true, // Run advanced checks
148
- includeAI: false, // Run AI-powered analysis
90
+ const audit = await runAudit({
91
+ url: 'https://example.com',
92
+ maxPages: 5,
93
+ options: { geo: true }
149
94
  });
150
95
 
151
- console.log(report.overallScore); // 0-100
152
- console.log(report.issues); // Array of issues
153
- console.log(report.healthScores); // Category scores
154
- console.log(report.checksRun); // Number of checks run
96
+ const fixes = await generateFixes(audit.issues);
155
97
  ```
156
98
 
157
- ### Issue Structure
99
+ ### Generate AI-Friendly robots.txt
158
100
 
159
101
  ```typescript
160
- interface AuditIssue {
161
- code: string; // e.g., 'TITLE_MISSING'
162
- severity: 'error' | 'warning' | 'notice';
163
- category: string; // e.g., 'on-page', 'security'
164
- title: string; // Human-readable title
165
- description?: string; // Detailed description
166
- impact?: string; // SEO impact explanation
167
- howToFix?: string; // Fix instructions
168
- affectedUrls?: string[];
169
- details?: Record<string, unknown>;
170
- }
171
- ```
172
-
173
- ### Health Scores
102
+ import { analyzers } from '@rankcli/agent-runtime';
174
103
 
175
- ```typescript
176
- interface HealthScores {
177
- crawlability: number; // 0-100
178
- onPage: number;
179
- content: number;
180
- performance: number;
181
- security: number;
182
- socialMeta: number;
183
- aiReadiness: number;
184
- mobile: number;
185
- }
104
+ const robotsTxt = analyzers.generateAIFriendlyRobotsTxt('https://example.com');
186
105
  ```
187
106
 
188
- ## Adding New Checks
189
-
190
- 1. Create a new file in `src/audit/checks/` or add to an existing one
191
- 2. Export the check function
192
- 3. Add to `src/audit/engine.ts` to include in the audit flow
193
- 4. Add to `src/audit/deno-entry.ts` to export for Deno
194
- 5. Run `pnpm build:deno` to regenerate the bundle
195
- 6. Add tests in a `.test.ts` file
196
-
197
- Example check:
107
+ ### Generate Schema Templates
198
108
 
199
109
  ```typescript
200
- // src/audit/checks/my-check.ts
201
- import { httpGet } from '../../utils/http.js';
202
- import type { AuditIssue } from '../types.js';
203
-
204
- export async function checkMyThing(url: string): Promise<AuditIssue[]> {
205
- const issues: AuditIssue[] = [];
206
-
207
- const response = await httpGet(url);
208
-
209
- if (/* condition */) {
210
- issues.push({
211
- code: 'MY_ISSUE_CODE',
212
- severity: 'warning',
213
- category: 'my-category',
214
- title: 'Issue title',
215
- description: 'What this means',
216
- howToFix: 'How to fix it',
217
- affectedUrls: [url],
218
- });
219
- }
220
-
221
- return issues;
222
- }
223
- ```
110
+ import { analyzers } from '@rankcli/agent-runtime';
224
111
 
225
- ## Testing
112
+ const schema = analyzers.generateSchemaTemplate('article', {
113
+ siteName: 'My Blog',
114
+ siteUrl: 'https://myblog.com',
115
+ authorName: 'John Doe'
116
+ });
117
+ ```
226
118
 
227
- Tests use Vitest:
119
+ ## Development
228
120
 
229
121
  ```bash
230
- # Run all tests
231
- pnpm test:run
122
+ pnpm install
123
+ pnpm dev # Watch mode
124
+ pnpm test # Run tests
125
+ pnpm build # Build Node.js + Deno
126
+ ```
232
127
 
233
- # Run specific test file
234
- pnpm test:run src/audit/checks/social-meta.test.ts
128
+ ## Related
235
129
 
236
- # Run with coverage
237
- pnpm test:coverage
238
- ```
130
+ - [@rankcli/cli](../cli) - Command-line interface
131
+ - [@rankcli/mcp-server](../mcp-server) - MCP server for AI assistants
132
+ - [rankcli.dev](https://rankcli.dev) - Documentation
239
133
 
240
134
  ## License
241
135
 
@@ -0,0 +1,7 @@
1
+ import {
2
+ analyzeUrl
3
+ } from "./chunk-PJLNXOLN.mjs";
4
+ import "./chunk-6BWS3CLP.mjs";
5
+ export {
6
+ analyzeUrl
7
+ };
@@ -0,0 +1,244 @@
1
+ // src/analyzers/image-optimization-analyzer.ts
2
+ import * as cheerio from "cheerio";
3
+ var MODERN_FORMATS = ["webp", "avif"];
4
+ var LEGACY_FORMATS = ["jpg", "jpeg", "png", "gif", "bmp"];
5
+ var MAX_RECOMMENDED_DIMENSION = 2e3;
6
+ function getImageFormat(src) {
7
+ if (src.startsWith("data:image/")) {
8
+ const match = src.match(/data:image\/(\w+)/);
9
+ return match ? match[1] : void 0;
10
+ }
11
+ try {
12
+ const url = new URL(src, "https://example.com");
13
+ const pathname = url.pathname.toLowerCase();
14
+ const ext = pathname.split(".").pop();
15
+ return ext && ext.length <= 5 ? ext : void 0;
16
+ } catch {
17
+ const ext = src.split(".").pop()?.toLowerCase();
18
+ return ext && ext.length <= 5 ? ext : void 0;
19
+ }
20
+ }
21
+ function analyzeImages(html, url) {
22
+ const $ = cheerio.load(html);
23
+ const issues = [];
24
+ const recommendations = [];
25
+ let score = 100;
26
+ const allImages = [];
27
+ const missingAlt = [];
28
+ const oversizedImages = [];
29
+ const decorativeWithoutRole = [];
30
+ let imagesWithAlt = 0;
31
+ let imagesWithDimensions = 0;
32
+ let imagesWithLazyLoading = 0;
33
+ let modernFormats = 0;
34
+ let legacyFormats = 0;
35
+ $("img").each((_, el) => {
36
+ const $img = $(el);
37
+ const src = $img.attr("src") || $img.attr("data-src") || "";
38
+ const alt = $img.attr("alt");
39
+ const width = parseInt($img.attr("width") || "0");
40
+ const height = parseInt($img.attr("height") || "0");
41
+ const loading = $img.attr("loading");
42
+ const role = $img.attr("role");
43
+ const format = getImageFormat(src);
44
+ const imageIssues = [];
45
+ const imgInfo = {
46
+ src: src.substring(0, 200),
47
+ // Truncate long data URLs
48
+ alt,
49
+ width: width || void 0,
50
+ height: height || void 0,
51
+ format,
52
+ loading,
53
+ issues: imageIssues
54
+ };
55
+ if (alt === void 0) {
56
+ imageIssues.push("Missing alt attribute");
57
+ missingAlt.push(imgInfo);
58
+ } else if (alt === "") {
59
+ if (role !== "presentation" && role !== "none") {
60
+ imageIssues.push('Empty alt without role="presentation"');
61
+ decorativeWithoutRole.push(imgInfo);
62
+ }
63
+ imagesWithAlt++;
64
+ } else {
65
+ imagesWithAlt++;
66
+ if (alt.toLowerCase().includes("image") || alt.toLowerCase().includes("picture") || alt.toLowerCase().includes("photo")) {
67
+ imageIssues.push("Alt text contains generic terms");
68
+ }
69
+ if (alt.length > 125) {
70
+ imageIssues.push("Alt text too long (>125 chars)");
71
+ }
72
+ if (/\.(jpg|jpeg|png|gif|webp|svg)$/i.test(alt)) {
73
+ imageIssues.push("Alt text appears to be a filename");
74
+ }
75
+ }
76
+ if (width > 0 && height > 0) {
77
+ imagesWithDimensions++;
78
+ if (width > MAX_RECOMMENDED_DIMENSION || height > MAX_RECOMMENDED_DIMENSION) {
79
+ imageIssues.push(`Dimensions too large (${width}x${height})`);
80
+ oversizedImages.push(imgInfo);
81
+ }
82
+ } else {
83
+ imageIssues.push("Missing width/height attributes");
84
+ }
85
+ if (loading === "lazy") {
86
+ imagesWithLazyLoading++;
87
+ }
88
+ if (format) {
89
+ if (MODERN_FORMATS.includes(format)) {
90
+ modernFormats++;
91
+ } else if (LEGACY_FORMATS.includes(format)) {
92
+ legacyFormats++;
93
+ }
94
+ }
95
+ allImages.push(imgInfo);
96
+ });
97
+ const totalImages = allImages.length;
98
+ if (missingAlt.length > 0) {
99
+ const percentage = Math.round(missingAlt.length / totalImages * 100);
100
+ const severity = percentage > 50 ? "critical" : percentage > 20 ? "warning" : "info";
101
+ issues.push({
102
+ code: "IMG_MISSING_ALT",
103
+ severity,
104
+ category: "content",
105
+ title: `${missingAlt.length} images missing alt attribute`,
106
+ description: `${percentage}% of images lack alt text, hurting accessibility and SEO.`,
107
+ impact: "Screen readers cannot describe images; search engines lose context",
108
+ howToFix: 'Add descriptive alt text to all images. Use alt="" for decorative images.',
109
+ affectedUrls: [url]
110
+ });
111
+ score -= Math.min(missingAlt.length * 3, 25);
112
+ }
113
+ const missingDimensions = totalImages - imagesWithDimensions;
114
+ if (missingDimensions > 0) {
115
+ issues.push({
116
+ code: "IMG_MISSING_DIMENSIONS",
117
+ severity: "warning",
118
+ category: "performance",
119
+ title: `${missingDimensions} images missing width/height`,
120
+ description: "Images without explicit dimensions cause layout shifts (CLS).",
121
+ impact: "Poor Core Web Vitals, degraded user experience",
122
+ howToFix: "Add width and height attributes to all images.",
123
+ affectedUrls: [url]
124
+ });
125
+ score -= Math.min(missingDimensions * 2, 20);
126
+ }
127
+ if (legacyFormats > 0 && modernFormats === 0) {
128
+ issues.push({
129
+ code: "IMG_NO_MODERN_FORMATS",
130
+ severity: "warning",
131
+ category: "performance",
132
+ title: "No modern image formats (WebP/AVIF)",
133
+ description: `All ${legacyFormats} images use legacy formats (JPEG/PNG).`,
134
+ impact: "Larger file sizes, slower page load",
135
+ howToFix: "Convert images to WebP or AVIF format. Use <picture> element for fallbacks.",
136
+ affectedUrls: [url]
137
+ });
138
+ score -= 10;
139
+ recommendations.push("Convert images to WebP/AVIF for 30-50% smaller file sizes");
140
+ }
141
+ if (oversizedImages.length > 0) {
142
+ issues.push({
143
+ code: "IMG_OVERSIZED",
144
+ severity: "warning",
145
+ category: "performance",
146
+ title: `${oversizedImages.length} images have excessive dimensions`,
147
+ description: `Images larger than ${MAX_RECOMMENDED_DIMENSION}px may be wasteful on most screens.`,
148
+ impact: "Unnecessary bandwidth usage, slower loading",
149
+ howToFix: "Resize images to match their display size. Use srcset for responsive images.",
150
+ affectedUrls: [url]
151
+ });
152
+ score -= Math.min(oversizedImages.length * 3, 15);
153
+ }
154
+ const firstImage = allImages[0];
155
+ if (firstImage?.loading === "lazy") {
156
+ issues.push({
157
+ code: "IMG_HERO_LAZY",
158
+ severity: "warning",
159
+ category: "performance",
160
+ title: 'First image has loading="lazy"',
161
+ description: "Above-the-fold images should not be lazy-loaded as it delays LCP.",
162
+ impact: "Slower Largest Contentful Paint",
163
+ howToFix: 'Remove loading="lazy" from hero/above-fold images. Keep it for below-fold images.',
164
+ affectedUrls: [url]
165
+ });
166
+ score -= 10;
167
+ }
168
+ const belowFoldWithoutLazy = allImages.slice(3).filter((img) => img.loading !== "lazy");
169
+ if (belowFoldWithoutLazy.length > 5) {
170
+ recommendations.push(`Consider adding loading="lazy" to ${belowFoldWithoutLazy.length} below-fold images`);
171
+ }
172
+ if (decorativeWithoutRole.length > 0) {
173
+ issues.push({
174
+ code: "IMG_DECORATIVE_NO_ROLE",
175
+ severity: "info",
176
+ category: "content",
177
+ title: `${decorativeWithoutRole.length} decorative images without role`,
178
+ description: 'Images with empty alt should have role="presentation" for accessibility.',
179
+ impact: "Minor accessibility issue",
180
+ howToFix: 'Add role="presentation" to decorative images with empty alt.',
181
+ affectedUrls: [url]
182
+ });
183
+ }
184
+ const elementsWithBgImage = $('[style*="background-image"], [style*="background:"]').length;
185
+ if (elementsWithBgImage > 3) {
186
+ recommendations.push(`${elementsWithBgImage} CSS background images detected - ensure they're not content images`);
187
+ }
188
+ const svgImages = $('svg, img[src$=".svg"]').length;
189
+ if (svgImages > 0) {
190
+ }
191
+ if (totalImages > 0 && imagesWithAlt === totalImages) {
192
+ recommendations.push("\u2713 All images have alt attributes");
193
+ }
194
+ if (modernFormats > legacyFormats) {
195
+ recommendations.push("\u2713 Majority of images use modern formats");
196
+ }
197
+ return {
198
+ score: Math.max(0, Math.min(100, score)),
199
+ totalImages,
200
+ imagesWithAlt,
201
+ imagesWithDimensions,
202
+ imagesWithLazyLoading,
203
+ modernFormats,
204
+ legacyFormats,
205
+ oversizedImages,
206
+ missingAlt,
207
+ decorativeWithoutRole,
208
+ issues,
209
+ recommendations
210
+ };
211
+ }
212
+ function generateResponsiveImage(options) {
213
+ const { src, alt, widths, sizes = "100vw", loading = "lazy", className } = options;
214
+ const basename = src.replace(/\.[^.]+$/, "");
215
+ const ext = src.split(".").pop() || "jpg";
216
+ const srcset = widths.map((w) => `${basename}-${w}w.webp ${w}w`).join(", ");
217
+ const fallbackSrcset = widths.map((w) => `${basename}-${w}w.${ext} ${w}w`).join(", ");
218
+ return `<picture>
219
+ <source
220
+ type="image/webp"
221
+ srcset="${srcset}"
222
+ sizes="${sizes}"
223
+ />
224
+ <source
225
+ type="image/${ext === "jpg" ? "jpeg" : ext}"
226
+ srcset="${fallbackSrcset}"
227
+ sizes="${sizes}"
228
+ />
229
+ <img
230
+ src="${src}"
231
+ alt="${alt}"
232
+ width="${widths[widths.length - 1]}"
233
+ height="auto"
234
+ loading="${loading}"${className ? `
235
+ class="${className}"` : ""}
236
+ decoding="async"
237
+ />
238
+ </picture>`;
239
+ }
240
+
241
+ export {
242
+ analyzeImages,
243
+ generateResponsiveImage
244
+ };