@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.
Files changed (81) hide show
  1. package/README.md +616 -101
  2. package/dist/core/build.d.ts.map +1 -1
  3. package/dist/core/build.js +42 -6
  4. package/dist/core/content.d.ts.map +1 -1
  5. package/dist/core/content.js +1 -2
  6. package/dist/core/dev.d.ts.map +1 -1
  7. package/dist/core/dev.js +2 -5
  8. package/dist/core/index.d.ts +13 -0
  9. package/dist/core/index.d.ts.map +1 -0
  10. package/dist/core/index.js +12 -0
  11. package/dist/core/invalidate.js +2 -2
  12. package/dist/core/isg/build-lock.js +1 -1
  13. package/dist/core/isg/deps.d.ts.map +1 -1
  14. package/dist/core/isg/deps.js +1 -3
  15. package/dist/core/isg/hash.js +1 -1
  16. package/dist/core/isg/index.d.ts +16 -0
  17. package/dist/core/isg/index.d.ts.map +1 -0
  18. package/dist/core/isg/index.js +22 -0
  19. package/dist/core/isg/manifest.js +1 -1
  20. package/dist/core/preview.d.ts.map +1 -1
  21. package/dist/core/preview.js +1 -2
  22. package/dist/core/templates.d.ts.map +1 -1
  23. package/dist/core/templates.js +4 -7
  24. package/dist/core/utils/index.d.ts +16 -0
  25. package/dist/core/utils/index.d.ts.map +1 -0
  26. package/dist/core/utils/index.js +22 -0
  27. package/dist/core/utils/partial-validation.d.ts.map +1 -1
  28. package/dist/core/utils/partial-validation.js +2 -1
  29. package/dist/index.d.ts +6 -8
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +3 -4
  32. package/dist/seo/auto-inject.d.ts +48 -0
  33. package/dist/seo/auto-inject.d.ts.map +1 -0
  34. package/dist/seo/auto-inject.js +108 -0
  35. package/dist/seo/generator.d.ts +77 -0
  36. package/dist/seo/generator.d.ts.map +1 -0
  37. package/dist/seo/generator.js +320 -0
  38. package/dist/seo/index.d.ts +12 -0
  39. package/dist/seo/index.d.ts.map +1 -0
  40. package/dist/seo/index.js +15 -0
  41. package/dist/seo/robots.d.ts +84 -0
  42. package/dist/seo/robots.d.ts.map +1 -0
  43. package/dist/seo/robots.js +165 -0
  44. package/dist/seo/sitemap.d.ts +37 -0
  45. package/dist/seo/sitemap.d.ts.map +1 -0
  46. package/dist/seo/sitemap.js +320 -0
  47. package/dist/seo/utils/escape-and-validation.d.ts +99 -0
  48. package/dist/seo/utils/escape-and-validation.d.ts.map +1 -0
  49. package/dist/seo/utils/escape-and-validation.js +319 -0
  50. package/dist/seo/utils/index.d.ts +7 -0
  51. package/dist/seo/utils/index.d.ts.map +1 -0
  52. package/dist/seo/utils/index.js +8 -0
  53. package/dist/seo/utils/url.d.ts +46 -0
  54. package/dist/seo/utils/url.d.ts.map +1 -0
  55. package/dist/seo/utils/url.js +66 -0
  56. package/dist/seo/utils.d.ts +94 -0
  57. package/dist/seo/utils.d.ts.map +1 -0
  58. package/dist/seo/utils.js +304 -0
  59. package/dist/types/config.d.ts +58 -0
  60. package/dist/types/config.d.ts.map +1 -1
  61. package/dist/types/content.d.ts +181 -0
  62. package/dist/types/content.d.ts.map +1 -1
  63. package/dist/types/index.d.ts +5 -2
  64. package/dist/types/index.d.ts.map +1 -1
  65. package/dist/types/index.js +1 -1
  66. package/dist/types/seo.d.ts +69 -0
  67. package/dist/types/seo.d.ts.map +1 -0
  68. package/dist/types/seo.js +36 -0
  69. package/dist/types/sitemap.d.ts +94 -0
  70. package/dist/types/sitemap.d.ts.map +1 -0
  71. package/dist/types/sitemap.js +4 -0
  72. package/package.json +1 -1
  73. package/dist/core/utils/partials.d.ts +0 -24
  74. package/dist/core/utils/partials.d.ts.map +0 -1
  75. package/dist/core/utils/partials.js +0 -85
  76. package/dist/tests/utils/test-mocks.d.ts +0 -69
  77. package/dist/tests/utils/test-mocks.d.ts.map +0 -1
  78. package/dist/tests/utils/test-mocks.js +0 -125
  79. package/dist/types.d.ts +0 -543
  80. package/dist/types.d.ts.map +0 -1
  81. package/dist/types.js +0 -1
