@stati/core 1.10.0 → 1.10.2
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.md +1 -1
- package/package.json +6 -1
- package/dist/seo/utils.d.ts +0 -94
- package/dist/seo/utils.d.ts.map +0 -1
- package/dist/seo/utils.js +0 -304
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stati/core",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/ianchak/stati.git",
|
|
27
|
+
"directory": "packages/core"
|
|
28
|
+
},
|
|
24
29
|
"scripts": {
|
|
25
30
|
"build": "tsc -p tsconfig.json",
|
|
26
31
|
"dev": "tsc -p tsconfig.json --watch",
|
package/dist/seo/utils.d.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SEO utility functions for HTML escaping, validation, and tag detection
|
|
3
|
-
*/
|
|
4
|
-
import type { SEOMetadata } from '../types/content.js';
|
|
5
|
-
import type { SEOValidationResult, SEOTagType } from '../types/seo.js';
|
|
6
|
-
/**
|
|
7
|
-
* Escape HTML entities to prevent XSS attacks.
|
|
8
|
-
* Uses memoization for performance with frequently repeated strings.
|
|
9
|
-
*
|
|
10
|
-
* Implements LRU-style cache eviction: when the cache is full, it's cleared
|
|
11
|
-
* and the new entry is added. This prevents unbounded memory growth while
|
|
12
|
-
* still providing caching benefits for repeated strings.
|
|
13
|
-
*
|
|
14
|
-
* @param text - The text to escape
|
|
15
|
-
* @returns HTML-safe string with special characters escaped
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```typescript
|
|
19
|
-
* escapeHtml('<script>alert("xss")</script>');
|
|
20
|
-
* // Returns: '<script>alert("xss")</script>'
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
export declare function escapeHtml(text: string): string;
|
|
24
|
-
/**
|
|
25
|
-
* Sanitize structured data to prevent XSS attacks and ensure safe JSON-LD output.
|
|
26
|
-
* Recursively processes objects and arrays, escaping string values and enforcing depth limits.
|
|
27
|
-
*
|
|
28
|
-
* @param data - The data to sanitize
|
|
29
|
-
* @param depth - Current recursion depth (internal use)
|
|
30
|
-
* @param maxDepth - Maximum allowed recursion depth (default: 50)
|
|
31
|
-
* @returns Sanitized data safe for JSON-LD output
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```typescript
|
|
35
|
-
* const data = {
|
|
36
|
-
* name: '<script>alert("xss")</script>',
|
|
37
|
-
* nested: { value: 'test' }
|
|
38
|
-
* };
|
|
39
|
-
* sanitizeStructuredData(data);
|
|
40
|
-
* // Returns: { name: '<script>...', nested: { value: 'test' } }
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
43
|
-
export declare function sanitizeStructuredData(data: unknown, depth?: number, maxDepth?: number): unknown;
|
|
44
|
-
/**
|
|
45
|
-
* Generate robots meta tag content from SEO metadata and robots configuration.
|
|
46
|
-
* Combines noindex flag and robots directives into a comma-separated string.
|
|
47
|
-
*
|
|
48
|
-
* @param seo - SEO metadata containing robots configuration
|
|
49
|
-
* @returns Comma-separated robots directives, or empty string if none
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* ```typescript
|
|
53
|
-
* generateRobotsContent({ noindex: true, robots: { follow: false } });
|
|
54
|
-
* // Returns: 'noindex, nofollow'
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
export declare function generateRobotsContent(seo: SEOMetadata): string;
|
|
58
|
-
/**
|
|
59
|
-
* Validate SEO metadata before processing.
|
|
60
|
-
* Checks for common issues like invalid URLs, improper lengths, and malformed data.
|
|
61
|
-
*
|
|
62
|
-
* @param seo - SEO metadata to validate
|
|
63
|
-
* @param _pageUrl - URL of the page being validated (for context in error messages)
|
|
64
|
-
* @returns Validation result with valid flag, errors, and warnings
|
|
65
|
-
*
|
|
66
|
-
* @example
|
|
67
|
-
* ```typescript
|
|
68
|
-
* const result = validateSEOMetadata({
|
|
69
|
-
* title: 'My Page',
|
|
70
|
-
* canonical: 'invalid-url'
|
|
71
|
-
* }, '/my-page');
|
|
72
|
-
* // Returns: { valid: false, errors: ['Invalid canonical URL...'], warnings: [] }
|
|
73
|
-
* ```
|
|
74
|
-
*/
|
|
75
|
-
export declare function validateSEOMetadata(seo: SEOMetadata, _pageUrl: string): SEOValidationResult;
|
|
76
|
-
/**
|
|
77
|
-
* Detect existing SEO tags in HTML to avoid duplication during auto-injection.
|
|
78
|
-
* Uses enhanced regex patterns to handle multi-line attributes and edge cases.
|
|
79
|
-
*
|
|
80
|
-
* Returns a Set of SEOTagType enum values indicating which tag types are already present.
|
|
81
|
-
* This allows for granular control: only missing tags will be generated.
|
|
82
|
-
*
|
|
83
|
-
* @param html - The HTML content to scan
|
|
84
|
-
* @returns Set of SEOTagType enum values for existing tags
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* ```typescript
|
|
88
|
-
* const html = '<head><title>My Page</title><meta name="description" content="..."></head>';
|
|
89
|
-
* const existing = detectExistingSEOTags(html);
|
|
90
|
-
* // Returns: Set { SEOTagType.Title, SEOTagType.Description }
|
|
91
|
-
* ```
|
|
92
|
-
*/
|
|
93
|
-
export declare function detectExistingSEOTags(html: string): Set<SEOTagType>;
|
|
94
|
-
//# sourceMappingURL=utils.d.ts.map
|
package/dist/seo/utils.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/seo/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,qBAAqB,CAAC;AACrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAUvE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4B/C;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,OAAO,EACb,KAAK,GAAE,MAAU,EACjB,QAAQ,GAAE,MAAW,GACpB,OAAO,CA0BT;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAiD9D;AAiBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAwE3F;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,UAAU,CAAC,CAuCnE"}
|
package/dist/seo/utils.js
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SEO utility functions for HTML escaping, validation, and tag detection
|
|
3
|
-
*/
|
|
4
|
-
import { URL } from 'node:url';
|
|
5
|
-
import { SEOTagType as SEOTagTypeEnum } from '../types/seo.js';
|
|
6
|
-
/**
|
|
7
|
-
* HTML escape cache for performance optimization.
|
|
8
|
-
* Stores up to 1000 frequently used strings to avoid repeated escaping.
|
|
9
|
-
*/
|
|
10
|
-
const escapeHtmlCache = new Map();
|
|
11
|
-
const ESCAPE_CACHE_MAX_SIZE = 1000;
|
|
12
|
-
/**
|
|
13
|
-
* Escape HTML entities to prevent XSS attacks.
|
|
14
|
-
* Uses memoization for performance with frequently repeated strings.
|
|
15
|
-
*
|
|
16
|
-
* Implements LRU-style cache eviction: when the cache is full, it's cleared
|
|
17
|
-
* and the new entry is added. This prevents unbounded memory growth while
|
|
18
|
-
* still providing caching benefits for repeated strings.
|
|
19
|
-
*
|
|
20
|
-
* @param text - The text to escape
|
|
21
|
-
* @returns HTML-safe string with special characters escaped
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* ```typescript
|
|
25
|
-
* escapeHtml('<script>alert("xss")</script>');
|
|
26
|
-
* // Returns: '<script>alert("xss")</script>'
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export function escapeHtml(text) {
|
|
30
|
-
// Check cache
|
|
31
|
-
const cached = escapeHtmlCache.get(text);
|
|
32
|
-
if (cached !== undefined) {
|
|
33
|
-
return cached;
|
|
34
|
-
}
|
|
35
|
-
// Compute result
|
|
36
|
-
const htmlEscapes = {
|
|
37
|
-
'&': '&',
|
|
38
|
-
'<': '<',
|
|
39
|
-
'>': '>',
|
|
40
|
-
'"': '"',
|
|
41
|
-
"'": ''',
|
|
42
|
-
};
|
|
43
|
-
const result = text.replace(/[&<>"']/g, (char) => htmlEscapes[char] || char);
|
|
44
|
-
// Store in cache with size limit
|
|
45
|
-
if (escapeHtmlCache.size < ESCAPE_CACHE_MAX_SIZE) {
|
|
46
|
-
escapeHtmlCache.set(text, result);
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
// Clear cache when full (prevents unbounded growth)
|
|
50
|
-
// This is a simple LRU-style eviction strategy
|
|
51
|
-
escapeHtmlCache.clear();
|
|
52
|
-
escapeHtmlCache.set(text, result);
|
|
53
|
-
}
|
|
54
|
-
return result;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Sanitize structured data to prevent XSS attacks and ensure safe JSON-LD output.
|
|
58
|
-
* Recursively processes objects and arrays, escaping string values and enforcing depth limits.
|
|
59
|
-
*
|
|
60
|
-
* @param data - The data to sanitize
|
|
61
|
-
* @param depth - Current recursion depth (internal use)
|
|
62
|
-
* @param maxDepth - Maximum allowed recursion depth (default: 50)
|
|
63
|
-
* @returns Sanitized data safe for JSON-LD output
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* ```typescript
|
|
67
|
-
* const data = {
|
|
68
|
-
* name: '<script>alert("xss")</script>',
|
|
69
|
-
* nested: { value: 'test' }
|
|
70
|
-
* };
|
|
71
|
-
* sanitizeStructuredData(data);
|
|
72
|
-
* // Returns: { name: '<script>...', nested: { value: 'test' } }
|
|
73
|
-
* ```
|
|
74
|
-
*/
|
|
75
|
-
export function sanitizeStructuredData(data, depth = 0, maxDepth = 50) {
|
|
76
|
-
// Prevent stack overflow from deeply nested objects
|
|
77
|
-
if (depth > maxDepth) {
|
|
78
|
-
console.warn(`Structured data exceeds maximum nesting depth of ${maxDepth}, truncating`);
|
|
79
|
-
return '[Object: max depth exceeded]';
|
|
80
|
-
}
|
|
81
|
-
// Handle primitives
|
|
82
|
-
if (typeof data === 'string') {
|
|
83
|
-
return escapeHtml(data);
|
|
84
|
-
}
|
|
85
|
-
if (typeof data !== 'object' || data === null) {
|
|
86
|
-
return data;
|
|
87
|
-
}
|
|
88
|
-
// Handle arrays
|
|
89
|
-
if (Array.isArray(data)) {
|
|
90
|
-
return data.map((item) => sanitizeStructuredData(item, depth + 1, maxDepth));
|
|
91
|
-
}
|
|
92
|
-
// Handle objects
|
|
93
|
-
const sanitized = {};
|
|
94
|
-
for (const [key, value] of Object.entries(data)) {
|
|
95
|
-
sanitized[key] = sanitizeStructuredData(value, depth + 1, maxDepth);
|
|
96
|
-
}
|
|
97
|
-
return sanitized;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Generate robots meta tag content from SEO metadata and robots configuration.
|
|
101
|
-
* Combines noindex flag and robots directives into a comma-separated string.
|
|
102
|
-
*
|
|
103
|
-
* @param seo - SEO metadata containing robots configuration
|
|
104
|
-
* @returns Comma-separated robots directives, or empty string if none
|
|
105
|
-
*
|
|
106
|
-
* @example
|
|
107
|
-
* ```typescript
|
|
108
|
-
* generateRobotsContent({ noindex: true, robots: { follow: false } });
|
|
109
|
-
* // Returns: 'noindex, nofollow'
|
|
110
|
-
* ```
|
|
111
|
-
*/
|
|
112
|
-
export function generateRobotsContent(seo) {
|
|
113
|
-
const directives = [];
|
|
114
|
-
// Collect directives from noindex flag
|
|
115
|
-
if (seo.noindex) {
|
|
116
|
-
directives.push('noindex');
|
|
117
|
-
}
|
|
118
|
-
// Handle robots config
|
|
119
|
-
if (typeof seo.robots === 'string') {
|
|
120
|
-
// If string doesn't include noindex but flag is set, prepend it
|
|
121
|
-
if (seo.noindex && !seo.robots.includes('noindex')) {
|
|
122
|
-
return `noindex, ${seo.robots}`;
|
|
123
|
-
}
|
|
124
|
-
return seo.robots;
|
|
125
|
-
}
|
|
126
|
-
else if (seo.robots) {
|
|
127
|
-
const robots = seo.robots;
|
|
128
|
-
// Only add if not already added via noindex flag
|
|
129
|
-
if (robots.index === false && !directives.includes('noindex')) {
|
|
130
|
-
directives.push('noindex');
|
|
131
|
-
}
|
|
132
|
-
if (robots.follow === false) {
|
|
133
|
-
directives.push('nofollow');
|
|
134
|
-
}
|
|
135
|
-
if (robots.archive === false) {
|
|
136
|
-
directives.push('noarchive');
|
|
137
|
-
}
|
|
138
|
-
if (robots.snippet === false) {
|
|
139
|
-
directives.push('nosnippet');
|
|
140
|
-
}
|
|
141
|
-
if (robots.imageindex === false) {
|
|
142
|
-
directives.push('noimageindex');
|
|
143
|
-
}
|
|
144
|
-
if (robots.translate === false) {
|
|
145
|
-
directives.push('notranslate');
|
|
146
|
-
}
|
|
147
|
-
if (robots.maxSnippet !== undefined) {
|
|
148
|
-
directives.push(`max-snippet:${robots.maxSnippet}`);
|
|
149
|
-
}
|
|
150
|
-
if (robots.maxImagePreview) {
|
|
151
|
-
directives.push(`max-image-preview:${robots.maxImagePreview}`);
|
|
152
|
-
}
|
|
153
|
-
if (robots.maxVideoPreview !== undefined) {
|
|
154
|
-
directives.push(`max-video-preview:${robots.maxVideoPreview}`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return directives.length > 0 ? directives.join(', ') : '';
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Validate URL format (http or https only).
|
|
161
|
-
*
|
|
162
|
-
* @param url - The URL to validate
|
|
163
|
-
* @returns True if the URL is valid
|
|
164
|
-
*/
|
|
165
|
-
function isValidUrl(url) {
|
|
166
|
-
try {
|
|
167
|
-
const parsed = new URL(url);
|
|
168
|
-
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Validate SEO metadata before processing.
|
|
176
|
-
* Checks for common issues like invalid URLs, improper lengths, and malformed data.
|
|
177
|
-
*
|
|
178
|
-
* @param seo - SEO metadata to validate
|
|
179
|
-
* @param _pageUrl - URL of the page being validated (for context in error messages)
|
|
180
|
-
* @returns Validation result with valid flag, errors, and warnings
|
|
181
|
-
*
|
|
182
|
-
* @example
|
|
183
|
-
* ```typescript
|
|
184
|
-
* const result = validateSEOMetadata({
|
|
185
|
-
* title: 'My Page',
|
|
186
|
-
* canonical: 'invalid-url'
|
|
187
|
-
* }, '/my-page');
|
|
188
|
-
* // Returns: { valid: false, errors: ['Invalid canonical URL...'], warnings: [] }
|
|
189
|
-
* ```
|
|
190
|
-
*/
|
|
191
|
-
export function validateSEOMetadata(seo, _pageUrl) {
|
|
192
|
-
const errors = [];
|
|
193
|
-
const warnings = [];
|
|
194
|
-
// Validate title length
|
|
195
|
-
if (seo.title) {
|
|
196
|
-
if (seo.title.length < 5) {
|
|
197
|
-
warnings.push(`Title is only ${seo.title.length} characters (recommended: 50-60)`);
|
|
198
|
-
}
|
|
199
|
-
else if (seo.title.length > 70) {
|
|
200
|
-
warnings.push(`Title is ${seo.title.length} characters (recommended: 50-60)`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// Validate description length
|
|
204
|
-
if (seo.description) {
|
|
205
|
-
if (seo.description.length < 50) {
|
|
206
|
-
warnings.push(`Description is only ${seo.description.length} characters (recommended: 150-160)`);
|
|
207
|
-
}
|
|
208
|
-
else if (seo.description.length > 160) {
|
|
209
|
-
warnings.push(`Description is ${seo.description.length} characters (recommended: 150-160)`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
// Validate canonical URL
|
|
213
|
-
if (seo.canonical && !isValidUrl(seo.canonical)) {
|
|
214
|
-
errors.push(`Invalid canonical URL: ${seo.canonical}`);
|
|
215
|
-
}
|
|
216
|
-
// Validate Open Graph image URL and dimensions
|
|
217
|
-
if (seo.openGraph?.image) {
|
|
218
|
-
const imageUrl = typeof seo.openGraph.image === 'string' ? seo.openGraph.image : seo.openGraph.image.url;
|
|
219
|
-
if (!isValidUrl(imageUrl) && !imageUrl.startsWith('/')) {
|
|
220
|
-
warnings.push(`Open Graph image URL may be invalid: ${imageUrl}`);
|
|
221
|
-
}
|
|
222
|
-
// Validate image dimensions if provided
|
|
223
|
-
if (typeof seo.openGraph.image !== 'string') {
|
|
224
|
-
const { width, height } = seo.openGraph.image;
|
|
225
|
-
if (width !== undefined && (!Number.isInteger(width) || width <= 0)) {
|
|
226
|
-
errors.push(`Open Graph image width must be a positive integer (got ${width})`);
|
|
227
|
-
}
|
|
228
|
-
if (height !== undefined && (!Number.isInteger(height) || height <= 0)) {
|
|
229
|
-
errors.push(`Open Graph image height must be a positive integer (got ${height})`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
// Validate Twitter image URL
|
|
234
|
-
if (seo.twitter?.image && !isValidUrl(seo.twitter.image) && !seo.twitter.image.startsWith('/')) {
|
|
235
|
-
warnings.push(`Twitter Card image URL may be invalid: ${seo.twitter.image}`);
|
|
236
|
-
}
|
|
237
|
-
// Validate structured data size
|
|
238
|
-
if (seo.structuredData) {
|
|
239
|
-
const jsonSize = JSON.stringify(seo.structuredData).length;
|
|
240
|
-
const maxSize = 100 * 1024; // 100KB limit
|
|
241
|
-
if (jsonSize > maxSize) {
|
|
242
|
-
warnings.push(`Structured data is ${(jsonSize / 1024).toFixed(2)}KB (recommended: <100KB)`);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
// Validate priority value
|
|
246
|
-
if (seo.priority !== undefined) {
|
|
247
|
-
if (typeof seo.priority !== 'number' || seo.priority < 0 || seo.priority > 1) {
|
|
248
|
-
errors.push('Priority must be a number between 0.0 and 1.0');
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return { valid: errors.length === 0, errors, warnings };
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Detect existing SEO tags in HTML to avoid duplication during auto-injection.
|
|
255
|
-
* Uses enhanced regex patterns to handle multi-line attributes and edge cases.
|
|
256
|
-
*
|
|
257
|
-
* Returns a Set of SEOTagType enum values indicating which tag types are already present.
|
|
258
|
-
* This allows for granular control: only missing tags will be generated.
|
|
259
|
-
*
|
|
260
|
-
* @param html - The HTML content to scan
|
|
261
|
-
* @returns Set of SEOTagType enum values for existing tags
|
|
262
|
-
*
|
|
263
|
-
* @example
|
|
264
|
-
* ```typescript
|
|
265
|
-
* const html = '<head><title>My Page</title><meta name="description" content="..."></head>';
|
|
266
|
-
* const existing = detectExistingSEOTags(html);
|
|
267
|
-
* // Returns: Set { SEOTagType.Title, SEOTagType.Description }
|
|
268
|
-
* ```
|
|
269
|
-
*/
|
|
270
|
-
export function detectExistingSEOTags(html) {
|
|
271
|
-
const existingTags = new Set();
|
|
272
|
-
// Extract just the <head> section for more efficient parsing
|
|
273
|
-
const headMatch = html.match(/<head[^>]*>([\s\S]*?)<\/head>/i);
|
|
274
|
-
if (!headMatch) {
|
|
275
|
-
console.warn('No <head> tag found in HTML, SEO auto-injection may not work correctly');
|
|
276
|
-
return existingTags;
|
|
277
|
-
}
|
|
278
|
-
const headContent = headMatch[1];
|
|
279
|
-
// More robust regex patterns that handle multi-line attributes and edge cases
|
|
280
|
-
const patterns = [
|
|
281
|
-
{ regex: /<title\s*>/i, type: SEOTagTypeEnum.Title },
|
|
282
|
-
{
|
|
283
|
-
regex: /<meta\s+[^>]*name\s*=\s*["']description["'][^>]*>/i,
|
|
284
|
-
type: SEOTagTypeEnum.Description,
|
|
285
|
-
},
|
|
286
|
-
{ regex: /<meta\s+[^>]*name\s*=\s*["']keywords["'][^>]*>/i, type: SEOTagTypeEnum.Keywords },
|
|
287
|
-
{ regex: /<meta\s+[^>]*name\s*=\s*["']author["'][^>]*>/i, type: SEOTagTypeEnum.Author },
|
|
288
|
-
{ regex: /<meta\s+[^>]*name\s*=\s*["']robots["'][^>]*>/i, type: SEOTagTypeEnum.Robots },
|
|
289
|
-
{ regex: /<link\s+[^>]*rel\s*=\s*["']canonical["'][^>]*>/i, type: SEOTagTypeEnum.Canonical },
|
|
290
|
-
{ regex: /<meta\s+[^>]*property\s*=\s*["']og:/i, type: SEOTagTypeEnum.OpenGraph },
|
|
291
|
-
{ regex: /<meta\s+[^>]*name\s*=\s*["']twitter:/i, type: SEOTagTypeEnum.Twitter },
|
|
292
|
-
{
|
|
293
|
-
regex: /<script\s+[^>]*type\s*=\s*["']application\/ld\+json["'][^>]*>/i,
|
|
294
|
-
type: SEOTagTypeEnum.StructuredData,
|
|
295
|
-
},
|
|
296
|
-
];
|
|
297
|
-
// Check all patterns in a single pass
|
|
298
|
-
for (const { regex, type } of patterns) {
|
|
299
|
-
if (headContent && regex.test(headContent)) {
|
|
300
|
-
existingTags.add(type);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return existingTags;
|
|
304
|
-
}
|