@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.
- package/README.COMPONENTS.md +126 -0
- package/README.md +14 -7
- package/dist/components/admin/componentusage/componentAnalysis.js +144 -0
- package/dist/components/admin/componentusage/componentDiscovery.js +85 -0
- package/dist/components/admin/deploy/deployment.integration.js +170 -0
- package/dist/components/admin/site-health/google-api-auth.js +69 -0
- package/dist/components/admin/site-health/seo-metrics.config.json +265 -0
- package/dist/components/admin/site-health/site-health-accessibility.js +158 -0
- package/dist/components/admin/site-health/site-health-axe-core.integration.js +119 -0
- package/dist/components/admin/site-health/site-health-axe-core.js +53 -0
- package/dist/components/admin/site-health/site-health-cache.js +23 -0
- package/dist/components/admin/site-health/site-health-core-web-vitals.integration.js +208 -0
- package/dist/components/admin/site-health/site-health-dependency-vulnerabilities.js +38 -0
- package/dist/components/admin/site-health/site-health-github.integration.js +81 -0
- package/dist/components/admin/site-health/site-health-github.js +34 -0
- package/dist/components/admin/site-health/site-health-google-analytics.integration.js +112 -0
- package/dist/components/admin/site-health/site-health-google-analytics.js +43 -0
- package/dist/components/admin/site-health/site-health-google-search-console.integration.js +118 -0
- package/dist/components/admin/site-health/site-health-google-search-console.js +43 -0
- package/dist/components/admin/site-health/site-health-indicators.js +71 -0
- package/dist/components/admin/site-health/site-health-on-site-seo.integration.js +578 -0
- package/dist/components/admin/site-health/site-health-on-site-seo.js +204 -0
- package/dist/components/admin/site-health/site-health-overview.js +65 -0
- package/dist/components/admin/site-health/site-health-performance.js +191 -0
- package/dist/components/admin/site-health/site-health-security.integration.js +109 -0
- package/dist/components/admin/site-health/site-health-security.js +169 -0
- package/dist/components/admin/site-health/site-health-seo.js +124 -0
- package/dist/components/admin/site-health/site-health-template.js +62 -0
- package/dist/components/admin/site-health/site-health-types.js +1 -0
- package/dist/components/admin/site-health/site-health-uptime.integration.js +29 -0
- package/dist/components/admin/site-health/site-health-uptime.js +30 -0
- package/dist/components/admin/site-health/site-health.css +427 -0
- package/dist/components/admin/sites/sites.integration.js +117 -0
- package/dist/components/cms/contentful.management.js +104 -0
- package/dist/components/shoppingcart/shipping.from.json +101 -0
- package/dist/components/shoppingcart/shipping.parcel.json +112 -0
- package/dist/components/shoppingcart/shipping.to.json +422 -0
- package/dist/components/shoppingcart/shoppingCartDiscountCodes.json +26 -0
- package/dist/components/shoppingcart/shoppingcart.components.js +1 -1
- package/dist/components/sitebuilder/config/ConfigBuilder.js +36 -140
- package/dist/components/sitebuilder/config/siteinfo-form.json +200 -0
- package/dist/components/sitebuilder/config/visualdesignform.json +244 -0
- package/dist/components/structured/buzzwordbingo.js +3 -2
- package/dist/data/404-data.json +128 -102
- package/dist/data/flickr.json +25 -0
- package/dist/data/form.json +368 -368
- package/dist/data/recipes.json +3251 -3251
- package/dist/data/references.json +138 -137
- package/dist/data/requestform.json +111 -0
- package/dist/data/requests.json +136 -135
- package/dist/data/resume.json +2573 -2575
- package/dist/data/routes.json +238 -238
- package/dist/data/routes2.json +141 -140
- package/dist/index.js +16 -3
- package/dist/index.server.js +36 -15
- package/dist/types/components/admin/componentusage/componentAnalysis.d.ts +35 -0
- package/dist/types/components/admin/componentusage/componentAnalysis.d.ts.map +1 -0
- package/dist/types/components/admin/componentusage/componentDiscovery.d.ts +10 -0
- package/dist/types/components/admin/componentusage/componentDiscovery.d.ts.map +1 -0
- package/dist/types/components/admin/deploy/deployment.integration.d.ts +26 -0
- package/dist/types/components/admin/deploy/deployment.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/google-api-auth.d.ts +37 -0
- package/dist/types/components/admin/site-health/google-api-auth.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-accessibility.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-accessibility.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-axe-core.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-axe-core.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts +63 -0
- package/dist/types/components/admin/site-health/site-health-axe-core.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-cache.d.ts +12 -0
- package/dist/types/components/admin/site-health/site-health-cache.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts +3 -0
- package/dist/types/components/admin/site-health/site-health-core-web-vitals.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-dependency-vulnerabilities.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-github.d.ts +8 -0
- package/dist/types/components/admin/site-health/site-health-github.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-github.integration.d.ts +26 -0
- package/dist/types/components/admin/site-health/site-health-github.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts +8 -0
- package/dist/types/components/admin/site-health/site-health-google-analytics.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts +26 -0
- package/dist/types/components/admin/site-health/site-health-google-analytics.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts +8 -0
- package/dist/types/components/admin/site-health/site-health-google-search-console.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts +46 -0
- package/dist/types/components/admin/site-health/site-health-google-search-console.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-indicators.d.ts +73 -0
- package/dist/types/components/admin/site-health/site-health-indicators.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts +4 -0
- package/dist/types/components/admin/site-health/site-health-on-site-seo.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts +34 -0
- package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-overview.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-overview.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-performance.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-performance.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-security.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-security.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-security.integration.d.ts +29 -0
- package/dist/types/components/admin/site-health/site-health-security.integration.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-seo.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-seo.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-template.d.ts +12 -0
- package/dist/types/components/admin/site-health/site-health-template.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-types.d.ts +186 -0
- package/dist/types/components/admin/site-health/site-health-types.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-uptime.d.ts +6 -0
- package/dist/types/components/admin/site-health/site-health-uptime.d.ts.map +1 -0
- package/dist/types/components/admin/site-health/site-health-uptime.integration.d.ts +10 -0
- package/dist/types/components/admin/site-health/site-health-uptime.integration.d.ts.map +1 -0
- package/dist/types/components/admin/sites/sites.integration.d.ts +40 -0
- package/dist/types/components/admin/sites/sites.integration.d.ts.map +1 -0
- package/dist/types/components/cms/contentful.management.d.ts +41 -0
- package/dist/types/components/cms/contentful.management.d.ts.map +1 -1
- package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts +4 -4
- package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts.map +1 -1
- package/dist/types/components/structured/buzzwordbingo.d.ts +1 -1
- package/dist/types/components/structured/buzzwordbingo.d.ts.map +1 -1
- package/dist/types/components/structured/buzzwordbingo.words.d.ts +2 -0
- package/dist/types/components/structured/buzzwordbingo.words.d.ts.map +1 -0
- package/dist/types/index.d.ts +16 -3
- package/dist/types/index.server.d.ts +36 -13
- package/dist/types/stories/admin/preview.d.ts +12 -0
- package/dist/types/stories/admin/preview.d.ts.map +1 -0
- package/dist/types/stories/admin/site-health.stories.d.ts +65 -0
- package/dist/types/stories/admin/site-health.stories.d.ts.map +1 -0
- package/dist/types/stories/structured/buzzword-bingo.stories.d.ts +1 -1
- package/dist/types/stories/structured/buzzword-bingo.stories.d.ts.map +1 -1
- package/dist/types/tests/site-health-axe-core.test.d.ts +2 -0
- package/dist/types/tests/site-health-axe-core.test.d.ts.map +1 -0
- package/dist/types/tests/site-health-cache.test.d.ts +2 -0
- package/dist/types/tests/site-health-cache.test.d.ts.map +1 -0
- package/dist/types/tests/site-health-indicators.test.d.ts +2 -0
- package/dist/types/tests/site-health-indicators.test.d.ts.map +1 -0
- package/dist/types/tests/site-health-overview.test.d.ts +2 -0
- package/dist/types/tests/site-health-overview.test.d.ts.map +1 -0
- package/dist/types/tests/site-health-template.test.d.ts +2 -0
- package/dist/types/tests/site-health-template.test.d.ts.map +1 -0
- package/dist/types/tests/sites.integration.test.d.ts +2 -0
- package/dist/types/tests/sites.integration.test.d.ts.map +1 -0
- package/package.json +14 -8
- package/dist/data/shipping.to.json +0 -422
- package/dist/data/siteinfo-form.json +0 -200
- package/dist/data/visualdesignform.json +0 -244
- package/dist/types/data/buzzwords.d.ts +0 -2
- package/dist/types/data/buzzwords.d.ts.map +0 -1
- /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
|
+
}
|