@djangocfg/seo 2.1.50

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 (68) hide show
  1. package/README.md +192 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.mjs +3780 -0
  4. package/dist/cli.mjs.map +1 -0
  5. package/dist/crawler/index.d.ts +88 -0
  6. package/dist/crawler/index.mjs +610 -0
  7. package/dist/crawler/index.mjs.map +1 -0
  8. package/dist/google-console/index.d.ts +95 -0
  9. package/dist/google-console/index.mjs +539 -0
  10. package/dist/google-console/index.mjs.map +1 -0
  11. package/dist/index.d.ts +285 -0
  12. package/dist/index.mjs +3236 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/link-checker/index.d.ts +76 -0
  15. package/dist/link-checker/index.mjs +326 -0
  16. package/dist/link-checker/index.mjs.map +1 -0
  17. package/dist/markdown-report-B3QdDzxE.d.ts +193 -0
  18. package/dist/reports/index.d.ts +24 -0
  19. package/dist/reports/index.mjs +836 -0
  20. package/dist/reports/index.mjs.map +1 -0
  21. package/dist/routes/index.d.ts +69 -0
  22. package/dist/routes/index.mjs +372 -0
  23. package/dist/routes/index.mjs.map +1 -0
  24. package/dist/scanner-Cz4Th2Pt.d.ts +60 -0
  25. package/dist/types/index.d.ts +144 -0
  26. package/dist/types/index.mjs +3 -0
  27. package/dist/types/index.mjs.map +1 -0
  28. package/package.json +114 -0
  29. package/src/analyzer.ts +256 -0
  30. package/src/cli/commands/audit.ts +260 -0
  31. package/src/cli/commands/content.ts +180 -0
  32. package/src/cli/commands/crawl.ts +32 -0
  33. package/src/cli/commands/index.ts +12 -0
  34. package/src/cli/commands/inspect.ts +60 -0
  35. package/src/cli/commands/links.ts +41 -0
  36. package/src/cli/commands/robots.ts +36 -0
  37. package/src/cli/commands/routes.ts +126 -0
  38. package/src/cli/commands/sitemap.ts +48 -0
  39. package/src/cli/index.ts +149 -0
  40. package/src/cli/types.ts +40 -0
  41. package/src/config.ts +207 -0
  42. package/src/content/index.ts +51 -0
  43. package/src/content/link-checker.ts +182 -0
  44. package/src/content/link-fixer.ts +188 -0
  45. package/src/content/scanner.ts +200 -0
  46. package/src/content/sitemap-generator.ts +321 -0
  47. package/src/content/types.ts +140 -0
  48. package/src/crawler/crawler.ts +425 -0
  49. package/src/crawler/index.ts +10 -0
  50. package/src/crawler/robots-parser.ts +171 -0
  51. package/src/crawler/sitemap-validator.ts +204 -0
  52. package/src/google-console/analyzer.ts +317 -0
  53. package/src/google-console/auth.ts +100 -0
  54. package/src/google-console/client.ts +281 -0
  55. package/src/google-console/index.ts +9 -0
  56. package/src/index.ts +144 -0
  57. package/src/link-checker/index.ts +461 -0
  58. package/src/reports/claude-context.ts +149 -0
  59. package/src/reports/generator.ts +244 -0
  60. package/src/reports/index.ts +27 -0
  61. package/src/reports/json-report.ts +320 -0
  62. package/src/reports/markdown-report.ts +246 -0
  63. package/src/reports/split-report.ts +252 -0
  64. package/src/routes/analyzer.ts +324 -0
  65. package/src/routes/index.ts +25 -0
  66. package/src/routes/scanner.ts +298 -0
  67. package/src/types/index.ts +222 -0
  68. package/src/utils/index.ts +154 -0
