@stati/core 1.10.1 → 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 CHANGED
@@ -732,7 +732,7 @@ export default defineConfig({
732
732
  ## Requirements
733
733
 
734
734
  - **Node.js** 22.0.0 or higher
735
- - **npm** 8.0.0 or higher (or equivalent package manager)
735
+ - **npm** 11.5.1 or higher (or equivalent package manager)
736
736
 
737
737
  ---
738
738
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stati/core",
3
- "version": "1.10.1",
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",
@@ -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: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
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: '&lt;script&gt;...', 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
@@ -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: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
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
- '&': '&amp;',
38
- '<': '&lt;',
39
- '>': '&gt;',
40
- '"': '&quot;',
41
- "'": '&#39;',
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: '&lt;script&gt;...', 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
- }