@pixelated-tech/components 3.3.6 → 3.4.1

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 (156) 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 +163 -0
  6. package/dist/components/admin/site-health/google-api-auth.js +69 -0
  7. package/dist/components/admin/site-health/seo-constants.js +14 -0
  8. package/dist/components/admin/site-health/seo-metrics.config.json +265 -0
  9. package/dist/components/admin/site-health/site-health-accessibility.js +158 -0
  10. package/dist/components/admin/site-health/site-health-axe-core.integration.js +119 -0
  11. package/dist/components/admin/site-health/site-health-axe-core.js +53 -0
  12. package/dist/components/admin/site-health/site-health-cache.js +23 -0
  13. package/dist/components/admin/site-health/site-health-core-web-vitals.integration.js +208 -0
  14. package/dist/components/admin/site-health/site-health-dependency-vulnerabilities.js +38 -0
  15. package/dist/components/admin/site-health/site-health-github.integration.js +81 -0
  16. package/dist/components/admin/site-health/site-health-github.js +34 -0
  17. package/dist/components/admin/site-health/site-health-google-analytics.integration.js +112 -0
  18. package/dist/components/admin/site-health/site-health-google-analytics.js +43 -0
  19. package/dist/components/admin/site-health/site-health-google-search-console.integration.js +118 -0
  20. package/dist/components/admin/site-health/site-health-google-search-console.js +43 -0
  21. package/dist/components/admin/site-health/site-health-indicators.js +71 -0
  22. package/dist/components/admin/site-health/site-health-on-site-seo.integration.js +669 -0
  23. package/dist/components/admin/site-health/site-health-on-site-seo.js +204 -0
  24. package/dist/components/admin/site-health/site-health-overview.js +65 -0
  25. package/dist/components/admin/site-health/site-health-performance.js +191 -0
  26. package/dist/components/admin/site-health/site-health-security.integration.js +109 -0
  27. package/dist/components/admin/site-health/site-health-security.js +169 -0
  28. package/dist/components/admin/site-health/site-health-seo.js +124 -0
  29. package/dist/components/admin/site-health/site-health-template.js +62 -0
  30. package/dist/components/admin/site-health/site-health-types.js +1 -0
  31. package/dist/components/admin/site-health/site-health-uptime.integration.js +29 -0
  32. package/dist/components/admin/site-health/site-health-uptime.js +30 -0
  33. package/dist/components/admin/site-health/site-health.css +427 -0
  34. package/dist/components/admin/sites/sites.integration.js +117 -0
  35. package/dist/components/cms/contentful.management.js +104 -0
  36. package/dist/components/cms/pixelated.linkedin1.js +0 -19
  37. package/dist/components/shoppingcart/shipping.from.json +101 -0
  38. package/dist/components/shoppingcart/shipping.parcel.json +112 -0
  39. package/dist/components/shoppingcart/shipping.to.json +422 -0
  40. package/dist/components/shoppingcart/shoppingCartDiscountCodes.json +26 -0
  41. package/dist/components/shoppingcart/shoppingcart.components.js +3 -2
  42. package/dist/components/sitebuilder/config/ConfigBuilder.js +36 -140
  43. package/dist/components/sitebuilder/config/siteinfo-form.json +200 -0
  44. package/dist/components/sitebuilder/config/visualdesignform.json +244 -0
  45. package/dist/components/structured/buzzwordbingo.js +3 -2
  46. package/dist/components/utilities/functions.js +5 -2
  47. package/dist/data/404-data.json +128 -102
  48. package/dist/data/flickr.json +25 -0
  49. package/dist/data/form.json +368 -368
  50. package/dist/data/recipes.json +3251 -3251
  51. package/dist/data/references.json +138 -137
  52. package/dist/data/requestform.json +111 -0
  53. package/dist/data/requests.json +136 -135
  54. package/dist/data/resume.json +2573 -2575
  55. package/dist/data/routes.json +238 -238
  56. package/dist/data/routes2.json +141 -140
  57. package/dist/index.js +17 -3
  58. package/dist/index.server.js +37 -15
  59. package/dist/types/components/admin/componentusage/componentAnalysis.d.ts +35 -0
  60. package/dist/types/components/admin/componentusage/componentAnalysis.d.ts.map +1 -0
  61. package/dist/types/components/admin/componentusage/componentDiscovery.d.ts +10 -0
  62. package/dist/types/components/admin/componentusage/componentDiscovery.d.ts.map +1 -0
  63. package/dist/types/components/admin/deploy/deployment.integration.d.ts +26 -0
  64. package/dist/types/components/admin/deploy/deployment.integration.d.ts.map +1 -0
  65. package/dist/types/components/admin/site-health/google-api-auth.d.ts +37 -0
  66. package/dist/types/components/admin/site-health/google-api-auth.d.ts.map +1 -0
  67. package/dist/types/components/admin/site-health/seo-constants.d.ts +8 -0
  68. package/dist/types/components/admin/site-health/seo-constants.d.ts.map +1 -0
  69. package/dist/types/components/admin/site-health/site-health-accessibility.d.ts +6 -0
  70. package/dist/types/components/admin/site-health/site-health-accessibility.d.ts.map +1 -0
  71. package/dist/types/components/admin/site-health/site-health-axe-core.d.ts +6 -0
  72. package/dist/types/components/admin/site-health/site-health-axe-core.d.ts.map +1 -0
  73. package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts +63 -0
  74. package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts.map +1 -0
  75. package/dist/types/components/admin/site-health/site-health-cache.d.ts +12 -0
  76. package/dist/types/components/admin/site-health/site-health-cache.d.ts.map +1 -0
  77. package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts +3 -0
  78. package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts.map +1 -0
  79. package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts +6 -0
  80. package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts.map +1 -0
  81. package/dist/types/components/admin/site-health/site-health-github.d.ts +8 -0
  82. package/dist/types/components/admin/site-health/site-health-github.d.ts.map +1 -0
  83. package/dist/types/components/admin/site-health/site-health-github.integration.d.ts +26 -0
  84. package/dist/types/components/admin/site-health/site-health-github.integration.d.ts.map +1 -0
  85. package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts +8 -0
  86. package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts.map +1 -0
  87. package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts +26 -0
  88. package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts.map +1 -0
  89. package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts +8 -0
  90. package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts.map +1 -0
  91. package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts +46 -0
  92. package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts.map +1 -0
  93. package/dist/types/components/admin/site-health/site-health-indicators.d.ts +73 -0
  94. package/dist/types/components/admin/site-health/site-health-indicators.d.ts.map +1 -0
  95. package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts +4 -0
  96. package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts.map +1 -0
  97. package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts +34 -0
  98. package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts.map +1 -0
  99. package/dist/types/components/admin/site-health/site-health-overview.d.ts +6 -0
  100. package/dist/types/components/admin/site-health/site-health-overview.d.ts.map +1 -0
  101. package/dist/types/components/admin/site-health/site-health-performance.d.ts +6 -0
  102. package/dist/types/components/admin/site-health/site-health-performance.d.ts.map +1 -0
  103. package/dist/types/components/admin/site-health/site-health-security.d.ts +6 -0
  104. package/dist/types/components/admin/site-health/site-health-security.d.ts.map +1 -0
  105. package/dist/types/components/admin/site-health/site-health-security.integration.d.ts +29 -0
  106. package/dist/types/components/admin/site-health/site-health-security.integration.d.ts.map +1 -0
  107. package/dist/types/components/admin/site-health/site-health-seo.d.ts +6 -0
  108. package/dist/types/components/admin/site-health/site-health-seo.d.ts.map +1 -0
  109. package/dist/types/components/admin/site-health/site-health-template.d.ts +12 -0
  110. package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -0
  111. package/dist/types/components/admin/site-health/site-health-types.d.ts +186 -0
  112. package/dist/types/components/admin/site-health/site-health-types.d.ts.map +1 -0
  113. package/dist/types/components/admin/site-health/site-health-uptime.d.ts +6 -0
  114. package/dist/types/components/admin/site-health/site-health-uptime.d.ts.map +1 -0
  115. package/dist/types/components/admin/site-health/site-health-uptime.integration.d.ts +10 -0
  116. package/dist/types/components/admin/site-health/site-health-uptime.integration.d.ts.map +1 -0
  117. package/dist/types/components/admin/sites/sites.integration.d.ts +40 -0
  118. package/dist/types/components/admin/sites/sites.integration.d.ts.map +1 -0
  119. package/dist/types/components/cms/contentful.management.d.ts +41 -0
  120. package/dist/types/components/cms/contentful.management.d.ts.map +1 -1
  121. package/dist/types/components/cms/pixelated.linkedin1.d.ts.map +1 -1
  122. package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts +4 -4
  123. package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts.map +1 -1
  124. package/dist/types/components/structured/buzzwordbingo.d.ts +1 -1
  125. package/dist/types/components/structured/buzzwordbingo.d.ts.map +1 -1
  126. package/dist/types/components/structured/buzzwordbingo.words.d.ts +2 -0
  127. package/dist/types/components/structured/buzzwordbingo.words.d.ts.map +1 -0
  128. package/dist/types/components/utilities/functions.d.ts.map +1 -1
  129. package/dist/types/index.d.ts +17 -3
  130. package/dist/types/index.server.d.ts +37 -13
  131. package/dist/types/stories/admin/preview.d.ts +12 -0
  132. package/dist/types/stories/admin/preview.d.ts.map +1 -0
  133. package/dist/types/stories/admin/site-health.stories.d.ts +65 -0
  134. package/dist/types/stories/admin/site-health.stories.d.ts.map +1 -0
  135. package/dist/types/stories/carousel/tiles.stories.d.ts.map +1 -1
  136. package/dist/types/stories/structured/buzzword-bingo.stories.d.ts +1 -1
  137. package/dist/types/stories/structured/buzzword-bingo.stories.d.ts.map +1 -1
  138. package/dist/types/tests/site-health-axe-core.test.d.ts +2 -0
  139. package/dist/types/tests/site-health-axe-core.test.d.ts.map +1 -0
  140. package/dist/types/tests/site-health-cache.test.d.ts +2 -0
  141. package/dist/types/tests/site-health-cache.test.d.ts.map +1 -0
  142. package/dist/types/tests/site-health-indicators.test.d.ts +2 -0
  143. package/dist/types/tests/site-health-indicators.test.d.ts.map +1 -0
  144. package/dist/types/tests/site-health-overview.test.d.ts +2 -0
  145. package/dist/types/tests/site-health-overview.test.d.ts.map +1 -0
  146. package/dist/types/tests/site-health-template.test.d.ts +2 -0
  147. package/dist/types/tests/site-health-template.test.d.ts.map +1 -0
  148. package/dist/types/tests/sites.integration.test.d.ts +2 -0
  149. package/dist/types/tests/sites.integration.test.d.ts.map +1 -0
  150. package/package.json +14 -8
  151. package/dist/data/shipping.to.json +0 -422
  152. package/dist/data/siteinfo-form.json +0 -200
  153. package/dist/data/visualdesignform.json +0 -244
  154. package/dist/types/data/buzzwords.d.ts +0 -2
  155. package/dist/types/data/buzzwords.d.ts.map +0 -1
  156. /package/dist/{data/buzzwords.js → components/structured/buzzwordbingo.words.js} +0 -0
