@pixelated-tech/components 3.3.6 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.COMPONENTS.md +126 -0
  2. package/README.md +14 -7
  3. package/dist/components/admin/componentusage/componentAnalysis.js +144 -0
  4. package/dist/components/admin/componentusage/componentDiscovery.js +85 -0
  5. package/dist/components/admin/deploy/deployment.integration.js +170 -0
  6. package/dist/components/admin/site-health/google-api-auth.js +69 -0
  7. package/dist/components/admin/site-health/seo-metrics.config.json +265 -0
  8. package/dist/components/admin/site-health/site-health-accessibility.js +158 -0
  9. package/dist/components/admin/site-health/site-health-axe-core.integration.js +119 -0
  10. package/dist/components/admin/site-health/site-health-axe-core.js +53 -0
  11. package/dist/components/admin/site-health/site-health-cache.js +23 -0
  12. package/dist/components/admin/site-health/site-health-core-web-vitals.integration.js +208 -0
  13. package/dist/components/admin/site-health/site-health-dependency-vulnerabilities.js +38 -0
  14. package/dist/components/admin/site-health/site-health-github.integration.js +81 -0
  15. package/dist/components/admin/site-health/site-health-github.js +34 -0
  16. package/dist/components/admin/site-health/site-health-google-analytics.integration.js +112 -0
  17. package/dist/components/admin/site-health/site-health-google-analytics.js +43 -0
  18. package/dist/components/admin/site-health/site-health-google-search-console.integration.js +118 -0
  19. package/dist/components/admin/site-health/site-health-google-search-console.js +43 -0
  20. package/dist/components/admin/site-health/site-health-indicators.js +71 -0
  21. package/dist/components/admin/site-health/site-health-on-site-seo.integration.js +578 -0
  22. package/dist/components/admin/site-health/site-health-on-site-seo.js +204 -0
  23. package/dist/components/admin/site-health/site-health-overview.js +65 -0
  24. package/dist/components/admin/site-health/site-health-performance.js +191 -0
  25. package/dist/components/admin/site-health/site-health-security.integration.js +109 -0
  26. package/dist/components/admin/site-health/site-health-security.js +169 -0
  27. package/dist/components/admin/site-health/site-health-seo.js +124 -0
  28. package/dist/components/admin/site-health/site-health-template.js +62 -0
  29. package/dist/components/admin/site-health/site-health-types.js +1 -0
  30. package/dist/components/admin/site-health/site-health-uptime.integration.js +29 -0
  31. package/dist/components/admin/site-health/site-health-uptime.js +30 -0
  32. package/dist/components/admin/site-health/site-health.css +427 -0
  33. package/dist/components/admin/sites/sites.integration.js +117 -0
  34. package/dist/components/cms/contentful.management.js +104 -0
  35. package/dist/components/shoppingcart/shipping.from.json +101 -0
  36. package/dist/components/shoppingcart/shipping.parcel.json +112 -0
  37. package/dist/components/shoppingcart/shipping.to.json +422 -0
  38. package/dist/components/shoppingcart/shoppingCartDiscountCodes.json +26 -0
  39. package/dist/components/shoppingcart/shoppingcart.components.js +1 -1
  40. package/dist/components/sitebuilder/config/ConfigBuilder.js +36 -140
  41. package/dist/components/sitebuilder/config/siteinfo-form.json +200 -0
  42. package/dist/components/sitebuilder/config/visualdesignform.json +244 -0
  43. package/dist/components/structured/buzzwordbingo.js +3 -2
  44. package/dist/data/404-data.json +128 -102
  45. package/dist/data/flickr.json +25 -0
  46. package/dist/data/form.json +368 -368
  47. package/dist/data/recipes.json +3251 -3251
  48. package/dist/data/references.json +138 -137
  49. package/dist/data/requestform.json +111 -0
  50. package/dist/data/requests.json +136 -135
  51. package/dist/data/resume.json +2573 -2575
  52. package/dist/data/routes.json +238 -238
  53. package/dist/data/routes2.json +141 -140
  54. package/dist/index.js +16 -3
  55. package/dist/index.server.js +36 -15
  56. package/dist/types/components/admin/componentusage/componentAnalysis.d.ts +35 -0
  57. package/dist/types/components/admin/componentusage/componentAnalysis.d.ts.map +1 -0
  58. package/dist/types/components/admin/componentusage/componentDiscovery.d.ts +10 -0
  59. package/dist/types/components/admin/componentusage/componentDiscovery.d.ts.map +1 -0
  60. package/dist/types/components/admin/deploy/deployment.integration.d.ts +26 -0
  61. package/dist/types/components/admin/deploy/deployment.integration.d.ts.map +1 -0
  62. package/dist/types/components/admin/site-health/google-api-auth.d.ts +37 -0
  63. package/dist/types/components/admin/site-health/google-api-auth.d.ts.map +1 -0
  64. package/dist/types/components/admin/site-health/site-health-accessibility.d.ts +6 -0
  65. package/dist/types/components/admin/site-health/site-health-accessibility.d.ts.map +1 -0
  66. package/dist/types/components/admin/site-health/site-health-axe-core.d.ts +6 -0
  67. package/dist/types/components/admin/site-health/site-health-axe-core.d.ts.map +1 -0
  68. package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts +63 -0
  69. package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts.map +1 -0
  70. package/dist/types/components/admin/site-health/site-health-cache.d.ts +12 -0
  71. package/dist/types/components/admin/site-health/site-health-cache.d.ts.map +1 -0
  72. package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts +3 -0
  73. package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts.map +1 -0
  74. package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts +6 -0
  75. package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts.map +1 -0
  76. package/dist/types/components/admin/site-health/site-health-github.d.ts +8 -0
  77. package/dist/types/components/admin/site-health/site-health-github.d.ts.map +1 -0
  78. package/dist/types/components/admin/site-health/site-health-github.integration.d.ts +26 -0
  79. package/dist/types/components/admin/site-health/site-health-github.integration.d.ts.map +1 -0
  80. package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts +8 -0
  81. package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts.map +1 -0
  82. package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts +26 -0
  83. package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts.map +1 -0
  84. package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts +8 -0
  85. package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts.map +1 -0
  86. package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts +46 -0
  87. package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts.map +1 -0
  88. package/dist/types/components/admin/site-health/site-health-indicators.d.ts +73 -0
  89. package/dist/types/components/admin/site-health/site-health-indicators.d.ts.map +1 -0
  90. package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts +4 -0
  91. package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts.map +1 -0
  92. package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts +34 -0
  93. package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts.map +1 -0
  94. package/dist/types/components/admin/site-health/site-health-overview.d.ts +6 -0
  95. package/dist/types/components/admin/site-health/site-health-overview.d.ts.map +1 -0
  96. package/dist/types/components/admin/site-health/site-health-performance.d.ts +6 -0
  97. package/dist/types/components/admin/site-health/site-health-performance.d.ts.map +1 -0
  98. package/dist/types/components/admin/site-health/site-health-security.d.ts +6 -0
  99. package/dist/types/components/admin/site-health/site-health-security.d.ts.map +1 -0
  100. package/dist/types/components/admin/site-health/site-health-security.integration.d.ts +29 -0
  101. package/dist/types/components/admin/site-health/site-health-security.integration.d.ts.map +1 -0
  102. package/dist/types/components/admin/site-health/site-health-seo.d.ts +6 -0
  103. package/dist/types/components/admin/site-health/site-health-seo.d.ts.map +1 -0
  104. package/dist/types/components/admin/site-health/site-health-template.d.ts +12 -0
  105. package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -0
  106. package/dist/types/components/admin/site-health/site-health-types.d.ts +186 -0
  107. package/dist/types/components/admin/site-health/site-health-types.d.ts.map +1 -0
  108. package/dist/types/components/admin/site-health/site-health-uptime.d.ts +6 -0
  109. package/dist/types/components/admin/site-health/site-health-uptime.d.ts.map +1 -0
  110. package/dist/types/components/admin/site-health/site-health-uptime.integration.d.ts +10 -0
  111. package/dist/types/components/admin/site-health/site-health-uptime.integration.d.ts.map +1 -0
  112. package/dist/types/components/admin/sites/sites.integration.d.ts +40 -0
  113. package/dist/types/components/admin/sites/sites.integration.d.ts.map +1 -0
  114. package/dist/types/components/cms/contentful.management.d.ts +41 -0
  115. package/dist/types/components/cms/contentful.management.d.ts.map +1 -1
  116. package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts +4 -4
  117. package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts.map +1 -1
  118. package/dist/types/components/structured/buzzwordbingo.d.ts +1 -1
  119. package/dist/types/components/structured/buzzwordbingo.d.ts.map +1 -1
  120. package/dist/types/components/structured/buzzwordbingo.words.d.ts +2 -0
  121. package/dist/types/components/structured/buzzwordbingo.words.d.ts.map +1 -0
  122. package/dist/types/index.d.ts +16 -3
  123. package/dist/types/index.server.d.ts +36 -13
  124. package/dist/types/stories/admin/preview.d.ts +12 -0
  125. package/dist/types/stories/admin/preview.d.ts.map +1 -0
  126. package/dist/types/stories/admin/site-health.stories.d.ts +65 -0
  127. package/dist/types/stories/admin/site-health.stories.d.ts.map +1 -0
  128. package/dist/types/stories/structured/buzzword-bingo.stories.d.ts +1 -1
  129. package/dist/types/stories/structured/buzzword-bingo.stories.d.ts.map +1 -1
  130. package/dist/types/tests/site-health-axe-core.test.d.ts +2 -0
  131. package/dist/types/tests/site-health-axe-core.test.d.ts.map +1 -0
  132. package/dist/types/tests/site-health-cache.test.d.ts +2 -0
  133. package/dist/types/tests/site-health-cache.test.d.ts.map +1 -0
  134. package/dist/types/tests/site-health-indicators.test.d.ts +2 -0
  135. package/dist/types/tests/site-health-indicators.test.d.ts.map +1 -0
  136. package/dist/types/tests/site-health-overview.test.d.ts +2 -0
  137. package/dist/types/tests/site-health-overview.test.d.ts.map +1 -0
  138. package/dist/types/tests/site-health-template.test.d.ts +2 -0
  139. package/dist/types/tests/site-health-template.test.d.ts.map +1 -0
  140. package/dist/types/tests/sites.integration.test.d.ts +2 -0
  141. package/dist/types/tests/sites.integration.test.d.ts.map +1 -0
  142. package/package.json +14 -8
  143. package/dist/data/shipping.to.json +0 -422
  144. package/dist/data/siteinfo-form.json +0 -200
  145. package/dist/data/visualdesignform.json +0 -244
  146. package/dist/types/data/buzzwords.d.ts +0 -2
  147. package/dist/types/data/buzzwords.d.ts.map +0 -1
  148. /package/dist/{data/buzzwords.js → components/structured/buzzwordbingo.words.js} +0 -0
