@stati/core 1.6.3 → 1.7.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 (82) hide show
  1. package/README.md +2 -2
  2. package/dist/core/build.d.ts.map +1 -1
  3. package/dist/core/build.js +45 -8
  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.d.ts.map +1 -1
  12. package/dist/core/invalidate.js +13 -7
  13. package/dist/core/isg/build-lock.js +1 -1
  14. package/dist/core/isg/deps.d.ts.map +1 -1
  15. package/dist/core/isg/deps.js +1 -3
  16. package/dist/core/isg/hash.js +1 -1
  17. package/dist/core/isg/index.d.ts +16 -0
  18. package/dist/core/isg/index.d.ts.map +1 -0
  19. package/dist/core/isg/index.js +22 -0
  20. package/dist/core/isg/manifest.js +1 -1
  21. package/dist/core/preview.d.ts.map +1 -1
  22. package/dist/core/preview.js +1 -2
  23. package/dist/core/templates.d.ts.map +1 -1
  24. package/dist/core/templates.js +4 -7
  25. package/dist/core/utils/index.d.ts +16 -0
  26. package/dist/core/utils/index.d.ts.map +1 -0
  27. package/dist/core/utils/index.js +22 -0
  28. package/dist/core/utils/partial-validation.d.ts.map +1 -1
  29. package/dist/core/utils/partial-validation.js +2 -1
  30. package/dist/index.d.ts +6 -8
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +3 -4
  33. package/dist/seo/auto-inject.d.ts +48 -0
  34. package/dist/seo/auto-inject.d.ts.map +1 -0
  35. package/dist/seo/auto-inject.js +108 -0
  36. package/dist/seo/generator.d.ts +77 -0
  37. package/dist/seo/generator.d.ts.map +1 -0
  38. package/dist/seo/generator.js +320 -0
  39. package/dist/seo/index.d.ts +12 -0
  40. package/dist/seo/index.d.ts.map +1 -0
  41. package/dist/seo/index.js +15 -0
  42. package/dist/seo/robots.d.ts +84 -0
  43. package/dist/seo/robots.d.ts.map +1 -0
  44. package/dist/seo/robots.js +165 -0
  45. package/dist/seo/sitemap.d.ts +37 -0
  46. package/dist/seo/sitemap.d.ts.map +1 -0
  47. package/dist/seo/sitemap.js +320 -0
  48. package/dist/seo/utils/escape-and-validation.d.ts +99 -0
  49. package/dist/seo/utils/escape-and-validation.d.ts.map +1 -0
  50. package/dist/seo/utils/escape-and-validation.js +319 -0
  51. package/dist/seo/utils/index.d.ts +7 -0
  52. package/dist/seo/utils/index.d.ts.map +1 -0
  53. package/dist/seo/utils/index.js +8 -0
  54. package/dist/seo/utils/url.d.ts +46 -0
  55. package/dist/seo/utils/url.d.ts.map +1 -0
  56. package/dist/seo/utils/url.js +66 -0
  57. package/dist/seo/utils.d.ts +94 -0
  58. package/dist/seo/utils.d.ts.map +1 -0
  59. package/dist/seo/utils.js +304 -0
  60. package/dist/types/config.d.ts +58 -0
  61. package/dist/types/config.d.ts.map +1 -1
  62. package/dist/types/content.d.ts +181 -0
  63. package/dist/types/content.d.ts.map +1 -1
  64. package/dist/types/index.d.ts +5 -2
  65. package/dist/types/index.d.ts.map +1 -1
  66. package/dist/types/index.js +1 -1
  67. package/dist/types/seo.d.ts +69 -0
  68. package/dist/types/seo.d.ts.map +1 -0
  69. package/dist/types/seo.js +36 -0
  70. package/dist/types/sitemap.d.ts +94 -0
  71. package/dist/types/sitemap.d.ts.map +1 -0
  72. package/dist/types/sitemap.js +4 -0
  73. package/package.json +1 -1
  74. package/dist/core/utils/partials.d.ts +0 -24
  75. package/dist/core/utils/partials.d.ts.map +0 -1
  76. package/dist/core/utils/partials.js +0 -85
  77. package/dist/tests/utils/test-mocks.d.ts +0 -69
  78. package/dist/tests/utils/test-mocks.d.ts.map +0 -1
  79. package/dist/tests/utils/test-mocks.js +0 -125
  80. package/dist/types.d.ts +0 -543
  81. package/dist/types.d.ts.map +0 -1
  82. package/dist/types.js +0 -1
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Automatic SEO tag injection utilities
3
+ * @module seo/auto-inject
4
+ */
5
+ import { detectExistingSEOTags } from './utils/index.js';
6
+ import { generateSEOMetadata } from './generator.js';
7
+ /**
8
+ * Helper function to conditionally log debug messages for SEO auto-injection.
9
+ * Checks both the explicit debug flag and the config-level debug setting.
10
+ *
11
+ * @param message - Debug message to log
12
+ * @param options - Object containing debug flag, config, and logger
13
+ */
14
+ function logDebug(message, options) {
15
+ if (options.debug || options.config.seo?.debug) {
16
+ const logMessage = `[SEO Auto-Inject] ${message}`;
17
+ options.logger.warning(logMessage);
18
+ }
19
+ }
20
+ /**
21
+ * Finds the position to inject SEO tags (before </head>)
22
+ * @param html - HTML content
23
+ * @returns Position index or -1 if not found
24
+ */
25
+ function findHeadClosePosition(html) {
26
+ // Case-insensitive search for </head>
27
+ const match = html.match(/<\/head>/i);
28
+ return match ? (match.index ?? -1) : -1;
29
+ }
30
+ /**
31
+ * Automatically injects SEO metadata into HTML if not already present
32
+ * @param html - Rendered HTML content
33
+ * @param options - Auto-injection options
34
+ * @returns HTML with injected SEO tags
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const html = '<html><head><title>Page</title></head><body>Content</body></html>';
39
+ * const enhanced = autoInjectSEO(html, {
40
+ * page: pageModel,
41
+ * config: statiConfig,
42
+ * siteUrl: 'https://example.com'
43
+ * });
44
+ * // Returns HTML with additional SEO meta tags injected
45
+ * ```
46
+ */
47
+ export function autoInjectSEO(html, options) {
48
+ const { page, config, siteUrl, debug, logger } = options;
49
+ // Check if auto-injection is enabled (default: true)
50
+ const autoInjectEnabled = config.seo?.autoInject !== false;
51
+ if (!autoInjectEnabled) {
52
+ logDebug(`Skipped for ${page.url} (disabled in config)`, { debug, config, logger });
53
+ return html;
54
+ }
55
+ // Detect existing SEO tags in the HTML
56
+ const existingTags = detectExistingSEOTags(html);
57
+ logDebug(`Existing tags in ${page.url}: ${Array.from(existingTags).join(', ')}`, {
58
+ debug,
59
+ config,
60
+ logger,
61
+ });
62
+ // Build context with optional exclude parameter and logger
63
+ const context = {
64
+ page,
65
+ config,
66
+ siteUrl,
67
+ logger,
68
+ };
69
+ // Only add exclude if we have existing tags
70
+ if (existingTags.size > 0) {
71
+ context.exclude = existingTags;
72
+ }
73
+ // Generate SEO metadata excluding existing tags
74
+ const seoMetadata = generateSEOMetadata(context);
75
+ // If no SEO metadata was generated (all tags exist), return original HTML
76
+ if (!seoMetadata || seoMetadata.trim().length === 0) {
77
+ logDebug(`No tags to inject for ${page.url} (all exist)`, { debug, config, logger });
78
+ return html;
79
+ }
80
+ // Find position to inject (before </head>)
81
+ const headClosePos = findHeadClosePosition(html);
82
+ if (headClosePos === -1) {
83
+ logDebug(`No </head> tag found in ${page.url}, skipping injection`, { debug, config, logger });
84
+ return html;
85
+ }
86
+ // Inject SEO metadata before </head>
87
+ const before = html.substring(0, headClosePos);
88
+ const after = html.substring(headClosePos);
89
+ // Add proper indentation (2 spaces) and newline
90
+ const injected = `${before} ${seoMetadata}\n${after}`;
91
+ logDebug(`Injected ${existingTags.size === 0 ? 'all' : 'missing'} SEO tags into ${page.url}`, {
92
+ debug,
93
+ config,
94
+ logger,
95
+ });
96
+ return injected;
97
+ }
98
+ /**
99
+ * Checks if auto-injection is enabled for a page
100
+ * @param config - Site configuration
101
+ * @param _page - Page model (reserved for future page-level overrides)
102
+ * @returns true if auto-injection should run
103
+ */
104
+ export function shouldAutoInject(config, _page) {
105
+ // Check global config
106
+ const globalEnabled = config.seo?.autoInject !== false;
107
+ return globalEnabled;
108
+ }
@@ -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"}