@stati/core 1.10.2 → 1.11.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.
Files changed (38) hide show
  1. package/dist/core/build.d.ts.map +1 -1
  2. package/dist/core/build.js +52 -4
  3. package/dist/core/invalidate.d.ts.map +1 -1
  4. package/dist/core/invalidate.js +3 -110
  5. package/dist/core/utils/glob-patterns.d.ts +40 -0
  6. package/dist/core/utils/glob-patterns.d.ts.map +1 -0
  7. package/dist/core/utils/glob-patterns.js +127 -0
  8. package/dist/core/utils/index.d.ts +1 -0
  9. package/dist/core/utils/index.d.ts.map +1 -1
  10. package/dist/core/utils/index.js +2 -0
  11. package/dist/index.d.ts +3 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/rss/generator.d.ts +26 -0
  15. package/dist/rss/generator.d.ts.map +1 -0
  16. package/dist/rss/generator.js +331 -0
  17. package/dist/rss/index.d.ts +8 -0
  18. package/dist/rss/index.d.ts.map +1 -0
  19. package/dist/rss/index.js +6 -0
  20. package/dist/rss/utils/index.d.ts +6 -0
  21. package/dist/rss/utils/index.d.ts.map +1 -0
  22. package/dist/rss/utils/index.js +5 -0
  23. package/dist/rss/utils/pattern-matching.d.ts +33 -0
  24. package/dist/rss/utils/pattern-matching.d.ts.map +1 -0
  25. package/dist/rss/utils/pattern-matching.js +87 -0
  26. package/dist/rss/validation.d.ts +30 -0
  27. package/dist/rss/validation.d.ts.map +1 -0
  28. package/dist/rss/validation.js +124 -0
  29. package/dist/seo/sitemap.d.ts.map +1 -1
  30. package/dist/seo/sitemap.js +3 -52
  31. package/dist/types/config.d.ts +2 -0
  32. package/dist/types/config.d.ts.map +1 -1
  33. package/dist/types/index.d.ts +1 -0
  34. package/dist/types/index.d.ts.map +1 -1
  35. package/dist/types/rss.d.ts +247 -0
  36. package/dist/types/rss.d.ts.map +1 -0
  37. package/dist/types/rss.js +4 -0
  38. package/package.json +1 -1