@@ -0,0 +1,77 @@
1
+ /**
2
+ * SEO metadata generation module
3
+ * Generates meta tags, Open Graph tags, Twitter Cards, and structured data
4
+ */
5
+ import type { SEOContext } from '../types/seo.js';
6
+ import type { PageModel } from '../types/content.js';
7
+ import type { StatiConfig, SiteConfig } from '../types/config.js';
8
+ import { SEOTagType } from '../types/seo.js';
9
+ /**
10
+ * Generate complete SEO metadata for a page.
11
+ * Supports both whitelist (include) and blacklist (exclude) modes for selective tag generation.
12
+ *
13
+ * Note: Validation errors are logged as warnings rather than throwing to allow builds to
14
+ * continue with degraded SEO. This prevents a single SEO issue from blocking the entire build.
15
+ *
16
+ * @param ctx - SEO context containing page, config, siteUrl, and optional include/exclude sets
17
+ * @returns HTML string containing all generated SEO tags
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const seoTags = generateSEOMetadata({
22
+ * page,
23
+ * config,
24
+ * siteUrl: 'https://example.com',
25
+ * exclude: new Set([SEOTagType.Twitter]) // Skip Twitter tags
26
+ * });
27
+ * ```
28
+ */
29
+ export declare function generateSEOMetadata(ctx: SEOContext): string;
30
+ /**
31
+ * Generate Open Graph protocol meta tags.
32
+ * Implements fallback chains for all OG properties to ensure complete metadata.
33
+ *
34
+ * @param ctx - SEO context containing page, config, and siteUrl
35
+ * @returns Array of Open Graph meta tag strings
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const ogTags = generateOpenGraphTags(ctx);
40
+ * // Returns: ['<meta property="og:title" content="...">', ...]
41
+ * ```
42
+ */
43
+ export declare function generateOpenGraphTags(ctx: SEOContext): string[];
44
+ /**
45
+ * Generate Twitter Card meta tags.
46
+ * Implements fallback chains to ensure complete card metadata.
47
+ *
48
+ * @param ctx - SEO context containing page, config, and siteUrl
49
+ * @returns Array of Twitter Card meta tag strings
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const twitterTags = generateTwitterCardTags(ctx);
54
+ * // Returns: ['<meta name="twitter:card" content="summary_large_image">', ...]
55
+ * ```
56
+ */
57
+ export declare function generateTwitterCardTags(ctx: SEOContext): string[];
58
+ /**
59
+ * Template helper function for generating SEO tags in Eta templates.
60
+ * Provides a convenient API for selective SEO tag generation.
61
+ *
62
+ * @param context - Object containing page, config, and optional site
63
+ * @param tags - Optional array of tag types to generate (strings or SEOTagType enums)
64
+ * @returns HTML string containing generated SEO tags
65
+ *
66
+ * @example
67
+ * ```eta
68
+ * <%~ stati.generateSEO(stati) %>
69
+ * <%~ stati.generateSEO(stati, ['title', 'description', 'opengraph']) %>
70
+ * ```
71
+ */
72
+ export declare function generateSEO(context: {
73
+ page: PageModel;
74
+ config: StatiConfig;
75
+ site?: SiteConfig;
76
+ }, tags?: Array<SEOTagType | string>): string;
77
+ //# sourceMappingURL=generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/seo/generator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAElE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAI7C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CAsH3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,EAAE,CAyE/D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,EAAE,CAuDjE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE;IACP,IAAI,EAAE,SAAS,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB,EACD,IAAI,CAAC,EAAE,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC,GAChC,MAAM,CA6CR"}
@@ -0,0 +1,320 @@
1
+ /**
2
+ * SEO metadata generation module
3
+ * Generates meta tags, Open Graph tags, Twitter Cards, and structured data
4
+ */
5
+ import { SEOTagType } from '../types/seo.js';
6
+ import { escapeHtml, validateSEOMetadata, generateRobotsContent } from './utils/index.js';
7
+ import { sanitizeStructuredData } from './utils/escape-and-validation.js';
8
+ /**
9
+ * Generate complete SEO metadata for a page.
10
+ * Supports both whitelist (include) and blacklist (exclude) modes for selective tag generation.
11
+ *
12
+ * Note: Validation errors are logged as warnings rather than throwing to allow builds to
13
+ * continue with degraded SEO. This prevents a single SEO issue from blocking the entire build.
14
+ *
15
+ * @param ctx - SEO context containing page, config, siteUrl, and optional include/exclude sets
16
+ * @returns HTML string containing all generated SEO tags
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const seoTags = generateSEOMetadata({
21
+ * page,
22
+ * config,
23
+ * siteUrl: 'https://example.com',
24
+ * exclude: new Set([SEOTagType.Twitter]) // Skip Twitter tags
25
+ * });
26
+ * ```
27
+ */
28
+ export function generateSEOMetadata(ctx) {
29
+ const { page, config, siteUrl, exclude, include, logger } = ctx;
30
+ const seo = page.frontMatter.seo || {};
31
+ // Validate SEO metadata
32
+ const validation = validateSEOMetadata(seo, page.url);
33
+ // Log validation errors as warnings instead of throwing
34
+ // This allows builds to continue even with SEO issues
35
+ if (!validation.valid) {
36
+ logger.warning(`SEO validation failed for ${page.url}:`);
37
+ validation.errors.forEach((error) => {
38
+ logger.warning(` - ${error}`);
39
+ });
40
+ if (config.seo?.debug) {
41
+ logger.warning('Build will continue, but SEO metadata may be incomplete or invalid.');
42
+ }
43
+ }
44
+ // Log warnings if any and debug is enabled
45
+ if (validation.warnings.length > 0 && config.seo?.debug) {
46
+ logger.warning(`SEO warnings for ${page.url}:`);
47
+ validation.warnings.forEach((warning) => {
48
+ logger.warning(` - ${warning}`);
49
+ });
50
+ }
51
+ const meta = [];
52
+ /**
53
+ * Helper function to determine if a specific tag type should be generated.
54
+ *
55
+ * Three modes:
56
+ * 1. Whitelist mode (include set provided): Only generate tags in the include set
57
+ * 2. Blacklist mode (exclude set provided): Generate all tags except those in exclude set
58
+ * 3. Default mode (neither provided): Generate all tags
59
+ *
60
+ * @param tagType - The SEO tag type to check
61
+ * @returns True if the tag should be generated
62
+ */
63
+ const shouldGenerate = (tagType) => {
64
+ // Whitelist mode: only generate explicitly included tags
65
+ if (include) {
66
+ return include.has(tagType);
67
+ }
68
+ // Blacklist mode: generate unless explicitly excluded
69
+ if (exclude) {
70
+ return !exclude.has(tagType);
71
+ }
72
+ // Default: generate all tags
73
+ return true;
74
+ };
75
+ // Title tag
76
+ if (shouldGenerate(SEOTagType.Title)) {
77
+ const title = seo.title || page.frontMatter.title || config.site.title;
78
+ meta.push(`<title>${escapeHtml(title)}</title>`);
79
+ }
80
+ // Description meta tag
81
+ if (shouldGenerate(SEOTagType.Description)) {
82
+ const description = seo.description || page.frontMatter.description;
83
+ if (description) {
84
+ meta.push(`<meta name="description" content="${escapeHtml(description)}">`);
85
+ }
86
+ }
87
+ // Keywords meta tag
88
+ if (shouldGenerate(SEOTagType.Keywords)) {
89
+ const keywords = seo.keywords || page.frontMatter.tags;
90
+ if (keywords && keywords.length > 0) {
91
+ const keywordsArray = Array.isArray(keywords) ? keywords : [keywords];
92
+ meta.push(`<meta name="keywords" content="${keywordsArray.map(escapeHtml).join(', ')}">`);
93
+ }
94
+ }
95
+ // Canonical link
96
+ if (shouldGenerate(SEOTagType.Canonical)) {
97
+ const canonical = seo.canonical || `${siteUrl}${page.url}`;
98
+ meta.push(`<link rel="canonical" href="${escapeHtml(canonical)}">`);
99
+ }
100
+ // Robots meta tag
101
+ if (shouldGenerate(SEOTagType.Robots)) {
102
+ const robotsContent = generateRobotsContent(seo);
103
+ if (robotsContent) {
104
+ meta.push(`<meta name="robots" content="${robotsContent}">`);
105
+ }
106
+ }
107
+ // Author meta tag
108
+ if (shouldGenerate(SEOTagType.Author)) {
109
+ const author = seo.author || config.seo?.defaultAuthor;
110
+ if (author) {
111
+ const authorName = typeof author === 'string' ? author : author.name;
112
+ meta.push(`<meta name="author" content="${escapeHtml(authorName)}">`);
113
+ }
114
+ }
115
+ // Open Graph tags
116
+ if (shouldGenerate(SEOTagType.OpenGraph)) {
117
+ meta.push(...generateOpenGraphTags(ctx));
118
+ }
119
+ // Twitter Card tags
120
+ if (shouldGenerate(SEOTagType.Twitter)) {
121
+ meta.push(...generateTwitterCardTags(ctx));
122
+ }
123
+ // JSON-LD Structured Data
124
+ if (shouldGenerate(SEOTagType.StructuredData) && seo.structuredData) {
125
+ const sanitized = sanitizeStructuredData(seo.structuredData, logger);
126
+ meta.push(`<script type="application/ld+json">${JSON.stringify(sanitized)}</script>`);
127
+ }
128
+ return meta.join('\n ');
129
+ }
130
+ /**
131
+ * Generate Open Graph protocol meta tags.
132
+ * Implements fallback chains for all OG properties to ensure complete metadata.
133
+ *
134
+ * @param ctx - SEO context containing page, config, and siteUrl
135
+ * @returns Array of Open Graph meta tag strings
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * const ogTags = generateOpenGraphTags(ctx);
140
+ * // Returns: ['<meta property="og:title" content="...">', ...]
141
+ * ```
142
+ */
143
+ export function generateOpenGraphTags(ctx) {
144
+ const { page, config, siteUrl } = ctx;
145
+ const seo = page.frontMatter.seo || {};
146
+ const og = seo.openGraph || {};
147
+ const tags = [];
148
+ // Basic OG tags with fallback chain
149
+ const ogTitle = og.title || seo.title || page.frontMatter.title || config.site.title;
150
+ const ogDescription = og.description || seo.description || page.frontMatter.description;
151
+ const ogUrl = og.url || seo.canonical || `${siteUrl}${page.url}`;
152
+ const ogType = og.type || 'website';
153
+ const ogSiteName = og.siteName || config.site.title;
154
+ tags.push(`<meta property="og:title" content="${escapeHtml(ogTitle)}">`);
155
+ if (ogDescription) {
156
+ tags.push(`<meta property="og:description" content="${escapeHtml(ogDescription)}">`);
157
+ }
158
+ tags.push(`<meta property="og:url" content="${escapeHtml(ogUrl)}">`);
159
+ tags.push(`<meta property="og:type" content="${escapeHtml(ogType)}">`);
160
+ tags.push(`<meta property="og:site_name" content="${escapeHtml(ogSiteName)}">`);
161
+ // OG Image
162
+ if (og.image) {
163
+ const image = typeof og.image === 'string' ? { url: og.image } : og.image;
164
+ const imageUrl = image.url.startsWith('/') ? `${siteUrl}${image.url}` : image.url;
165
+ tags.push(`<meta property="og:image" content="${escapeHtml(imageUrl)}">`);
166
+ if (image.alt) {
167
+ tags.push(`<meta property="og:image:alt" content="${escapeHtml(image.alt)}">`);
168
+ }
169
+ if (image.width) {
170
+ tags.push(`<meta property="og:image:width" content="${image.width}">`);
171
+ }
172
+ if (image.height) {
173
+ tags.push(`<meta property="og:image:height" content="${image.height}">`);
174
+ }
175
+ }
176
+ // OG Locale
177
+ if (og.locale || config.site.defaultLocale) {
178
+ const locale = og.locale || config.site.defaultLocale;
179
+ if (locale) {
180
+ tags.push(`<meta property="og:locale" content="${escapeHtml(locale)}">`);
181
+ }
182
+ }
183
+ // OG Article metadata (only for article type)
184
+ if (og.article && ogType === 'article') {
185
+ const article = og.article;
186
+ if (article.publishedTime) {
187
+ tags.push(`<meta property="article:published_time" content="${escapeHtml(article.publishedTime)}">`);
188
+ }
189
+ if (article.modifiedTime) {
190
+ tags.push(`<meta property="article:modified_time" content="${escapeHtml(article.modifiedTime)}">`);
191
+ }
192
+ if (article.author) {
193
+ tags.push(`<meta property="article:author" content="${escapeHtml(article.author)}">`);
194
+ }
195
+ if (article.section) {
196
+ tags.push(`<meta property="article:section" content="${escapeHtml(article.section)}">`);
197
+ }
198
+ if (article.tags && article.tags.length > 0) {
199
+ article.tags.forEach((tag) => {
200
+ tags.push(`<meta property="article:tag" content="${escapeHtml(tag)}">`);
201
+ });
202
+ }
203
+ }
204
+ return tags;
205
+ }
206
+ /**
207
+ * Generate Twitter Card meta tags.
208
+ * Implements fallback chains to ensure complete card metadata.
209
+ *
210
+ * @param ctx - SEO context containing page, config, and siteUrl
211
+ * @returns Array of Twitter Card meta tag strings
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * const twitterTags = generateTwitterCardTags(ctx);
216
+ * // Returns: ['<meta name="twitter:card" content="summary_large_image">', ...]
217
+ * ```
218
+ */
219
+ export function generateTwitterCardTags(ctx) {
220
+ const { page, config, siteUrl } = ctx;
221
+ const seo = page.frontMatter.seo || {};
222
+ const twitter = seo.twitter || {};
223
+ const tags = [];
224
+ // Card type
225
+ const card = twitter.card || 'summary_large_image';
226
+ tags.push(`<meta name="twitter:card" content="${card}">`);
227
+ // Twitter site (optional)
228
+ if (twitter.site) {
229
+ tags.push(`<meta name="twitter:site" content="${escapeHtml(twitter.site)}">`);
230
+ }
231
+ // Twitter creator with fallback chain
232
+ if (twitter.creator) {
233
+ tags.push(`<meta name="twitter:creator" content="${escapeHtml(twitter.creator)}">`);
234
+ }
235
+ else if (seo.author) {
236
+ const authorName = typeof seo.author === 'string' ? seo.author : seo.author.name;
237
+ tags.push(`<meta name="twitter:creator" content="${escapeHtml(authorName)}">`);
238
+ }
239
+ else if (config.seo?.defaultAuthor) {
240
+ tags.push(`<meta name="twitter:creator" content="${escapeHtml(config.seo.defaultAuthor.name)}">`);
241
+ }
242
+ // Title and description with fallback chains
243
+ const twitterTitle = twitter.title || seo.title || page.frontMatter.title || config.site.title;
244
+ const twitterDescription = twitter.description || seo.description || page.frontMatter.description;
245
+ tags.push(`<meta name="twitter:title" content="${escapeHtml(twitterTitle)}">`);
246
+ if (twitterDescription) {
247
+ tags.push(`<meta name="twitter:description" content="${escapeHtml(twitterDescription)}">`);
248
+ }
249
+ // Image with fallback to Open Graph image
250
+ const imageUrl = twitter.image ||
251
+ (seo.openGraph?.image
252
+ ? typeof seo.openGraph.image === 'string'
253
+ ? seo.openGraph.image
254
+ : seo.openGraph.image.url
255
+ : undefined);
256
+ if (imageUrl) {
257
+ const fullImageUrl = imageUrl.startsWith('/') ? `${siteUrl}${imageUrl}` : imageUrl;
258
+ tags.push(`<meta name="twitter:image" content="${escapeHtml(fullImageUrl)}">`);
259
+ if (twitter.imageAlt) {
260
+ tags.push(`<meta name="twitter:image:alt" content="${escapeHtml(twitter.imageAlt)}">`);
261
+ }
262
+ }
263
+ return tags;
264
+ }
265
+ /**
266
+ * Template helper function for generating SEO tags in Eta templates.
267
+ * Provides a convenient API for selective SEO tag generation.
268
+ *
269
+ * @param context - Object containing page, config, and optional site
270
+ * @param tags - Optional array of tag types to generate (strings or SEOTagType enums)
271
+ * @returns HTML string containing generated SEO tags
272
+ *
273
+ * @example
274
+ * ```eta
275
+ * <%~ stati.generateSEO(stati) %>
276
+ * <%~ stati.generateSEO(stati, ['title', 'description', 'opengraph']) %>
277
+ * ```
278
+ */
279
+ export function generateSEO(context, tags) {
280
+ // Convert tag names to SEOTagType enum values
281
+ let include = undefined;
282
+ if (tags && tags.length > 0) {
283
+ include = new Set();
284
+ for (const tag of tags) {
285
+ if (typeof tag === 'string') {
286
+ // Convert string to enum by checking if the string matches any enum value
287
+ // The enum values are lowercase strings like 'title', 'description', etc.
288
+ const enumEntry = Object.entries(SEOTagType).find(([_, value]) => value === tag);
289
+ if (enumEntry) {
290
+ include.add(enumEntry[1]);
291
+ }
292
+ }
293
+ else {
294
+ include.add(tag);
295
+ }
296
+ }
297
+ }
298
+ // Extract site URL from config
299
+ const siteUrl = context.config.site?.baseUrl || context.site?.baseUrl || '';
300
+ // Generate SEO metadata with a no-op logger (warnings won't be shown in template context)
301
+ const noOpLogger = {
302
+ info: () => { },
303
+ success: () => { },
304
+ warning: () => { },
305
+ error: () => { },
306
+ building: () => { },
307
+ processing: () => { },
308
+ stats: () => { },
309
+ };
310
+ const seoContext = {
311
+ page: context.page,
312
+ config: context.config,
313
+ siteUrl,
314
+ logger: noOpLogger,
315
+ };
316
+ if (include) {
317
+ seoContext.include = include;
318
+ }
319
+ return generateSEOMetadata(seoContext);
320
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @fileoverview SEO functionality exports
3
+ * Barrel file for all Stati SEO functionality including metadata generation,
4
+ * sitemaps, robots.txt, and auto-injection.
5
+ */
6
+ export { generateSEOMetadata, generateSEO, generateOpenGraphTags, generateTwitterCardTags, } from './generator.js';
7
+ export { generateSitemap, generateSitemapEntry, generateSitemapXml, generateSitemapIndexXml, } from './sitemap.js';
8
+ export { generateRobotsTxt, generateRobotsTxtFromConfig } from './robots.js';
9
+ export { autoInjectSEO, shouldAutoInject } from './auto-inject.js';
10
+ export type { AutoInjectOptions } from './auto-inject.js';
11
+ export { escapeHtml, generateRobotsContent, validateSEOMetadata, detectExistingSEOTags, normalizeUrlPath, resolveAbsoluteUrl, isValidUrl, } from './utils/index.js';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/seo/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,qBAAqB,EACrB,uBAAuB,GACxB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,aAAa,CAAC;AAG7E,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACnE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG1D,OAAO,EACL,UAAU,EACV,qBAAqB,EACrB,mBAAmB,EACnB,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,UAAU,GACX,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @fileoverview SEO functionality exports
3
+ * Barrel file for all Stati SEO functionality including metadata generation,
4
+ * sitemaps, robots.txt, and auto-injection.
5
+ */
6
+ // SEO metadata generation
7
+ export { generateSEOMetadata, generateSEO, generateOpenGraphTags, generateTwitterCardTags, } from './generator.js';
8
+ // Sitemap generation
9
+ export { generateSitemap, generateSitemapEntry, generateSitemapXml, generateSitemapIndexXml, } from './sitemap.js';
10
+ // Robots.txt generation
11
+ export { generateRobotsTxt, generateRobotsTxtFromConfig } from './robots.js';
12
+ // Auto-injection
13
+ export { autoInjectSEO, shouldAutoInject } from './auto-inject.js';
14
+ // SEO utilities
15
+ export { escapeHtml, generateRobotsContent, validateSEOMetadata, detectExistingSEOTags, normalizeUrlPath, resolveAbsoluteUrl, isValidUrl, } from './utils/index.js';
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Robots.txt generation utilities for Stati
3
+ * @module seo/robots
4
+ */
5
+ import type { RobotsTxtConfig } from '../types/config.js';
6
+ /**
7
+ * User agent rule entry for robots.txt
8
+ */
9
+ export interface UserAgentRule {
10
+ /** User agent name (e.g., 'Googlebot', '*') */
11
+ userAgent: string;
12
+ /** Paths to allow */
13
+ allow?: string[];
14
+ /** Paths to disallow */
15
+ disallow?: string[];
16
+ /** Crawl delay in seconds */
17
+ crawlDelay?: number;
18
+ }
19
+ /**
20
+ * Options for generating robots.txt content
21
+ */
22
+ export interface RobotsTxtOptions {
23
+ /** User agent rules */
24
+ rules?: UserAgentRule[];
25
+ /** Sitemap URLs to include */
26
+ sitemaps?: string[];
27
+ /** Additional custom directives */
28
+ custom?: string[];
29
+ /** Site base URL for resolving sitemap paths */
30
+ siteUrl?: string;
31
+ }
32
+ /**
33
+ * Generates robots.txt content from options
34
+ * @param options - Robots.txt generation options
35
+ * @returns Generated robots.txt content
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const content = generateRobotsTxt({
40
+ * rules: [
41
+ * {
42
+ * userAgent: 'Googlebot',
43
+ * allow: ['/'],
44
+ * crawlDelay: 1
45
+ * },
46
+ * {
47
+ * userAgent: '*',
48
+ * disallow: ['/admin/', '/api/']
49
+ * }
50
+ * ],
51
+ * sitemaps: ['https://example.com/sitemap.xml'],
52
+ * custom: ['# Custom comment']
53
+ * });
54
+ * ```
55
+ */
56
+ export declare function generateRobotsTxt(options?: RobotsTxtOptions): string;
57
+ /**
58
+ * Converts Stati RobotsTxtConfig to RobotsTxtOptions
59
+ * @param config - Stati robots.txt configuration
60
+ * @param siteUrl - Site base URL
61
+ * @returns Robots.txt generation options
62
+ */
63
+ export declare function configToOptions(config: RobotsTxtConfig, siteUrl?: string): RobotsTxtOptions;
64
+ /**
65
+ * Generates robots.txt content from Stati configuration
66
+ * @param config - Stati robots.txt configuration
67
+ * @param siteUrl - Site base URL for resolving sitemap paths
68
+ * @returns Generated robots.txt content
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * const content = generateRobotsTxtFromConfig(
73
+ * {
74
+ * rules: [
75
+ * { userAgent: '*', allow: ['/'] }
76
+ * ],
77
+ * sitemaps: ['/sitemap.xml']
78
+ * },
79
+ * 'https://example.com'
80
+ * );
81
+ * ```
82
+ */
83
+ export declare function generateRobotsTxtFromConfig(config: RobotsTxtConfig, siteUrl?: string): string;
84
+ //# sourceMappingURL=robots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../../src/seo/robots.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAG1D;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,qBAAqB;IACrB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,wBAAwB;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,6BAA6B;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uBAAuB;IACvB,KAAK,CAAC,EAAE,aAAa,EAAE,CAAC;IACxB,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,mCAAmC;IACnC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAcD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,gBAAqB,GAAG,MAAM,CA2CxE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAqE3F;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAG7F"}
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Robots.txt generation utilities for Stati
3
+ * @module seo/robots
4
+ */
5
+ import { isValidUrl, resolveAbsoluteUrl, normalizeUrlPath } from './utils/index.js';
6
+ /**
7
+ * Default robots.txt configuration (allow all)
8
+ */
9
+ const DEFAULT_ROBOTS_CONFIG = {
10
+ rules: [
11
+ {
12
+ userAgent: '*',
13
+ allow: ['/'],
14
+ },
15
+ ],
16
+ };
17
+ /**
18
+ * Generates robots.txt content from options
19
+ * @param options - Robots.txt generation options
20
+ * @returns Generated robots.txt content
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const content = generateRobotsTxt({
25
+ * rules: [
26
+ * {
27
+ * userAgent: 'Googlebot',
28
+ * allow: ['/'],
29
+ * crawlDelay: 1
30
+ * },
31
+ * {
32
+ * userAgent: '*',
33
+ * disallow: ['/admin/', '/api/']
34
+ * }
35
+ * ],
36
+ * sitemaps: ['https://example.com/sitemap.xml'],
37
+ * custom: ['# Custom comment']
38
+ * });
39
+ * ```
40
+ */
41
+ export function generateRobotsTxt(options = {}) {
42
+ const lines = [];
43
+ const { rules = [], sitemaps = [], custom = [], siteUrl } = options;
44
+ // Use default rules if none provided
45
+ const effectiveRules = rules.length > 0 ? rules : (DEFAULT_ROBOTS_CONFIG.rules ?? []);
46
+ // Generate rules
47
+ effectiveRules.forEach((rule) => {
48
+ lines.push(`User-agent: ${rule.userAgent}`);
49
+ if (rule.allow) {
50
+ rule.allow.forEach((path) => lines.push(`Allow: ${normalizeUrlPath(path)}`));
51
+ }
52
+ if (rule.disallow) {
53
+ rule.disallow.forEach((path) => lines.push(`Disallow: ${normalizeUrlPath(path)}`));
54
+ }
55
+ if (typeof rule.crawlDelay === 'number' && rule.crawlDelay > 0) {
56
+ lines.push(`Crawl-delay: ${rule.crawlDelay}`);
57
+ }
58
+ lines.push(''); // Add a blank line after each rule
59
+ });
60
+ // Add sitemaps
61
+ if (sitemaps.length > 0) {
62
+ sitemaps.forEach((sitemap) => {
63
+ // Ensure sitemap URL is absolute
64
+ const sitemapUrl = siteUrl && !isValidUrl(sitemap) ? resolveAbsoluteUrl(sitemap, siteUrl) : sitemap;
65
+ lines.push(`Sitemap: ${sitemapUrl}`);
66
+ });
67
+ lines.push(''); // Add a blank line
68
+ }
69
+ // Add custom directives
70
+ if (custom.length > 0) {
71
+ lines.push(...custom, '');
72
+ }
73
+ return lines.join('\n');
74
+ }
75
+ /**
76
+ * Converts Stati RobotsTxtConfig to RobotsTxtOptions
77
+ * @param config - Stati robots.txt configuration
78
+ * @param siteUrl - Site base URL
79
+ * @returns Robots.txt generation options
80
+ */
81
+ export function configToOptions(config, siteUrl) {
82
+ const options = {};
83
+ // Add siteUrl if provided
84
+ if (siteUrl !== undefined) {
85
+ options.siteUrl = siteUrl;
86
+ }
87
+ const rules = [];
88
+ // Convert user agent specific rules
89
+ if (config.userAgents && config.userAgents.length > 0) {
90
+ for (const ua of config.userAgents) {
91
+ const rule = {
92
+ userAgent: ua.userAgent,
93
+ };
94
+ if (ua.allow !== undefined) {
95
+ rule.allow = ua.allow;
96
+ }
97
+ if (ua.disallow !== undefined) {
98
+ rule.disallow = ua.disallow;
99
+ }
100
+ rules.push(rule);
101
+ }
102
+ }
103
+ // Add global rules as wildcard user agent
104
+ if (config.allow || config.disallow || config.crawlDelay !== undefined) {
105
+ const rule = {
106
+ userAgent: '*',
107
+ };
108
+ if (config.allow !== undefined) {
109
+ rule.allow = config.allow;
110
+ }
111
+ if (config.disallow !== undefined) {
112
+ rule.disallow = config.disallow;
113
+ }
114
+ if (config.crawlDelay !== undefined) {
115
+ rule.crawlDelay = config.crawlDelay;
116
+ }
117
+ rules.push(rule);
118
+ }
119
+ if (rules.length > 0) {
120
+ options.rules = rules;
121
+ }
122
+ // Handle sitemap configuration
123
+ const sitemaps = [];
124
+ if (config.sitemap) {
125
+ if (typeof config.sitemap === 'string') {
126
+ // Explicit sitemap URL
127
+ sitemaps.push(config.sitemap);
128
+ }
129
+ else if (config.sitemap === true) {
130
+ // Auto-include sitemap.xml
131
+ sitemaps.push('/sitemap.xml');
132
+ }
133
+ }
134
+ if (sitemaps.length > 0) {
135
+ options.sitemaps = sitemaps;
136
+ }
137
+ // Add custom directives
138
+ if (config.customLines && config.customLines.length > 0) {
139
+ options.custom = config.customLines;
140
+ }
141
+ return options;
142
+ }
143
+ /**
144
+ * Generates robots.txt content from Stati configuration
145
+ * @param config - Stati robots.txt configuration
146
+ * @param siteUrl - Site base URL for resolving sitemap paths
147
+ * @returns Generated robots.txt content
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const content = generateRobotsTxtFromConfig(
152
+ * {
153
+ * rules: [
154
+ * { userAgent: '*', allow: ['/'] }
155
+ * ],
156
+ * sitemaps: ['/sitemap.xml']
157
+ * },
158
+ * 'https://example.com'
159
+ * );
160
+ * ```
161
+ */
162
+ export function generateRobotsTxtFromConfig(config, siteUrl) {
163
+ const options = configToOptions(config, siteUrl);
164
+ return generateRobotsTxt(options);
165
+ }