@@ -0,0 +1,539 @@
1
+ import { searchconsole } from '@googleapis/searchconsole';
2
+ import consola2 from 'consola';
3
+ import pLimit from 'p-limit';
4
+ import pRetry from 'p-retry';
5
+ import { JWT } from 'google-auth-library';
6
+ import { existsSync, readFileSync } from 'fs';
7
+
8
+ // src/google-console/client.ts
9
+ var SCOPES = [
10
+ "https://www.googleapis.com/auth/webmasters.readonly",
11
+ "https://www.googleapis.com/auth/webmasters"
12
+ ];
13
+ function loadCredentials(config) {
14
+ if (config.serviceAccountJson) {
15
+ return config.serviceAccountJson;
16
+ }
17
+ if (config.serviceAccountPath) {
18
+ if (!existsSync(config.serviceAccountPath)) {
19
+ throw new Error(`Service account file not found: ${config.serviceAccountPath}`);
20
+ }
21
+ const content = readFileSync(config.serviceAccountPath, "utf-8");
22
+ return JSON.parse(content);
23
+ }
24
+ const envJson = process.env.GOOGLE_SERVICE_ACCOUNT_JSON;
25
+ if (envJson) {
26
+ return JSON.parse(envJson);
27
+ }
28
+ const defaultPath = "./service_account.json";
29
+ if (existsSync(defaultPath)) {
30
+ const content = readFileSync(defaultPath, "utf-8");
31
+ return JSON.parse(content);
32
+ }
33
+ throw new Error(
34
+ "No service account credentials found. Provide serviceAccountPath, serviceAccountJson, or set GOOGLE_SERVICE_ACCOUNT_JSON env variable."
35
+ );
36
+ }
37
+ function createAuthClient(config) {
38
+ const credentials = loadCredentials(config);
39
+ const auth = new JWT({
40
+ email: credentials.client_email,
41
+ key: credentials.private_key,
42
+ scopes: SCOPES
43
+ });
44
+ auth._serviceAccountEmail = credentials.client_email;
45
+ return auth;
46
+ }
47
+ async function verifyAuth(auth, siteUrl) {
48
+ const email = auth._serviceAccountEmail || auth.email;
49
+ try {
50
+ await auth.authorize();
51
+ consola2.success("Google Search Console authentication verified");
52
+ consola2.info(`Service account: ${email}`);
53
+ if (siteUrl) {
54
+ const domain = new URL(siteUrl).hostname;
55
+ const gscUrl = `https://search.google.com/search-console/users?resource_id=sc-domain%3A${domain}`;
56
+ consola2.info(`Ensure this email has Full access in GSC: ${gscUrl}`);
57
+ }
58
+ return true;
59
+ } catch (error) {
60
+ consola2.error("Authentication failed");
61
+ consola2.info(`Service account email: ${email}`);
62
+ consola2.info("Make sure this email is added to GSC with Full access");
63
+ return false;
64
+ }
65
+ }
66
+
67
+ // src/google-console/client.ts
68
+ var GoogleConsoleClient = class {
69
+ auth;
70
+ searchconsole;
71
+ siteUrl;
72
+ gscSiteUrl;
73
+ // Format for GSC API (may be sc-domain:xxx)
74
+ limit = pLimit(2);
75
+ // Max 2 concurrent requests (Cloudflare-friendly)
76
+ requestDelay = 500;
77
+ // Delay between requests in ms
78
+ constructor(config) {
79
+ this.auth = createAuthClient(config);
80
+ this.searchconsole = searchconsole({ version: "v1", auth: this.auth });
81
+ this.siteUrl = config.siteUrl;
82
+ if (config.gscSiteUrl) {
83
+ this.gscSiteUrl = config.gscSiteUrl;
84
+ } else {
85
+ const domain = new URL(config.siteUrl).hostname;
86
+ this.gscSiteUrl = `sc-domain:${domain}`;
87
+ }
88
+ consola2.debug(`GSC site URL: ${this.gscSiteUrl}`);
89
+ }
90
+ /**
91
+ * Delay helper for rate limiting
92
+ */
93
+ delay(ms) {
94
+ return new Promise((resolve) => setTimeout(resolve, ms));
95
+ }
96
+ /**
97
+ * Verify the client is authenticated
98
+ */
99
+ async verify() {
100
+ return verifyAuth(this.auth, this.siteUrl);
101
+ }
102
+ /**
103
+ * List all sites in Search Console
104
+ */
105
+ async listSites() {
106
+ try {
107
+ const response = await this.searchconsole.sites.list();
108
+ return response.data.siteEntry?.map((site) => site.siteUrl || "") || [];
109
+ } catch (error) {
110
+ consola2.error("Failed to list sites:", error);
111
+ throw error;
112
+ }
113
+ }
114
+ /**
115
+ * Inspect a single URL
116
+ */
117
+ async inspectUrl(url) {
118
+ return this.limit(async () => {
119
+ return pRetry(
120
+ async () => {
121
+ const response = await this.searchconsole.urlInspection.index.inspect({
122
+ requestBody: {
123
+ inspectionUrl: url,
124
+ siteUrl: this.gscSiteUrl,
125
+ languageCode: "en-US"
126
+ }
127
+ });
128
+ const result = response.data.inspectionResult;
129
+ if (!result?.indexStatusResult) {
130
+ throw new Error(`No inspection result for URL: ${url}`);
131
+ }
132
+ return this.mapInspectionResult(url, result);
133
+ },
134
+ {
135
+ retries: 2,
136
+ minTimeout: 2e3,
137
+ maxTimeout: 1e4,
138
+ factor: 2,
139
+ // Exponential backoff
140
+ onFailedAttempt: (ctx) => {
141
+ if (ctx.retriesLeft === 0) {
142
+ consola2.warn(`Failed: ${url}`);
143
+ }
144
+ }
145
+ }
146
+ );
147
+ });
148
+ }
149
+ /**
150
+ * Inspect multiple URLs in batch
151
+ * Stops early if too many consecutive errors (likely rate limiting)
152
+ */
153
+ async inspectUrls(urls) {
154
+ consola2.info(`Inspecting ${urls.length} URLs...`);
155
+ const results = [];
156
+ const errors = [];
157
+ let consecutiveErrors = 0;
158
+ const maxConsecutiveErrors = 3;
159
+ for (const url of urls) {
160
+ try {
161
+ const result = await this.inspectUrl(url);
162
+ results.push(result);
163
+ consecutiveErrors = 0;
164
+ await this.delay(this.requestDelay);
165
+ } catch (error) {
166
+ const err = error;
167
+ errors.push({ url, error: err });
168
+ consecutiveErrors++;
169
+ if (consecutiveErrors >= maxConsecutiveErrors) {
170
+ console.log("");
171
+ consola2.error(`Stopping after ${maxConsecutiveErrors} consecutive failures`);
172
+ this.showRateLimitHelp();
173
+ break;
174
+ }
175
+ }
176
+ }
177
+ if (errors.length > 0 && consecutiveErrors < maxConsecutiveErrors) {
178
+ consola2.warn(`Failed to inspect ${errors.length} URLs`);
179
+ }
180
+ if (results.length > 0) {
181
+ consola2.success(`Successfully inspected ${results.length}/${urls.length} URLs`);
182
+ } else if (errors.length > 0) {
183
+ consola2.warn("No URLs were successfully inspected");
184
+ }
185
+ return results;
186
+ }
187
+ /**
188
+ * Show help message for rate limiting issues
189
+ */
190
+ showRateLimitHelp() {
191
+ consola2.info("Possible causes:");
192
+ consola2.info(" 1. Google API quota exceeded (2000 requests/day)");
193
+ consola2.info(" 2. Cloudflare blocking Google's crawler");
194
+ consola2.info(" 3. Service account not added to GSC");
195
+ console.log("");
196
+ consola2.info("Solutions:");
197
+ consola2.info(" \u2022 Check GSC access: https://search.google.com/search-console/users");
198
+ console.log("");
199
+ consola2.info(" \u2022 Cloudflare WAF rule to allow Googlebot:");
200
+ consola2.info(" 1. Dashboard \u2192 Security \u2192 WAF \u2192 Custom rules \u2192 Create rule");
201
+ consola2.info(' 2. Name: "Allow Googlebot"');
202
+ consola2.info(' 3. Field: "Known Bots" | Operator: "equals" | Value: "true"');
203
+ consola2.info(' 4. Or click "Edit expression" and paste: (cf.client.bot)');
204
+ consola2.info(" 5. Action: Skip \u2192 check all rules");
205
+ consola2.info(" 6. Deploy");
206
+ consola2.info(" Docs: https://developers.cloudflare.com/waf/custom-rules/use-cases/allow-traffic-from-verified-bots/");
207
+ console.log("");
208
+ }
209
+ /**
210
+ * Get search analytics data
211
+ */
212
+ async getSearchAnalytics(options) {
213
+ try {
214
+ const response = await this.searchconsole.searchanalytics.query({
215
+ siteUrl: this.gscSiteUrl,
216
+ requestBody: {
217
+ startDate: options.startDate,
218
+ endDate: options.endDate,
219
+ dimensions: options.dimensions || ["page"],
220
+ rowLimit: options.rowLimit || 1e3
221
+ }
222
+ });
223
+ return response.data.rows || [];
224
+ } catch (error) {
225
+ consola2.error("Failed to get search analytics:", error);
226
+ throw error;
227
+ }
228
+ }
229
+ /**
230
+ * Get list of sitemaps
231
+ */
232
+ async getSitemaps() {
233
+ try {
234
+ const response = await this.searchconsole.sitemaps.list({
235
+ siteUrl: this.gscSiteUrl
236
+ });
237
+ return response.data.sitemap || [];
238
+ } catch (error) {
239
+ consola2.error("Failed to get sitemaps:", error);
240
+ throw error;
241
+ }
242
+ }
243
+ /**
244
+ * Map API response to our types
245
+ */
246
+ mapInspectionResult(url, result) {
247
+ const indexStatus = result.indexStatusResult;
248
+ return {
249
+ url,
250
+ inspectionResultLink: result.inspectionResultLink || void 0,
251
+ indexStatusResult: {
252
+ verdict: indexStatus.verdict || "VERDICT_UNSPECIFIED",
253
+ coverageState: indexStatus.coverageState || "COVERAGE_STATE_UNSPECIFIED",
254
+ indexingState: indexStatus.indexingState || "INDEXING_STATE_UNSPECIFIED",
255
+ robotsTxtState: indexStatus.robotsTxtState || "ROBOTS_TXT_STATE_UNSPECIFIED",
256
+ pageFetchState: indexStatus.pageFetchState || "PAGE_FETCH_STATE_UNSPECIFIED",
257
+ lastCrawlTime: indexStatus.lastCrawlTime || void 0,
258
+ crawledAs: indexStatus.crawledAs,
259
+ googleCanonical: indexStatus.googleCanonical || void 0,
260
+ userCanonical: indexStatus.userCanonical || void 0,
261
+ sitemap: indexStatus.sitemap || void 0,
262
+ referringUrls: indexStatus.referringUrls || void 0
263
+ },
264
+ mobileUsabilityResult: result.mobileUsabilityResult ? {
265
+ verdict: result.mobileUsabilityResult.verdict || "VERDICT_UNSPECIFIED",
266
+ issues: result.mobileUsabilityResult.issues?.map((issue) => ({
267
+ issueType: issue.issueType || "UNKNOWN",
268
+ message: issue.message || ""
269
+ }))
270
+ } : void 0,
271
+ richResultsResult: result.richResultsResult ? {
272
+ verdict: result.richResultsResult.verdict || "VERDICT_UNSPECIFIED",
273
+ detectedItems: result.richResultsResult.detectedItems?.map((item) => ({
274
+ richResultType: item.richResultType || "UNKNOWN",
275
+ items: item.items?.map((i) => ({
276
+ name: i.name || "",
277
+ issues: i.issues?.map((issue) => ({
278
+ issueMessage: issue.issueMessage || "",
279
+ severity: issue.severity || "WARNING"
280
+ }))
281
+ }))
282
+ }))
283
+ } : void 0
284
+ };
285
+ }
286
+ };
287
+
288
+ // src/google-console/analyzer.ts
289
+ function analyzeInspectionResults(results) {
290
+ const issues = [];
291
+ for (const result of results) {
292
+ issues.push(...analyzeUrlInspection(result));
293
+ }
294
+ return issues.sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity));
295
+ }
296
+ function analyzeUrlInspection(result) {
297
+ const issues = [];
298
+ const { indexStatusResult, mobileUsabilityResult, richResultsResult } = result;
299
+ switch (indexStatusResult.coverageState) {
300
+ case "CRAWLED_CURRENTLY_NOT_INDEXED":
301
+ issues.push({
302
+ id: `crawled-not-indexed-${hash(result.url)}`,
303
+ url: result.url,
304
+ category: "indexing",
305
+ severity: "error",
306
+ title: "Page crawled but not indexed",
307
+ description: "Google crawled this page but decided not to index it. This often indicates low content quality or duplicate content.",
308
+ recommendation: "Improve content quality, ensure uniqueness, add more valuable information, and check for duplicate content issues.",
309
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
310
+ metadata: { coverageState: indexStatusResult.coverageState }
311
+ });
312
+ break;
313
+ case "DISCOVERED_CURRENTLY_NOT_INDEXED":
314
+ issues.push({
315
+ id: `discovered-not-indexed-${hash(result.url)}`,
316
+ url: result.url,
317
+ category: "indexing",
318
+ severity: "warning",
319
+ title: "Page discovered but not crawled",
320
+ description: "Google discovered this URL but has not crawled it yet. This may indicate crawl budget issues or low priority.",
321
+ recommendation: "Improve internal linking to this page, submit URL through Google Search Console, or add to sitemap.",
322
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
323
+ metadata: { coverageState: indexStatusResult.coverageState }
324
+ });
325
+ break;
326
+ case "DUPLICATE_WITHOUT_USER_SELECTED_CANONICAL":
327
+ issues.push({
328
+ id: `duplicate-no-canonical-${hash(result.url)}`,
329
+ url: result.url,
330
+ category: "indexing",
331
+ severity: "warning",
332
+ title: "Duplicate page without canonical",
333
+ description: "This page is considered a duplicate but no canonical URL has been specified. Google chose a canonical for you.",
334
+ recommendation: "Add a canonical tag pointing to the preferred version of this page.",
335
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
336
+ metadata: {
337
+ coverageState: indexStatusResult.coverageState,
338
+ googleCanonical: indexStatusResult.googleCanonical
339
+ }
340
+ });
341
+ break;
342
+ case "DUPLICATE_GOOGLE_CHOSE_DIFFERENT_CANONICAL":
343
+ issues.push({
344
+ id: `canonical-mismatch-${hash(result.url)}`,
345
+ url: result.url,
346
+ category: "indexing",
347
+ severity: "warning",
348
+ title: "Google chose different canonical",
349
+ description: "You specified a canonical URL, but Google chose a different one. This may cause indexing issues.",
350
+ recommendation: "Review canonical tags and ensure they point to the correct URL. Check for duplicate content.",
351
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
352
+ metadata: {
353
+ coverageState: indexStatusResult.coverageState,
354
+ userCanonical: indexStatusResult.userCanonical,
355
+ googleCanonical: indexStatusResult.googleCanonical
356
+ }
357
+ });
358
+ break;
359
+ }
360
+ switch (indexStatusResult.indexingState) {
361
+ case "BLOCKED_BY_META_TAG":
362
+ issues.push({
363
+ id: `blocked-meta-noindex-${hash(result.url)}`,
364
+ url: result.url,
365
+ category: "indexing",
366
+ severity: "error",
367
+ title: "Blocked by noindex meta tag",
368
+ description: "This page has a noindex meta tag preventing it from being indexed.",
369
+ recommendation: "Remove the noindex meta tag if you want this page to be indexed. If intentional, no action needed.",
370
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
371
+ metadata: { indexingState: indexStatusResult.indexingState }
372
+ });
373
+ break;
374
+ case "BLOCKED_BY_HTTP_HEADER":
375
+ issues.push({
376
+ id: `blocked-http-header-${hash(result.url)}`,
377
+ url: result.url,
378
+ category: "indexing",
379
+ severity: "error",
380
+ title: "Blocked by X-Robots-Tag header",
381
+ description: "This page has a noindex directive in the X-Robots-Tag HTTP header.",
382
+ recommendation: "Remove the X-Robots-Tag: noindex header if you want this page to be indexed.",
383
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
384
+ metadata: { indexingState: indexStatusResult.indexingState }
385
+ });
386
+ break;
387
+ case "BLOCKED_BY_ROBOTS_TXT":
388
+ issues.push({
389
+ id: `blocked-robots-txt-${hash(result.url)}`,
390
+ url: result.url,
391
+ category: "crawling",
392
+ severity: "error",
393
+ title: "Blocked by robots.txt",
394
+ description: "This page is blocked from crawling by robots.txt rules.",
395
+ recommendation: "Update robots.txt to allow crawling if you want this page to be indexed.",
396
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
397
+ metadata: { indexingState: indexStatusResult.indexingState }
398
+ });
399
+ break;
400
+ }
401
+ switch (indexStatusResult.pageFetchState) {
402
+ case "SOFT_404":
403
+ issues.push({
404
+ id: `soft-404-${hash(result.url)}`,
405
+ url: result.url,
406
+ category: "technical",
407
+ severity: "error",
408
+ title: "Soft 404 error",
409
+ description: "This page returns a 200 status but Google detected it as a 404 page (empty or low-value content).",
410
+ recommendation: "Either return a proper 404 status code or add meaningful content to this page.",
411
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
412
+ metadata: { pageFetchState: indexStatusResult.pageFetchState }
413
+ });
414
+ break;
415
+ case "NOT_FOUND":
416
+ issues.push({
417
+ id: `404-error-${hash(result.url)}`,
418
+ url: result.url,
419
+ category: "technical",
420
+ severity: "error",
421
+ title: "404 Not Found",
422
+ description: "This page returns a 404 error.",
423
+ recommendation: "Either restore the page content or set up a redirect to a relevant page.",
424
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
425
+ metadata: { pageFetchState: indexStatusResult.pageFetchState }
426
+ });
427
+ break;
428
+ case "SERVER_ERROR":
429
+ issues.push({
430
+ id: `server-error-${hash(result.url)}`,
431
+ url: result.url,
432
+ category: "technical",
433
+ severity: "critical",
434
+ title: "Server error (5xx)",
435
+ description: "This page returns a server error when Google tries to crawl it.",
436
+ recommendation: "Fix the server-side error. Check server logs for details.",
437
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
438
+ metadata: { pageFetchState: indexStatusResult.pageFetchState }
439
+ });
440
+ break;
441
+ case "REDIRECT_ERROR":
442
+ issues.push({
443
+ id: `redirect-error-${hash(result.url)}`,
444
+ url: result.url,
445
+ category: "technical",
446
+ severity: "error",
447
+ title: "Redirect error",
448
+ description: "There is a redirect issue with this page (redirect loop, too many redirects, or invalid redirect).",
449
+ recommendation: "Fix the redirect chain. Ensure redirects point to valid, accessible pages.",
450
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
451
+ metadata: { pageFetchState: indexStatusResult.pageFetchState }
452
+ });
453
+ break;
454
+ case "ACCESS_DENIED":
455
+ case "ACCESS_FORBIDDEN":
456
+ issues.push({
457
+ id: `access-denied-${hash(result.url)}`,
458
+ url: result.url,
459
+ category: "technical",
460
+ severity: "error",
461
+ title: "Access denied (401/403)",
462
+ description: "Google cannot access this page due to authentication requirements.",
463
+ recommendation: "Ensure the page is publicly accessible without authentication for Googlebot.",
464
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
465
+ metadata: { pageFetchState: indexStatusResult.pageFetchState }
466
+ });
467
+ break;
468
+ }
469
+ if (mobileUsabilityResult?.verdict === "FAIL" && mobileUsabilityResult.issues) {
470
+ for (const issue of mobileUsabilityResult.issues) {
471
+ issues.push({
472
+ id: `mobile-${issue.issueType}-${hash(result.url)}`,
473
+ url: result.url,
474
+ category: "mobile",
475
+ severity: "warning",
476
+ title: `Mobile usability: ${formatIssueType(issue.issueType)}`,
477
+ description: issue.message || "Mobile usability issue detected.",
478
+ recommendation: getMobileRecommendation(issue.issueType),
479
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
480
+ metadata: { issueType: issue.issueType }
481
+ });
482
+ }
483
+ }
484
+ if (richResultsResult?.verdict === "FAIL" && richResultsResult.detectedItems) {
485
+ for (const item of richResultsResult.detectedItems) {
486
+ for (const i of item.items || []) {
487
+ for (const issueDetail of i.issues || []) {
488
+ issues.push({
489
+ id: `rich-result-${item.richResultType}-${hash(result.url)}`,
490
+ url: result.url,
491
+ category: "structured-data",
492
+ severity: issueDetail.severity === "ERROR" ? "error" : "warning",
493
+ title: `${item.richResultType}: ${i.name}`,
494
+ description: issueDetail.issueMessage,
495
+ recommendation: "Fix the structured data markup according to Google guidelines.",
496
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
497
+ metadata: { richResultType: item.richResultType }
498
+ });
499
+ }
500
+ }
501
+ }
502
+ }
503
+ return issues;
504
+ }
505
+ function severityOrder(severity) {
506
+ const order = {
507
+ critical: 0,
508
+ error: 1,
509
+ warning: 2,
510
+ info: 3
511
+ };
512
+ return order[severity];
513
+ }
514
+ function hash(str) {
515
+ let hash2 = 0;
516
+ for (let i = 0; i < str.length; i++) {
517
+ const char = str.charCodeAt(i);
518
+ hash2 = (hash2 << 5) - hash2 + char;
519
+ hash2 = hash2 & hash2;
520
+ }
521
+ return Math.abs(hash2).toString(36);
522
+ }
523
+ function formatIssueType(type) {
524
+ return type.replace(/_/g, " ").toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase());
525
+ }
526
+ function getMobileRecommendation(issueType) {
527
+ const recommendations = {
528
+ MOBILE_FRIENDLY_RULE_USES_INCOMPATIBLE_PLUGINS: "Remove Flash or other incompatible plugins. Use HTML5 alternatives.",
529
+ MOBILE_FRIENDLY_RULE_CONFIGURE_VIEWPORT: 'Add a viewport meta tag: <meta name="viewport" content="width=device-width, initial-scale=1">',
530
+ MOBILE_FRIENDLY_RULE_CONTENT_NOT_SIZED_TO_VIEWPORT: "Ensure content width fits the viewport. Use responsive CSS.",
531
+ MOBILE_FRIENDLY_RULE_TAP_TARGETS_TOO_SMALL: "Increase the size of touch targets (buttons, links) to at least 48x48 pixels.",
532
+ MOBILE_FRIENDLY_RULE_TEXT_TOO_SMALL: "Use at least 16px font size for body text."
533
+ };
534
+ return recommendations[issueType] || "Fix the mobile usability issue according to Google guidelines.";
535
+ }
536
+
537
+ export { GoogleConsoleClient, analyzeInspectionResults, createAuthClient, loadCredentials, verifyAuth };
538
+ //# sourceMappingURL=index.mjs.map
539
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/google-console/auth.ts","../../src/google-console/client.ts","../../src/google-console/analyzer.ts"],"names":["consola","hash"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * @djangocfg/seo - Google Console Authentication\n * Service Account authentication for Google Search Console API\n */\n\nimport { JWT } from 'google-auth-library';\nimport { readFileSync, existsSync } from 'node:fs';\nimport consola from 'consola';\nimport type { GoogleConsoleConfig } from '../types/index.js';\n\nconst SCOPES = [\n 'https://www.googleapis.com/auth/webmasters.readonly',\n 'https://www.googleapis.com/auth/webmasters',\n];\n\nexport interface ServiceAccountCredentials {\n client_email: string;\n private_key: string;\n project_id?: string;\n}\n\n/**\n * Load service account credentials from file or config\n */\nexport function loadCredentials(config: GoogleConsoleConfig): ServiceAccountCredentials {\n if (config.serviceAccountJson) {\n return config.serviceAccountJson;\n }\n\n if (config.serviceAccountPath) {\n if (!existsSync(config.serviceAccountPath)) {\n throw new Error(`Service account file not found: ${config.serviceAccountPath}`);\n }\n\n const content = readFileSync(config.serviceAccountPath, 'utf-8');\n return JSON.parse(content) as ServiceAccountCredentials;\n }\n\n // Try to load from environment variable\n const envJson = process.env.GOOGLE_SERVICE_ACCOUNT_JSON;\n if (envJson) {\n return JSON.parse(envJson) as ServiceAccountCredentials;\n }\n\n // Try default path\n const defaultPath = './service_account.json';\n if (existsSync(defaultPath)) {\n const content = readFileSync(defaultPath, 'utf-8');\n return JSON.parse(content) as ServiceAccountCredentials;\n }\n\n throw new Error(\n 'No service account credentials found. Provide serviceAccountPath, serviceAccountJson, or set GOOGLE_SERVICE_ACCOUNT_JSON env variable.'\n );\n}\n\n/**\n * Create authenticated JWT client\n */\nexport function createAuthClient(config: GoogleConsoleConfig): JWT {\n const credentials = loadCredentials(config);\n\n const auth = new JWT({\n email: credentials.client_email,\n key: credentials.private_key,\n scopes: SCOPES,\n });\n\n // Store email for later display\n (auth as any)._serviceAccountEmail = credentials.client_email;\n\n return auth;\n}\n\n/**\n * Verify authentication is working\n */\nexport async function verifyAuth(auth: JWT, siteUrl?: string): Promise<boolean> {\n const email = (auth as any)._serviceAccountEmail || auth.email;\n\n try {\n await auth.authorize();\n consola.success('Google Search Console authentication verified');\n consola.info(`Service account: ${email}`);\n\n // Build GSC users URL with domain\n if (siteUrl) {\n const domain = new URL(siteUrl).hostname;\n const gscUrl = `https://search.google.com/search-console/users?resource_id=sc-domain%3A${domain}`;\n consola.info(`Ensure this email has Full access in GSC: ${gscUrl}`);\n }\n\n return true;\n } catch (error) {\n consola.error('Authentication failed');\n consola.info(`Service account email: ${email}`);\n consola.info('Make sure this email is added to GSC with Full access');\n return false;\n }\n}\n","/**\n * @djangocfg/seo - Google Search Console Client\n * Main client for interacting with Google Search Console API\n */\n\nimport { searchconsole, type searchconsole_v1 } from '@googleapis/searchconsole';\nimport type { JWT } from 'google-auth-library';\nimport consola from 'consola';\nimport pLimit from 'p-limit';\nimport pRetry from 'p-retry';\nimport { createAuthClient, verifyAuth } from './auth.js';\nimport type {\n GoogleConsoleConfig,\n UrlInspectionResult,\n CoverageState,\n IndexingState,\n IndexingVerdict,\n RobotsTxtState,\n PageFetchState,\n} from '../types/index.js';\n\nexport class GoogleConsoleClient {\n private auth: JWT;\n private searchconsole: searchconsole_v1.Searchconsole;\n private siteUrl: string;\n private gscSiteUrl: string; // Format for GSC API (may be sc-domain:xxx)\n private limit = pLimit(2); // Max 2 concurrent requests (Cloudflare-friendly)\n private requestDelay = 500; // Delay between requests in ms\n\n constructor(config: GoogleConsoleConfig) {\n this.auth = createAuthClient(config);\n this.searchconsole = searchconsole({ version: 'v1', auth: this.auth });\n this.siteUrl = config.siteUrl;\n\n // Support both URL prefix and domain property formats\n // If gscSiteUrl provided, use it; otherwise try domain property format\n if (config.gscSiteUrl) {\n this.gscSiteUrl = config.gscSiteUrl;\n } else {\n // Default to domain property format (most common)\n const domain = new URL(config.siteUrl).hostname;\n this.gscSiteUrl = `sc-domain:${domain}`;\n }\n\n consola.debug(`GSC site URL: ${this.gscSiteUrl}`);\n }\n\n /**\n * Delay helper for rate limiting\n */\n private delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n\n /**\n * Verify the client is authenticated\n */\n async verify(): Promise<boolean> {\n return verifyAuth(this.auth, this.siteUrl);\n }\n\n /**\n * List all sites in Search Console\n */\n async listSites(): Promise<string[]> {\n try {\n const response = await this.searchconsole.sites.list();\n return response.data.siteEntry?.map((site) => site.siteUrl || '') || [];\n } catch (error) {\n consola.error('Failed to list sites:', error);\n throw error;\n }\n }\n\n /**\n * Inspect a single URL\n */\n async inspectUrl(url: string): Promise<UrlInspectionResult> {\n return this.limit(async () => {\n return pRetry(\n async () => {\n const response = await this.searchconsole.urlInspection.index.inspect({\n requestBody: {\n inspectionUrl: url,\n siteUrl: this.gscSiteUrl,\n languageCode: 'en-US',\n },\n });\n\n const result = response.data.inspectionResult;\n\n if (!result?.indexStatusResult) {\n throw new Error(`No inspection result for URL: ${url}`);\n }\n\n return this.mapInspectionResult(url, result);\n },\n {\n retries: 2,\n minTimeout: 2000,\n maxTimeout: 10000,\n factor: 2, // Exponential backoff\n onFailedAttempt: (ctx) => {\n // Only log on final failure to reduce noise\n if (ctx.retriesLeft === 0) {\n consola.warn(`Failed: ${url}`);\n }\n },\n }\n );\n });\n }\n\n /**\n * Inspect multiple URLs in batch\n * Stops early if too many consecutive errors (likely rate limiting)\n */\n async inspectUrls(urls: string[]): Promise<UrlInspectionResult[]> {\n consola.info(`Inspecting ${urls.length} URLs...`);\n\n const results: UrlInspectionResult[] = [];\n const errors: Array<{ url: string; error: Error }> = [];\n let consecutiveErrors = 0;\n const maxConsecutiveErrors = 3; // Stop after 3 consecutive failures\n\n // Process URLs sequentially with delay to avoid rate limiting\n for (const url of urls) {\n try {\n const result = await this.inspectUrl(url);\n results.push(result);\n consecutiveErrors = 0; // Reset on success\n // Add delay between requests\n await this.delay(this.requestDelay);\n } catch (error) {\n const err = error as Error;\n errors.push({ url, error: err });\n consecutiveErrors++;\n\n // Early exit on consecutive errors (likely rate limiting or auth issue)\n if (consecutiveErrors >= maxConsecutiveErrors) {\n console.log('');\n consola.error(`Stopping after ${maxConsecutiveErrors} consecutive failures`);\n this.showRateLimitHelp();\n break;\n }\n }\n }\n\n if (errors.length > 0 && consecutiveErrors < maxConsecutiveErrors) {\n consola.warn(`Failed to inspect ${errors.length} URLs`);\n }\n\n if (results.length > 0) {\n consola.success(`Successfully inspected ${results.length}/${urls.length} URLs`);\n } else if (errors.length > 0) {\n consola.warn('No URLs were successfully inspected');\n }\n\n return results;\n }\n\n /**\n * Show help message for rate limiting issues\n */\n private showRateLimitHelp(): void {\n consola.info('Possible causes:');\n consola.info(' 1. Google API quota exceeded (2000 requests/day)');\n consola.info(' 2. Cloudflare blocking Google\\'s crawler');\n consola.info(' 3. Service account not added to GSC');\n console.log('');\n consola.info('Solutions:');\n consola.info(' • Check GSC access: https://search.google.com/search-console/users');\n console.log('');\n consola.info(' • Cloudflare WAF rule to allow Googlebot:');\n consola.info(' 1. Dashboard → Security → WAF → Custom rules → Create rule');\n consola.info(' 2. Name: \"Allow Googlebot\"');\n consola.info(' 3. Field: \"Known Bots\" | Operator: \"equals\" | Value: \"true\"');\n consola.info(' 4. Or click \"Edit expression\" and paste: (cf.client.bot)');\n consola.info(' 5. Action: Skip → check all rules');\n consola.info(' 6. Deploy');\n consola.info(' Docs: https://developers.cloudflare.com/waf/custom-rules/use-cases/allow-traffic-from-verified-bots/');\n console.log('');\n }\n\n /**\n * Get search analytics data\n */\n async getSearchAnalytics(\n options: {\n startDate: string;\n endDate: string;\n dimensions?: ('query' | 'page' | 'country' | 'device' | 'date')[];\n rowLimit?: number;\n }\n ): Promise<searchconsole_v1.Schema$ApiDataRow[]> {\n try {\n const response = await this.searchconsole.searchanalytics.query({\n siteUrl: this.gscSiteUrl,\n requestBody: {\n startDate: options.startDate,\n endDate: options.endDate,\n dimensions: options.dimensions || ['page'],\n rowLimit: options.rowLimit || 1000,\n },\n });\n\n return response.data.rows || [];\n } catch (error) {\n consola.error('Failed to get search analytics:', error);\n throw error;\n }\n }\n\n /**\n * Get list of sitemaps\n */\n async getSitemaps(): Promise<searchconsole_v1.Schema$WmxSitemap[]> {\n try {\n const response = await this.searchconsole.sitemaps.list({\n siteUrl: this.gscSiteUrl,\n });\n\n return response.data.sitemap || [];\n } catch (error) {\n consola.error('Failed to get sitemaps:', error);\n throw error;\n }\n }\n\n /**\n * Map API response to our types\n */\n private mapInspectionResult(\n url: string,\n result: searchconsole_v1.Schema$UrlInspectionResult\n ): UrlInspectionResult {\n const indexStatus = result.indexStatusResult!;\n\n return {\n url,\n inspectionResultLink: result.inspectionResultLink || undefined,\n indexStatusResult: {\n verdict: (indexStatus.verdict as IndexingVerdict) || 'VERDICT_UNSPECIFIED',\n coverageState: (indexStatus.coverageState as CoverageState) || 'COVERAGE_STATE_UNSPECIFIED',\n indexingState: (indexStatus.indexingState as IndexingState) || 'INDEXING_STATE_UNSPECIFIED',\n robotsTxtState: (indexStatus.robotsTxtState as RobotsTxtState) || 'ROBOTS_TXT_STATE_UNSPECIFIED',\n pageFetchState: (indexStatus.pageFetchState as PageFetchState) || 'PAGE_FETCH_STATE_UNSPECIFIED',\n lastCrawlTime: indexStatus.lastCrawlTime || undefined,\n crawledAs: indexStatus.crawledAs as 'DESKTOP' | 'MOBILE' | undefined,\n googleCanonical: indexStatus.googleCanonical || undefined,\n userCanonical: indexStatus.userCanonical || undefined,\n sitemap: indexStatus.sitemap || undefined,\n referringUrls: indexStatus.referringUrls || undefined,\n },\n mobileUsabilityResult: result.mobileUsabilityResult\n ? {\n verdict: (result.mobileUsabilityResult.verdict as IndexingVerdict) || 'VERDICT_UNSPECIFIED',\n issues: result.mobileUsabilityResult.issues?.map((issue) => ({\n issueType: issue.issueType || 'UNKNOWN',\n message: issue.message || '',\n })),\n }\n : undefined,\n richResultsResult: result.richResultsResult\n ? {\n verdict: (result.richResultsResult.verdict as IndexingVerdict) || 'VERDICT_UNSPECIFIED',\n detectedItems: result.richResultsResult.detectedItems?.map((item) => ({\n richResultType: item.richResultType || 'UNKNOWN',\n items: item.items?.map((i) => ({\n name: i.name || '',\n issues: i.issues?.map((issue) => ({\n issueMessage: issue.issueMessage || '',\n severity: (issue.severity as 'ERROR' | 'WARNING') || 'WARNING',\n })),\n })),\n })),\n }\n : undefined,\n };\n }\n}\n","/**\n * @djangocfg/seo - Google Console Analyzer\n * Analyze URL inspection results and detect SEO issues\n */\n\nimport type {\n UrlInspectionResult,\n SeoIssue,\n IssueSeverity,\n IssueCategory,\n} from '../types/index.js';\n\n/**\n * Analyze URL inspection results and extract SEO issues\n */\nexport function analyzeInspectionResults(results: UrlInspectionResult[]): SeoIssue[] {\n const issues: SeoIssue[] = [];\n\n for (const result of results) {\n issues.push(...analyzeUrlInspection(result));\n }\n\n return issues.sort((a, b) => severityOrder(a.severity) - severityOrder(b.severity));\n}\n\nfunction analyzeUrlInspection(result: UrlInspectionResult): SeoIssue[] {\n const issues: SeoIssue[] = [];\n const { indexStatusResult, mobileUsabilityResult, richResultsResult } = result;\n\n // Indexing Issues\n switch (indexStatusResult.coverageState) {\n case 'CRAWLED_CURRENTLY_NOT_INDEXED':\n issues.push({\n id: `crawled-not-indexed-${hash(result.url)}`,\n url: result.url,\n category: 'indexing',\n severity: 'error',\n title: 'Page crawled but not indexed',\n description:\n 'Google crawled this page but decided not to index it. This often indicates low content quality or duplicate content.',\n recommendation:\n 'Improve content quality, ensure uniqueness, add more valuable information, and check for duplicate content issues.',\n detectedAt: new Date().toISOString(),\n metadata: { coverageState: indexStatusResult.coverageState },\n });\n break;\n\n case 'DISCOVERED_CURRENTLY_NOT_INDEXED':\n issues.push({\n id: `discovered-not-indexed-${hash(result.url)}`,\n url: result.url,\n category: 'indexing',\n severity: 'warning',\n title: 'Page discovered but not crawled',\n description:\n 'Google discovered this URL but has not crawled it yet. This may indicate crawl budget issues or low priority.',\n recommendation:\n 'Improve internal linking to this page, submit URL through Google Search Console, or add to sitemap.',\n detectedAt: new Date().toISOString(),\n metadata: { coverageState: indexStatusResult.coverageState },\n });\n break;\n\n case 'DUPLICATE_WITHOUT_USER_SELECTED_CANONICAL':\n issues.push({\n id: `duplicate-no-canonical-${hash(result.url)}`,\n url: result.url,\n category: 'indexing',\n severity: 'warning',\n title: 'Duplicate page without canonical',\n description:\n 'This page is considered a duplicate but no canonical URL has been specified. Google chose a canonical for you.',\n recommendation:\n 'Add a canonical tag pointing to the preferred version of this page.',\n detectedAt: new Date().toISOString(),\n metadata: {\n coverageState: indexStatusResult.coverageState,\n googleCanonical: indexStatusResult.googleCanonical,\n },\n });\n break;\n\n case 'DUPLICATE_GOOGLE_CHOSE_DIFFERENT_CANONICAL':\n issues.push({\n id: `canonical-mismatch-${hash(result.url)}`,\n url: result.url,\n category: 'indexing',\n severity: 'warning',\n title: 'Google chose different canonical',\n description:\n 'You specified a canonical URL, but Google chose a different one. This may cause indexing issues.',\n recommendation:\n 'Review canonical tags and ensure they point to the correct URL. Check for duplicate content.',\n detectedAt: new Date().toISOString(),\n metadata: {\n coverageState: indexStatusResult.coverageState,\n userCanonical: indexStatusResult.userCanonical,\n googleCanonical: indexStatusResult.googleCanonical,\n },\n });\n break;\n }\n\n // Indexing State Issues\n switch (indexStatusResult.indexingState) {\n case 'BLOCKED_BY_META_TAG':\n issues.push({\n id: `blocked-meta-noindex-${hash(result.url)}`,\n url: result.url,\n category: 'indexing',\n severity: 'error',\n title: 'Blocked by noindex meta tag',\n description: 'This page has a noindex meta tag preventing it from being indexed.',\n recommendation:\n 'Remove the noindex meta tag if you want this page to be indexed. If intentional, no action needed.',\n detectedAt: new Date().toISOString(),\n metadata: { indexingState: indexStatusResult.indexingState },\n });\n break;\n\n case 'BLOCKED_BY_HTTP_HEADER':\n issues.push({\n id: `blocked-http-header-${hash(result.url)}`,\n url: result.url,\n category: 'indexing',\n severity: 'error',\n title: 'Blocked by X-Robots-Tag header',\n description: 'This page has a noindex directive in the X-Robots-Tag HTTP header.',\n recommendation:\n 'Remove the X-Robots-Tag: noindex header if you want this page to be indexed.',\n detectedAt: new Date().toISOString(),\n metadata: { indexingState: indexStatusResult.indexingState },\n });\n break;\n\n case 'BLOCKED_BY_ROBOTS_TXT':\n issues.push({\n id: `blocked-robots-txt-${hash(result.url)}`,\n url: result.url,\n category: 'crawling',\n severity: 'error',\n title: 'Blocked by robots.txt',\n description: 'This page is blocked from crawling by robots.txt rules.',\n recommendation:\n 'Update robots.txt to allow crawling if you want this page to be indexed.',\n detectedAt: new Date().toISOString(),\n metadata: { indexingState: indexStatusResult.indexingState },\n });\n break;\n }\n\n // Page Fetch Issues\n switch (indexStatusResult.pageFetchState) {\n case 'SOFT_404':\n issues.push({\n id: `soft-404-${hash(result.url)}`,\n url: result.url,\n category: 'technical',\n severity: 'error',\n title: 'Soft 404 error',\n description:\n 'This page returns a 200 status but Google detected it as a 404 page (empty or low-value content).',\n recommendation:\n 'Either return a proper 404 status code or add meaningful content to this page.',\n detectedAt: new Date().toISOString(),\n metadata: { pageFetchState: indexStatusResult.pageFetchState },\n });\n break;\n\n case 'NOT_FOUND':\n issues.push({\n id: `404-error-${hash(result.url)}`,\n url: result.url,\n category: 'technical',\n severity: 'error',\n title: '404 Not Found',\n description: 'This page returns a 404 error.',\n recommendation:\n 'Either restore the page content or set up a redirect to a relevant page.',\n detectedAt: new Date().toISOString(),\n metadata: { pageFetchState: indexStatusResult.pageFetchState },\n });\n break;\n\n case 'SERVER_ERROR':\n issues.push({\n id: `server-error-${hash(result.url)}`,\n url: result.url,\n category: 'technical',\n severity: 'critical',\n title: 'Server error (5xx)',\n description: 'This page returns a server error when Google tries to crawl it.',\n recommendation:\n 'Fix the server-side error. Check server logs for details.',\n detectedAt: new Date().toISOString(),\n metadata: { pageFetchState: indexStatusResult.pageFetchState },\n });\n break;\n\n case 'REDIRECT_ERROR':\n issues.push({\n id: `redirect-error-${hash(result.url)}`,\n url: result.url,\n category: 'technical',\n severity: 'error',\n title: 'Redirect error',\n description:\n 'There is a redirect issue with this page (redirect loop, too many redirects, or invalid redirect).',\n recommendation:\n 'Fix the redirect chain. Ensure redirects point to valid, accessible pages.',\n detectedAt: new Date().toISOString(),\n metadata: { pageFetchState: indexStatusResult.pageFetchState },\n });\n break;\n\n case 'ACCESS_DENIED':\n case 'ACCESS_FORBIDDEN':\n issues.push({\n id: `access-denied-${hash(result.url)}`,\n url: result.url,\n category: 'technical',\n severity: 'error',\n title: 'Access denied (401/403)',\n description: 'Google cannot access this page due to authentication requirements.',\n recommendation:\n 'Ensure the page is publicly accessible without authentication for Googlebot.',\n detectedAt: new Date().toISOString(),\n metadata: { pageFetchState: indexStatusResult.pageFetchState },\n });\n break;\n }\n\n // Mobile Usability Issues\n if (mobileUsabilityResult?.verdict === 'FAIL' && mobileUsabilityResult.issues) {\n for (const issue of mobileUsabilityResult.issues) {\n issues.push({\n id: `mobile-${issue.issueType}-${hash(result.url)}`,\n url: result.url,\n category: 'mobile',\n severity: 'warning',\n title: `Mobile usability: ${formatIssueType(issue.issueType)}`,\n description: issue.message || 'Mobile usability issue detected.',\n recommendation: getMobileRecommendation(issue.issueType),\n detectedAt: new Date().toISOString(),\n metadata: { issueType: issue.issueType },\n });\n }\n }\n\n // Rich Results Issues\n if (richResultsResult?.verdict === 'FAIL' && richResultsResult.detectedItems) {\n for (const item of richResultsResult.detectedItems) {\n for (const i of item.items || []) {\n for (const issueDetail of i.issues || []) {\n issues.push({\n id: `rich-result-${item.richResultType}-${hash(result.url)}`,\n url: result.url,\n category: 'structured-data',\n severity: issueDetail.severity === 'ERROR' ? 'error' : 'warning',\n title: `${item.richResultType}: ${i.name}`,\n description: issueDetail.issueMessage,\n recommendation:\n 'Fix the structured data markup according to Google guidelines.',\n detectedAt: new Date().toISOString(),\n metadata: { richResultType: item.richResultType },\n });\n }\n }\n }\n }\n\n return issues;\n}\n\nfunction severityOrder(severity: IssueSeverity): number {\n const order: Record<IssueSeverity, number> = {\n critical: 0,\n error: 1,\n warning: 2,\n info: 3,\n };\n return order[severity];\n}\n\nfunction hash(str: string): string {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash = (hash << 5) - hash + char;\n hash = hash & hash;\n }\n return Math.abs(hash).toString(36);\n}\n\nfunction formatIssueType(type: string): string {\n return type\n .replace(/_/g, ' ')\n .toLowerCase()\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n\nfunction getMobileRecommendation(issueType: string): string {\n const recommendations: Record<string, string> = {\n MOBILE_FRIENDLY_RULE_USES_INCOMPATIBLE_PLUGINS:\n 'Remove Flash or other incompatible plugins. Use HTML5 alternatives.',\n MOBILE_FRIENDLY_RULE_CONFIGURE_VIEWPORT:\n 'Add a viewport meta tag: <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">',\n MOBILE_FRIENDLY_RULE_CONTENT_NOT_SIZED_TO_VIEWPORT:\n 'Ensure content width fits the viewport. Use responsive CSS.',\n MOBILE_FRIENDLY_RULE_TAP_TARGETS_TOO_SMALL:\n 'Increase the size of touch targets (buttons, links) to at least 48x48 pixels.',\n MOBILE_FRIENDLY_RULE_TEXT_TOO_SMALL:\n 'Use at least 16px font size for body text.',\n };\n\n return recommendations[issueType] || 'Fix the mobile usability issue according to Google guidelines.';\n}\n"]}