@orion-studios/payload-seo-audit 1.0.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.
Files changed (165) hide show
  1. package/README.md +127 -0
  2. package/bin/init.js +267 -0
  3. package/dist/api/backlinks-import.d.ts +4 -0
  4. package/dist/api/backlinks-import.d.ts.map +1 -0
  5. package/dist/api/backlinks-import.js +182 -0
  6. package/dist/api/cron.d.ts +4 -0
  7. package/dist/api/cron.d.ts.map +1 -0
  8. package/dist/api/cron.js +89 -0
  9. package/dist/api/index.d.ts +10 -0
  10. package/dist/api/index.d.ts.map +1 -0
  11. package/dist/api/index.js +21 -0
  12. package/dist/api/page-result.d.ts +4 -0
  13. package/dist/api/page-result.d.ts.map +1 -0
  14. package/dist/api/page-result.js +93 -0
  15. package/dist/api/page-results.d.ts +4 -0
  16. package/dist/api/page-results.d.ts.map +1 -0
  17. package/dist/api/page-results.js +83 -0
  18. package/dist/api/run-stream.d.ts +4 -0
  19. package/dist/api/run-stream.d.ts.map +1 -0
  20. package/dist/api/run-stream.js +273 -0
  21. package/dist/api/run.d.ts +4 -0
  22. package/dist/api/run.d.ts.map +1 -0
  23. package/dist/api/run.js +102 -0
  24. package/dist/api/snapshot-report.d.ts +4 -0
  25. package/dist/api/snapshot-report.d.ts.map +1 -0
  26. package/dist/api/snapshot-report.js +130 -0
  27. package/dist/api/snapshots.d.ts +4 -0
  28. package/dist/api/snapshots.d.ts.map +1 -0
  29. package/dist/api/snapshots.js +138 -0
  30. package/dist/api/trend.d.ts +4 -0
  31. package/dist/api/trend.d.ts.map +1 -0
  32. package/dist/api/trend.js +71 -0
  33. package/dist/collections/SeoAuthoritySnapshots.d.ts +3 -0
  34. package/dist/collections/SeoAuthoritySnapshots.d.ts.map +1 -0
  35. package/dist/collections/SeoAuthoritySnapshots.js +83 -0
  36. package/dist/collections/SeoKeywordVisibility.d.ts +3 -0
  37. package/dist/collections/SeoKeywordVisibility.d.ts.map +1 -0
  38. package/dist/collections/SeoKeywordVisibility.js +65 -0
  39. package/dist/collections/SeoPageResults.d.ts +3 -0
  40. package/dist/collections/SeoPageResults.d.ts.map +1 -0
  41. package/dist/collections/SeoPageResults.js +170 -0
  42. package/dist/collections/SeoSnapshots.d.ts +3 -0
  43. package/dist/collections/SeoSnapshots.d.ts.map +1 -0
  44. package/dist/collections/SeoSnapshots.js +131 -0
  45. package/dist/components/hooks/useSeoApi.d.ts +7 -0
  46. package/dist/components/hooks/useSeoApi.d.ts.map +1 -0
  47. package/dist/components/hooks/useSeoApi.js +31 -0
  48. package/dist/components/hooks/useSeoPageResults.d.ts +19 -0
  49. package/dist/components/hooks/useSeoPageResults.d.ts.map +1 -0
  50. package/dist/components/hooks/useSeoPageResults.js +62 -0
  51. package/dist/components/hooks/useSeoSnapshot.d.ts +8 -0
  52. package/dist/components/hooks/useSeoSnapshot.d.ts.map +1 -0
  53. package/dist/components/hooks/useSeoSnapshot.js +39 -0
  54. package/dist/components/hooks/useSeoTrend.d.ts +8 -0
  55. package/dist/components/hooks/useSeoTrend.d.ts.map +1 -0
  56. package/dist/components/hooks/useSeoTrend.js +38 -0
  57. package/dist/components/layout/SeoReportHeader.d.ts +10 -0
  58. package/dist/components/layout/SeoReportHeader.d.ts.map +1 -0
  59. package/dist/components/layout/SeoReportHeader.js +18 -0
  60. package/dist/components/layout/SeoReportShell.d.ts +9 -0
  61. package/dist/components/layout/SeoReportShell.d.ts.map +1 -0
  62. package/dist/components/layout/SeoReportShell.js +17 -0
  63. package/dist/components/pdf/PdfDownloadButton.d.ts +9 -0
  64. package/dist/components/pdf/PdfDownloadButton.d.ts.map +1 -0
  65. package/dist/components/pdf/PdfDownloadButton.js +80 -0
  66. package/dist/components/tables/IssueTable.d.ts +11 -0
  67. package/dist/components/tables/IssueTable.d.ts.map +1 -0
  68. package/dist/components/tables/IssueTable.js +121 -0
  69. package/dist/components/tables/PageResultsTable.d.ts +18 -0
  70. package/dist/components/tables/PageResultsTable.d.ts.map +1 -0
  71. package/dist/components/tables/PageResultsTable.js +96 -0
  72. package/dist/components/types.d.ts +107 -0
  73. package/dist/components/types.d.ts.map +1 -0
  74. package/dist/components/types.js +22 -0
  75. package/dist/components/utils/formatters.d.ts +15 -0
  76. package/dist/components/utils/formatters.d.ts.map +1 -0
  77. package/dist/components/utils/formatters.js +98 -0
  78. package/dist/components/utils/scoreHelpers.d.ts +17 -0
  79. package/dist/components/utils/scoreHelpers.d.ts.map +1 -0
  80. package/dist/components/utils/scoreHelpers.js +139 -0
  81. package/dist/components/views/SeoDashboard.d.ts +3 -0
  82. package/dist/components/views/SeoDashboard.d.ts.map +1 -0
  83. package/dist/components/views/SeoDashboard.js +239 -0
  84. package/dist/components/views/SeoPageReport.d.ts +3 -0
  85. package/dist/components/views/SeoPageReport.d.ts.map +1 -0
  86. package/dist/components/views/SeoPageReport.js +234 -0
  87. package/dist/components/views/SeoSnapshotReport.d.ts +3 -0
  88. package/dist/components/views/SeoSnapshotReport.d.ts.map +1 -0
  89. package/dist/components/views/SeoSnapshotReport.js +224 -0
  90. package/dist/components/visualization/CategoryScoreCard.d.ts +11 -0
  91. package/dist/components/visualization/CategoryScoreCard.d.ts.map +1 -0
  92. package/dist/components/visualization/CategoryScoreCard.js +17 -0
  93. package/dist/components/visualization/CategoryScoreGrid.d.ts +9 -0
  94. package/dist/components/visualization/CategoryScoreGrid.d.ts.map +1 -0
  95. package/dist/components/visualization/CategoryScoreGrid.js +32 -0
  96. package/dist/components/visualization/IssueCategoryChart.d.ts +8 -0
  97. package/dist/components/visualization/IssueCategoryChart.d.ts.map +1 -0
  98. package/dist/components/visualization/IssueCategoryChart.js +47 -0
  99. package/dist/components/visualization/MetricCard.d.ts +11 -0
  100. package/dist/components/visualization/MetricCard.d.ts.map +1 -0
  101. package/dist/components/visualization/MetricCard.js +17 -0
  102. package/dist/components/visualization/MetricCardRow.d.ts +7 -0
  103. package/dist/components/visualization/MetricCardRow.d.ts.map +1 -0
  104. package/dist/components/visualization/MetricCardRow.js +12 -0
  105. package/dist/components/visualization/ScoreBar.d.ts +11 -0
  106. package/dist/components/visualization/ScoreBar.d.ts.map +1 -0
  107. package/dist/components/visualization/ScoreBar.js +34 -0
  108. package/dist/components/visualization/ScoreGauge.d.ts +11 -0
  109. package/dist/components/visualization/ScoreGauge.d.ts.map +1 -0
  110. package/dist/components/visualization/ScoreGauge.js +28 -0
  111. package/dist/components/visualization/ScoreTrendChart.d.ts +9 -0
  112. package/dist/components/visualization/ScoreTrendChart.d.ts.map +1 -0
  113. package/dist/components/visualization/ScoreTrendChart.js +43 -0
  114. package/dist/components/visualization/SeverityBadge.d.ts +8 -0
  115. package/dist/components/visualization/SeverityBadge.d.ts.map +1 -0
  116. package/dist/components/visualization/SeverityBadge.js +14 -0
  117. package/dist/config.d.ts +38 -0
  118. package/dist/config.d.ts.map +1 -0
  119. package/dist/config.js +36 -0
  120. package/dist/exports/components.d.ts +4 -0
  121. package/dist/exports/components.d.ts.map +1 -0
  122. package/dist/exports/components.js +9 -0
  123. package/dist/globals/SeoDashboard.d.ts +3 -0
  124. package/dist/globals/SeoDashboard.d.ts.map +1 -0
  125. package/dist/globals/SeoDashboard.js +25 -0
  126. package/dist/index.d.ts +6 -0
  127. package/dist/index.d.ts.map +1 -0
  128. package/dist/index.js +39 -0
  129. package/dist/utilities/access.d.ts +8 -0
  130. package/dist/utilities/access.d.ts.map +1 -0
  131. package/dist/utilities/access.js +11 -0
  132. package/dist/utilities/auth.d.ts +7 -0
  133. package/dist/utilities/auth.d.ts.map +1 -0
  134. package/dist/utilities/auth.js +28 -0
  135. package/dist/utilities/checks.d.ts +3 -0
  136. package/dist/utilities/checks.d.ts.map +1 -0
  137. package/dist/utilities/checks.js +255 -0
  138. package/dist/utilities/crawler.d.ts +14 -0
  139. package/dist/utilities/crawler.d.ts.map +1 -0
  140. package/dist/utilities/crawler.js +152 -0
  141. package/dist/utilities/gsc.d.ts +15 -0
  142. package/dist/utilities/gsc.d.ts.map +1 -0
  143. package/dist/utilities/gsc.js +69 -0
  144. package/dist/utilities/helpers.d.ts +7 -0
  145. package/dist/utilities/helpers.d.ts.map +1 -0
  146. package/dist/utilities/helpers.js +44 -0
  147. package/dist/utilities/pagespeed.d.ts +3 -0
  148. package/dist/utilities/pagespeed.d.ts.map +1 -0
  149. package/dist/utilities/pagespeed.js +49 -0
  150. package/dist/utilities/providers.d.ts +3 -0
  151. package/dist/utilities/providers.d.ts.map +1 -0
  152. package/dist/utilities/providers.js +18 -0
  153. package/dist/utilities/runAudit.d.ts +14 -0
  154. package/dist/utilities/runAudit.d.ts.map +1 -0
  155. package/dist/utilities/runAudit.js +224 -0
  156. package/dist/utilities/scoring.d.ts +3 -0
  157. package/dist/utilities/scoring.d.ts.map +1 -0
  158. package/dist/utilities/scoring.js +45 -0
  159. package/dist/utilities/triggers.d.ts +3 -0
  160. package/dist/utilities/triggers.d.ts.map +1 -0
  161. package/dist/utilities/triggers.js +39 -0
  162. package/dist/utilities/types.d.ts +87 -0
  163. package/dist/utilities/types.d.ts.map +1 -0
  164. package/dist/utilities/types.js +2 -0
  165. package/package.json +63 -0
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDeltaDisplay = exports.getSeverityBarColor = exports.getSeverityColorClasses = exports.getGradeColorClass = exports.getScoreGrade = exports.getScoreBarColorClass = exports.getScoreTextClass = exports.getScoreBgClass = exports.getScoreColor = void 0;
4
+ const getScoreColor = (score) => {
5
+ if (score >= 90)
6
+ return '#059669';
7
+ if (score >= 80)
8
+ return '#16a34a';
9
+ if (score >= 60)
10
+ return '#d97706';
11
+ if (score >= 40)
12
+ return '#ea580c';
13
+ return '#dc2626';
14
+ };
15
+ exports.getScoreColor = getScoreColor;
16
+ const getScoreBgClass = (score) => {
17
+ if (score >= 90)
18
+ return 'bg-emerald-50 border-emerald-200';
19
+ if (score >= 80)
20
+ return 'bg-green-50 border-green-200';
21
+ if (score >= 60)
22
+ return 'bg-amber-50 border-amber-200';
23
+ if (score >= 40)
24
+ return 'bg-orange-50 border-orange-200';
25
+ return 'bg-red-50 border-red-200';
26
+ };
27
+ exports.getScoreBgClass = getScoreBgClass;
28
+ const getScoreTextClass = (score) => {
29
+ if (score >= 90)
30
+ return 'text-emerald-700';
31
+ if (score >= 80)
32
+ return 'text-green-700';
33
+ if (score >= 60)
34
+ return 'text-amber-700';
35
+ if (score >= 40)
36
+ return 'text-orange-700';
37
+ return 'text-red-700';
38
+ };
39
+ exports.getScoreTextClass = getScoreTextClass;
40
+ const getScoreBarColorClass = (score) => {
41
+ if (score >= 90)
42
+ return 'bg-emerald-500';
43
+ if (score >= 80)
44
+ return 'bg-green-500';
45
+ if (score >= 60)
46
+ return 'bg-amber-500';
47
+ if (score >= 40)
48
+ return 'bg-orange-500';
49
+ return 'bg-red-500';
50
+ };
51
+ exports.getScoreBarColorClass = getScoreBarColorClass;
52
+ const getScoreGrade = (score) => {
53
+ if (score >= 90)
54
+ return 'A';
55
+ if (score >= 80)
56
+ return 'B';
57
+ if (score >= 60)
58
+ return 'C';
59
+ if (score >= 40)
60
+ return 'D';
61
+ return 'F';
62
+ };
63
+ exports.getScoreGrade = getScoreGrade;
64
+ const getGradeColorClass = (grade) => {
65
+ switch (grade) {
66
+ case 'A':
67
+ return 'text-emerald-600';
68
+ case 'B':
69
+ return 'text-green-600';
70
+ case 'C':
71
+ return 'text-amber-600';
72
+ case 'D':
73
+ return 'text-orange-600';
74
+ default:
75
+ return 'text-red-600';
76
+ }
77
+ };
78
+ exports.getGradeColorClass = getGradeColorClass;
79
+ const getSeverityColorClasses = (severity) => {
80
+ switch (severity) {
81
+ case 'critical':
82
+ return {
83
+ badge: 'bg-red-100 text-red-700 border-red-200',
84
+ text: 'text-red-700',
85
+ bg: 'bg-red-50',
86
+ };
87
+ case 'high':
88
+ return {
89
+ badge: 'bg-orange-100 text-orange-700 border-orange-200',
90
+ text: 'text-orange-700',
91
+ bg: 'bg-orange-50',
92
+ };
93
+ case 'medium':
94
+ return {
95
+ badge: 'bg-amber-100 text-amber-700 border-amber-200',
96
+ text: 'text-amber-700',
97
+ bg: 'bg-amber-50',
98
+ };
99
+ case 'low':
100
+ return {
101
+ badge: 'bg-blue-100 text-blue-700 border-blue-200',
102
+ text: 'text-blue-700',
103
+ bg: 'bg-blue-50',
104
+ };
105
+ default:
106
+ return {
107
+ badge: 'bg-slate-100 text-slate-600 border-slate-200',
108
+ text: 'text-slate-600',
109
+ bg: 'bg-slate-50',
110
+ };
111
+ }
112
+ };
113
+ exports.getSeverityColorClasses = getSeverityColorClasses;
114
+ const getSeverityBarColor = (severity) => {
115
+ switch (severity) {
116
+ case 'critical':
117
+ return 'bg-red-500';
118
+ case 'high':
119
+ return 'bg-orange-500';
120
+ case 'medium':
121
+ return 'bg-amber-500';
122
+ case 'low':
123
+ return 'bg-blue-500';
124
+ default:
125
+ return 'bg-slate-400';
126
+ }
127
+ };
128
+ exports.getSeverityBarColor = getSeverityBarColor;
129
+ const getDeltaDisplay = (current, previous) => {
130
+ if (previous == null)
131
+ return null;
132
+ const delta = current - previous;
133
+ if (delta === 0)
134
+ return { text: '0', className: 'text-slate-500' };
135
+ if (delta > 0)
136
+ return { text: `+${delta}`, className: 'text-green-600' };
137
+ return { text: `${delta}`, className: 'text-red-600' };
138
+ };
139
+ exports.getDeltaDisplay = getDeltaDisplay;
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ export declare const SeoDashboard: React.FC;
3
+ //# sourceMappingURL=SeoDashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SeoDashboard.d.ts","sourceRoot":"","sources":["../../../src/components/views/SeoDashboard.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAoD,MAAM,OAAO,CAAA;AAqBxE,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAsShC,CAAA"}
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ 'use client';
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.SeoDashboard = void 0;
38
+ const react_1 = __importStar(require("react"));
39
+ const ScoreGauge_1 = require("../visualization/ScoreGauge");
40
+ const MetricCard_1 = require("../visualization/MetricCard");
41
+ const MetricCardRow_1 = require("../visualization/MetricCardRow");
42
+ const CategoryScoreGrid_1 = require("../visualization/CategoryScoreGrid");
43
+ const ScoreTrendChart_1 = require("../visualization/ScoreTrendChart");
44
+ const IssueTable_1 = require("../tables/IssueTable");
45
+ const SeoReportHeader_1 = require("../layout/SeoReportHeader");
46
+ const scoreHelpers_1 = require("../utils/scoreHelpers");
47
+ const formatters_1 = require("../utils/formatters");
48
+ const SeoDashboard = () => {
49
+ const [snapshots, setSnapshots] = (0, react_1.useState)([]);
50
+ const [latestIssues, setLatestIssues] = (0, react_1.useState)([]);
51
+ const [trendPoints, setTrendPoints] = (0, react_1.useState)([]);
52
+ const [loading, setLoading] = (0, react_1.useState)(true);
53
+ const [running, setRunning] = (0, react_1.useState)(false);
54
+ const [runningStep, setRunningStep] = (0, react_1.useState)('');
55
+ const [runningProgress, setRunningProgress] = (0, react_1.useState)(0);
56
+ const [error, setError] = (0, react_1.useState)(null);
57
+ const fetchData = (0, react_1.useCallback)(async () => {
58
+ setLoading(true);
59
+ setError(null);
60
+ try {
61
+ const [snapshotsRes, trendRes] = await Promise.all([
62
+ fetch('/api/seo/snapshots?limit=12', { credentials: 'include' }),
63
+ fetch('/api/seo/trend?limit=12', { credentials: 'include' }),
64
+ ]);
65
+ if (!snapshotsRes.ok)
66
+ throw new Error(`Snapshots request failed with ${snapshotsRes.status}`);
67
+ const snapshotsData = (await snapshotsRes.json());
68
+ setSnapshots(snapshotsData.snapshots || []);
69
+ setLatestIssues(snapshotsData.latestIssues || []);
70
+ if (trendRes.ok) {
71
+ const trendData = (await trendRes.json());
72
+ setTrendPoints(trendData.points || []);
73
+ }
74
+ }
75
+ catch (err) {
76
+ setError(err instanceof Error ? err.message : 'Failed to load SEO data.');
77
+ }
78
+ finally {
79
+ setLoading(false);
80
+ }
81
+ }, []);
82
+ (0, react_1.useEffect)(() => {
83
+ void fetchData();
84
+ }, [fetchData]);
85
+ const runAuditNow = (0, react_1.useCallback)(async () => {
86
+ setRunning(true);
87
+ setRunningStep('Starting audit...');
88
+ setRunningProgress(0);
89
+ setError(null);
90
+ try {
91
+ const response = await fetch('/api/seo/run-stream', {
92
+ method: 'POST',
93
+ credentials: 'include',
94
+ headers: { 'content-type': 'application/json' },
95
+ body: JSON.stringify({ runType: 'manual' }),
96
+ });
97
+ if (!response.ok || !response.body) {
98
+ throw new Error(`Request failed with ${response.status}`);
99
+ }
100
+ const reader = response.body.getReader();
101
+ const decoder = new TextDecoder();
102
+ let buffer = '';
103
+ while (true) {
104
+ const { done, value } = await reader.read();
105
+ if (done)
106
+ break;
107
+ buffer += decoder.decode(value, { stream: true });
108
+ const events = buffer.split('\n\n');
109
+ buffer = events.pop() || '';
110
+ for (const event of events) {
111
+ if (!event.trim())
112
+ continue;
113
+ const lines = event.split('\n');
114
+ let eventType = '';
115
+ let eventData = '';
116
+ for (const line of lines) {
117
+ if (line.startsWith('event:')) {
118
+ eventType = line.substring(6).trim();
119
+ }
120
+ else if (line.startsWith('data:')) {
121
+ eventData = line.substring(5).trim();
122
+ }
123
+ }
124
+ if (eventType && eventData) {
125
+ try {
126
+ const data = JSON.parse(eventData);
127
+ if (eventType === 'progress') {
128
+ setRunningStep(data.message || 'Processing...');
129
+ const step = data.step || '';
130
+ if (step === 'auth')
131
+ setRunningProgress(5);
132
+ else if (step === 'site')
133
+ setRunningProgress(10);
134
+ else if (step === 'snapshot')
135
+ setRunningProgress(15);
136
+ else if (step === 'crawl')
137
+ setRunningProgress(25);
138
+ else if (step === 'crawl-complete')
139
+ setRunningProgress(40);
140
+ else if (step === 'checks')
141
+ setRunningProgress(50);
142
+ else if (step === 'checks-complete')
143
+ setRunningProgress(60);
144
+ else if (step === 'pagespeed')
145
+ setRunningProgress(65);
146
+ else if (step === 'pagespeed-complete' || step === 'pagespeed-skipped')
147
+ setRunningProgress(75);
148
+ else if (step === 'gsc')
149
+ setRunningProgress(80);
150
+ else if (step === 'gsc-complete' || step === 'gsc-skipped')
151
+ setRunningProgress(85);
152
+ else if (step === 'saving')
153
+ setRunningProgress(90);
154
+ }
155
+ else if (eventType === 'complete') {
156
+ setRunningProgress(100);
157
+ setRunningStep('Audit complete!');
158
+ await fetchData();
159
+ setTimeout(() => setRunning(false), 1000);
160
+ return;
161
+ }
162
+ else if (eventType === 'error') {
163
+ throw new Error(data.message || 'Audit failed');
164
+ }
165
+ }
166
+ catch (parseErr) {
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ catch (err) {
173
+ setError(err instanceof Error ? err.message : 'Failed to run SEO audit.');
174
+ setRunning(false);
175
+ }
176
+ }, [fetchData]);
177
+ const latest = snapshots[0];
178
+ const previous = snapshots[1];
179
+ const overallScore = latest?.scores?.overall ?? 0;
180
+ const previousOverall = previous?.scores?.overall;
181
+ const delta = (0, scoreHelpers_1.getDeltaDisplay)(overallScore, previousOverall);
182
+ const totalIssues = (0, react_1.useMemo)(() => {
183
+ if (!latest?.issueCounts)
184
+ return 0;
185
+ const c = latest.issueCounts;
186
+ return (c.critical ?? 0) + (c.high ?? 0) + (c.medium ?? 0) + (c.low ?? 0) + (c.info ?? 0);
187
+ }, [latest]);
188
+ if (loading) {
189
+ return (react_1.default.createElement("section", { className: "seo-report mb-6 rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-700 dark:bg-slate-800" },
190
+ react_1.default.createElement("div", { className: "flex items-center gap-3" },
191
+ react_1.default.createElement("div", { className: "h-5 w-5 animate-spin rounded-full border-2 border-slate-300 border-t-slate-600 dark:border-slate-600 dark:border-t-slate-300" }),
192
+ react_1.default.createElement("p", { className: "text-sm text-slate-500 dark:text-slate-400" }, "Loading SEO data..."))));
193
+ }
194
+ return (react_1.default.createElement("section", { className: "seo-report mb-6 space-y-6 rounded-xl border border-slate-200 bg-white p-6 dark:border-slate-700 dark:bg-slate-800" },
195
+ react_1.default.createElement(SeoReportHeader_1.SeoReportHeader, { title: "SEO Health Report", subtitle: latest
196
+ ? `Last audit: ${(0, formatters_1.formatTimestamp)(latest.startedAt)}`
197
+ : 'No audits have been run yet.', actions: react_1.default.createElement(react_1.default.Fragment, null,
198
+ react_1.default.createElement("button", { type: "button", onClick: () => void fetchData(), className: "rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-200 dark:hover:bg-slate-600" }, "Refresh"),
199
+ react_1.default.createElement("button", { type: "button", onClick: () => void runAuditNow(), disabled: running, className: "rounded-md bg-slate-900 px-3 py-2 text-sm text-white disabled:opacity-60 dark:bg-slate-100 dark:text-slate-900" }, running ? 'Running Audit...' : 'Run Audit Now')) }),
200
+ running && runningStep && (react_1.default.createElement("div", { className: "rounded-md border border-blue-200 bg-blue-50 px-4 py-3 dark:border-blue-800 dark:bg-blue-900/30" },
201
+ react_1.default.createElement("div", { className: "flex items-center gap-3 mb-2" },
202
+ react_1.default.createElement("div", { className: "h-4 w-4 animate-spin rounded-full border-2 border-blue-300 border-t-blue-600 dark:border-blue-700 dark:border-t-blue-400" }),
203
+ react_1.default.createElement("p", { className: "text-sm text-blue-700 dark:text-blue-300" }, runningStep),
204
+ react_1.default.createElement("span", { className: "ml-auto text-sm font-medium text-blue-700 dark:text-blue-300" },
205
+ runningProgress,
206
+ "%")),
207
+ react_1.default.createElement("div", { className: "h-2 w-full overflow-hidden rounded-full bg-blue-100 dark:bg-blue-900" },
208
+ react_1.default.createElement("div", { className: "h-full rounded-full bg-blue-600 transition-all duration-500 ease-out dark:bg-blue-400", style: { width: `${runningProgress}%` } })))),
209
+ error && (react_1.default.createElement("div", { className: "rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/30 dark:text-red-300" }, error)),
210
+ !latest ? (react_1.default.createElement("div", { className: "flex flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed border-slate-200 py-16 dark:border-slate-700" },
211
+ react_1.default.createElement("p", { className: "text-lg font-medium text-slate-600 dark:text-slate-300" }, "No SEO audits yet"),
212
+ react_1.default.createElement("p", { className: "text-sm text-slate-500 dark:text-slate-400" }, "Run your first audit to see your site's SEO health."))) : (react_1.default.createElement(react_1.default.Fragment, null,
213
+ react_1.default.createElement("div", { className: "flex flex-col items-center gap-2 rounded-lg border border-slate-200 bg-gradient-to-br from-slate-50 to-white py-8 dark:border-slate-700 dark:from-slate-800/50 dark:to-slate-900/50" },
214
+ react_1.default.createElement(ScoreGauge_1.ScoreGauge, { score: overallScore, size: 140, showGrade: true }),
215
+ delta && (react_1.default.createElement("span", { className: `text-sm font-medium ${delta.className}` },
216
+ delta.text,
217
+ " from previous"))),
218
+ react_1.default.createElement(MetricCardRow_1.MetricCardRow, null,
219
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "Pages Crawled", value: (0, formatters_1.formatNumber)(latest.metrics?.pagesCrawled) }),
220
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "Critical Issues", value: latest.issueCounts?.critical ?? 0, valueClassName: (latest.issueCounts?.critical ?? 0) > 0 ? 'text-red-600 dark:text-red-400' : 'text-slate-900 dark:text-slate-100' }),
221
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "High Issues", value: latest.issueCounts?.high ?? 0, valueClassName: (latest.issueCounts?.high ?? 0) > 0 ? 'text-orange-600 dark:text-orange-400' : 'text-slate-900 dark:text-slate-100' }),
222
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "Lighthouse", value: latest.metrics?.avgLighthousePerformance != null
223
+ ? `${Math.round(latest.metrics.avgLighthousePerformance)}`
224
+ : '-', subtext: "Performance" }),
225
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "Avg LCP", value: (0, formatters_1.formatMs)(latest.metrics?.avgLCPMs), subtext: "Largest Contentful Paint" }),
226
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "Avg CLS", value: (0, formatters_1.formatCLS)(latest.metrics?.avgCLS), subtext: "Cumulative Layout Shift" })),
227
+ react_1.default.createElement("div", null,
228
+ react_1.default.createElement("h2", { className: "mb-3 text-sm font-semibold text-slate-900 dark:text-slate-100" }, "Category Breakdown"),
229
+ react_1.default.createElement(CategoryScoreGrid_1.CategoryScoreGrid, { scores: latest.scores || {}, previousScores: previous?.scores })),
230
+ react_1.default.createElement("div", null,
231
+ react_1.default.createElement("h2", { className: "mb-3 text-sm font-semibold text-slate-900 dark:text-slate-100" }, "Score Trend"),
232
+ react_1.default.createElement(ScoreTrendChart_1.ScoreTrendChart, { points: trendPoints })),
233
+ react_1.default.createElement("div", null,
234
+ react_1.default.createElement("div", { className: "mb-3 flex items-center justify-between" },
235
+ react_1.default.createElement("h2", { className: "text-sm font-semibold text-slate-900 dark:text-slate-100" }, "Top Issues"),
236
+ latest && (react_1.default.createElement("a", { href: `/admin/collections/seo-snapshots/${latest.id}`, className: "text-sm text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200" }, "View Full Report \u2192"))),
237
+ react_1.default.createElement(IssueTable_1.IssueTable, { issues: latestIssues, pageSize: 10 }))))));
238
+ };
239
+ exports.SeoDashboard = SeoDashboard;
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ export declare const SeoPageReport: React.FC;
3
+ //# sourceMappingURL=SeoPageReport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SeoPageReport.d.ts","sourceRoot":"","sources":["../../../src/components/views/SeoPageReport.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAoD,MAAM,OAAO,CAAA;AAyDxE,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EA8SjC,CAAA"}
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ 'use client';
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.SeoPageReport = void 0;
38
+ const react_1 = __importStar(require("react"));
39
+ const lucide_react_1 = require("lucide-react");
40
+ const ScoreGauge_1 = require("../visualization/ScoreGauge");
41
+ const ScoreBar_1 = require("../visualization/ScoreBar");
42
+ const MetricCard_1 = require("../visualization/MetricCard");
43
+ const SeverityBadge_1 = require("../visualization/SeverityBadge");
44
+ const SeoReportShell_1 = require("../layout/SeoReportShell");
45
+ const SeoReportHeader_1 = require("../layout/SeoReportHeader");
46
+ const PdfDownloadButton_1 = require("../pdf/PdfDownloadButton");
47
+ const formatters_1 = require("../utils/formatters");
48
+ const types_1 = require("../types");
49
+ const getIdFromPath = () => {
50
+ if (typeof window === 'undefined')
51
+ return null;
52
+ const segments = window.location.pathname.split('/');
53
+ const idx = segments.indexOf('seo-page-results');
54
+ if (idx >= 0 && segments[idx + 1]) {
55
+ return segments[idx + 1];
56
+ }
57
+ return null;
58
+ };
59
+ const CATEGORY_ICONS = {
60
+ metadata: react_1.default.createElement(lucide_react_1.FileText, { size: 16 }),
61
+ indexability: react_1.default.createElement(lucide_react_1.Search, { size: 16 }),
62
+ structure: react_1.default.createElement(lucide_react_1.LayoutGrid, { size: 16 }),
63
+ links: react_1.default.createElement(lucide_react_1.Link2, { size: 16 }),
64
+ media: react_1.default.createElement(lucide_react_1.ImageIcon, { size: 16 }),
65
+ structuredData: react_1.default.createElement(lucide_react_1.Code2, { size: 16 }),
66
+ performance: react_1.default.createElement(lucide_react_1.Gauge, { size: 16 }),
67
+ };
68
+ const CHAR_COUNT_STATUS_CLASS = {
69
+ good: 'text-green-600 bg-green-50 border-green-200',
70
+ warning: 'text-amber-600 bg-amber-50 border-amber-200',
71
+ error: 'text-red-600 bg-red-50 border-red-200',
72
+ };
73
+ const CATEGORIES = [
74
+ 'metadata',
75
+ 'indexability',
76
+ 'structure',
77
+ 'links',
78
+ 'media',
79
+ 'structuredData',
80
+ 'performance',
81
+ ];
82
+ const SeoPageReport = () => {
83
+ const [pageResult, setPageResult] = (0, react_1.useState)(null);
84
+ const [loading, setLoading] = (0, react_1.useState)(true);
85
+ const [error, setError] = (0, react_1.useState)(null);
86
+ const pageResultId = (0, react_1.useMemo)(() => getIdFromPath(), []);
87
+ const fetchPageResult = (0, react_1.useCallback)(async () => {
88
+ if (!pageResultId) {
89
+ setError('Could not determine page result ID.');
90
+ setLoading(false);
91
+ return;
92
+ }
93
+ setLoading(true);
94
+ setError(null);
95
+ try {
96
+ const response = await fetch(`/api/seo/page-result?id=${pageResultId}`, {
97
+ credentials: 'include',
98
+ });
99
+ if (!response.ok) {
100
+ const body = (await response.json().catch(() => ({})));
101
+ throw new Error(body.error || `Request failed with ${response.status}`);
102
+ }
103
+ const data = (await response.json());
104
+ setPageResult(data.pageResult);
105
+ }
106
+ catch (err) {
107
+ setError(err instanceof Error ? err.message : 'Failed to load page result.');
108
+ }
109
+ finally {
110
+ setLoading(false);
111
+ }
112
+ }, [pageResultId]);
113
+ (0, react_1.useEffect)(() => {
114
+ void fetchPageResult();
115
+ }, [fetchPageResult]);
116
+ const issuesByCategory = (0, react_1.useMemo)(() => {
117
+ if (!pageResult?.issues)
118
+ return {};
119
+ const groups = {};
120
+ for (const issue of pageResult.issues) {
121
+ const cat = issue.category || 'other';
122
+ if (!groups[cat])
123
+ groups[cat] = [];
124
+ groups[cat].push(issue);
125
+ }
126
+ const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
127
+ for (const cat of Object.keys(groups)) {
128
+ groups[cat].sort((a, b) => (order[a.severity] ?? 5) - (order[b.severity] ?? 5));
129
+ }
130
+ return groups;
131
+ }, [pageResult]);
132
+ if (loading) {
133
+ return (react_1.default.createElement(SeoReportShell_1.SeoReportShell, null,
134
+ react_1.default.createElement("div", { className: "flex items-center gap-3 py-12" },
135
+ react_1.default.createElement("div", { className: "h-5 w-5 animate-spin rounded-full border-2 border-slate-300 border-t-slate-600 dark:border-slate-600 dark:border-t-slate-300" }),
136
+ react_1.default.createElement("p", { className: "text-sm text-slate-500 dark:text-slate-400" }, "Loading page report..."))));
137
+ }
138
+ if (error || !pageResult) {
139
+ return (react_1.default.createElement(SeoReportShell_1.SeoReportShell, { onBack: () => window.history.back(), backLabel: "Back" },
140
+ react_1.default.createElement("div", { className: "rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-900/30 dark:text-red-300" }, error || 'No data available.')));
141
+ }
142
+ const overallScore = pageResult.overallScore ?? 0;
143
+ const titleInfo = (0, formatters_1.formatCharCount)(pageResult.title, 20, 65);
144
+ const descInfo = (0, formatters_1.formatCharCount)(pageResult.metaDescription, 70, 165);
145
+ return (react_1.default.createElement(SeoReportShell_1.SeoReportShell, { onBack: () => window.history.back(), backLabel: "Back to Snapshot" },
146
+ react_1.default.createElement("div", { className: "space-y-6", id: "seo-page-report" },
147
+ react_1.default.createElement(SeoReportHeader_1.SeoReportHeader, { title: "Page SEO Report", subtitle: pageResult.url, actions: react_1.default.createElement(PdfDownloadButton_1.PdfDownloadButton, { targetId: "seo-page-report", filename: `page-seo-${pageResult.path || pageResultId}` }), badges: pageResult.statusCode ? (react_1.default.createElement("span", { className: `inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium ${pageResult.statusCode === 200
148
+ ? 'border-green-200 bg-green-50 text-green-700'
149
+ : 'border-red-200 bg-red-50 text-red-700'}` }, pageResult.statusCode)) : undefined }),
150
+ react_1.default.createElement("div", { className: "flex flex-col items-center gap-2 rounded-lg border border-slate-200 bg-gradient-to-br from-slate-50 to-white py-8 dark:border-slate-700 dark:from-slate-800/50 dark:to-slate-900/50" },
151
+ react_1.default.createElement(ScoreGauge_1.ScoreGauge, { score: overallScore, size: 140, showGrade: true }),
152
+ react_1.default.createElement("p", { className: "mt-1 text-xs text-slate-500 dark:text-slate-400" },
153
+ "Checked: ",
154
+ (0, formatters_1.formatTimestamp)(pageResult.checkedAt))),
155
+ react_1.default.createElement("div", null,
156
+ react_1.default.createElement("h2", { className: "mb-3 text-sm font-semibold text-slate-900 dark:text-slate-100" }, "Page Metadata"),
157
+ react_1.default.createElement("div", { className: "space-y-3 rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-700 dark:bg-slate-800/50" },
158
+ react_1.default.createElement("div", null,
159
+ react_1.default.createElement("div", { className: "flex items-center justify-between" },
160
+ react_1.default.createElement("span", { className: "text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400" }, "Title Tag"),
161
+ react_1.default.createElement("span", { className: `inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium ${CHAR_COUNT_STATUS_CLASS[titleInfo.status]}` },
162
+ titleInfo.count,
163
+ " chars \u00B7 ",
164
+ titleInfo.label)),
165
+ react_1.default.createElement("p", { className: "mt-1 text-sm text-slate-800 dark:text-slate-200" }, pageResult.title || react_1.default.createElement("span", { className: "italic text-slate-400 dark:text-slate-500" }, "Missing"))),
166
+ react_1.default.createElement("hr", { className: "border-slate-100 dark:border-slate-700" }),
167
+ react_1.default.createElement("div", null,
168
+ react_1.default.createElement("div", { className: "flex items-center justify-between" },
169
+ react_1.default.createElement("span", { className: "text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400" }, "Meta Description"),
170
+ react_1.default.createElement("span", { className: `inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium ${CHAR_COUNT_STATUS_CLASS[descInfo.status]}` },
171
+ descInfo.count,
172
+ " chars \u00B7 ",
173
+ descInfo.label)),
174
+ react_1.default.createElement("p", { className: "mt-1 text-sm text-slate-800 dark:text-slate-200" }, pageResult.metaDescription || (react_1.default.createElement("span", { className: "italic text-slate-400 dark:text-slate-500" }, "Missing")))),
175
+ react_1.default.createElement("hr", { className: "border-slate-100 dark:border-slate-700" }),
176
+ react_1.default.createElement("div", null,
177
+ react_1.default.createElement("span", { className: "text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400" }, "Canonical URL"),
178
+ react_1.default.createElement("p", { className: "mt-1 text-sm text-slate-800 dark:text-slate-200" }, pageResult.canonical || react_1.default.createElement("span", { className: "italic text-slate-400 dark:text-slate-500" }, "Not set"))),
179
+ pageResult.robotsMeta && (react_1.default.createElement(react_1.default.Fragment, null,
180
+ react_1.default.createElement("hr", { className: "border-slate-100 dark:border-slate-700" }),
181
+ react_1.default.createElement("div", null,
182
+ react_1.default.createElement("span", { className: "text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400" }, "Robots Meta"),
183
+ react_1.default.createElement("p", { className: "mt-1 text-sm text-slate-800 dark:text-slate-200" }, pageResult.robotsMeta)))),
184
+ react_1.default.createElement("hr", { className: "border-slate-100 dark:border-slate-700" }),
185
+ react_1.default.createElement("div", null,
186
+ react_1.default.createElement("span", { className: "text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400" }, "H1 Tags"),
187
+ react_1.default.createElement("p", { className: "mt-1 text-sm text-slate-800 dark:text-slate-200" },
188
+ pageResult.h1Count ?? 0,
189
+ ' ',
190
+ react_1.default.createElement("span", { className: "text-xs text-slate-500 dark:text-slate-400" }, "(should be exactly 1)"))))),
191
+ react_1.default.createElement("div", null,
192
+ react_1.default.createElement("h2", { className: "mb-3 text-sm font-semibold text-slate-900 dark:text-slate-100" }, "Score Breakdown"),
193
+ react_1.default.createElement("div", { className: "space-y-4 rounded-lg border border-slate-200 bg-white p-4 dark:border-slate-700 dark:bg-slate-800/50" }, CATEGORIES.map((category) => {
194
+ const score = category === 'performance'
195
+ ? overallScore
196
+ : pageResult.scoreBreakdown?.[category] ?? 0;
197
+ return (react_1.default.createElement(ScoreBar_1.ScoreBar, { key: category, label: types_1.CATEGORY_LABELS[category], score: score, icon: CATEGORY_ICONS[category] }));
198
+ }))),
199
+ react_1.default.createElement("div", null,
200
+ react_1.default.createElement("h2", { className: "mb-3 text-sm font-semibold text-slate-900 dark:text-slate-100" }, "Page Statistics"),
201
+ react_1.default.createElement("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6" },
202
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "Internal Links", value: (0, formatters_1.formatNumber)(pageResult.internalLinks) }),
203
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "External Links", value: (0, formatters_1.formatNumber)(pageResult.externalLinks) }),
204
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "Total Images", value: (0, formatters_1.formatNumber)(pageResult.imagesTotal) }),
205
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "Missing Alt", value: (0, formatters_1.formatNumber)(pageResult.imagesMissingAlt), valueClassName: (pageResult.imagesMissingAlt ?? 0) > 0 ? 'text-orange-600 dark:text-orange-400' : 'text-slate-900 dark:text-slate-100' }),
206
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "Structured Data", value: (0, formatters_1.formatNumber)(pageResult.structuredDataBlocks) }),
207
+ react_1.default.createElement(MetricCard_1.MetricCard, { label: "Heading Issues", value: (0, formatters_1.formatNumber)(pageResult.headingOrderIssues), valueClassName: (pageResult.headingOrderIssues ?? 0) > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-slate-900 dark:text-slate-100' }))),
208
+ react_1.default.createElement("div", null,
209
+ react_1.default.createElement("h2", { className: "mb-3 text-sm font-semibold text-slate-900 dark:text-slate-100" },
210
+ "Issues (",
211
+ (pageResult.issues || []).length,
212
+ ")"),
213
+ (pageResult.issues || []).length === 0 ? (react_1.default.createElement("div", { className: "flex h-24 items-center justify-center rounded-lg border border-slate-200 bg-green-50 dark:border-green-800 dark:bg-green-900/30" },
214
+ react_1.default.createElement("p", { className: "text-sm text-green-700 dark:text-green-300" }, "No issues found on this page. Great job!"))) : (react_1.default.createElement("div", { className: "space-y-4" }, CATEGORIES.map((category) => {
215
+ const issues = issuesByCategory[category];
216
+ if (!issues || issues.length === 0)
217
+ return null;
218
+ return (react_1.default.createElement("div", { key: category, className: "rounded-lg border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-800/50" },
219
+ react_1.default.createElement("div", { className: "flex items-center gap-2 border-b border-slate-100 px-4 py-3 dark:border-slate-700" },
220
+ react_1.default.createElement("span", { className: "text-slate-400 dark:text-slate-500" }, CATEGORY_ICONS[category]),
221
+ react_1.default.createElement("h3", { className: "text-sm font-medium text-slate-900 dark:text-slate-100" }, types_1.CATEGORY_LABELS[category]),
222
+ react_1.default.createElement("span", { className: "text-xs text-slate-500 dark:text-slate-400" },
223
+ "(",
224
+ issues.length,
225
+ ")")),
226
+ react_1.default.createElement("div", { className: "divide-y divide-slate-100 dark:divide-slate-700" }, issues.map((issue, idx) => (react_1.default.createElement("div", { key: `${issue.fingerprint || issue.ruleID}-${idx}`, className: "px-4 py-3" },
227
+ react_1.default.createElement("div", { className: "flex items-start gap-2" },
228
+ react_1.default.createElement(SeverityBadge_1.SeverityBadge, { severity: issue.severity }),
229
+ react_1.default.createElement("div", { className: "flex-1" },
230
+ react_1.default.createElement("p", { className: "text-sm text-slate-800 dark:text-slate-200" }, issue.message),
231
+ issue.recommendation && (react_1.default.createElement("p", { className: "mt-1.5 rounded bg-blue-50 px-3 py-2 text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-300" }, issue.recommendation))))))))));
232
+ })))))));
233
+ };
234
+ exports.SeoPageReport = SeoPageReport;
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ export declare const SeoSnapshotReport: React.FC;
3
+ //# sourceMappingURL=SeoSnapshotReport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SeoSnapshotReport.d.ts","sourceRoot":"","sources":["../../../src/components/views/SeoSnapshotReport.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAoD,MAAM,OAAO,CAAA;AAgCxE,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EA0UrC,CAAA"}