@@ -0,0 +1,578 @@
1
+ "use server";
2
+ /**
3
+ * On-Site SEO Analysis Integration Services
4
+ * Server-side utilities for performing comprehensive SEO analysis on websites
5
+ * Note: This makes external HTTP requests and should only be used server-side
6
+ */
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const seoMetricsConfig = JSON.parse(fs.readFileSync(path.join(__dirname, 'seo-metrics.config.json'), 'utf8'));
13
+ /**
14
+ * Registry of data collection functions
15
+ */
16
+ const dataCollectors = {
17
+ collectSemanticTagsData,
18
+ collectTitleTagsData,
19
+ collectMetaKeywordsData,
20
+ collectMetaDescriptionsData
21
+ };
22
+ /**
23
+ * Registry of scoring functions
24
+ */
25
+ const scorers = {
26
+ calculateSemanticTagsScore,
27
+ calculateTitleTagsScore,
28
+ calculateMetaKeywordsScore,
29
+ calculateMetaDescriptionsScore
30
+ };
31
+ /**
32
+ * Data collection functions
33
+ */
34
+ function collectSemanticTagsData(html) {
35
+ const hasHeader = /<header[^>]*>/i.test(html);
36
+ const hasFooter = /<footer[^>]*>/i.test(html);
37
+ const hasNav = /<nav[^>]*>/i.test(html);
38
+ const hasMain = /<main[^>]*>/i.test(html);
39
+ const hasSection = /<section[^>]*>/i.test(html);
40
+ const hasArticle = /<article[^>]*>/i.test(html);
41
+ const hasAside = /<aside[^>]*>/i.test(html);
42
+ const hasFigure = /<figure[^>]*>/i.test(html);
43
+ const hasFigcaption = /<figcaption[^>]*>/i.test(html);
44
+ const hasTime = /<time[^>]*>/i.test(html);
45
+ const hasMark = /<mark[^>]*>/i.test(html);
46
+ return {
47
+ requiredTags: [
48
+ { tag: 'header', present: hasHeader },
49
+ { tag: 'footer', present: hasFooter },
50
+ { tag: 'nav', present: hasNav },
51
+ { tag: 'main', present: hasMain },
52
+ { tag: 'section', present: hasSection }
53
+ ],
54
+ optionalTags: [
55
+ { tag: 'article', present: hasArticle },
56
+ { tag: 'aside', present: hasAside },
57
+ { tag: 'figure', present: hasFigure },
58
+ { tag: 'figcaption', present: hasFigcaption },
59
+ { tag: 'time', present: hasTime },
60
+ { tag: 'mark', present: hasMark }
61
+ ]
62
+ };
63
+ }
64
+ function collectTitleTagsData(html, titleMatch) {
65
+ return {
66
+ content: titleMatch ? titleMatch[1].trim() : '',
67
+ length: titleMatch ? titleMatch[1].trim().length : 0
68
+ };
69
+ }
70
+ function collectMetaKeywordsData(html) {
71
+ const keywordsMatch = html.match(/<meta[^>]*name=["']keywords["'][^>]*content=["']([^"']*)["'][^>]*>/i);
72
+ const content = keywordsMatch ? keywordsMatch[1].trim() : '';
73
+ const length = content.length;
74
+ return {
75
+ content,
76
+ length
77
+ };
78
+ }
79
+ function collectMetaDescriptionsData(html) {
80
+ const descriptionMatch = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["'][^>]*>/i);
81
+ const content = descriptionMatch ? descriptionMatch[1].trim() : '';
82
+ const length = content.length;
83
+ return {
84
+ content,
85
+ length,
86
+ optimal: length > 0 && length <= 160
87
+ };
88
+ }
89
+ /**
90
+ * Generic function to analyze pattern-based metrics
91
+ */
92
+ function analyzePatternMetric(html, metric) {
93
+ if (!metric.pattern) {
94
+ return { score: 0, displayValue: 'No pattern defined' };
95
+ }
96
+ const regex = new RegExp(metric.pattern, 'gi');
97
+ const matches = html.match(regex) || [];
98
+ const count = matches.length;
99
+ let score = 0;
100
+ let displayValue = '';
101
+ let details = undefined;
102
+ // Apply count logic
103
+ switch (metric.countLogic) {
104
+ case 'exact':
105
+ score = count === (metric.expectedCount || 1) ? 1 : 0;
106
+ break;
107
+ case 'min':
108
+ score = count >= (metric.expectedCount || 1) ? 1 : 0;
109
+ break;
110
+ case 'max':
111
+ score = count <= (metric.expectedCount || 1) ? 1 : 0;
112
+ break;
113
+ case 'or':
114
+ score = count > 0 ? 1 : 0;
115
+ break;
116
+ case 'count':
117
+ default:
118
+ score = count > 0 ? 1 : 0;
119
+ break;
120
+ }
121
+ // Apply score logic
122
+ if (metric.scoreLogic === 'optimal' && metric.optimalRange) {
123
+ const { min, max } = metric.optimalRange;
124
+ score = count >= min && count <= max ? 1 : 0;
125
+ }
126
+ else if (metric.scoreLogic === 'percentage') {
127
+ // For percentage-based scoring, we'd need additional logic
128
+ score = count > 0 ? 1 : 0;
129
+ }
130
+ // Generate display value
131
+ if (metric.displayTemplate) {
132
+ displayValue = metric.displayTemplate
133
+ .replace('{{count}}', count.toString())
134
+ .replace('{{matches}}', matches.length.toString());
135
+ }
136
+ else {
137
+ displayValue = `${count} match(es) found`;
138
+ }
139
+ // Generate details based on metric type
140
+ if (metric.id === 'h1-tags' || metric.id === 'h2-tags') {
141
+ const headings = matches.map(match => {
142
+ const text = match.replace(/<[^>]*>/g, '').trim();
143
+ return { tag: metric.id.replace('-tags', ''), text };
144
+ });
145
+ details = { items: headings };
146
+ }
147
+ else if (metric.id === 'image-alt-text') {
148
+ // Special logic for image alt text - check each image individually
149
+ const images = matches.map(match => {
150
+ const srcMatch = match.match(/src=["']([^"']*)["']/);
151
+ const altMatch = match.match(/alt=["']([^"']*)["']/);
152
+ return {
153
+ src: srcMatch ? srcMatch[1] : '',
154
+ alt: altMatch ? altMatch[1] : null
155
+ };
156
+ });
157
+ const imagesWithAlt = images.filter(img => img.alt !== null).length;
158
+ const totalImages = images.length;
159
+ score = totalImages > 0 && imagesWithAlt === totalImages ? 1 : 0;
160
+ displayValue = `${imagesWithAlt}/${totalImages} images have alt text`;
161
+ details = { items: images };
162
+ }
163
+ else if (metric.id === 'canonical-urls') {
164
+ const canonicalMatch = matches[0]?.match(/href=["']([^"']*)["']/);
165
+ const canonicalUrl = canonicalMatch ? canonicalMatch[1] : null;
166
+ displayValue = canonicalUrl || 'No canonical URL found';
167
+ }
168
+ else if (metric.id === 'language-tags') {
169
+ const langMatch = matches[0]?.match(/lang=["']([^"']*)["']/);
170
+ const lang = langMatch ? langMatch[1] : null;
171
+ displayValue = lang ? `Language: ${lang}` : 'No language tag found';
172
+ }
173
+ return { score, displayValue, details };
174
+ }
175
+ function calculateSemanticTagsScore(semanticData) {
176
+ const requiredTagsPresent = semanticData.requiredTags.filter(tag => tag.present).length;
177
+ const optionalTagsPresent = semanticData.optionalTags.filter(tag => tag.present).length;
178
+ const totalSemanticTags = requiredTagsPresent + optionalTagsPresent;
179
+ return {
180
+ score: requiredTagsPresent >= 5 ? 1 : 0,
181
+ displayValue: `${requiredTagsPresent}/5 required, ${optionalTagsPresent} optional (${totalSemanticTags} total)`,
182
+ details: {
183
+ items: [
184
+ { type: 'required', tags: semanticData.requiredTags },
185
+ { type: 'optional', tags: semanticData.optionalTags },
186
+ { type: 'summary', requiredCount: requiredTagsPresent, optionalCount: optionalTagsPresent, totalCount: totalSemanticTags }
187
+ ]
188
+ }
189
+ };
190
+ }
191
+ function calculateTitleTagsScore(titleData) {
192
+ const score = titleData.length > 0 && titleData.length <= 60 ? 1 : 0;
193
+ const displayValue = titleData.content || 'No title tag found';
194
+ return {
195
+ score,
196
+ displayValue,
197
+ details: {
198
+ items: [
199
+ { type: 'title', content: titleData.content, length: titleData.length, optimal: titleData.length > 0 && titleData.length <= 60 }
200
+ ]
201
+ }
202
+ };
203
+ }
204
+ function calculateMetaKeywordsScore(keywordsData) {
205
+ const score = keywordsData.length > 0 ? 1 : 0;
206
+ const displayValue = keywordsData.content || 'No meta keywords found';
207
+ return {
208
+ score,
209
+ displayValue,
210
+ details: {
211
+ items: [
212
+ { type: 'keywords', content: keywordsData.content, length: keywordsData.length, present: keywordsData.length > 0 }
213
+ ]
214
+ }
215
+ };
216
+ }
217
+ function calculateMetaDescriptionsScore(descriptionData) {
218
+ const score = descriptionData.optimal ? 1 : 0;
219
+ const displayValue = descriptionData.content || 'No meta description found';
220
+ return {
221
+ score,
222
+ displayValue,
223
+ details: {
224
+ items: [
225
+ { type: 'description', content: descriptionData.content, length: descriptionData.length, optimal: descriptionData.optimal }
226
+ ]
227
+ }
228
+ };
229
+ }
230
+ /**
231
+ * Crawl the site to discover internal pages
232
+ */
233
+ async function crawlSite(baseUrl, maxPages = 10) {
234
+ const visited = new Set();
235
+ const toVisit = [baseUrl];
236
+ const discovered = [];
237
+ try {
238
+ // Parse base URL for domain matching
239
+ const baseUrlObj = new URL(baseUrl);
240
+ const baseDomain = baseUrlObj.hostname;
241
+ while (toVisit.length > 0 && discovered.length < maxPages) {
242
+ const currentUrl = toVisit.shift();
243
+ if (visited.has(currentUrl))
244
+ continue;
245
+ visited.add(currentUrl);
246
+ discovered.push(currentUrl);
247
+ try {
248
+ const response = await fetch(currentUrl, {
249
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)' }
250
+ });
251
+ if (!response.ok)
252
+ continue;
253
+ const html = await response.text();
254
+ // Extract internal links
255
+ const linkRegex = /<a[^>]*href=["']([^"']*)["'][^>]*>/gi;
256
+ let match;
257
+ while ((match = linkRegex.exec(html)) !== null) {
258
+ try {
259
+ const href = match[1];
260
+ const absoluteUrl = new URL(href, currentUrl).toString();
261
+ // Only include same domain links
262
+ if (new URL(absoluteUrl).hostname === baseDomain && !visited.has(absoluteUrl) && !toVisit.includes(absoluteUrl)) {
263
+ toVisit.push(absoluteUrl);
264
+ }
265
+ }
266
+ catch {
267
+ // Invalid URL, skip
268
+ }
269
+ }
270
+ }
271
+ catch (error) {
272
+ console.warn(`Failed to crawl ${currentUrl}:`, error);
273
+ }
274
+ }
275
+ }
276
+ catch (error) {
277
+ console.warn('Error during site crawling:', error);
278
+ }
279
+ return discovered.slice(0, maxPages);
280
+ }
281
+ /**
282
+ * Analyze a single page for on-page SEO elements using configuration
283
+ */
284
+ async function analyzeSinglePage(url) {
285
+ try {
286
+ const response = await fetch(url, {
287
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)' }
288
+ });
289
+ if (!response.ok) {
290
+ // Return a basic analysis for non-200 responses
291
+ return {
292
+ url,
293
+ title: undefined,
294
+ statusCode: response.status,
295
+ audits: [],
296
+ crawledAt: new Date().toISOString()
297
+ };
298
+ }
299
+ const html = await response.text();
300
+ const audits = [];
301
+ // Extract page title for title tag analysis
302
+ const titleMatch = html.match(/<title[^>]*>([^<]*)<\/title>/i);
303
+ const pageTitle = titleMatch ? titleMatch[1].trim() : undefined;
304
+ // Process on-page metrics from configuration
305
+ const config = seoMetricsConfig;
306
+ const onPageCategory = config.categories['on-page'];
307
+ for (const metric of Object.values(onPageCategory.metrics)) {
308
+ let score = 0;
309
+ let displayValue = '';
310
+ let details = undefined;
311
+ // Use data collector and scorer if available
312
+ if (metric.dataCollector && metric.scorer) {
313
+ const collector = dataCollectors[metric.dataCollector];
314
+ const scorer = scorers[metric.scorer];
315
+ if (collector && scorer) {
316
+ const rawData = collector(html, titleMatch);
317
+ const result = scorer(rawData);
318
+ score = result.score;
319
+ displayValue = result.displayValue;
320
+ details = result.details;
321
+ }
322
+ }
323
+ else if (metric.pattern) {
324
+ // Use pattern-based analysis
325
+ const result = analyzePatternMetric(html, metric);
326
+ score = result.score;
327
+ displayValue = result.displayValue;
328
+ details = result.details;
329
+ }
330
+ audits.push({
331
+ id: metric.id,
332
+ title: metric.title,
333
+ score,
334
+ scoreDisplayMode: metric.scoreDisplayMode,
335
+ displayValue,
336
+ category: 'on-page',
337
+ details
338
+ });
339
+ }
340
+ return {
341
+ url,
342
+ title: pageTitle,
343
+ statusCode: response.status,
344
+ audits,
345
+ crawledAt: new Date().toISOString()
346
+ };
347
+ }
348
+ catch (error) {
349
+ console.warn(`Failed to analyze ${url}:`, error);
350
+ // Return a basic analysis for failed requests
351
+ return {
352
+ url,
353
+ title: undefined,
354
+ statusCode: 0, // Indicates analysis failed
355
+ audits: [],
356
+ crawledAt: new Date().toISOString()
357
+ };
358
+ }
359
+ }
360
+ async function performSiteWideAudits(baseUrl) {
361
+ const audits = [];
362
+ try {
363
+ // Parse base URL
364
+ const baseUrlObj = new URL(baseUrl);
365
+ const baseDomain = baseUrlObj.hostname;
366
+ const protocol = baseUrlObj.protocol;
367
+ // Process on-site metrics from configuration
368
+ const config = seoMetricsConfig;
369
+ const onSiteCategory = config.categories['on-site'];
370
+ for (const metric of Object.values(onSiteCategory.metrics)) {
371
+ let score = 0;
372
+ let displayValue = '';
373
+ // Handle metrics without custom collectors/scorers
374
+ switch (metric.id) {
375
+ case 'https':
376
+ score = protocol === 'https:' ? 1 : 0;
377
+ displayValue = score ? 'Site uses HTTPS' : 'Site does not use HTTPS';
378
+ break;
379
+ case 'url-structure': {
380
+ const hasQueryParams = baseUrlObj.search.length > 0;
381
+ score = hasQueryParams ? 0 : 1;
382
+ displayValue = score ? 'Clean URL structure' : 'URL contains query parameters';
383
+ break;
384
+ }
385
+ case 'robots-txt':
386
+ try {
387
+ const robotsUrl = `${protocol}//${baseDomain}/robots.txt`;
388
+ const robotsResponse = await fetch(robotsUrl);
389
+ score = robotsResponse.ok ? 1 : 0;
390
+ displayValue = score ? 'Robots.txt accessible' : 'Robots.txt not found or inaccessible';
391
+ }
392
+ catch {
393
+ score = 0;
394
+ displayValue = 'Robots.txt not accessible';
395
+ }
396
+ break;
397
+ case 'sitemap-xml':
398
+ try {
399
+ const sitemapUrl = `${protocol}//${baseDomain}/sitemap.xml`;
400
+ const sitemapResponse = await fetch(sitemapUrl);
401
+ score = sitemapResponse.ok ? 1 : 0;
402
+ displayValue = score ? 'Sitemap.xml accessible' : 'Sitemap.xml not found or inaccessible';
403
+ }
404
+ catch {
405
+ score = 0;
406
+ displayValue = 'Sitemap.xml not accessible';
407
+ }
408
+ break;
409
+ case 'internal-linking':
410
+ score = 1; // Placeholder - would need full crawl analysis
411
+ displayValue = 'Internal links found during crawl';
412
+ break;
413
+ case 'navigation':
414
+ score = 1; // Placeholder - would need content analysis
415
+ displayValue = 'Navigation structure found';
416
+ break;
417
+ case 'broken-links':
418
+ score = 1; // Placeholder - would need comprehensive link checking
419
+ displayValue = 'No obvious broken links detected';
420
+ break;
421
+ case 'manifest-file':
422
+ try {
423
+ const manifestUrl = `${protocol}//${baseDomain}/manifest.webmanifest`;
424
+ const manifestResponse = await fetch(manifestUrl);
425
+ score = manifestResponse.ok ? 1 : 0;
426
+ displayValue = score ? 'Manifest.webmanifest accessible' : 'Manifest.webmanifest not found or inaccessible';
427
+ }
428
+ catch {
429
+ score = 0;
430
+ displayValue = 'Manifest.webmanifest not accessible';
431
+ }
432
+ break;
433
+ default:
434
+ score = 0;
435
+ displayValue = 'Not implemented';
436
+ }
437
+ audits.push({
438
+ id: metric.id,
439
+ title: metric.title,
440
+ score,
441
+ scoreDisplayMode: metric.scoreDisplayMode,
442
+ displayValue,
443
+ category: 'on-site'
444
+ });
445
+ }
446
+ }
447
+ catch (error) {
448
+ console.error('Error performing site-wide audits:', error);
449
+ }
450
+ return audits;
451
+ }
452
+ /**
453
+ * Fetch and parse sitemap.xml to get all site URLs
454
+ */
455
+ async function getUrlsFromSitemap(baseUrl) {
456
+ try {
457
+ const sitemapUrl = `${baseUrl}/sitemap.xml`;
458
+ const response = await fetch(sitemapUrl);
459
+ if (!response.ok) {
460
+ throw new Error(`Failed to fetch sitemap: ${response.status}`);
461
+ }
462
+ const xmlText = await response.text();
463
+ const baseUrlObj = new URL(baseUrl);
464
+ // Simple XML parsing to extract URLs
465
+ const urlRegex = /<loc>([^<]+)<\/loc>/g;
466
+ const urls = [];
467
+ let match;
468
+ while ((match = urlRegex.exec(xmlText)) !== null) {
469
+ const url = match[1].trim();
470
+ // Only include URLs from the same domain and that look like valid page URLs
471
+ try {
472
+ const urlObj = new URL(url);
473
+ if (urlObj.hostname === baseUrlObj.hostname &&
474
+ !url.includes('/images/') &&
475
+ !url.includes('/css/') &&
476
+ !url.includes('/js/') &&
477
+ !url.includes('/wp-content/') &&
478
+ !url.includes('/wp-includes/') &&
479
+ !url.match(/\.(jpg|jpeg|png|gif|svg|ico|css|js|woff|woff2|ttf|eot)$/i)) {
480
+ urls.push(url);
481
+ }
482
+ }
483
+ catch {
484
+ // Invalid URL, skip
485
+ }
486
+ }
487
+ return urls.slice(0, 20); // Limit to 20 pages to prevent excessive analysis
488
+ }
489
+ catch (error) {
490
+ console.warn('Failed to fetch sitemap:', error);
491
+ return [];
492
+ }
493
+ }
494
+ /**
495
+ * Main function to perform comprehensive on-site SEO analysis
496
+ */
497
+ export async function performOnSiteSEOAnalysis(baseUrl) {
498
+ try {
499
+ let pagesToAnalyze = [];
500
+ // Try to get URLs from sitemap first
501
+ const sitemapUrls = await getUrlsFromSitemap(baseUrl);
502
+ if (sitemapUrls.length > 0) {
503
+ pagesToAnalyze = sitemapUrls;
504
+ }
505
+ else {
506
+ // Fallback to crawling if sitemap not available
507
+ pagesToAnalyze = await crawlSite(baseUrl, 5);
508
+ }
509
+ if (pagesToAnalyze.length === 0) {
510
+ return {
511
+ site: baseUrl,
512
+ url: baseUrl,
513
+ overallScore: null,
514
+ pagesAnalyzed: [],
515
+ onSiteAudits: [],
516
+ totalPages: 0,
517
+ timestamp: new Date().toISOString(),
518
+ status: 'error',
519
+ error: 'No pages could be analyzed'
520
+ };
521
+ }
522
+ // Analyze each page
523
+ const pagesAnalyzed = [];
524
+ for (const pageUrl of pagesToAnalyze) {
525
+ try {
526
+ const pageAnalysis = await analyzeSinglePage(pageUrl);
527
+ pagesAnalyzed.push(pageAnalysis);
528
+ }
529
+ catch (error) {
530
+ console.warn(`Failed to analyze ${pageUrl}:`, error);
531
+ }
532
+ }
533
+ // Perform site-wide audits
534
+ const onSiteAudits = await performSiteWideAudits(baseUrl);
535
+ // Calculate overall score (simplified - average of all page scores)
536
+ let totalScore = 0;
537
+ let totalAudits = 0;
538
+ for (const page of pagesAnalyzed) {
539
+ for (const audit of page.audits) {
540
+ if (audit.score !== null) {
541
+ totalScore += audit.score;
542
+ totalAudits++;
543
+ }
544
+ }
545
+ }
546
+ for (const audit of onSiteAudits) {
547
+ if (audit.score !== null) {
548
+ totalScore += audit.score;
549
+ totalAudits++;
550
+ }
551
+ }
552
+ const overallScore = totalAudits > 0 ? Math.round((totalScore / totalAudits) * 100) / 100 : null;
553
+ return {
554
+ site: baseUrl,
555
+ url: baseUrl,
556
+ overallScore,
557
+ pagesAnalyzed,
558
+ onSiteAudits,
559
+ totalPages: pagesAnalyzed.length,
560
+ timestamp: new Date().toISOString(),
561
+ status: 'success'
562
+ };
563
+ }
564
+ catch (error) {
565
+ console.error('Error performing on-site SEO analysis:', error);
566
+ return {
567
+ site: baseUrl,
568
+ url: baseUrl,
569
+ overallScore: null,
570
+ pagesAnalyzed: [],
571
+ onSiteAudits: [],
572
+ totalPages: 0,
573
+ timestamp: new Date().toISOString(),
574
+ status: 'error',
575
+ error: error instanceof Error ? error.message : 'Unknown error'
576
+ };
577
+ }
578
+ }