@stati/core 1.6.4 → 1.7.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.
- package/README.md +616 -101
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +42 -6
- package/dist/core/content.d.ts.map +1 -1
- package/dist/core/content.js +1 -2
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +2 -5
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +12 -0
- package/dist/core/invalidate.js +2 -2
- package/dist/core/isg/build-lock.js +1 -1
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +1 -3
- package/dist/core/isg/hash.js +1 -1
- package/dist/core/isg/index.d.ts +16 -0
- package/dist/core/isg/index.d.ts.map +1 -0
- package/dist/core/isg/index.js +22 -0
- package/dist/core/isg/manifest.js +1 -1
- package/dist/core/preview.d.ts.map +1 -1
- package/dist/core/preview.js +1 -2
- package/dist/core/templates.d.ts.map +1 -1
- package/dist/core/templates.js +4 -7
- package/dist/core/utils/index.d.ts +16 -0
- package/dist/core/utils/index.d.ts.map +1 -0
- package/dist/core/utils/index.js +22 -0
- package/dist/core/utils/partial-validation.d.ts.map +1 -1
- package/dist/core/utils/partial-validation.js +2 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/seo/auto-inject.d.ts +48 -0
- package/dist/seo/auto-inject.d.ts.map +1 -0
- package/dist/seo/auto-inject.js +108 -0
- package/dist/seo/generator.d.ts +77 -0
- package/dist/seo/generator.d.ts.map +1 -0
- package/dist/seo/generator.js +320 -0
- package/dist/seo/index.d.ts +12 -0
- package/dist/seo/index.d.ts.map +1 -0
- package/dist/seo/index.js +15 -0
- package/dist/seo/robots.d.ts +84 -0
- package/dist/seo/robots.d.ts.map +1 -0
- package/dist/seo/robots.js +165 -0
- package/dist/seo/sitemap.d.ts +37 -0
- package/dist/seo/sitemap.d.ts.map +1 -0
- package/dist/seo/sitemap.js +320 -0
- package/dist/seo/utils/escape-and-validation.d.ts +99 -0
- package/dist/seo/utils/escape-and-validation.d.ts.map +1 -0
- package/dist/seo/utils/escape-and-validation.js +319 -0
- package/dist/seo/utils/index.d.ts +7 -0
- package/dist/seo/utils/index.d.ts.map +1 -0
- package/dist/seo/utils/index.js +8 -0
- package/dist/seo/utils/url.d.ts +46 -0
- package/dist/seo/utils/url.d.ts.map +1 -0
- package/dist/seo/utils/url.js +66 -0
- package/dist/seo/utils.d.ts +94 -0
- package/dist/seo/utils.d.ts.map +1 -0
- package/dist/seo/utils.js +304 -0
- package/dist/types/config.d.ts +58 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/content.d.ts +181 -0
- package/dist/types/content.d.ts.map +1 -1
- package/dist/types/index.d.ts +5 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/seo.d.ts +69 -0
- package/dist/types/seo.d.ts.map +1 -0
- package/dist/types/seo.js +36 -0
- package/dist/types/sitemap.d.ts +94 -0
- package/dist/types/sitemap.d.ts.map +1 -0
- package/dist/types/sitemap.js +4 -0
- package/package.json +1 -1
- package/dist/core/utils/partials.d.ts +0 -24
- package/dist/core/utils/partials.d.ts.map +0 -1
- package/dist/core/utils/partials.js +0 -85
- package/dist/tests/utils/test-mocks.d.ts +0 -69
- package/dist/tests/utils/test-mocks.d.ts.map +0 -1
- package/dist/tests/utils/test-mocks.js +0 -125
- package/dist/types.d.ts +0 -543
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap generation utilities for Stati
|
|
3
|
+
* @module seo/sitemap
|
|
4
|
+
*/
|
|
5
|
+
import type { SitemapEntry, SitemapConfig, SitemapGenerationResult } from '../types/sitemap.js';
|
|
6
|
+
import type { PageModel } from '../types/content.js';
|
|
7
|
+
import type { StatiConfig } from '../types/config.js';
|
|
8
|
+
/**
|
|
9
|
+
* Generates a sitemap entry from a page model
|
|
10
|
+
* @param page - Page to generate entry for
|
|
11
|
+
* @param config - Stati configuration
|
|
12
|
+
* @param sitemapConfig - Sitemap configuration
|
|
13
|
+
* @returns Sitemap entry or null if page should be excluded
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateSitemapEntry(page: PageModel, config: StatiConfig, sitemapConfig: SitemapConfig): SitemapEntry | null;
|
|
16
|
+
/**
|
|
17
|
+
* Generates sitemap XML from entries
|
|
18
|
+
* @param entries - Sitemap entries
|
|
19
|
+
* @returns Complete sitemap XML
|
|
20
|
+
*/
|
|
21
|
+
export declare function generateSitemapXml(entries: SitemapEntry[]): string;
|
|
22
|
+
/**
|
|
23
|
+
* Generates sitemap index XML for multiple sitemaps
|
|
24
|
+
* @param sitemapUrls - Array of sitemap URLs
|
|
25
|
+
* @param siteUrl - Base site URL
|
|
26
|
+
* @returns Sitemap index XML
|
|
27
|
+
*/
|
|
28
|
+
export declare function generateSitemapIndexXml(sitemapUrls: string[], siteUrl: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Generates sitemap(s) from pages
|
|
31
|
+
* @param pages - All pages to include in sitemap
|
|
32
|
+
* @param config - Stati configuration
|
|
33
|
+
* @param sitemapConfig - Sitemap configuration
|
|
34
|
+
* @returns Sitemap generation result with XML content
|
|
35
|
+
*/
|
|
36
|
+
export declare function generateSitemap(pages: PageModel[], config: StatiConfig, sitemapConfig: SitemapConfig): SitemapGenerationResult;
|
|
37
|
+
//# sourceMappingURL=sitemap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/seo/sitemap.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EAEb,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AA6ItD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,YAAY,GAAG,IAAI,CA4ErB;AA2BD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,MAAM,CAUlE;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAatF;AAoBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,SAAS,EAAE,EAClB,MAAM,EAAE,WAAW,EACnB,aAAa,EAAE,aAAa,GAC3B,uBAAuB,CA8CzB"}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sitemap generation utilities for Stati
|
|
3
|
+
* @module seo/sitemap
|
|
4
|
+
*/
|
|
5
|
+
import { escapeHtml, resolveAbsoluteUrl } from './utils/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Maximum entries per sitemap file (per sitemap.org spec)
|
|
8
|
+
*/
|
|
9
|
+
const MAX_SITEMAP_ENTRIES = 50000;
|
|
10
|
+
/**
|
|
11
|
+
* Formats a date for sitemap lastmod field (W3C Datetime / ISO 8601)
|
|
12
|
+
* @param date - Date to format
|
|
13
|
+
* @returns ISO 8601 formatted date string (YYYY-MM-DD)
|
|
14
|
+
*/
|
|
15
|
+
function formatSitemapDate(date) {
|
|
16
|
+
const parts = date.toISOString().split('T');
|
|
17
|
+
if (parts[0]) {
|
|
18
|
+
return parts[0];
|
|
19
|
+
}
|
|
20
|
+
return date.toISOString().substring(0, 10);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validates and clamps priority value to 0.0-1.0 range
|
|
24
|
+
* @param priority - Priority value to validate
|
|
25
|
+
* @returns Clamped priority value
|
|
26
|
+
*/
|
|
27
|
+
function validatePriority(priority) {
|
|
28
|
+
if (isNaN(priority)) {
|
|
29
|
+
return 0.5;
|
|
30
|
+
}
|
|
31
|
+
return Math.max(0.0, Math.min(1.0, priority));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Validates change frequency value
|
|
35
|
+
* @param changefreq - Change frequency to validate
|
|
36
|
+
* @returns Valid change frequency or undefined
|
|
37
|
+
*/
|
|
38
|
+
function validateChangeFreq(changefreq) {
|
|
39
|
+
const validFreqs = ['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never'];
|
|
40
|
+
if (changefreq && validFreqs.includes(changefreq)) {
|
|
41
|
+
return changefreq;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Escapes regex special characters in a string, except glob wildcards
|
|
47
|
+
* @param str - String to escape
|
|
48
|
+
* @returns String with regex special characters escaped
|
|
49
|
+
*/
|
|
50
|
+
function escapeRegexExceptGlob(str) {
|
|
51
|
+
// Escape all regex special characters except * and ?
|
|
52
|
+
return str.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Checks if a page URL matches a list of patterns.
|
|
56
|
+
* @param url - The URL to check.
|
|
57
|
+
* @param patterns - An array of glob-style patterns.
|
|
58
|
+
* @returns `true` if the URL matches any pattern, `false` otherwise.
|
|
59
|
+
*/
|
|
60
|
+
function urlMatchesAnyPattern(url, patterns) {
|
|
61
|
+
for (const pattern of patterns) {
|
|
62
|
+
// Simple glob patterns
|
|
63
|
+
if (pattern.includes('*') || pattern.includes('?')) {
|
|
64
|
+
// Escape regex special characters first, then convert glob wildcards
|
|
65
|
+
const escapedPattern = escapeRegexExceptGlob(pattern);
|
|
66
|
+
const regexPattern = escapedPattern.replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
67
|
+
const regex = new RegExp('^' + regexPattern + '$');
|
|
68
|
+
if (regex.test(url)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (url === pattern || url.startsWith(pattern)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Checks if a page matches exclude patterns
|
|
80
|
+
* @param page - Page to check
|
|
81
|
+
* @param patterns - Exclude patterns (glob or regex)
|
|
82
|
+
* @returns true if page should be excluded
|
|
83
|
+
*/
|
|
84
|
+
function matchesExcludePattern(page, patterns) {
|
|
85
|
+
if (!patterns || patterns.length === 0) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return urlMatchesAnyPattern(page.url, patterns);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Checks if a page matches include patterns
|
|
92
|
+
* @param page - Page to check
|
|
93
|
+
* @param patterns - Include patterns (glob or regex)
|
|
94
|
+
* @returns true if page should be included
|
|
95
|
+
*/
|
|
96
|
+
function matchesIncludePattern(page, patterns) {
|
|
97
|
+
if (!patterns || patterns.length === 0) {
|
|
98
|
+
return true; // Include all by default
|
|
99
|
+
}
|
|
100
|
+
return urlMatchesAnyPattern(page.url, patterns);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Determines priority for a page based on priority rules
|
|
104
|
+
* @param page - Page to evaluate
|
|
105
|
+
* @param rules - Priority rules from config
|
|
106
|
+
* @param defaultPriority - Default priority value
|
|
107
|
+
* @returns Calculated priority
|
|
108
|
+
*/
|
|
109
|
+
function determinePriority(page, rules, defaultPriority = 0.5) {
|
|
110
|
+
if (!rules || rules.length === 0) {
|
|
111
|
+
return defaultPriority;
|
|
112
|
+
}
|
|
113
|
+
// Check each rule in order (first match wins)
|
|
114
|
+
for (const rule of rules) {
|
|
115
|
+
const pattern = rule.pattern;
|
|
116
|
+
if (pattern.includes('*') || pattern.includes('?')) {
|
|
117
|
+
// Escape regex special characters first, then convert glob wildcards
|
|
118
|
+
const escapedPattern = escapeRegexExceptGlob(pattern);
|
|
119
|
+
const regexPattern = escapedPattern.replace(/\*/g, '.*').replace(/\?/g, '.');
|
|
120
|
+
const regex = new RegExp('^' + regexPattern + '$');
|
|
121
|
+
if (regex.test(page.url)) {
|
|
122
|
+
return validatePriority(rule.priority);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (page.url === pattern || page.url.startsWith(pattern)) {
|
|
126
|
+
return validatePriority(rule.priority);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return defaultPriority;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Generates a sitemap entry from a page model
|
|
133
|
+
* @param page - Page to generate entry for
|
|
134
|
+
* @param config - Stati configuration
|
|
135
|
+
* @param sitemapConfig - Sitemap configuration
|
|
136
|
+
* @returns Sitemap entry or null if page should be excluded
|
|
137
|
+
*/
|
|
138
|
+
export function generateSitemapEntry(page, config, sitemapConfig) {
|
|
139
|
+
// Check frontmatter exclude flag
|
|
140
|
+
if (page.frontMatter.sitemap?.exclude === true) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
// Check exclude patterns
|
|
144
|
+
if (matchesExcludePattern(page, sitemapConfig.excludePatterns)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
// Check include patterns
|
|
148
|
+
if (!matchesIncludePattern(page, sitemapConfig.includePatterns)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
// Apply filter function if provided
|
|
152
|
+
if (sitemapConfig.filter && !sitemapConfig.filter(page)) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const siteUrl = config.site.baseUrl;
|
|
156
|
+
// Determine lastmod
|
|
157
|
+
let lastmod;
|
|
158
|
+
if (page.frontMatter.sitemap?.lastmod) {
|
|
159
|
+
lastmod = formatSitemapDate(new Date(page.frontMatter.sitemap.lastmod));
|
|
160
|
+
}
|
|
161
|
+
else if (page.frontMatter.updated) {
|
|
162
|
+
lastmod = formatSitemapDate(new Date(page.frontMatter.updated));
|
|
163
|
+
}
|
|
164
|
+
else if (page.frontMatter.date) {
|
|
165
|
+
lastmod = formatSitemapDate(new Date(page.frontMatter.date));
|
|
166
|
+
}
|
|
167
|
+
// Determine changefreq
|
|
168
|
+
let changefreq;
|
|
169
|
+
if (page.frontMatter.sitemap?.changefreq) {
|
|
170
|
+
changefreq = validateChangeFreq(page.frontMatter.sitemap.changefreq);
|
|
171
|
+
}
|
|
172
|
+
if (!changefreq && sitemapConfig.defaultChangeFreq) {
|
|
173
|
+
changefreq = sitemapConfig.defaultChangeFreq;
|
|
174
|
+
}
|
|
175
|
+
// Determine priority
|
|
176
|
+
let priority;
|
|
177
|
+
if (page.frontMatter.sitemap?.priority !== undefined) {
|
|
178
|
+
priority = validatePriority(page.frontMatter.sitemap.priority);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
priority = determinePriority(page, sitemapConfig.priorityRules, sitemapConfig.defaultPriority);
|
|
182
|
+
}
|
|
183
|
+
// Resolve absolute URL
|
|
184
|
+
let url = resolveAbsoluteUrl(page.url, siteUrl);
|
|
185
|
+
// Apply transformUrl if provided
|
|
186
|
+
if (sitemapConfig.transformUrl) {
|
|
187
|
+
url = sitemapConfig.transformUrl(url, page, config);
|
|
188
|
+
}
|
|
189
|
+
// Build entry
|
|
190
|
+
const entry = { url };
|
|
191
|
+
if (lastmod !== undefined) {
|
|
192
|
+
entry.lastmod = lastmod;
|
|
193
|
+
}
|
|
194
|
+
if (changefreq !== undefined) {
|
|
195
|
+
entry.changefreq = changefreq;
|
|
196
|
+
}
|
|
197
|
+
if (priority !== undefined) {
|
|
198
|
+
entry.priority = priority;
|
|
199
|
+
}
|
|
200
|
+
// Apply transformEntry if provided
|
|
201
|
+
if (sitemapConfig.transformEntry) {
|
|
202
|
+
return sitemapConfig.transformEntry(entry, page);
|
|
203
|
+
}
|
|
204
|
+
return entry;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Generates XML for a single sitemap entry
|
|
208
|
+
* @param entry - Sitemap entry
|
|
209
|
+
* @returns XML string for entry
|
|
210
|
+
*/
|
|
211
|
+
function generateEntryXml(entry) {
|
|
212
|
+
let xml = ' <url>\n';
|
|
213
|
+
xml += ` <loc>${escapeHtml(entry.url)}</loc>\n`;
|
|
214
|
+
if (entry.lastmod) {
|
|
215
|
+
xml += ` <lastmod>${escapeHtml(entry.lastmod)}</lastmod>\n`;
|
|
216
|
+
}
|
|
217
|
+
if (entry.changefreq) {
|
|
218
|
+
xml += ` <changefreq>${escapeHtml(entry.changefreq)}</changefreq>\n`;
|
|
219
|
+
}
|
|
220
|
+
if (entry.priority !== undefined) {
|
|
221
|
+
xml += ` <priority>${entry.priority.toFixed(1)}</priority>\n`;
|
|
222
|
+
}
|
|
223
|
+
xml += ' </url>\n';
|
|
224
|
+
return xml;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Generates sitemap XML from entries
|
|
228
|
+
* @param entries - Sitemap entries
|
|
229
|
+
* @returns Complete sitemap XML
|
|
230
|
+
*/
|
|
231
|
+
export function generateSitemapXml(entries) {
|
|
232
|
+
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
233
|
+
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
xml += generateEntryXml(entry);
|
|
236
|
+
}
|
|
237
|
+
xml += '</urlset>\n';
|
|
238
|
+
return xml;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Generates sitemap index XML for multiple sitemaps
|
|
242
|
+
* @param sitemapUrls - Array of sitemap URLs
|
|
243
|
+
* @param siteUrl - Base site URL
|
|
244
|
+
* @returns Sitemap index XML
|
|
245
|
+
*/
|
|
246
|
+
export function generateSitemapIndexXml(sitemapUrls, siteUrl) {
|
|
247
|
+
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
248
|
+
xml += '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
|
|
249
|
+
for (const url of sitemapUrls) {
|
|
250
|
+
const absoluteUrl = resolveAbsoluteUrl(url, siteUrl);
|
|
251
|
+
xml += ' <sitemap>\n';
|
|
252
|
+
xml += ` <loc>${escapeHtml(absoluteUrl)}</loc>\n`;
|
|
253
|
+
xml += ' </sitemap>\n';
|
|
254
|
+
}
|
|
255
|
+
xml += '</sitemapindex>\n';
|
|
256
|
+
return xml;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Splits entries into multiple sitemaps if needed
|
|
260
|
+
* @param entries - All sitemap entries
|
|
261
|
+
* @returns Array of entry arrays (one per sitemap file)
|
|
262
|
+
*/
|
|
263
|
+
function splitEntriesIntoSitemaps(entries) {
|
|
264
|
+
if (entries.length <= MAX_SITEMAP_ENTRIES) {
|
|
265
|
+
return [entries];
|
|
266
|
+
}
|
|
267
|
+
const sitemaps = [];
|
|
268
|
+
for (let i = 0; i < entries.length; i += MAX_SITEMAP_ENTRIES) {
|
|
269
|
+
sitemaps.push(entries.slice(i, i + MAX_SITEMAP_ENTRIES));
|
|
270
|
+
}
|
|
271
|
+
return sitemaps;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Generates sitemap(s) from pages
|
|
275
|
+
* @param pages - All pages to include in sitemap
|
|
276
|
+
* @param config - Stati configuration
|
|
277
|
+
* @param sitemapConfig - Sitemap configuration
|
|
278
|
+
* @returns Sitemap generation result with XML content
|
|
279
|
+
*/
|
|
280
|
+
export function generateSitemap(pages, config, sitemapConfig) {
|
|
281
|
+
// Generate entries for all pages
|
|
282
|
+
const entries = [];
|
|
283
|
+
for (const page of pages) {
|
|
284
|
+
const entry = generateSitemapEntry(page, config, sitemapConfig);
|
|
285
|
+
if (entry) {
|
|
286
|
+
entries.push(entry);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Check if we need multiple sitemaps
|
|
290
|
+
const sitemapGroups = splitEntriesIntoSitemaps(entries);
|
|
291
|
+
if (sitemapGroups.length === 1 && sitemapGroups[0]) {
|
|
292
|
+
// Single sitemap
|
|
293
|
+
const xml = generateSitemapXml(sitemapGroups[0]);
|
|
294
|
+
return {
|
|
295
|
+
xml,
|
|
296
|
+
entryCount: entries.length,
|
|
297
|
+
sizeInBytes: Buffer.byteLength(xml, 'utf8'),
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// Multiple sitemaps - generate index
|
|
301
|
+
const sitemapUrls = [];
|
|
302
|
+
const sitemapFiles = [];
|
|
303
|
+
for (let i = 0; i < sitemapGroups.length; i++) {
|
|
304
|
+
const group = sitemapGroups[i];
|
|
305
|
+
if (!group)
|
|
306
|
+
continue;
|
|
307
|
+
const filename = `sitemap-${i + 1}.xml`;
|
|
308
|
+
const xml = generateSitemapXml(group);
|
|
309
|
+
sitemapUrls.push(`/${filename}`);
|
|
310
|
+
sitemapFiles.push({ filename, xml });
|
|
311
|
+
}
|
|
312
|
+
// Generate index
|
|
313
|
+
const indexXml = generateSitemapIndexXml(sitemapUrls, config.site.baseUrl);
|
|
314
|
+
return {
|
|
315
|
+
xml: indexXml,
|
|
316
|
+
entryCount: entries.length,
|
|
317
|
+
sizeInBytes: Buffer.byteLength(indexXml, 'utf8'),
|
|
318
|
+
sitemaps: sitemapFiles,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
import type { Logger } from '../../types/logging.js';
|
|
7
|
+
/**
|
|
8
|
+
* Escape HTML entities to prevent XSS attacks.
|
|
9
|
+
* Uses memoization for performance with frequently repeated strings.
|
|
10
|
+
*
|
|
11
|
+
* Implements LRU-style cache eviction: when the cache is full, it's cleared
|
|
12
|
+
* and the new entry is added. This prevents unbounded memory growth while
|
|
13
|
+
* still providing caching benefits for repeated strings.
|
|
14
|
+
*
|
|
15
|
+
* @param text - The text to escape
|
|
16
|
+
* @returns HTML-safe string with special characters escaped
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* escapeHtml('<script>alert("xss")</script>');
|
|
21
|
+
* // Returns: '<script>alert("xss")</script>'
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function escapeHtml(text: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* Sanitize structured data to prevent XSS attacks and ensure safe JSON-LD output.
|
|
27
|
+
* Recursively processes objects and arrays, escaping string values and enforcing depth limits.
|
|
28
|
+
*
|
|
29
|
+
* Security: Objects exceeding max depth are completely removed rather than replaced with
|
|
30
|
+
* a string placeholder to prevent potential XSS vectors.
|
|
31
|
+
*
|
|
32
|
+
* @param data - The data to sanitize
|
|
33
|
+
* @param logger - Logger instance for warnings
|
|
34
|
+
* @param depth - Current recursion depth (internal use)
|
|
35
|
+
* @param maxDepth - Maximum allowed recursion depth (default: 50)
|
|
36
|
+
* @returns Sanitized data safe for JSON-LD output, or undefined if max depth exceeded
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* const data = {
|
|
41
|
+
* name: '<script>alert("xss")</script>',
|
|
42
|
+
* nested: { value: 'test' }
|
|
43
|
+
* };
|
|
44
|
+
* sanitizeStructuredData(data, logger);
|
|
45
|
+
* // Returns: { name: '<script>...', nested: { value: 'test' } }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function sanitizeStructuredData(data: unknown, logger: Logger, depth?: number, maxDepth?: number): unknown;
|
|
49
|
+
/**
|
|
50
|
+
* Generate robots meta tag content from SEO metadata and robots configuration.
|
|
51
|
+
* Combines noindex flag and robots directives into a comma-separated string.
|
|
52
|
+
*
|
|
53
|
+
* @param seo - SEO metadata containing robots configuration
|
|
54
|
+
* @returns Comma-separated robots directives, or empty string if none
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* generateRobotsContent({ noindex: true, robots: { follow: false } });
|
|
59
|
+
* // Returns: 'noindex, nofollow'
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export declare function generateRobotsContent(seo: SEOMetadata): string;
|
|
63
|
+
/**
|
|
64
|
+
* Validate SEO metadata before processing.
|
|
65
|
+
* Checks for common issues like invalid URLs, improper lengths, and malformed data.
|
|
66
|
+
*
|
|
67
|
+
* @param seo - SEO metadata to validate
|
|
68
|
+
* @param _pageUrl - URL of the page being validated (for context in error messages)
|
|
69
|
+
* @returns Validation result with valid flag, errors, and warnings
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const result = validateSEOMetadata({
|
|
74
|
+
* title: 'My Page',
|
|
75
|
+
* canonical: 'invalid-url'
|
|
76
|
+
* }, '/my-page');
|
|
77
|
+
* // Returns: { valid: false, errors: ['Invalid canonical URL...'], warnings: [] }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export declare function validateSEOMetadata(seo: SEOMetadata, _pageUrl: string): SEOValidationResult;
|
|
81
|
+
/**
|
|
82
|
+
* Detect existing SEO tags in HTML to avoid duplication during auto-injection.
|
|
83
|
+
* Uses enhanced regex patterns to handle multi-line attributes and edge cases.
|
|
84
|
+
*
|
|
85
|
+
* Returns a Set of SEOTagType enum values indicating which tag types are already present.
|
|
86
|
+
* This allows for granular control: only missing tags will be generated.
|
|
87
|
+
*
|
|
88
|
+
* @param html - The HTML content to scan
|
|
89
|
+
* @returns Set of SEOTagType enum values for existing tags
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* const html = '<head><title>My Page</title><meta name="description" content="..."></head>';
|
|
94
|
+
* const existing = detectExistingSEOTags(html);
|
|
95
|
+
* // Returns: Set { SEOTagType.Title, SEOTagType.Description }
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export declare function detectExistingSEOTags(html: string): Set<SEOTagType>;
|
|
99
|
+
//# sourceMappingURL=escape-and-validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape-and-validation.d.ts","sourceRoot":"","sources":["../../../src/seo/utils/escape-and-validation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,wBAAwB,CAAC;AACxE,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AASrD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4B/C;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,MAAU,EACjB,QAAQ,GAAE,MAAW,GACpB,OAAO,CA0CT;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"}
|