@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,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
|
+
}
|