@@ -0,0 +1,669 @@
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
+ import puppeteer from 'puppeteer';
11
+ import { EXCLUDED_URL_PATTERNS, EXCLUDED_FILE_EXTENSIONS, EXCLUDED_DIRECTORY_NAMES } from './seo-constants';
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const seoMetricsConfig = JSON.parse(fs.readFileSync(path.join(__dirname, 'seo-metrics.config.json'), 'utf8'));
15
+ /**
16
+ * Registry of data collection functions
17
+ */
18
+ const dataCollectors = {
19
+ collectSemanticTagsData,
20
+ collectTitleTagsData,
21
+ collectMetaKeywordsData,
22
+ collectMetaDescriptionsData
23
+ };
24
+ /**
25
+ * Registry of scoring functions
26
+ */
27
+ const scorers = {
28
+ calculateSemanticTagsScore,
29
+ calculateTitleTagsScore,
30
+ calculateMetaKeywordsScore,
31
+ calculateMetaDescriptionsScore
32
+ };
33
+ /**
34
+ * Data collection functions
35
+ */
36
+ function collectSemanticTagsData(html) {
37
+ const hasHeader = /<header[^>]*>/i.test(html);
38
+ const hasFooter = /<footer[^>]*>/i.test(html);
39
+ const hasNav = /<nav[^>]*>/i.test(html);
40
+ const hasMain = /<main[^>]*>/i.test(html);
41
+ const hasSection = /<section[^>]*>/i.test(html);
42
+ const hasArticle = /<article[^>]*>/i.test(html);
43
+ const hasAside = /<aside[^>]*>/i.test(html);
44
+ const hasFigure = /<figure[^>]*>/i.test(html);
45
+ const hasFigcaption = /<figcaption[^>]*>/i.test(html);
46
+ const hasTime = /<time[^>]*>/i.test(html);
47
+ const hasMark = /<mark[^>]*>/i.test(html);
48
+ return {
49
+ requiredTags: [
50
+ { tag: 'header', present: hasHeader },
51
+ { tag: 'footer', present: hasFooter },
52
+ { tag: 'nav', present: hasNav },
53
+ { tag: 'main', present: hasMain },
54
+ { tag: 'section', present: hasSection }
55
+ ],
56
+ optionalTags: [
57
+ { tag: 'article', present: hasArticle },
58
+ { tag: 'aside', present: hasAside },
59
+ { tag: 'figure', present: hasFigure },
60
+ { tag: 'figcaption', present: hasFigcaption },
61
+ { tag: 'time', present: hasTime },
62
+ { tag: 'mark', present: hasMark }
63
+ ]
64
+ };
65
+ }
66
+ function collectTitleTagsData(html, titleMatch) {
67
+ return {
68
+ content: titleMatch ? titleMatch[1].trim() : '',
69
+ length: titleMatch ? titleMatch[1].trim().length : 0
70
+ };
71
+ }
72
+ function collectMetaKeywordsData(html) {
73
+ const keywordsMatch = html.match(/<meta[^>]*name=["']keywords["'][^>]*content=["']([^"']*)["'][^>]*>/i);
74
+ const content = keywordsMatch ? keywordsMatch[1].trim() : '';
75
+ const length = content.length;
76
+ return {
77
+ content,
78
+ length
79
+ };
80
+ }
81
+ function collectMetaDescriptionsData(html) {
82
+ const descriptionMatch = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["'][^>]*>/i);
83
+ const content = descriptionMatch ? descriptionMatch[1].trim() : '';
84
+ const length = content.length;
85
+ return {
86
+ content,
87
+ length,
88
+ optimal: length > 0 && length <= 160
89
+ };
90
+ }
91
+ /**
92
+ * Generic function to analyze pattern-based metrics
93
+ */
94
+ function analyzePatternMetric(html, metric) {
95
+ if (!metric.pattern) {
96
+ return { score: 0, displayValue: 'No pattern defined' };
97
+ }
98
+ const regex = new RegExp(metric.pattern, 'gi');
99
+ const matches = html.match(regex) || [];
100
+ const count = matches.length;
101
+ let score = 0;
102
+ let displayValue = '';
103
+ let details = undefined;
104
+ // Apply count logic
105
+ switch (metric.countLogic) {
106
+ case 'exact':
107
+ score = count === (metric.expectedCount || 1) ? 1 : 0;
108
+ break;
109
+ case 'min':
110
+ score = count >= (metric.expectedCount || 1) ? 1 : 0;
111
+ break;
112
+ case 'max':
113
+ score = count <= (metric.expectedCount || 1) ? 1 : 0;
114
+ break;
115
+ case 'or':
116
+ score = count > 0 ? 1 : 0;
117
+ break;
118
+ case 'count':
119
+ default:
120
+ score = count > 0 ? 1 : 0;
121
+ break;
122
+ }
123
+ // Apply score logic
124
+ if (metric.scoreLogic === 'optimal' && metric.optimalRange) {
125
+ const { min, max } = metric.optimalRange;
126
+ score = count >= min && count <= max ? 1 : 0;
127
+ }
128
+ else if (metric.scoreLogic === 'percentage') {
129
+ // For percentage-based scoring, we'd need additional logic
130
+ score = count > 0 ? 1 : 0;
131
+ }
132
+ // Generate display value
133
+ if (metric.displayTemplate) {
134
+ displayValue = metric.displayTemplate
135
+ .replace('{{count}}', count.toString())
136
+ .replace('{{matches}}', matches.length.toString());
137
+ }
138
+ else {
139
+ displayValue = `${count} match(es) found`;
140
+ }
141
+ // Generate details based on metric type
142
+ if (metric.id === 'h1-tags' || metric.id === 'h2-tags') {
143
+ const headings = matches.map(match => {
144
+ const text = match.replace(/<[^>]*>/g, '').trim();
145
+ return { tag: metric.id.replace('-tags', ''), text };
146
+ });
147
+ details = { items: headings };
148
+ }
149
+ else if (metric.id === 'image-alt-text') {
150
+ // Special logic for image alt text - check each image individually
151
+ const images = matches.map(match => {
152
+ const srcMatch = match.match(/src=["']([^"']*)["']/);
153
+ const altMatch = match.match(/alt=["']([^"']*)["']/);
154
+ return {
155
+ src: srcMatch ? srcMatch[1] : '',
156
+ alt: altMatch ? altMatch[1] : null
157
+ };
158
+ });
159
+ const imagesWithAlt = images.filter(img => img.alt !== null).length;
160
+ const totalImages = images.length;
161
+ score = totalImages > 0 && imagesWithAlt === totalImages ? 1 : 0;
162
+ displayValue = `${imagesWithAlt}/${totalImages} images have alt text`;
163
+ details = { items: images };
164
+ }
165
+ else if (metric.id === 'canonical-urls') {
166
+ const canonicalMatch = matches[0]?.match(/href=["']([^"']*)["']/);
167
+ const canonicalUrl = canonicalMatch ? canonicalMatch[1] : null;
168
+ displayValue = canonicalUrl || 'No canonical URL found';
169
+ }
170
+ else if (metric.id === 'language-tags') {
171
+ const langMatch = matches[0]?.match(/lang=["']([^"']*)["']/);
172
+ const lang = langMatch ? langMatch[1] : null;
173
+ displayValue = lang ? `Language: ${lang}` : 'No language tag found';
174
+ }
175
+ return { score, displayValue, details };
176
+ }
177
+ function calculateSemanticTagsScore(semanticData) {
178
+ const requiredTagsPresent = semanticData.requiredTags.filter(tag => tag.present).length;
179
+ const optionalTagsPresent = semanticData.optionalTags.filter(tag => tag.present).length;
180
+ const totalSemanticTags = requiredTagsPresent + optionalTagsPresent;
181
+ return {
182
+ score: requiredTagsPresent >= 5 ? 1 : 0,
183
+ displayValue: `${requiredTagsPresent}/5 required, ${optionalTagsPresent} optional (${totalSemanticTags} total)`,
184
+ details: {
185
+ items: [
186
+ { type: 'required', tags: semanticData.requiredTags },
187
+ { type: 'optional', tags: semanticData.optionalTags },
188
+ { type: 'summary', requiredCount: requiredTagsPresent, optionalCount: optionalTagsPresent, totalCount: totalSemanticTags }
189
+ ]
190
+ }
191
+ };
192
+ }
193
+ function calculateTitleTagsScore(titleData) {
194
+ const score = titleData.length > 0 && titleData.length <= 60 ? 1 : 0;
195
+ const displayValue = titleData.content || 'No title tag found';
196
+ return {
197
+ score,
198
+ displayValue,
199
+ details: {
200
+ items: [
201
+ { type: 'title', content: titleData.content, length: titleData.length, optimal: titleData.length > 0 && titleData.length <= 60 }
202
+ ]
203
+ }
204
+ };
205
+ }
206
+ function calculateMetaKeywordsScore(keywordsData) {
207
+ const score = keywordsData.length > 0 ? 1 : 0;
208
+ const displayValue = keywordsData.content || 'No meta keywords found';
209
+ return {
210
+ score,
211
+ displayValue,
212
+ details: {
213
+ items: [
214
+ { type: 'keywords', content: keywordsData.content, length: keywordsData.length, present: keywordsData.length > 0 }
215
+ ]
216
+ }
217
+ };
218
+ }
219
+ function calculateMetaDescriptionsScore(descriptionData) {
220
+ const score = descriptionData.optimal ? 1 : 0;
221
+ const displayValue = descriptionData.content || 'No meta description found';
222
+ return {
223
+ score,
224
+ displayValue,
225
+ details: {
226
+ items: [
227
+ { type: 'description', content: descriptionData.content, length: descriptionData.length, optimal: descriptionData.optimal }
228
+ ]
229
+ }
230
+ };
231
+ }
232
+ /**
233
+ * Crawl the site to discover internal pages
234
+ */
235
+ async function crawlSite(baseUrl, maxPages = 10) {
236
+ const visited = new Set();
237
+ const toVisit = [baseUrl];
238
+ const discovered = [];
239
+ try {
240
+ // Parse base URL for domain matching
241
+ const baseUrlObj = new URL(baseUrl);
242
+ const baseDomain = baseUrlObj.hostname;
243
+ while (toVisit.length > 0 && discovered.length < maxPages) {
244
+ const currentUrl = toVisit.shift();
245
+ if (visited.has(currentUrl))
246
+ continue;
247
+ visited.add(currentUrl);
248
+ discovered.push(currentUrl);
249
+ try {
250
+ const response = await fetch(currentUrl, {
251
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SEO Analysis Bot)' }
252
+ });
253
+ if (!response.ok)
254
+ continue;
255
+ const html = await response.text();
256
+ // Extract internal links
257
+ const linkRegex = /<a[^>]*href=["']([^"']*)["'][^>]*>/gi;
258
+ let match;
259
+ while ((match = linkRegex.exec(html)) !== null) {
260
+ try {
261
+ const href = match[1];
262
+ const absoluteUrl = new URL(href, currentUrl).toString();
263
+ // Only include same domain links that look like pages
264
+ const urlObj = new URL(absoluteUrl);
265
+ const pathname = urlObj.pathname.toLowerCase();
266
+ // Exclude common non-page directories and files
267
+ const isExcluded = EXCLUDED_URL_PATTERNS.some(pattern => pathname.includes(pattern)) ||
268
+ pathname.match(EXCLUDED_FILE_EXTENSIONS) ||
269
+ EXCLUDED_DIRECTORY_NAMES.some(dir => pathname.endsWith(`/${dir}`));
270
+ if (urlObj.hostname === baseDomain && !visited.has(absoluteUrl) && !toVisit.includes(absoluteUrl) && !isExcluded) {
271
+ toVisit.push(absoluteUrl);
272
+ }
273
+ }
274
+ catch {
275
+ // Invalid URL, skip
276
+ }
277
+ }
278
+ }
279
+ catch (error) {
280
+ console.warn(`Failed to crawl ${currentUrl}:`, error);
281
+ }
282
+ }
283
+ }
284
+ catch (error) {
285
+ console.warn('Error during site crawling:', error);
286
+ }
287
+ return discovered.slice(0, maxPages);
288
+ }
289
+ /**
290
+ * Analyze a single page for on-page SEO elements using Puppeteer for full rendering
291
+ */
292
+ async function analyzeSinglePage(url) {
293
+ let browser;
294
+ try {
295
+ // Reuse browser instance if available, otherwise create new one
296
+ browser = globalThis.__seoBrowser;
297
+ if (!browser || browser.isConnected() === false) {
298
+ browser = await puppeteer.launch({
299
+ headless: true,
300
+ args: [
301
+ '--no-sandbox',
302
+ '--disable-setuid-sandbox',
303
+ '--disable-dev-shm-usage',
304
+ '--disable-accelerated-2d-canvas',
305
+ '--no-first-run',
306
+ '--no-zygote',
307
+ '--disable-gpu',
308
+ '--disable-web-security',
309
+ '--disable-features=VizDisplayCompositor',
310
+ '--disable-extensions',
311
+ '--disable-plugins',
312
+ '--disable-default-apps',
313
+ '--disable-background-timer-throttling',
314
+ '--disable-backgrounding-occluded-windows',
315
+ '--disable-renderer-backgrounding'
316
+ ]
317
+ });
318
+ globalThis.__seoBrowser = browser;
319
+ }
320
+ const page = await browser.newPage();
321
+ // Block unnecessary resources for faster loading while preserving SEO-relevant content
322
+ await page.setRequestInterception(true);
323
+ page.on('request', (request) => {
324
+ const resourceType = request.resourceType();
325
+ const url = request.url();
326
+ // Block heavy resources that slow down loading but aren't needed for HTML structure
327
+ if (resourceType === 'image' ||
328
+ resourceType === 'media' ||
329
+ url.includes('.jpg') ||
330
+ url.includes('.jpeg') ||
331
+ url.includes('.png') ||
332
+ url.includes('.gif') ||
333
+ url.includes('.webp') ||
334
+ url.includes('google-analytics.com') ||
335
+ url.includes('googletagmanager.com') ||
336
+ url.includes('facebook.com/tr') ||
337
+ url.includes('doubleclick.net')) {
338
+ request.abort();
339
+ }
340
+ else {
341
+ request.continue();
342
+ }
343
+ });
344
+ // Set smaller viewport for faster rendering
345
+ await page.setViewport({ width: 800, height: 600 });
346
+ // Set user agent to avoid bot detection
347
+ await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36');
348
+ // Navigate to the page with faster waiting strategy
349
+ const response = await page.goto(url, {
350
+ waitUntil: 'domcontentloaded', // Wait for DOM instead of all network requests
351
+ timeout: 15000 // Reduced timeout
352
+ });
353
+ // Wait for H1 elements to be rendered (if any) with a short timeout
354
+ try {
355
+ await page.waitForSelector('h1', { timeout: 2000 });
356
+ }
357
+ catch {
358
+ // H1 not found within timeout, continue anyway
359
+ }
360
+ // Get title and heading counts directly from DOM for speed
361
+ const pageData = await page.evaluate(() => {
362
+ return {
363
+ title: document.title,
364
+ h1Count: document.querySelectorAll('h1').length,
365
+ h2Count: document.querySelectorAll('h2').length,
366
+ h1Elements: Array.from(document.querySelectorAll('h1')).map(h1 => ({
367
+ text: h1.textContent?.trim() || ''
368
+ })),
369
+ h2Elements: Array.from(document.querySelectorAll('h2')).map(h2 => ({
370
+ text: h2.textContent?.trim() || ''
371
+ }))
372
+ };
373
+ });
374
+ // Get the rendered HTML for other pattern-based checks
375
+ const html = await page.content();
376
+ await page.close();
377
+ const audits = [];
378
+ // Process on-page metrics from configuration
379
+ const config = seoMetricsConfig;
380
+ const onPageCategory = config.categories['on-page'];
381
+ for (const metric of Object.values(onPageCategory.metrics)) {
382
+ let score = 0;
383
+ let displayValue = '';
384
+ let details = undefined;
385
+ // Use data collector and scorer if available
386
+ if (metric.dataCollector && metric.scorer) {
387
+ const collector = dataCollectors[metric.dataCollector];
388
+ const scorer = scorers[metric.scorer];
389
+ if (collector && scorer) {
390
+ const rawData = collector(html, pageData.title);
391
+ const result = scorer(rawData);
392
+ score = result.score;
393
+ displayValue = result.displayValue;
394
+ details = result.details;
395
+ }
396
+ }
397
+ else if (metric.pattern) {
398
+ // Use pattern-based analysis
399
+ const result = analyzePatternMetric(html, metric);
400
+ score = result.score;
401
+ displayValue = result.displayValue;
402
+ details = result.details;
403
+ }
404
+ // Override H1 and H2 results with direct DOM counts for accuracy and speed
405
+ if (metric.id === 'h1-tags') {
406
+ score = pageData.h1Count === (metric.expectedCount || 1) ? 1 : 0;
407
+ displayValue = `${pageData.h1Count} H1 tag(s) found`;
408
+ details = { items: pageData.h1Elements.map((h1) => ({ tag: 'h1', text: h1.text })) };
409
+ }
410
+ else if (metric.id === 'h2-tags') {
411
+ score = pageData.h2Count > 0 ? 1 : 0;
412
+ displayValue = `${pageData.h2Count} H2 tag(s) found`;
413
+ details = { items: pageData.h2Elements.map((h2) => ({ tag: 'h2', text: h2.text })) };
414
+ }
415
+ audits.push({
416
+ id: metric.id,
417
+ title: metric.title,
418
+ score,
419
+ scoreDisplayMode: metric.scoreDisplayMode,
420
+ displayValue,
421
+ category: 'on-page',
422
+ details
423
+ });
424
+ }
425
+ return {
426
+ url,
427
+ title: pageData.title,
428
+ statusCode: response.status(),
429
+ audits,
430
+ crawledAt: new Date().toISOString()
431
+ };
432
+ }
433
+ catch (error) {
434
+ console.warn(`Failed to analyze ${url}:`, error);
435
+ // Return a basic analysis for failed requests
436
+ return {
437
+ url,
438
+ title: undefined,
439
+ statusCode: 0, // Indicates analysis failed
440
+ audits: [],
441
+ crawledAt: new Date().toISOString()
442
+ };
443
+ }
444
+ finally {
445
+ if (browser) {
446
+ await browser.close();
447
+ }
448
+ }
449
+ }
450
+ async function performSiteWideAudits(baseUrl) {
451
+ const audits = [];
452
+ try {
453
+ // Parse base URL
454
+ const baseUrlObj = new URL(baseUrl);
455
+ const baseDomain = baseUrlObj.hostname;
456
+ const protocol = baseUrlObj.protocol;
457
+ // Process on-site metrics from configuration
458
+ const config = seoMetricsConfig;
459
+ const onSiteCategory = config.categories['on-site'];
460
+ for (const metric of Object.values(onSiteCategory.metrics)) {
461
+ let score = 0;
462
+ let displayValue = '';
463
+ // Handle metrics without custom collectors/scorers
464
+ switch (metric.id) {
465
+ case 'https':
466
+ score = protocol === 'https:' ? 1 : 0;
467
+ displayValue = score ? 'Site uses HTTPS' : 'Site does not use HTTPS';
468
+ break;
469
+ case 'url-structure': {
470
+ const hasQueryParams = baseUrlObj.search.length > 0;
471
+ score = hasQueryParams ? 0 : 1;
472
+ displayValue = score ? 'Clean URL structure' : 'URL contains query parameters';
473
+ break;
474
+ }
475
+ case 'robots-txt':
476
+ try {
477
+ const robotsUrl = `${protocol}//${baseDomain}/robots.txt`;
478
+ const robotsResponse = await fetch(robotsUrl);
479
+ score = robotsResponse.ok ? 1 : 0;
480
+ displayValue = score ? 'Robots.txt accessible' : 'Robots.txt not found or inaccessible';
481
+ }
482
+ catch {
483
+ score = 0;
484
+ displayValue = 'Robots.txt not accessible';
485
+ }
486
+ break;
487
+ case 'sitemap-xml':
488
+ try {
489
+ const sitemapUrl = `${protocol}//${baseDomain}/sitemap.xml`;
490
+ const sitemapResponse = await fetch(sitemapUrl);
491
+ score = sitemapResponse.ok ? 1 : 0;
492
+ displayValue = score ? 'Sitemap.xml accessible' : 'Sitemap.xml not found or inaccessible';
493
+ }
494
+ catch {
495
+ score = 0;
496
+ displayValue = 'Sitemap.xml not accessible';
497
+ }
498
+ break;
499
+ case 'internal-linking':
500
+ score = 1; // Placeholder - would need full crawl analysis
501
+ displayValue = 'Internal links found during crawl';
502
+ break;
503
+ case 'navigation':
504
+ score = 1; // Placeholder - would need content analysis
505
+ displayValue = 'Navigation structure found';
506
+ break;
507
+ case 'broken-links':
508
+ score = 1; // Placeholder - would need comprehensive link checking
509
+ displayValue = 'No obvious broken links detected';
510
+ break;
511
+ case 'manifest-file':
512
+ try {
513
+ const manifestUrl = `${protocol}//${baseDomain}/manifest.webmanifest`;
514
+ const manifestResponse = await fetch(manifestUrl);
515
+ score = manifestResponse.ok ? 1 : 0;
516
+ displayValue = score ? 'Manifest.webmanifest accessible' : 'Manifest.webmanifest not found or inaccessible';
517
+ }
518
+ catch {
519
+ score = 0;
520
+ displayValue = 'Manifest.webmanifest not accessible';
521
+ }
522
+ break;
523
+ default:
524
+ score = 0;
525
+ displayValue = 'Not implemented';
526
+ }
527
+ audits.push({
528
+ id: metric.id,
529
+ title: metric.title,
530
+ score,
531
+ scoreDisplayMode: metric.scoreDisplayMode,
532
+ displayValue,
533
+ category: 'on-site'
534
+ });
535
+ }
536
+ }
537
+ catch (error) {
538
+ console.error('Error performing site-wide audits:', error);
539
+ }
540
+ return audits;
541
+ }
542
+ /**
543
+ * Fetch and parse sitemap.xml to get all site URLs
544
+ */
545
+ async function getUrlsFromSitemap(baseUrl) {
546
+ try {
547
+ const sitemapUrl = `${baseUrl}/sitemap.xml`;
548
+ const response = await fetch(sitemapUrl);
549
+ if (!response.ok) {
550
+ throw new Error(`Failed to fetch sitemap: ${response.status}`);
551
+ }
552
+ const xmlText = await response.text();
553
+ const baseUrlObj = new URL(baseUrl);
554
+ // Simple XML parsing to extract URLs
555
+ const urlRegex = /<loc>([^<]+)<\/loc>/g;
556
+ const urls = [];
557
+ let match;
558
+ while ((match = urlRegex.exec(xmlText)) !== null) {
559
+ const url = match[1].trim();
560
+ // Only include URLs from the same domain and that look like valid page URLs
561
+ try {
562
+ const urlObj = new URL(url);
563
+ if (urlObj.hostname === baseUrlObj.hostname) {
564
+ const pathname = urlObj.pathname.toLowerCase();
565
+ // Exclude common non-page directories and files
566
+ const isExcluded = EXCLUDED_URL_PATTERNS.some(pattern => pathname.includes(pattern)) ||
567
+ pathname.match(EXCLUDED_FILE_EXTENSIONS) ||
568
+ EXCLUDED_DIRECTORY_NAMES.some(dir => pathname.endsWith(`/${dir}`));
569
+ if (!isExcluded) {
570
+ urls.push(url);
571
+ }
572
+ }
573
+ }
574
+ catch {
575
+ // Invalid URL, skip
576
+ }
577
+ }
578
+ return urls.slice(0, 20); // Limit to 20 pages to prevent excessive analysis
579
+ }
580
+ catch (error) {
581
+ console.warn('Failed to fetch sitemap:', error);
582
+ return [];
583
+ }
584
+ }
585
+ /**
586
+ * Main function to perform comprehensive on-site SEO analysis
587
+ */
588
+ export async function performOnSiteSEOAnalysis(baseUrl) {
589
+ try {
590
+ let pagesToAnalyze = [];
591
+ // Try to get URLs from sitemap first
592
+ const sitemapUrls = await getUrlsFromSitemap(baseUrl);
593
+ if (sitemapUrls.length > 0) {
594
+ pagesToAnalyze = sitemapUrls;
595
+ }
596
+ else {
597
+ // Fallback to crawling if sitemap not available
598
+ pagesToAnalyze = await crawlSite(baseUrl, 5);
599
+ }
600
+ if (pagesToAnalyze.length === 0) {
601
+ return {
602
+ site: baseUrl,
603
+ url: baseUrl,
604
+ overallScore: null,
605
+ pagesAnalyzed: [],
606
+ onSiteAudits: [],
607
+ totalPages: 0,
608
+ timestamp: new Date().toISOString(),
609
+ status: 'error',
610
+ error: 'No pages could be analyzed'
611
+ };
612
+ }
613
+ // Analyze each page
614
+ const pagesAnalyzed = [];
615
+ for (const pageUrl of pagesToAnalyze) {
616
+ try {
617
+ const pageAnalysis = await analyzeSinglePage(pageUrl);
618
+ pagesAnalyzed.push(pageAnalysis);
619
+ }
620
+ catch (error) {
621
+ console.warn(`Failed to analyze ${pageUrl}:`, error);
622
+ }
623
+ }
624
+ // Perform site-wide audits
625
+ const onSiteAudits = await performSiteWideAudits(baseUrl);
626
+ // Calculate overall score (simplified - average of all page scores)
627
+ let totalScore = 0;
628
+ let totalAudits = 0;
629
+ for (const page of pagesAnalyzed) {
630
+ for (const audit of page.audits) {
631
+ if (audit.score !== null) {
632
+ totalScore += audit.score;
633
+ totalAudits++;
634
+ }
635
+ }
636
+ }
637
+ for (const audit of onSiteAudits) {
638
+ if (audit.score !== null) {
639
+ totalScore += audit.score;
640
+ totalAudits++;
641
+ }
642
+ }
643
+ const overallScore = totalAudits > 0 ? Math.round((totalScore / totalAudits) * 100) / 100 : null;
644
+ return {
645
+ site: baseUrl,
646
+ url: baseUrl,
647
+ overallScore,
648
+ pagesAnalyzed,
649
+ onSiteAudits,
650
+ totalPages: pagesAnalyzed.length,
651
+ timestamp: new Date().toISOString(),
652
+ status: 'success'
653
+ };
654
+ }
655
+ catch (error) {
656
+ console.error('Error performing on-site SEO analysis:', error);
657
+ return {
658
+ site: baseUrl,
659
+ url: baseUrl,
660
+ overallScore: null,
661
+ pagesAnalyzed: [],
662
+ onSiteAudits: [],
663
+ totalPages: 0,
664
+ timestamp: new Date().toISOString(),
665
+ status: 'error',
666
+ error: error instanceof Error ? error.message : 'Unknown error'
667
+ };
668
+ }
669
+ }