@@ -0,0 +1,331 @@
1
+ /**
2
+ * RSS feed generation utilities for Stati
3
+ * @module rss/generator
4
+ */
5
+ import { escapeHtml } from '../seo/utils/index.js';
6
+ import { filterByPatterns } from './utils/index.js';
7
+ /**
8
+ * Formats a date for RSS pubDate field (RFC 822)
9
+ * @param date - Date to format
10
+ * @returns RFC 822 formatted date string
11
+ */
12
+ function formatRSSDate(date) {
13
+ return date.toUTCString();
14
+ }
15
+ /**
16
+ * Converts a date string or Date object to RFC 822 format
17
+ * @param dateInput - Date string or Date object
18
+ * @returns RFC 822 formatted date string or undefined if invalid
19
+ */
20
+ function parseAndFormatDate(dateInput) {
21
+ if (!dateInput)
22
+ return undefined;
23
+ let date;
24
+ if (typeof dateInput === 'string') {
25
+ date = new Date(dateInput);
26
+ }
27
+ else {
28
+ date = dateInput;
29
+ }
30
+ if (isNaN(date.getTime())) {
31
+ return undefined;
32
+ }
33
+ return formatRSSDate(date);
34
+ }
35
+ /**
36
+ * Filters pages based on feed configuration
37
+ * @param pages - All pages
38
+ * @param feedConfig - Feed configuration
39
+ * @returns Filtered pages
40
+ */
41
+ function filterPages(pages, feedConfig) {
42
+ const options = {};
43
+ if (feedConfig.contentPatterns) {
44
+ options.includePatterns = feedConfig.contentPatterns;
45
+ }
46
+ if (feedConfig.excludePatterns) {
47
+ options.excludePatterns = feedConfig.excludePatterns;
48
+ }
49
+ let filtered = filterByPatterns(pages, (page) => page.sourcePath, options);
50
+ // Apply custom filter function if provided
51
+ if (feedConfig.filter) {
52
+ filtered = filtered.filter(feedConfig.filter);
53
+ }
54
+ return filtered;
55
+ }
56
+ /**
57
+ * Helper to get date value from page for sorting
58
+ * @param page - Page to get date from
59
+ * @returns Date value or undefined
60
+ */
61
+ function getPageDate(page) {
62
+ return page.frontMatter.publishedAt || page.frontMatter.date;
63
+ }
64
+ /**
65
+ * Sorts pages based on feed configuration
66
+ * @param pages - Pages to sort
67
+ * @param feedConfig - Feed configuration
68
+ * @returns Sorted pages
69
+ */
70
+ function sortPages(pages, feedConfig) {
71
+ const sorted = [...pages];
72
+ const sortBy = feedConfig.sortBy || 'date-desc';
73
+ if (sortBy === 'custom' && feedConfig.sortFn) {
74
+ return sorted.sort(feedConfig.sortFn);
75
+ }
76
+ return sorted.sort((a, b) => {
77
+ switch (sortBy) {
78
+ case 'date-desc':
79
+ case 'date-asc': {
80
+ const dateA = getPageDate(a);
81
+ const dateB = getPageDate(b);
82
+ if (!dateA && !dateB)
83
+ return 0;
84
+ if (!dateA)
85
+ return 1;
86
+ if (!dateB)
87
+ return -1;
88
+ const comparison = new Date(dateA).getTime() - new Date(dateB).getTime();
89
+ return sortBy === 'date-asc' ? comparison : -comparison;
90
+ }
91
+ case 'title-asc':
92
+ case 'title-desc': {
93
+ const titleA = (a.frontMatter.title || '').toLowerCase();
94
+ const titleB = (b.frontMatter.title || '').toLowerCase();
95
+ const comparison = titleA.localeCompare(titleB);
96
+ return sortBy === 'title-asc' ? comparison : -comparison;
97
+ }
98
+ default:
99
+ return 0;
100
+ }
101
+ });
102
+ }
103
+ /**
104
+ * Type guard to ensure we have a valid record-like object
105
+ * @param value - Value to check
106
+ * @returns True if value is a non-null object
107
+ */
108
+ function isValidRecord(value) {
109
+ return value !== null && value !== undefined && typeof value === 'object';
110
+ }
111
+ /**
112
+ * Gets a value from page using field mapping with improved type safety
113
+ * @param page - Page to extract value from
114
+ * @param mapping - Field mapping (property name or function)
115
+ * @param defaultProp - Default property name to use
116
+ * @param logger - Logger instance for warnings
117
+ * @returns Extracted value or undefined
118
+ */
119
+ function getFieldValue(page, mapping, defaultProp, logger) {
120
+ if (typeof mapping === 'function') {
121
+ try {
122
+ return mapping(page);
123
+ }
124
+ catch (error) {
125
+ // Log error but don't crash - return undefined for invalid mappings
126
+ if (logger) {
127
+ const errorMessage = error instanceof Error ? error.message : String(error);
128
+ logger.warning(`Field mapping function failed for page ${page.slug}: ${errorMessage}`);
129
+ }
130
+ return undefined;
131
+ }
132
+ }
133
+ const prop = mapping || defaultProp;
134
+ if (!prop)
135
+ return undefined;
136
+ // Type-safe access to frontMatter properties using type guard
137
+ // FrontMatter interface allows index signature for custom properties
138
+ if (!isValidRecord(page.frontMatter)) {
139
+ return undefined;
140
+ }
141
+ const value = page.frontMatter[prop];
142
+ // Return value with proper type checking
143
+ return value !== undefined && value !== null ? value : undefined;
144
+ }
145
+ /**
146
+ * Builds the absolute URL for a page
147
+ * @param page - Page to build URL for
148
+ * @param config - Stati configuration
149
+ * @returns Absolute URL
150
+ */
151
+ function buildPageUrl(page, config) {
152
+ const baseUrl = config.site.baseUrl.replace(/\/$/, '');
153
+ const pageUrl = page.url.startsWith('/') ? page.url : '/' + page.url;
154
+ return baseUrl + pageUrl;
155
+ }
156
+ /**
157
+ * Generates RSS XML for a single feed
158
+ * @param pages - All pages in the site
159
+ * @param config - Stati configuration
160
+ * @param feedConfig - Feed configuration
161
+ * @param logger - Logger instance for warnings (optional)
162
+ * @returns RSS generation result
163
+ */
164
+ export function generateRSSFeed(pages, config, feedConfig, logger) {
165
+ // Filter and sort pages
166
+ let feedPages = sortPages(filterPages(pages, feedConfig), feedConfig);
167
+ // Limit to maxItems if specified
168
+ if (feedConfig.maxItems && feedConfig.maxItems > 0) {
169
+ feedPages = feedPages.slice(0, feedConfig.maxItems);
170
+ }
171
+ // Build feed metadata
172
+ const feedLink = feedConfig.link || config.site.baseUrl;
173
+ const feedTitle = feedConfig.title;
174
+ const feedDescription = feedConfig.description;
175
+ // Start building XML
176
+ const xmlLines = [];
177
+ xmlLines.push('<?xml version="1.0" encoding="UTF-8"?>');
178
+ // Build namespace attributes
179
+ const namespaces = feedConfig.namespaces || {};
180
+ const nsAttrs = Object.entries(namespaces)
181
+ .map(([prefix, uri]) => `xmlns:${prefix}="${escapeHtml(uri)}"`)
182
+ .join(' ');
183
+ xmlLines.push(`<rss version="2.0"${nsAttrs ? ' ' + nsAttrs : ''}>`);
184
+ xmlLines.push(' <channel>');
185
+ xmlLines.push(` <title>${escapeHtml(feedTitle)}</title>`);
186
+ xmlLines.push(` <link>${escapeHtml(feedLink)}</link>`);
187
+ xmlLines.push(` <description>${escapeHtml(feedDescription)}</description>`);
188
+ // Optional channel elements
189
+ if (feedConfig.language) {
190
+ xmlLines.push(` <language>${escapeHtml(feedConfig.language)}</language>`);
191
+ }
192
+ if (feedConfig.copyright) {
193
+ xmlLines.push(` <copyright>${escapeHtml(feedConfig.copyright)}</copyright>`);
194
+ }
195
+ if (feedConfig.managingEditor) {
196
+ xmlLines.push(` <managingEditor>${escapeHtml(feedConfig.managingEditor)}</managingEditor>`);
197
+ }
198
+ if (feedConfig.webMaster) {
199
+ xmlLines.push(` <webMaster>${escapeHtml(feedConfig.webMaster)}</webMaster>`);
200
+ }
201
+ // Publication date and last build date (use same timestamp for consistency)
202
+ const buildDate = new Date();
203
+ xmlLines.push(` <pubDate>${formatRSSDate(buildDate)}</pubDate>`);
204
+ // Last build date
205
+ xmlLines.push(` <lastBuildDate>${formatRSSDate(buildDate)}</lastBuildDate>`);
206
+ if (feedConfig.category) {
207
+ xmlLines.push(` <category>${escapeHtml(feedConfig.category)}</category>`);
208
+ }
209
+ // Generator
210
+ xmlLines.push(` <generator>Stati Static Site Generator</generator>`);
211
+ if (feedConfig.ttl) {
212
+ xmlLines.push(` <ttl>${feedConfig.ttl}</ttl>`);
213
+ }
214
+ // Image
215
+ if (feedConfig.image) {
216
+ xmlLines.push(` <image>`);
217
+ xmlLines.push(` <url>${escapeHtml(feedConfig.image.url)}</url>`);
218
+ xmlLines.push(` <title>${escapeHtml(feedConfig.image.title)}</title>`);
219
+ xmlLines.push(` <link>${escapeHtml(feedConfig.image.link)}</link>`);
220
+ if (feedConfig.image.width) {
221
+ xmlLines.push(` <width>${feedConfig.image.width}</width>`);
222
+ }
223
+ if (feedConfig.image.height) {
224
+ xmlLines.push(` <height>${feedConfig.image.height}</height>`);
225
+ }
226
+ xmlLines.push(` </image>`);
227
+ }
228
+ // Add items
229
+ const itemMapping = feedConfig.itemMapping || {};
230
+ for (const page of feedPages) {
231
+ xmlLines.push(` <item>`);
232
+ // Title
233
+ const title = getFieldValue(page, itemMapping.title, 'title', logger);
234
+ if (title) {
235
+ xmlLines.push(` <title>${escapeHtml(title)}</title>`);
236
+ }
237
+ // Link
238
+ let link;
239
+ if (itemMapping.link) {
240
+ link = itemMapping.link(page, config);
241
+ }
242
+ else {
243
+ link = buildPageUrl(page, config);
244
+ }
245
+ xmlLines.push(` <link>${escapeHtml(link)}</link>`);
246
+ // Description (or full content)
247
+ if (itemMapping.includeContent && page.content) {
248
+ xmlLines.push(` <description><![CDATA[${page.content}]]></description>`);
249
+ }
250
+ else {
251
+ const description = getFieldValue(page, itemMapping.description, 'description', logger);
252
+ if (description) {
253
+ xmlLines.push(` <description>${escapeHtml(description)}</description>`);
254
+ }
255
+ }
256
+ // Pub date
257
+ const pubDate = getFieldValue(page, itemMapping.pubDate, 'publishedAt', logger) ||
258
+ getFieldValue(page, itemMapping.pubDate, 'date', logger);
259
+ const formattedPubDate = parseAndFormatDate(pubDate);
260
+ if (formattedPubDate) {
261
+ xmlLines.push(` <pubDate>${formattedPubDate}</pubDate>`);
262
+ }
263
+ // Author
264
+ const author = getFieldValue(page, itemMapping.author, 'author', logger);
265
+ if (author) {
266
+ xmlLines.push(` <author>${escapeHtml(author)}</author>`);
267
+ }
268
+ // Category/categories
269
+ const category = getFieldValue(page, itemMapping.category, 'tags', logger);
270
+ if (category) {
271
+ const categories = Array.isArray(category) ? category : [category];
272
+ for (const cat of categories) {
273
+ xmlLines.push(` <category>${escapeHtml(cat)}</category>`);
274
+ }
275
+ }
276
+ // GUID
277
+ let guid;
278
+ if (itemMapping.guid) {
279
+ guid = itemMapping.guid(page, config);
280
+ }
281
+ else {
282
+ guid = link;
283
+ }
284
+ xmlLines.push(` <guid isPermaLink="true">${escapeHtml(guid)}</guid>`);
285
+ // Enclosure
286
+ if (feedConfig.enclosure) {
287
+ const enclosure = feedConfig.enclosure(page);
288
+ if (enclosure) {
289
+ xmlLines.push(` <enclosure url="${escapeHtml(enclosure.url)}" length="${enclosure.length}" type="${escapeHtml(enclosure.type)}" />`);
290
+ }
291
+ }
292
+ // Custom elements
293
+ if (feedConfig.customItemElements) {
294
+ const customElements = feedConfig.customItemElements(page);
295
+ for (const [key, value] of Object.entries(customElements)) {
296
+ if (value !== undefined && value !== null) {
297
+ xmlLines.push(` <${key}>${escapeHtml(String(value))}</${key}>`);
298
+ }
299
+ }
300
+ }
301
+ xmlLines.push(` </item>`);
302
+ }
303
+ xmlLines.push(' </channel>');
304
+ xmlLines.push('</rss>');
305
+ const xml = xmlLines.join('\n');
306
+ return {
307
+ filename: feedConfig.filename,
308
+ itemCount: feedPages.length,
309
+ sizeInBytes: Buffer.byteLength(xml, 'utf8'),
310
+ xml,
311
+ };
312
+ }
313
+ /**
314
+ * Generates all RSS feeds configured in the site
315
+ * @param pages - All pages in the site
316
+ * @param config - Stati configuration
317
+ * @param logger - Logger instance for warnings (optional)
318
+ * @returns Array of RSS generation results
319
+ */
320
+ export function generateRSSFeeds(pages, config, logger) {
321
+ const rssConfig = config.rss;
322
+ if (!rssConfig || !rssConfig.enabled || !rssConfig.feeds) {
323
+ return [];
324
+ }
325
+ const results = [];
326
+ for (const feedConfig of rssConfig.feeds) {
327
+ const result = generateRSSFeed(pages, config, feedConfig, logger);
328
+ results.push(result);
329
+ }
330
+ return results;
331
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * RSS feed generation module
3
+ * @module rss
4
+ */
5
+ export { generateRSSFeed, generateRSSFeeds } from './generator.js';
6
+ export { validateRSSConfig, validateRSSFeedConfig } from './validation.js';
7
+ export type { RSSValidationResult } from './validation.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rss/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC3E,YAAY,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * RSS feed generation module
3
+ * @module rss
4
+ */
5
+ export { generateRSSFeed, generateRSSFeeds } from './generator.js';
6
+ export { validateRSSConfig, validateRSSFeedConfig } from './validation.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * RSS utilities barrel export
3
+ * @module rss/utils
4
+ */
5
+ export { matchesAnyPattern, urlMatchesAnyPattern, filterByPatterns } from './pattern-matching.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rss/utils/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * RSS utilities barrel export
3
+ * @module rss/utils
4
+ */
5
+ export { matchesAnyPattern, urlMatchesAnyPattern, filterByPatterns } from './pattern-matching.js';
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared pattern matching utilities for Stati
3
+ * Provides glob pattern matching functionality used across RSS, sitemap, and other modules
4
+ * @module utils/pattern-matching
5
+ */
6
+ /**
7
+ * Checks if a path matches any of the given glob patterns
8
+ * @param path - Path to check (will be normalized to forward slashes)
9
+ * @param patterns - Array of glob patterns
10
+ * @param allowPrefix - If true, also match if path starts with pattern (for non-glob patterns)
11
+ * @returns true if path matches any pattern
12
+ */
13
+ export declare function matchesAnyPattern(path: string, patterns: string[], allowPrefix?: boolean): boolean;
14
+ /**
15
+ * Checks if a path matches any of the given glob patterns (URL-based)
16
+ * This is a convenience wrapper for URL-based matching
17
+ * @param url - URL to check
18
+ * @param patterns - Array of glob patterns
19
+ * @returns true if URL matches any pattern
20
+ */
21
+ export declare function urlMatchesAnyPattern(url: string, patterns: string[]): boolean;
22
+ /**
23
+ * Filters an array of items based on include and exclude patterns
24
+ * @param items - Items to filter
25
+ * @param getPath - Function to extract path from item
26
+ * @param options - Filter options
27
+ * @returns Filtered items
28
+ */
29
+ export declare function filterByPatterns<T>(items: T[], getPath: (item: T) => string, options?: {
30
+ includePatterns?: string[];
31
+ excludePatterns?: string[];
32
+ }): T[];
33
+ //# sourceMappingURL=pattern-matching.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pattern-matching.d.ts","sourceRoot":"","sources":["../../../src/rss/utils/pattern-matching.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,WAAW,UAAO,GAAG,OAAO,CAgD/F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAE7E;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAChC,KAAK,EAAE,CAAC,EAAE,EACV,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,EAC5B,OAAO,GAAE;IACP,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB,GACL,CAAC,EAAE,CAkBL"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shared pattern matching utilities for Stati
3
+ * Provides glob pattern matching functionality used across RSS, sitemap, and other modules
4
+ * @module utils/pattern-matching
5
+ */
6
+ import { globToRegex } from '../../core/utils/index.js';
7
+ /**
8
+ * Checks if a path matches any of the given glob patterns
9
+ * @param path - Path to check (will be normalized to forward slashes)
10
+ * @param patterns - Array of glob patterns
11
+ * @param allowPrefix - If true, also match if path starts with pattern (for non-glob patterns)
12
+ * @returns true if path matches any pattern
13
+ */
14
+ export function matchesAnyPattern(path, patterns, allowPrefix = true) {
15
+ if (patterns.length === 0) {
16
+ return false;
17
+ }
18
+ // Normalize path to use forward slashes
19
+ const normalizedPath = path.replace(/\\/g, '/');
20
+ for (const pattern of patterns) {
21
+ const normalizedPattern = pattern.replace(/\\/g, '/');
22
+ // Check if pattern is a glob pattern
23
+ if (normalizedPattern.includes('*') || normalizedPattern.includes('?')) {
24
+ const regex = globToRegex(normalizedPattern);
25
+ if (regex.test(normalizedPath)) {
26
+ return true;
27
+ }
28
+ }
29
+ else {
30
+ // For non-glob patterns, check exact match first
31
+ if (normalizedPath === normalizedPattern) {
32
+ return true;
33
+ }
34
+ // For prefix matching with path boundaries
35
+ if (allowPrefix) {
36
+ // Special case: root pattern "/" should ONLY match "/" exactly
37
+ if (normalizedPattern === '/') {
38
+ continue; // Already checked exact match above, skip prefix matching
39
+ }
40
+ // For patterns ending with "/", they are explicitly prefix patterns
41
+ if (normalizedPattern.endsWith('/')) {
42
+ if (normalizedPath.startsWith(normalizedPattern)) {
43
+ return true;
44
+ }
45
+ }
46
+ else {
47
+ // For other patterns, match if:
48
+ // 1. Path starts with pattern followed by "/"
49
+ // This ensures "/api" matches "/api/foo" but not "/apitest"
50
+ if (normalizedPath.startsWith(normalizedPattern + '/')) {
51
+ return true;
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+ return false;
58
+ }
59
+ /**
60
+ * Checks if a path matches any of the given glob patterns (URL-based)
61
+ * This is a convenience wrapper for URL-based matching
62
+ * @param url - URL to check
63
+ * @param patterns - Array of glob patterns
64
+ * @returns true if URL matches any pattern
65
+ */
66
+ export function urlMatchesAnyPattern(url, patterns) {
67
+ return matchesAnyPattern(url, patterns, true);
68
+ }
69
+ /**
70
+ * Filters an array of items based on include and exclude patterns
71
+ * @param items - Items to filter
72
+ * @param getPath - Function to extract path from item
73
+ * @param options - Filter options
74
+ * @returns Filtered items
75
+ */
76
+ export function filterByPatterns(items, getPath, options = {}) {
77
+ let filtered = [...items];
78
+ // Apply include patterns first (if specified)
79
+ if (options.includePatterns && options.includePatterns.length > 0) {
80
+ filtered = filtered.filter((item) => matchesAnyPattern(getPath(item), options.includePatterns, true));
81
+ }
82
+ // Apply exclude patterns
83
+ if (options.excludePatterns && options.excludePatterns.length > 0) {
84
+ filtered = filtered.filter((item) => !matchesAnyPattern(getPath(item), options.excludePatterns, true));
85
+ }
86
+ return filtered;
87
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * RSS feed validation utilities
3
+ * @module rss/validation
4
+ */
5
+ import type { RSSFeedConfig, RSSConfig } from '../types/rss.js';
6
+ /**
7
+ * Validation result for RSS configuration
8
+ */
9
+ export interface RSSValidationResult {
10
+ /** Whether validation passed */
11
+ valid: boolean;
12
+ /** Array of validation errors (empty if valid) */
13
+ errors: string[];
14
+ /** Array of validation warnings (non-critical issues) */
15
+ warnings: string[];
16
+ }
17
+ /**
18
+ * Validates a single RSS feed configuration
19
+ * @param feedConfig - Feed configuration to validate
20
+ * @param feedIndex - Index of feed in config array (for error messages)
21
+ * @returns Validation result
22
+ */
23
+ export declare function validateRSSFeedConfig(feedConfig: RSSFeedConfig, feedIndex?: number): RSSValidationResult;
24
+ /**
25
+ * Validates RSS configuration
26
+ * @param rssConfig - RSS configuration to validate
27
+ * @returns Validation result
28
+ */
29
+ export declare function validateRSSConfig(rssConfig: RSSConfig | undefined): RSSValidationResult;
30
+ //# sourceMappingURL=validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/rss/validation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,gCAAgC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,yDAAyD;IACzD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,aAAa,EACzB,SAAS,SAAI,GACZ,mBAAmB,CA0FrB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,GAAG,mBAAmB,CAuCvF"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * RSS feed validation utilities
3
+ * @module rss/validation
4
+ */
5
+ /**
6
+ * Validates a single RSS feed configuration
7
+ * @param feedConfig - Feed configuration to validate
8
+ * @param feedIndex - Index of feed in config array (for error messages)
9
+ * @returns Validation result
10
+ */
11
+ export function validateRSSFeedConfig(feedConfig, feedIndex = 0) {
12
+ const errors = [];
13
+ const warnings = [];
14
+ // Required fields
15
+ if (!feedConfig.filename) {
16
+ errors.push(`Feed ${feedIndex}: 'filename' is required`);
17
+ }
18
+ else if (!feedConfig.filename.endsWith('.xml')) {
19
+ warnings.push(`Feed ${feedIndex}: filename '${feedConfig.filename}' should end with .xml`);
20
+ }
21
+ if (!feedConfig.title || feedConfig.title.trim() === '') {
22
+ errors.push(`Feed ${feedIndex}: 'title' is required and cannot be empty`);
23
+ }
24
+ if (!feedConfig.description || feedConfig.description.trim() === '') {
25
+ errors.push(`Feed ${feedIndex}: 'description' is required and cannot be empty`);
26
+ }
27
+ // Validate optional fields
28
+ if (feedConfig.ttl !== undefined) {
29
+ if (typeof feedConfig.ttl !== 'number' || feedConfig.ttl < 0) {
30
+ errors.push(`Feed ${feedIndex}: 'ttl' must be a non-negative number`);
31
+ }
32
+ else if (feedConfig.ttl === 0) {
33
+ warnings.push(`Feed ${feedIndex}: ttl is 0, which means feed should not be cached`);
34
+ }
35
+ }
36
+ if (feedConfig.maxItems !== undefined) {
37
+ if (typeof feedConfig.maxItems !== 'number' || feedConfig.maxItems < 1) {
38
+ errors.push(`Feed ${feedIndex}: 'maxItems' must be a positive number`);
39
+ }
40
+ }
41
+ // Validate sort configuration
42
+ if (feedConfig.sortBy === 'custom' && !feedConfig.sortFn) {
43
+ errors.push(`Feed ${feedIndex}: 'sortFn' is required when sortBy is 'custom'`);
44
+ }
45
+ const validSortOptions = ['date-desc', 'date-asc', 'title-asc', 'title-desc', 'custom'];
46
+ if (feedConfig.sortBy && !validSortOptions.includes(feedConfig.sortBy)) {
47
+ errors.push(`Feed ${feedIndex}: 'sortBy' must be one of: ${validSortOptions.join(', ')}`);
48
+ }
49
+ // Validate image configuration
50
+ if (feedConfig.image) {
51
+ if (!feedConfig.image.url) {
52
+ errors.push(`Feed ${feedIndex}: image.url is required when image is specified`);
53
+ }
54
+ if (!feedConfig.image.title) {
55
+ errors.push(`Feed ${feedIndex}: image.title is required when image is specified`);
56
+ }
57
+ if (!feedConfig.image.link) {
58
+ errors.push(`Feed ${feedIndex}: image.link is required when image is specified`);
59
+ }
60
+ if (feedConfig.image.width !== undefined && feedConfig.image.width > 144) {
61
+ warnings.push(`Feed ${feedIndex}: image width ${feedConfig.image.width} exceeds recommended maximum of 144 pixels`);
62
+ }
63
+ if (feedConfig.image.height !== undefined && feedConfig.image.height > 400) {
64
+ warnings.push(`Feed ${feedIndex}: image height ${feedConfig.image.height} exceeds recommended maximum of 400 pixels`);
65
+ }
66
+ }
67
+ // Validate email formats
68
+ const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+/;
69
+ if (feedConfig.managingEditor && !emailPattern.test(feedConfig.managingEditor)) {
70
+ warnings.push(`Feed ${feedIndex}: managingEditor should start with a valid email address (format: 'email@example.com (Name)')`);
71
+ }
72
+ if (feedConfig.webMaster && !emailPattern.test(feedConfig.webMaster)) {
73
+ warnings.push(`Feed ${feedIndex}: webMaster should start with a valid email address (format: 'email@example.com (Name)')`);
74
+ }
75
+ // Validate patterns
76
+ if (feedConfig.contentPatterns && feedConfig.contentPatterns.length === 0) {
77
+ warnings.push(`Feed ${feedIndex}: contentPatterns is empty - feed may not include any content`);
78
+ }
79
+ return {
80
+ valid: errors.length === 0,
81
+ errors,
82
+ warnings,
83
+ };
84
+ }
85
+ /**
86
+ * Validates RSS configuration
87
+ * @param rssConfig - RSS configuration to validate
88
+ * @returns Validation result
89
+ */
90
+ export function validateRSSConfig(rssConfig) {
91
+ const errors = [];
92
+ const warnings = [];
93
+ if (!rssConfig) {
94
+ return { valid: true, errors: [], warnings: ['RSS configuration is not defined'] };
95
+ }
96
+ if (!rssConfig.enabled) {
97
+ return { valid: true, errors: [], warnings: ['RSS feed generation is disabled'] };
98
+ }
99
+ if (!rssConfig.feeds || rssConfig.feeds.length === 0) {
100
+ errors.push('At least one feed configuration is required when RSS is enabled');
101
+ return { valid: false, errors, warnings };
102
+ }
103
+ // Validate each feed
104
+ const feedFilenames = new Set();
105
+ rssConfig.feeds.forEach((feedConfig, index) => {
106
+ const result = validateRSSFeedConfig(feedConfig, index);
107
+ errors.push(...result.errors);
108
+ warnings.push(...result.warnings);
109
+ // Check for duplicate filenames
110
+ if (feedConfig.filename) {
111
+ if (feedFilenames.has(feedConfig.filename)) {
112
+ errors.push(`Duplicate filename '${feedConfig.filename}' found in feed ${index}`);
113
+ }
114
+ else {
115
+ feedFilenames.add(feedConfig.filename);
116
+ }
117
+ }
118
+ });
119
+ return {
120
+ valid: errors.length === 0,
121
+ errors,
122
+ warnings,
123
+ };
124
+ }
@@ -1 +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;AAqJtD;;;;;;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"}
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;AAoGtD;;;;;;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"}