vaza-content 0.2.1 → 0.2.2
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/dist/adapters/astro/index.cjs +5 -5
- package/dist/adapters/astro/index.js +1 -1
- package/dist/adapters/next/index.cjs +7 -7
- package/dist/adapters/next/index.js +1 -1
- package/dist/adapters/nuxt/index.cjs +5 -5
- package/dist/adapters/nuxt/index.js +1 -1
- package/dist/adapters/sveltekit/index.cjs +6 -6
- package/dist/adapters/sveltekit/index.js +1 -1
- package/dist/{chunk-FHOZ3ACN.cjs → chunk-H3D7F4TA.cjs} +10 -8
- package/dist/chunk-H3D7F4TA.cjs.map +1 -0
- package/dist/{chunk-VUZWHEZ2.js → chunk-OKXBDPYF.js} +13 -11
- package/dist/chunk-OKXBDPYF.js.map +1 -0
- package/dist/cli/index.cjs +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/index.cjs +8 -8
- package/dist/index.js +1 -1
- package/dist/process-VXDWM664.cjs +7 -0
- package/dist/{process-M4NZQGJJ.cjs.map → process-VXDWM664.cjs.map} +1 -1
- package/dist/process-ZQV5M2TB.js +7 -0
- package/package.json +1 -1
- package/dist/chunk-FHOZ3ACN.cjs.map +0 -1
- package/dist/chunk-VUZWHEZ2.js.map +0 -1
- package/dist/process-IELSAOA3.js +0 -7
- package/dist/process-M4NZQGJJ.cjs +0 -7
- /package/dist/{process-IELSAOA3.js.map → process-ZQV5M2TB.js.map} +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
var
|
|
6
|
+
var _chunkH3D7F4TAcjs = require('../../chunk-H3D7F4TA.cjs');
|
|
7
7
|
|
|
8
8
|
// src/adapters/astro/index.ts
|
|
9
9
|
var _fs = require('fs');
|
|
@@ -22,17 +22,17 @@ async function buildVazaContent(config) {
|
|
|
22
22
|
return processAndWrite(config);
|
|
23
23
|
}
|
|
24
24
|
async function processAndWrite(config) {
|
|
25
|
-
const output = await
|
|
25
|
+
const output = await _chunkH3D7F4TAcjs.processCollections.call(void 0, config);
|
|
26
26
|
const outDir = ".vaza-content";
|
|
27
27
|
ensureDir(outDir);
|
|
28
28
|
ensureDir("public");
|
|
29
29
|
if (_optionalChain([config, 'access', _ => _.sitemap, 'optionalAccess', _2 => _2.enabled]) !== false) {
|
|
30
30
|
writeJson(_path.join.call(void 0, outDir, "sitemap-data.json"), output.sitemap);
|
|
31
|
-
const xml =
|
|
31
|
+
const xml = _chunkH3D7F4TAcjs.renderSitemapXml.call(void 0, output.sitemap);
|
|
32
32
|
_fs.writeFileSync.call(void 0, "public/sitemap.xml", xml, "utf-8");
|
|
33
33
|
}
|
|
34
34
|
if (_optionalChain([config, 'access', _3 => _3.rss, 'optionalAccess', _4 => _4.enabled]) !== false) {
|
|
35
|
-
const xml =
|
|
35
|
+
const xml = _chunkH3D7F4TAcjs.renderRssXml.call(void 0, output.rss, config);
|
|
36
36
|
const rssPath = _nullishCoalesce(_optionalChain([config, 'access', _5 => _5.rss, 'optionalAccess', _6 => _6.path]), () => ( "/rss.xml"));
|
|
37
37
|
_fs.writeFileSync.call(void 0, _path.join.call(void 0, "public", rssPath.replace(/^\//, "")), xml, "utf-8");
|
|
38
38
|
}
|
|
@@ -52,7 +52,7 @@ async function processAndWrite(config) {
|
|
|
52
52
|
writeJson(_path.join.call(void 0, outDir, "integrity-report.json"), output.integrity);
|
|
53
53
|
const slugManifest = output.entries.map((e) => e.slug);
|
|
54
54
|
writeJson(_path.join.call(void 0, outDir, "slugs.json"), slugManifest);
|
|
55
|
-
|
|
55
|
+
_chunkH3D7F4TAcjs.logger.info(`Generated ${output.entries.length} entries`);
|
|
56
56
|
return output;
|
|
57
57
|
}
|
|
58
58
|
function ensureDir(dir) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
var
|
|
6
|
+
var _chunkH3D7F4TAcjs = require('../../chunk-H3D7F4TA.cjs');
|
|
7
7
|
|
|
8
8
|
// src/adapters/next/index.ts
|
|
9
9
|
var _fs = require('fs');
|
|
@@ -17,7 +17,7 @@ function withVazaContent(nextConfig, vazaConfig) {
|
|
|
17
17
|
if (options.isServer && vazaConfig && !processingPromise) {
|
|
18
18
|
processingPromise = processAndWrite(vazaConfig);
|
|
19
19
|
processingPromise.catch((err) => {
|
|
20
|
-
|
|
20
|
+
_chunkH3D7F4TAcjs.logger.error("Build error:", err);
|
|
21
21
|
process.exit(1);
|
|
22
22
|
});
|
|
23
23
|
}
|
|
@@ -32,17 +32,17 @@ async function buildVazaContent(config) {
|
|
|
32
32
|
return processAndWrite(config);
|
|
33
33
|
}
|
|
34
34
|
async function processAndWrite(config) {
|
|
35
|
-
const output = await
|
|
35
|
+
const output = await _chunkH3D7F4TAcjs.processCollections.call(void 0, config);
|
|
36
36
|
const outDir = ".vaza-content";
|
|
37
37
|
ensureDir(outDir);
|
|
38
38
|
if (_optionalChain([config, 'access', _ => _.sitemap, 'optionalAccess', _2 => _2.enabled]) !== false) {
|
|
39
39
|
writeJson(_path.join.call(void 0, outDir, "sitemap-data.json"), output.sitemap);
|
|
40
|
-
const xml =
|
|
40
|
+
const xml = _chunkH3D7F4TAcjs.renderSitemapXml.call(void 0, output.sitemap);
|
|
41
41
|
ensureDir("public");
|
|
42
42
|
_fs.writeFileSync.call(void 0, "public/sitemap.xml", xml, "utf-8");
|
|
43
43
|
}
|
|
44
44
|
if (_optionalChain([config, 'access', _3 => _3.rss, 'optionalAccess', _4 => _4.enabled]) !== false) {
|
|
45
|
-
const xml =
|
|
45
|
+
const xml = _chunkH3D7F4TAcjs.renderRssXml.call(void 0, output.rss, config);
|
|
46
46
|
const rssPath = _nullishCoalesce(_optionalChain([config, 'access', _5 => _5.rss, 'optionalAccess', _6 => _6.path]), () => ( "/rss.xml"));
|
|
47
47
|
const rssFile = _path.join.call(void 0, "public", rssPath.replace(/^\//, ""));
|
|
48
48
|
ensureDir(_path.dirname.call(void 0, rssFile));
|
|
@@ -64,8 +64,8 @@ async function processAndWrite(config) {
|
|
|
64
64
|
writeJson(_path.join.call(void 0, outDir, "integrity-report.json"), output.integrity);
|
|
65
65
|
const slugManifest = output.entries.map((e) => e.slug);
|
|
66
66
|
writeJson(_path.join.call(void 0, outDir, "slugs.json"), slugManifest);
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
_chunkH3D7F4TAcjs.logger.info(`Generated ${output.entries.length} entries`);
|
|
68
|
+
_chunkH3D7F4TAcjs.logger.info(`Outputs written to ${outDir}/ and public/`);
|
|
69
69
|
return output;
|
|
70
70
|
}
|
|
71
71
|
function ensureDir(dir) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
var
|
|
6
|
+
var _chunkH3D7F4TAcjs = require('../../chunk-H3D7F4TA.cjs');
|
|
7
7
|
|
|
8
8
|
// src/adapters/nuxt/index.ts
|
|
9
9
|
var _fs = require('fs');
|
|
@@ -23,17 +23,17 @@ async function buildVazaContent(config) {
|
|
|
23
23
|
return processAndWrite(config);
|
|
24
24
|
}
|
|
25
25
|
async function processAndWrite(config) {
|
|
26
|
-
const output = await
|
|
26
|
+
const output = await _chunkH3D7F4TAcjs.processCollections.call(void 0, config);
|
|
27
27
|
const outDir = ".vaza-content";
|
|
28
28
|
ensureDir(outDir);
|
|
29
29
|
ensureDir("public");
|
|
30
30
|
if (_optionalChain([config, 'access', _2 => _2.sitemap, 'optionalAccess', _3 => _3.enabled]) !== false) {
|
|
31
31
|
writeJson(_path.join.call(void 0, outDir, "sitemap-data.json"), output.sitemap);
|
|
32
|
-
const xml =
|
|
32
|
+
const xml = _chunkH3D7F4TAcjs.renderSitemapXml.call(void 0, output.sitemap);
|
|
33
33
|
_fs.writeFileSync.call(void 0, "public/sitemap.xml", xml, "utf-8");
|
|
34
34
|
}
|
|
35
35
|
if (_optionalChain([config, 'access', _4 => _4.rss, 'optionalAccess', _5 => _5.enabled]) !== false) {
|
|
36
|
-
const xml =
|
|
36
|
+
const xml = _chunkH3D7F4TAcjs.renderRssXml.call(void 0, output.rss, config);
|
|
37
37
|
const rssPath = _nullishCoalesce(_optionalChain([config, 'access', _6 => _6.rss, 'optionalAccess', _7 => _7.path]), () => ( "/rss.xml"));
|
|
38
38
|
_fs.writeFileSync.call(void 0, _path.join.call(void 0, "public", rssPath.replace(/^\//, "")), xml, "utf-8");
|
|
39
39
|
}
|
|
@@ -53,7 +53,7 @@ async function processAndWrite(config) {
|
|
|
53
53
|
writeJson(_path.join.call(void 0, outDir, "integrity-report.json"), output.integrity);
|
|
54
54
|
const slugManifest = output.entries.map((e) => e.slug);
|
|
55
55
|
writeJson(_path.join.call(void 0, outDir, "slugs.json"), slugManifest);
|
|
56
|
-
|
|
56
|
+
_chunkH3D7F4TAcjs.logger.info(`Generated ${output.entries.length} entries`);
|
|
57
57
|
return output;
|
|
58
58
|
}
|
|
59
59
|
function ensureDir(dir) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
var
|
|
6
|
+
var _chunkH3D7F4TAcjs = require('../../chunk-H3D7F4TA.cjs');
|
|
7
7
|
|
|
8
8
|
// src/adapters/sveltekit/index.ts
|
|
9
9
|
var _fs = require('fs');
|
|
@@ -23,18 +23,18 @@ async function buildVazaContent(config) {
|
|
|
23
23
|
return processAndWrite(config);
|
|
24
24
|
}
|
|
25
25
|
async function processAndWrite(config) {
|
|
26
|
-
const output = await
|
|
26
|
+
const output = await _chunkH3D7F4TAcjs.processCollections.call(void 0, config);
|
|
27
27
|
const outDir = ".vaza-content";
|
|
28
28
|
const staticDir = "static";
|
|
29
29
|
ensureDir(outDir);
|
|
30
30
|
ensureDir(staticDir);
|
|
31
31
|
if (_optionalChain([config, 'access', _ => _.sitemap, 'optionalAccess', _2 => _2.enabled]) !== false) {
|
|
32
32
|
writeJson(_path.join.call(void 0, outDir, "sitemap-data.json"), output.sitemap);
|
|
33
|
-
const xml =
|
|
33
|
+
const xml = _chunkH3D7F4TAcjs.renderSitemapXml.call(void 0, output.sitemap);
|
|
34
34
|
_fs.writeFileSync.call(void 0, _path.join.call(void 0, staticDir, "sitemap.xml"), xml, "utf-8");
|
|
35
35
|
}
|
|
36
36
|
if (_optionalChain([config, 'access', _3 => _3.rss, 'optionalAccess', _4 => _4.enabled]) !== false) {
|
|
37
|
-
const xml =
|
|
37
|
+
const xml = _chunkH3D7F4TAcjs.renderRssXml.call(void 0, output.rss, config);
|
|
38
38
|
const rssPath = _nullishCoalesce(_optionalChain([config, 'access', _5 => _5.rss, 'optionalAccess', _6 => _6.path]), () => ( "/rss.xml"));
|
|
39
39
|
_fs.writeFileSync.call(void 0, _path.join.call(void 0, staticDir, rssPath.replace(/^\//, "")), xml, "utf-8");
|
|
40
40
|
}
|
|
@@ -54,8 +54,8 @@ async function processAndWrite(config) {
|
|
|
54
54
|
writeJson(_path.join.call(void 0, outDir, "integrity-report.json"), output.integrity);
|
|
55
55
|
const slugManifest = output.entries.map((e) => e.slug);
|
|
56
56
|
writeJson(_path.join.call(void 0, outDir, "slugs.json"), slugManifest);
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
_chunkH3D7F4TAcjs.logger.info(`Generated ${output.entries.length} entries`);
|
|
58
|
+
_chunkH3D7F4TAcjs.logger.info(`Outputs written to ${outDir}/ and ${staticDir}/`);
|
|
59
59
|
return output;
|
|
60
60
|
}
|
|
61
61
|
function ensureDir(dir) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/generate/json-ld/article.ts
|
|
2
2
|
function generateArticleSchema(entry, site) {
|
|
3
|
-
if (!entry.title || !entry.publishDate
|
|
3
|
+
if (!entry.title || !entry.publishDate) {
|
|
4
4
|
return null;
|
|
5
5
|
}
|
|
6
6
|
const schema = {
|
|
@@ -9,17 +9,19 @@ function generateArticleSchema(entry, site) {
|
|
|
9
9
|
headline: entry.title,
|
|
10
10
|
description: entry.description || entry.excerpt,
|
|
11
11
|
datePublished: entry.publishDate.toISOString(),
|
|
12
|
-
author: {
|
|
13
|
-
"@type": "Person",
|
|
14
|
-
name: entry.author.name,
|
|
15
|
-
...entry.author.url && { url: entry.author.url }
|
|
16
|
-
},
|
|
17
12
|
publisher: {
|
|
18
13
|
"@type": "Organization",
|
|
19
14
|
name: site.name,
|
|
20
15
|
...site.url && { url: site.url }
|
|
21
16
|
}
|
|
22
17
|
};
|
|
18
|
+
if (entry.author) {
|
|
19
|
+
schema.author = {
|
|
20
|
+
"@type": "Person",
|
|
21
|
+
name: entry.author.name,
|
|
22
|
+
...entry.author.url && { url: entry.author.url }
|
|
23
|
+
};
|
|
24
|
+
}
|
|
23
25
|
if (entry.updateDate) {
|
|
24
26
|
schema.dateModified = entry.updateDate.toISOString();
|
|
25
27
|
}
|
|
@@ -379,7 +381,7 @@ async function generateOgImages(entries, config) {
|
|
|
379
381
|
});
|
|
380
382
|
const pngData = resvg.render();
|
|
381
383
|
const pngBuffer = pngData.asPng();
|
|
382
|
-
const parentDir = _path.
|
|
384
|
+
const parentDir = _path.dirname.call(void 0, outputPath);
|
|
383
385
|
if (!_fs.existsSync.call(void 0, parentDir)) {
|
|
384
386
|
_fs.mkdirSync.call(void 0, parentDir, { recursive: true });
|
|
385
387
|
}
|
|
@@ -1261,4 +1263,4 @@ function printIntegrityReport(report) {
|
|
|
1261
1263
|
|
|
1262
1264
|
|
|
1263
1265
|
exports.DEFAULT_META_TITLE_MAX = DEFAULT_META_TITLE_MAX; exports.DEFAULT_META_DESC_MAX = DEFAULT_META_DESC_MAX; exports.DEFAULT_FRESHNESS_MAX_AGE = DEFAULT_FRESHNESS_MAX_AGE; exports.DEFAULT_RSS_LIMIT = DEFAULT_RSS_LIMIT; exports.DEFAULT_SITEMAP_PRIORITY = DEFAULT_SITEMAP_PRIORITY; exports.DEFAULT_SITEMAP_CHANGE_FREQ = DEFAULT_SITEMAP_CHANGE_FREQ; exports.generateBreadcrumbs = generateBreadcrumbs; exports.generateJsonLd = generateJsonLd; exports.setLogLevel = setLogLevel; exports.logger = logger; exports.generateOgImages = generateOgImages; exports.detectRedirects = detectRedirects; exports.generateRss = generateRss; exports.renderRssXml = renderRssXml; exports.generateSitemap = generateSitemap; exports.renderSitemapXml = renderSitemapXml; exports.generateTaxonomy = generateTaxonomy; exports.checkIntegrity = checkIntegrity; exports.normalize = normalize2; exports.processCollections = processCollections;
|
|
1264
|
-
//# sourceMappingURL=chunk-
|
|
1266
|
+
//# sourceMappingURL=chunk-H3D7F4TA.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/mehdi/vaza-content/dist/chunk-H3D7F4TA.cjs","../src/generate/json-ld/article.ts","../src/generate/breadcrumbs.ts","../src/generate/json-ld/breadcrumb.ts","../src/generate/json-ld/event.ts","../src/generate/json-ld/faq.ts","../src/generate/json-ld/how-to.ts","../src/generate/json-ld/product.ts","../src/generate/json-ld/recipe.ts","../src/generate/json-ld/index.ts","../src/generate/og-images.ts","../src/constants.ts","../src/logger.ts","../src/utils/concurrency.ts","../src/generate/redirects.ts","../src/utils/levenshtein.ts","../src/generate/rss.ts","../src/generate/sitemap.ts","../src/generate/taxonomy.ts","../src/integrity/alt-text.ts","../src/integrity/broken-links.ts","../src/integrity/duplicate-content.ts","../src/integrity/duplicate-slugs.ts","../src/integrity/freshness.ts","../src/integrity/meta-length.ts","../src/integrity/orphan-pages.ts","../src/integrity/index.ts","../src/normalize/blur-placeholder.ts","../src/normalize/excerpt.ts","../src/normalize/description.ts","../src/normalize/image-dimensions.ts","../src/normalize/word-count.ts","../src/normalize/reading-time.ts","../src/normalize/slug.ts","../src/normalize/toc.ts","../src/normalize/index.ts","../src/process.ts"],"names":[],"mappings":"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","file":"/Users/mehdi/vaza-content/dist/chunk-H3D7F4TA.cjs","sourcesContent":[null,"import type { SiteConfig, VazaEntry } from \"../../types.js\";\n\n/**\n * Generate Article/BlogPosting schema.\n * Triggered when entry has title + publishDate.\n */\nexport function generateArticleSchema(\n entry: VazaEntry,\n site: SiteConfig,\n): Record<string, unknown> | null {\n if (!entry.title || !entry.publishDate) {\n return null;\n }\n\n const schema: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": entry.jsonLdType === \"Article\" ? \"Article\" : \"BlogPosting\",\n headline: entry.title,\n description: entry.description || entry.excerpt,\n datePublished: entry.publishDate.toISOString(),\n publisher: {\n \"@type\": \"Organization\",\n name: site.name,\n ...(site.url && { url: site.url }),\n },\n };\n\n if (entry.author) {\n schema.author = {\n \"@type\": \"Person\",\n name: entry.author.name,\n ...(entry.author.url && { url: entry.author.url }),\n };\n }\n\n if (entry.updateDate) {\n schema.dateModified = entry.updateDate.toISOString();\n }\n\n if (entry.image) {\n schema.image = {\n \"@type\": \"ImageObject\",\n url: entry.image.src.startsWith(\"http\")\n ? entry.image.src\n : `${site.url.replace(/\\/$/, \"\")}${entry.image.src}`,\n ...(entry.image.width && { width: entry.image.width }),\n ...(entry.image.height && { height: entry.image.height }),\n };\n }\n\n if (entry.wordCount) {\n schema.wordCount = entry.wordCount;\n }\n\n return schema;\n}\n","export interface BreadcrumbItem {\n name: string;\n url: string;\n}\n\n/**\n * Generate breadcrumb items from a slug.\n * Splits by \"/\" and creates array of {name, url} pairs.\n * Includes Home as first item.\n */\nexport function generateBreadcrumbs(\n slug: string,\n title: string,\n siteUrl: string,\n basePath: string,\n): BreadcrumbItem[] {\n const url = siteUrl.replace(/\\/$/, \"\");\n const path = basePath.replace(/\\/$/, \"\");\n\n const items: BreadcrumbItem[] = [{ name: \"Home\", url: `${url}/` }];\n\n // Add base path segments if they exist\n if (path) {\n const pathSegments = path.split(\"/\").filter(Boolean);\n let accumulated = url;\n for (const segment of pathSegments) {\n accumulated += `/${segment}`;\n items.push({\n name: segment.charAt(0).toUpperCase() + segment.slice(1),\n url: `${accumulated}/`,\n });\n }\n }\n\n // Split slug by \"/\" for nested slugs\n const slugSegments = slug.split(\"/\").filter(Boolean);\n\n if (slugSegments.length > 1) {\n let accumulated = `${url}${path}`;\n // Add intermediate slug segments (not the last one)\n for (let i = 0; i < slugSegments.length - 1; i++) {\n accumulated += `/${slugSegments[i]}`;\n const segmentName = slugSegments[i]\n .replace(/-/g, \" \")\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n items.push({\n name: segmentName,\n url: `${accumulated}/`,\n });\n }\n }\n\n // Add the current page (final item uses the provided title)\n items.push({\n name: title,\n url: `${url}${path}/${slug}`,\n });\n\n return items;\n}\n","import type { SiteConfig, VazaEntry } from \"../../types.js\";\nimport { generateBreadcrumbs } from \"../breadcrumbs.js\";\n\n/**\n * Generate BreadcrumbList schema.\n * Always generated for every entry.\n */\nexport function generateBreadcrumbSchema(\n entry: VazaEntry,\n site: SiteConfig,\n basePath: string,\n): Record<string, unknown> {\n const siteUrl = site.url.replace(/\\/$/, \"\");\n const crumbs = generateBreadcrumbs(\n entry.slug,\n entry.title,\n siteUrl,\n basePath,\n );\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"BreadcrumbList\",\n itemListElement: crumbs.map((crumb, index) => ({\n \"@type\": \"ListItem\",\n position: index + 1,\n name: crumb.name,\n item: crumb.url,\n })),\n };\n}\n","import type { SiteConfig, VazaEntry } from \"../../types.js\";\n\n/**\n * Generate Event schema.\n * Triggered when entry has eventDate.\n */\nexport function generateEventSchema(\n entry: VazaEntry,\n site: SiteConfig,\n): Record<string, unknown> | null {\n if (!entry.eventDate) {\n return null;\n }\n\n const schema: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Event\",\n name: entry.title,\n description: entry.description || entry.excerpt,\n startDate: entry.eventDate.toISOString(),\n organizer: {\n \"@type\": \"Organization\",\n name: site.name,\n url: site.url,\n },\n };\n\n if (entry.eventLocation) {\n schema.location = {\n \"@type\": \"Place\",\n name: entry.eventLocation,\n };\n }\n\n if (entry.image) {\n schema.image = entry.image.src.startsWith(\"http\")\n ? entry.image.src\n : `${site.url.replace(/\\/$/, \"\")}${entry.image.src}`;\n }\n\n return schema;\n}\n","import type { SiteConfig, VazaEntry } from \"../../types.js\";\n\n/**\n * Generate FAQPage schema.\n * Triggered when entry has faqs array with items.\n */\nexport function generateFaqSchema(\n entry: VazaEntry,\n _site: SiteConfig,\n): Record<string, unknown> | null {\n if (!entry.faqs || entry.faqs.length === 0) {\n return null;\n }\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"FAQPage\",\n mainEntity: entry.faqs.map((faq) => ({\n \"@type\": \"Question\",\n name: faq.question,\n acceptedAnswer: {\n \"@type\": \"Answer\",\n text: faq.answer,\n },\n })),\n };\n}\n","import type { SiteConfig, VazaEntry } from \"../../types.js\";\n\n/**\n * Generate HowTo schema.\n * Triggered when entry has steps array.\n */\nexport function generateHowToSchema(\n entry: VazaEntry,\n _site: SiteConfig,\n): Record<string, unknown> | null {\n if (!entry.steps || entry.steps.length === 0) {\n return null;\n }\n\n return {\n \"@context\": \"https://schema.org\",\n \"@type\": \"HowTo\",\n name: entry.title,\n description: entry.description || entry.excerpt,\n step: entry.steps.map((step, index) => ({\n \"@type\": \"HowToStep\",\n position: index + 1,\n name: step.name,\n text: step.text,\n })),\n };\n}\n","import type { SiteConfig, VazaEntry } from \"../../types.js\";\n\n/**\n * Generate Product schema.\n * Triggered when entry has price.\n */\nexport function generateProductSchema(\n entry: VazaEntry,\n site: SiteConfig,\n): Record<string, unknown> | null {\n if (!entry.price) {\n return null;\n }\n\n const schema: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Product\",\n name: entry.title,\n description: entry.description || entry.excerpt,\n offers: {\n \"@type\": \"Offer\",\n price: entry.price.amount,\n priceCurrency: entry.price.currency,\n availability: \"https://schema.org/InStock\",\n },\n };\n\n if (entry.image) {\n schema.image = entry.image.src.startsWith(\"http\")\n ? entry.image.src\n : `${site.url.replace(/\\/$/, \"\")}${entry.image.src}`;\n }\n\n return schema;\n}\n","import type { SiteConfig, VazaEntry } from \"../../types.js\";\n\n/**\n * Generate Recipe schema.\n * Triggered when entry has ingredients.\n */\nexport function generateRecipeSchema(\n entry: VazaEntry,\n site: SiteConfig,\n): Record<string, unknown> | null {\n if (!entry.ingredients || entry.ingredients.length === 0) {\n return null;\n }\n\n const schema: Record<string, unknown> = {\n \"@context\": \"https://schema.org\",\n \"@type\": \"Recipe\",\n name: entry.title,\n description: entry.description || entry.excerpt,\n recipeIngredient: entry.ingredients,\n };\n\n if (entry.cookTime) {\n schema.cookTime = entry.cookTime;\n }\n\n if (entry.author) {\n schema.author = {\n \"@type\": \"Person\",\n name: entry.author.name,\n };\n }\n\n if (entry.image) {\n schema.image = entry.image.src.startsWith(\"http\")\n ? entry.image.src\n : `${site.url.replace(/\\/$/, \"\")}${entry.image.src}`;\n }\n\n return schema;\n}\n","import type { SiteConfig, VazaConfig, VazaEntry } from \"../../types.js\";\nimport { generateArticleSchema } from \"./article.js\";\nimport { generateBreadcrumbSchema } from \"./breadcrumb.js\";\nimport { generateEventSchema } from \"./event.js\";\nimport { generateFaqSchema } from \"./faq.js\";\nimport { generateHowToSchema } from \"./how-to.js\";\nimport { generateProductSchema } from \"./product.js\";\nimport { generateRecipeSchema } from \"./recipe.js\";\n\ntype SchemaGenerator = (\n entry: VazaEntry,\n site: SiteConfig,\n basePath: string,\n) => Record<string, unknown> | null;\n\n/**\n * Built-in schema generators. Each is tried for every entry;\n * if it returns non-null the schema is included.\n */\nconst builtinGenerators: SchemaGenerator[] = [\n (entry, site) => generateArticleSchema(entry, site),\n (entry, site) => generateFaqSchema(entry, site),\n (entry, site, basePath) => generateBreadcrumbSchema(entry, site, basePath),\n (entry, site) => generateHowToSchema(entry, site),\n (entry, site) => generateProductSchema(entry, site),\n (entry, site) => generateRecipeSchema(entry, site),\n (entry, site) => generateEventSchema(entry, site),\n];\n\n/**\n * Auto-detect which schemas apply to each entry and generate them.\n * Returns slug -> array of schema objects.\n */\nexport function generateJsonLd(\n entries: VazaEntry[],\n config: VazaConfig,\n): Record<string, Record<string, unknown>[]> {\n const result: Record<string, Record<string, unknown>[]> = {};\n const site = config.site;\n\n // Fallback basePath if entries aren't tagged with _basePath\n const collectionKeys = Object.keys(config.collections);\n const fallbackBasePath =\n collectionKeys.length > 0\n ? config.collections[collectionKeys[0]].basePath\n : \"\";\n\n // Collect custom schema generators from config\n const customGenerators: SchemaGenerator[] = [];\n if (config.jsonLd?.customSchemas) {\n for (const [, generator] of Object.entries(config.jsonLd.customSchemas)) {\n customGenerators.push((entry, s) => generator(entry, s));\n }\n }\n\n const allGenerators = [...builtinGenerators, ...customGenerators];\n\n for (const entry of entries) {\n const schemas: Record<string, unknown>[] = [];\n const basePath = entry._basePath ?? fallbackBasePath;\n\n for (const generator of allGenerators) {\n const schema = generator(entry, site, basePath);\n if (schema) {\n schemas.push(schema);\n }\n }\n\n if (schemas.length > 0) {\n result[entry.slug] = schemas;\n }\n }\n\n return result;\n}\n","import {\n existsSync,\n mkdirSync,\n readFileSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport {\n OG_IMAGE_CONCURRENCY,\n OG_IMAGE_HEIGHT,\n OG_IMAGE_WIDTH,\n} from \"../constants.js\";\nimport { logger } from \"../logger.js\";\nimport type { OgImagesConfig, VazaConfig, VazaEntry } from \"../types.js\";\nimport { pMap } from \"../utils/concurrency.js\";\n\nconst FONT_CACHE_DIR = \".vaza-content/fonts\";\nconst INTER_FONT_URL =\n \"https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfAZ9hiA.woff2\";\n\n/**\n * Generate OG images using satori + @resvg/resvg-js.\n * Returns slug -> output path map.\n * Skips if file already exists and entry hasn't changed.\n */\nexport async function generateOgImages(\n entries: VazaEntry[],\n config: VazaConfig,\n): Promise<Record<string, string>> {\n const ogConfig = config.ogImages;\n if (!ogConfig?.enabled) return {};\n\n const outputDir = ogConfig.outputDir ?? join(process.cwd(), \"public\", \"og\");\n const template = ogConfig.template ?? \"minimal\";\n const concurrency = Math.max(1, ogConfig.concurrency ?? OG_IMAGE_CONCURRENCY);\n\n // Ensure output directory exists\n if (!existsSync(outputDir)) {\n mkdirSync(outputDir, { recursive: true });\n }\n\n // Dynamic imports for optional dependencies\n const satori = await import(\"satori\").then((m) => m.default ?? m);\n const { Resvg } = await import(\"@resvg/resvg-js\");\n\n // Load font\n const fontData = await loadFont(ogConfig);\n\n // Load template renderer\n const templateRenderer = ogConfig.render ?? (await loadTemplate(template));\n\n const results: Record<string, string> = {};\n\n // Separate entries into cached and uncached\n const toGenerate: VazaEntry[] = [];\n for (const entry of entries) {\n const outputPath = join(outputDir, `${entry.slug}.png`);\n if (existsSync(outputPath)) {\n const stat = statSync(outputPath);\n const entryDate = entry.updateDate ?? entry.publishDate;\n if (stat.mtime >= entryDate) {\n results[entry.slug] = outputPath;\n continue;\n }\n }\n toGenerate.push(entry);\n }\n\n if (toGenerate.length === 0) {\n logger.debug(\"OG images: all cached, nothing to generate\");\n return results;\n }\n\n logger.info(\n `Generating ${toGenerate.length} OG images (concurrency: ${concurrency})`,\n );\n\n await pMap(\n toGenerate,\n async (entry) => {\n const outputPath = join(outputDir, `${entry.slug}.png`);\n\n try {\n // Render the template\n const element = templateRenderer(entry, ogConfig);\n\n // Convert to SVG with satori\n const svg = await satori(element as Parameters<typeof satori>[0], {\n width: OG_IMAGE_WIDTH,\n height: OG_IMAGE_HEIGHT,\n fonts: [\n {\n name: \"Inter\",\n data: fontData,\n weight: 400,\n style: \"normal\" as const,\n },\n ],\n });\n\n // Convert SVG to PNG with resvg\n const resvg = new Resvg(svg, {\n fitTo: { mode: \"width\" as const, value: OG_IMAGE_WIDTH },\n });\n const pngData = resvg.render();\n const pngBuffer = pngData.asPng();\n\n // Ensure parent directory exists for nested slugs\n const parentDir = dirname(outputPath);\n if (!existsSync(parentDir)) {\n mkdirSync(parentDir, { recursive: true });\n }\n\n writeFileSync(outputPath, pngBuffer);\n results[entry.slug] = outputPath;\n logger.debug(`OG image generated: ${entry.slug}`);\n } catch (err) {\n logger.error(`Failed to generate OG image for \"${entry.slug}\":`, err);\n }\n },\n concurrency,\n );\n\n return results;\n}\n\n/**\n * Load font data. Priority:\n * 1. Custom fontPath from config\n * 2. Cached Inter font\n * 3. Fetch Inter from Google Fonts and cache\n */\nasync function loadFont(config: OgImagesConfig): Promise<ArrayBuffer> {\n // 1. Custom font\n if (config.fontPath && existsSync(config.fontPath)) {\n logger.debug(`Using custom font: ${config.fontPath}`);\n const buffer = readFileSync(config.fontPath);\n return buffer.buffer.slice(\n buffer.byteOffset,\n buffer.byteOffset + buffer.byteLength,\n );\n }\n\n // 2. Cached font\n const cacheDir = join(process.cwd(), FONT_CACHE_DIR);\n const cachedPath = join(cacheDir, \"inter-regular.woff2\");\n\n if (existsSync(cachedPath)) {\n logger.debug(\"Using cached Inter font\");\n const buffer = readFileSync(cachedPath);\n return buffer.buffer.slice(\n buffer.byteOffset,\n buffer.byteOffset + buffer.byteLength,\n );\n }\n\n // 3. Fetch and cache\n logger.info(\"Downloading Inter font for OG images...\");\n try {\n const response = await fetch(INTER_FONT_URL);\n if (!response.ok) {\n throw new Error(`Font fetch failed: ${response.status}`);\n }\n const arrayBuffer = await response.arrayBuffer();\n\n if (!existsSync(cacheDir)) {\n mkdirSync(cacheDir, { recursive: true });\n }\n writeFileSync(cachedPath, Buffer.from(arrayBuffer));\n logger.debug(\"Inter font cached\");\n\n return arrayBuffer;\n } catch (err) {\n logger.warn(\"Could not fetch Inter font, using empty fallback:\", err);\n return new ArrayBuffer(0);\n }\n}\n\n/**\n * Load a built-in template renderer.\n */\nasync function loadTemplate(\n template: string,\n): Promise<(entry: VazaEntry, config: OgImagesConfig) => unknown> {\n switch (template) {\n case \"blog\": {\n const mod = await import(\"./og-templates/blog.js\");\n return mod.blogTemplate;\n }\n case \"product\": {\n const mod = await import(\"./og-templates/product.js\");\n return mod.productTemplate;\n }\n case \"dark\": {\n const mod = await import(\"./og-templates/dark.js\");\n return mod.darkTemplate;\n }\n default: {\n const mod = await import(\"./og-templates/minimal.js\");\n return mod.minimalTemplate;\n }\n }\n}\n","// Content processing\nexport const WORDS_PER_MINUTE = 200;\nexport const EXCERPT_MAX_LENGTH = 160;\n\n// SEO defaults\nexport const DEFAULT_META_TITLE_MAX = 60;\nexport const DEFAULT_META_DESC_MAX = 120;\nexport const DEFAULT_FRESHNESS_MAX_AGE = \"6m\";\nexport const DEFAULT_RSS_LIMIT = 50;\nexport const DEFAULT_SITEMAP_PRIORITY = 0.7;\nexport const DEFAULT_SITEMAP_CHANGE_FREQ = \"weekly\";\n\n// OG Images\nexport const OG_IMAGE_WIDTH = 1200;\nexport const OG_IMAGE_HEIGHT = 630;\nexport const OG_IMAGE_CONCURRENCY = 5;\n","export type LogLevel = \"silent\" | \"error\" | \"warn\" | \"info\" | \"debug\";\n\nconst LEVELS: Record<LogLevel, number> = {\n silent: 0,\n error: 1,\n warn: 2,\n info: 3,\n debug: 4,\n};\n\nlet currentLevel: LogLevel = \"info\";\n\nexport function setLogLevel(level: LogLevel): void {\n currentLevel = level;\n}\n\nfunction shouldLog(level: LogLevel): boolean {\n return LEVELS[level] <= LEVELS[currentLevel];\n}\n\nexport const logger = {\n error(...args: unknown[]) {\n if (shouldLog(\"error\")) console.error(\"[vaza-content]\", ...args);\n },\n warn(...args: unknown[]) {\n if (shouldLog(\"warn\")) console.warn(\"[vaza-content]\", ...args);\n },\n info(...args: unknown[]) {\n if (shouldLog(\"info\")) console.log(\"[vaza-content]\", ...args);\n },\n debug(...args: unknown[]) {\n if (shouldLog(\"debug\")) console.log(\"[vaza-content] [debug]\", ...args);\n },\n};\n","/**\n * Run async tasks with a concurrency limit.\n */\nexport async function pMap<T, R>(\n items: T[],\n fn: (item: T) => Promise<R>,\n concurrency: number,\n): Promise<R[]> {\n const results: R[] = new Array(items.length);\n let nextIndex = 0;\n\n async function worker() {\n while (nextIndex < items.length) {\n const index = nextIndex++;\n results[index] = await fn(items[index]);\n }\n }\n\n const workers = Array.from(\n { length: Math.min(concurrency, items.length) },\n () => worker(),\n );\n await Promise.all(workers);\n return results;\n}\n","import { execSync } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { basename, dirname, join } from \"node:path\";\nimport { logger } from \"../logger.js\";\nimport type { RedirectEntry, VazaConfig, VazaEntry } from \"../types.js\";\nimport { similarity } from \"../utils/levenshtein.js\";\n\nconst MANIFEST_DIR = \".vaza-content\";\nconst MANIFEST_FILE = \"slugs.json\";\nconst MIN_SIMILARITY = 0.5;\n\n/**\n * Detect slug changes that need redirects.\n * Tries git diff first, falls back to reading/writing a slugs.json manifest.\n */\nexport async function detectRedirects(\n entries: VazaEntry[],\n config: VazaConfig,\n): Promise<RedirectEntry[]> {\n const strategy = config.redirects?.strategy ?? \"both\";\n const redirects: RedirectEntry[] = [];\n\n if (strategy === \"git\" || strategy === \"both\") {\n const gitRedirects = detectGitRenames();\n redirects.push(...gitRedirects);\n }\n\n if (strategy === \"manifest\" || strategy === \"both\") {\n const manifestRedirects = detectManifestChanges(entries);\n redirects.push(...manifestRedirects);\n }\n\n // Deduplicate by \"from\" path\n const seen = new Set<string>();\n const unique: RedirectEntry[] = [];\n for (const r of redirects) {\n if (!seen.has(r.from)) {\n seen.add(r.from);\n unique.push(r);\n }\n }\n\n if (unique.length > 0) {\n logger.info(`Detected ${unique.length} redirect(s)`);\n }\n\n return unique;\n}\n\n/**\n * Detect renames via git log.\n */\nfunction detectGitRenames(): RedirectEntry[] {\n const redirects: RedirectEntry[] = [];\n\n try {\n const output = execSync(\n \"git log --all --diff-filter=R --summary --format=\",\n {\n encoding: \"utf-8\",\n timeout: 10_000,\n },\n );\n\n const renameRegex = /rename\\s+(.+?)\\{(.+?)\\s*=>\\s*(.+?)\\}\\s*\\((\\d+)%\\)/g;\n const simpleRenameRegex = /rename\\s+(.+?)\\s+=>\\s+(.+?)\\s+\\((\\d+)%\\)/g;\n\n let match: RegExpExecArray | null;\n\n match = renameRegex.exec(output);\n while (match) {\n const prefix = match[1];\n const oldPart = match[2].trim();\n const newPart = match[3].trim();\n\n const oldPath = `${prefix}${oldPart}`;\n const newPath = `${prefix}${newPart}`;\n\n const oldSlug = extractSlug(oldPath);\n const newSlug = extractSlug(newPath);\n\n if (oldSlug && newSlug && oldSlug !== newSlug) {\n redirects.push({\n from: oldSlug,\n to: newSlug,\n status: 301,\n });\n }\n match = renameRegex.exec(output);\n }\n\n match = simpleRenameRegex.exec(output);\n while (match) {\n const oldPath = match[1].trim();\n const newPath = match[2].trim();\n\n const oldSlug = extractSlug(oldPath);\n const newSlug = extractSlug(newPath);\n\n if (oldSlug && newSlug && oldSlug !== newSlug) {\n redirects.push({\n from: oldSlug,\n to: newSlug,\n status: 301,\n });\n }\n match = simpleRenameRegex.exec(output);\n }\n } catch {\n logger.debug(\n \"Git not available or not a git repo, skipping git rename detection\",\n );\n }\n\n return redirects;\n}\n\n/**\n * Detect changes by comparing current slugs against a stored manifest.\n * Uses Levenshtein distance to match removed slugs to added slugs.\n */\nfunction detectManifestChanges(entries: VazaEntry[]): RedirectEntry[] {\n const redirects: RedirectEntry[] = [];\n const manifestPath = join(process.cwd(), MANIFEST_DIR, MANIFEST_FILE);\n\n const currentSlugs = entries.map((e) => e.slug).sort();\n\n if (existsSync(manifestPath)) {\n try {\n const raw = readFileSync(manifestPath, \"utf-8\");\n const previousSlugs: string[] = JSON.parse(raw);\n\n const currentSet = new Set(currentSlugs);\n const previousSet = new Set(previousSlugs);\n\n const removedSlugs = previousSlugs.filter((s) => !currentSet.has(s));\n const addedSlugs = currentSlugs.filter((s) => !previousSet.has(s));\n\n if (removedSlugs.length > 0 && addedSlugs.length > 0) {\n // Match each removed slug to its best match among added slugs\n const usedAdded = new Set<string>();\n\n for (const removed of removedSlugs) {\n let bestMatch = \"\";\n let bestScore = 0;\n\n for (const added of addedSlugs) {\n if (usedAdded.has(added)) continue;\n const score = similarity(removed, added);\n if (score > bestScore) {\n bestScore = score;\n bestMatch = added;\n }\n }\n\n if (bestMatch && bestScore >= MIN_SIMILARITY) {\n usedAdded.add(bestMatch);\n redirects.push({\n from: removed,\n to: bestMatch,\n status: 301,\n });\n logger.debug(\n `Redirect: ${removed} -> ${bestMatch} (similarity: ${bestScore.toFixed(2)})`,\n );\n }\n }\n }\n } catch {\n logger.warn(\"Corrupted slug manifest, will be overwritten\");\n }\n }\n\n // Write current slugs to manifest\n const dir = dirname(manifestPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(manifestPath, JSON.stringify(currentSlugs, null, 2));\n\n return redirects;\n}\n\n/**\n * Extract a slug from a file path by removing directory prefix and extension.\n */\nfunction extractSlug(filePath: string): string | null {\n const name = basename(filePath);\n if (!name) return null;\n return name.replace(/\\.(mdx?|tsx?|jsx?)$/, \"\");\n}\n","/**\n * Compute the Levenshtein distance between two strings.\n */\nexport function levenshtein(a: string, b: string): number {\n const m = a.length;\n const n = b.length;\n const dp: number[][] = Array.from({ length: m + 1 }, () =>\n new Array(n + 1).fill(0),\n );\n\n for (let i = 0; i <= m; i++) dp[i][0] = i;\n for (let j = 0; j <= n; j++) dp[0][j] = j;\n\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n if (a[i - 1] === b[j - 1]) {\n dp[i][j] = dp[i - 1][j - 1];\n } else {\n dp[i][j] = 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);\n }\n }\n }\n\n return dp[m][n];\n}\n\n/**\n * Compute similarity ratio between two strings (0 to 1).\n */\nexport function similarity(a: string, b: string): number {\n const maxLen = Math.max(a.length, b.length);\n if (maxLen === 0) return 1;\n return 1 - levenshtein(a, b) / maxLen;\n}\n","import { DEFAULT_RSS_LIMIT } from \"../constants.js\";\nimport type { RssItem, VazaConfig, VazaEntry } from \"../types.js\";\n\n/**\n * Generate RSS items from entries, sorted by publishDate desc, limited by config.\n */\nexport function generateRss(\n entries: VazaEntry[],\n config: VazaConfig,\n): RssItem[] {\n const siteUrl = config.site.url.replace(/\\/$/, \"\");\n const limit = config.rss?.limit ?? DEFAULT_RSS_LIMIT;\n\n // Fallback basePath if entries aren't tagged with _basePath\n const collectionKeys = Object.keys(config.collections);\n const fallbackBasePath =\n collectionKeys.length > 0\n ? config.collections[collectionKeys[0]].basePath\n : \"\";\n\n const sorted = [...entries]\n .filter((e) => !e.noindex)\n .sort((a, b) => b.publishDate.getTime() - a.publishDate.getTime());\n\n const limited = sorted.slice(0, limit);\n\n return limited.map((entry) => {\n const basePath = (entry._basePath ?? fallbackBasePath).replace(/\\/$/, \"\");\n const link = `${siteUrl}${basePath}/${entry.slug}`;\n return {\n title: entry.title,\n link,\n description: entry.description || entry.excerpt,\n pubDate: entry.publishDate.toUTCString(),\n guid: link,\n author: entry.author?.name,\n };\n });\n}\n\n/**\n * Render RSS items to valid RSS 2.0 XML.\n */\nexport function renderRssXml(items: RssItem[], config: VazaConfig): string {\n const siteUrl = config.site.url.replace(/\\/$/, \"\");\n\n const lines: string[] = [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n '<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">',\n \" <channel>\",\n ` <title>${escapeXml(config.site.name)}</title>`,\n ` <link>${escapeXml(siteUrl)}</link>`,\n ` <description>${escapeXml(config.site.description ?? \"\")}</description>`,\n ` <language>${config.site.language ?? \"en\"}</language>`,\n ` <lastBuildDate>${new Date().toUTCString()}</lastBuildDate>`,\n ];\n\n if (config.rss?.path) {\n lines.push(\n ` <atom:link href=\"${escapeXml(siteUrl + config.rss.path)}\" rel=\"self\" type=\"application/rss+xml\" />`,\n );\n }\n\n for (const item of items) {\n lines.push(\" <item>\");\n lines.push(` <title>${escapeXml(item.title)}</title>`);\n lines.push(` <link>${escapeXml(item.link)}</link>`);\n lines.push(\n ` <description>${escapeXml(item.description)}</description>`,\n );\n lines.push(` <pubDate>${item.pubDate}</pubDate>`);\n lines.push(` <guid isPermaLink=\"true\">${escapeXml(item.guid)}</guid>`);\n if (item.author) {\n lines.push(` <author>${escapeXml(item.author)}</author>`);\n }\n lines.push(\" </item>\");\n }\n\n lines.push(\" </channel>\");\n lines.push(\"</rss>\");\n return lines.join(\"\\n\");\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n","import {\n DEFAULT_SITEMAP_CHANGE_FREQ,\n DEFAULT_SITEMAP_PRIORITY,\n} from \"../constants.js\";\nimport type { SitemapEntry, VazaConfig, VazaEntry } from \"../types.js\";\n\n/**\n * Generate sitemap entries from VazaEntry[].\n */\nexport function generateSitemap(\n entries: VazaEntry[],\n config: VazaConfig,\n): SitemapEntry[] {\n const siteUrl = config.site.url.replace(/\\/$/, \"\");\n const defaultChangeFreq =\n config.sitemap?.changeFrequency ?? DEFAULT_SITEMAP_CHANGE_FREQ;\n const defaultPriority = config.sitemap?.priority ?? DEFAULT_SITEMAP_PRIORITY;\n\n // Fallback basePath if entries aren't tagged with _basePath\n const collectionKeys = Object.keys(config.collections);\n const fallbackBasePath =\n collectionKeys.length > 0\n ? config.collections[collectionKeys[0]].basePath\n : \"\";\n\n const sitemapEntries: SitemapEntry[] = [];\n\n for (const entry of entries) {\n if (entry.noindex) continue;\n\n const basePath = (entry._basePath ?? fallbackBasePath).replace(/\\/$/, \"\");\n const loc = `${siteUrl}${basePath}/${entry.slug}`;\n const lastmod = (entry.updateDate ?? entry.publishDate).toISOString();\n\n const sitemapEntry: SitemapEntry = {\n loc,\n lastmod,\n changefreq: defaultChangeFreq,\n priority: defaultPriority,\n };\n\n if (entry.image) {\n sitemapEntry.images = [\n {\n loc: entry.image.src.startsWith(\"http\")\n ? entry.image.src\n : `${siteUrl}${entry.image.src}`,\n title: entry.image.alt,\n },\n ];\n }\n\n sitemapEntries.push(sitemapEntry);\n }\n\n // Append additional static paths\n if (config.sitemap?.additionalPaths) {\n for (const path of config.sitemap.additionalPaths) {\n sitemapEntries.push({\n loc: `${siteUrl}${path}`,\n changefreq: defaultChangeFreq,\n priority: defaultPriority,\n });\n }\n }\n\n return sitemapEntries;\n}\n\n/**\n * Render sitemap entries to valid XML with image namespace.\n */\nexport function renderSitemapXml(entries: SitemapEntry[]): string {\n const hasImages = entries.some((e) => e.images && e.images.length > 0);\n\n const lines: string[] = [\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>',\n `<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"${hasImages ? ' xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\"' : \"\"}>`,\n ];\n\n for (const entry of entries) {\n lines.push(\" <url>\");\n lines.push(` <loc>${escapeXml(entry.loc)}</loc>`);\n if (entry.lastmod) {\n lines.push(` <lastmod>${entry.lastmod}</lastmod>`);\n }\n if (entry.changefreq) {\n lines.push(` <changefreq>${entry.changefreq}</changefreq>`);\n }\n if (entry.priority !== undefined) {\n lines.push(` <priority>${entry.priority}</priority>`);\n }\n if (entry.images) {\n for (const img of entry.images) {\n lines.push(\" <image:image>\");\n lines.push(` <image:loc>${escapeXml(img.loc)}</image:loc>`);\n if (img.title) {\n lines.push(\n ` <image:title>${escapeXml(img.title)}</image:title>`,\n );\n }\n lines.push(\" </image:image>\");\n }\n }\n lines.push(\" </url>\");\n }\n\n lines.push(\"</urlset>\");\n return lines.join(\"\\n\");\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n","import type { TaxonomyData, VazaConfig, VazaEntry } from \"../types.js\";\n\n/**\n * Build tag and category maps from entries.\n */\nexport function generateTaxonomy(\n entries: VazaEntry[],\n _config: VazaConfig,\n): TaxonomyData {\n const tags: Record<string, string[]> = {};\n const categories: Record<string, string[]> = {};\n\n for (const entry of entries) {\n if (entry.tags) {\n for (const tag of entry.tags) {\n const normalized = tag.toLowerCase().trim();\n if (!tags[normalized]) {\n tags[normalized] = [];\n }\n tags[normalized].push(entry.slug);\n }\n }\n\n if (entry.category) {\n const normalized = entry.category.toLowerCase().trim();\n if (!categories[normalized]) {\n categories[normalized] = [];\n }\n categories[normalized].push(entry.slug);\n }\n }\n\n return { tags, categories };\n}\n","import type { IntegrityIssue, Severity, VazaEntry } from \"../types.js\";\n\n/**\n * Check if entries with images have alt text.\n * Also scan body for markdown images  with empty alt text.\n */\nexport function checkAltText(\n entries: VazaEntry[],\n severity: Severity = \"warn\",\n): IntegrityIssue[] {\n const issues: IntegrityIssue[] = [];\n\n // Match markdown images with empty alt: \n const emptyAltRegex = /!\\[\\s*\\]\\([^)]+\\)/g;\n\n for (const entry of entries) {\n // Check featured image alt text\n if (entry.image && !entry.image.alt?.trim()) {\n issues.push({\n type: \"missing-alt-text\",\n severity,\n message: `Featured image is missing alt text (src: \"${entry.image.src}\")`,\n slug: entry.slug,\n });\n }\n\n // Check body for images with empty alt text\n emptyAltRegex.lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = emptyAltRegex.exec(entry.body)) !== null) {\n issues.push({\n type: \"missing-alt-text\",\n severity,\n message: `Markdown image with empty alt text: ${match[0]}`,\n slug: entry.slug,\n });\n }\n }\n\n return issues;\n}\n","import type { IntegrityIssue, Severity, VazaEntry } from \"../types.js\";\n\n/**\n * Scan each entry's body for internal markdown links [text](/path)\n * and check if any entry has a matching slug/path.\n */\nexport function checkBrokenLinks(\n entries: VazaEntry[],\n severity: Severity = \"warn\",\n): IntegrityIssue[] {\n const slugs = new Set(entries.map((e) => e.slug));\n const issues: IntegrityIssue[] = [];\n\n // Match markdown links with internal paths: [text](/some-path)\n const linkRegex = /\\[([^\\]]*)\\]\\(\\/([^)]*)\\)/g;\n\n for (const entry of entries) {\n let match: RegExpExecArray | null;\n linkRegex.lastIndex = 0;\n\n while ((match = linkRegex.exec(entry.body)) !== null) {\n const rawPath = match[2];\n // Strip trailing slash, query string, and hash\n const cleanPath = rawPath.split(/[?#]/)[0].replace(/\\/$/, \"\");\n // Normalize: remove leading slashes, take last segment as potential slug\n const segments = cleanPath.split(\"/\").filter(Boolean);\n const lastSegment = segments[segments.length - 1];\n\n // Check if any slug matches the full clean path or the last segment\n const found =\n slugs.has(cleanPath) ||\n (lastSegment !== undefined && slugs.has(lastSegment));\n\n if (!found) {\n issues.push({\n type: \"broken-link\",\n severity,\n message: `Broken internal link to \"/${rawPath}\"`,\n slug: entry.slug,\n });\n }\n }\n }\n\n return issues;\n}\n","import type { IntegrityIssue, Severity, VazaEntry } from \"../types.js\";\n\n/**\n * Normalize a title for comparison: lowercase, collapse whitespace,\n * strip punctuation.\n */\nfunction normalize(title: string): string {\n return title\n .toLowerCase()\n .replace(/[^\\w\\s]/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/**\n * Check for entries with identical or very similar titles.\n * Uses case-insensitive normalized comparison.\n */\nexport function checkDuplicateContent(\n entries: VazaEntry[],\n severity: Severity = \"warn\",\n): IntegrityIssue[] {\n const issues: IntegrityIssue[] = [];\n const seen = new Map<string, string[]>();\n\n for (const entry of entries) {\n const key = normalize(entry.title);\n if (!key) continue;\n\n const existing = seen.get(key);\n if (existing) {\n existing.push(entry.slug);\n } else {\n seen.set(key, [entry.slug]);\n }\n }\n\n for (const [, slugs] of seen) {\n if (slugs.length > 1) {\n issues.push({\n type: \"duplicate-content\",\n severity,\n message: `Entries have near-identical titles: ${slugs.join(\", \")}`,\n slug: slugs[0],\n });\n }\n }\n\n return issues;\n}\n","import type { IntegrityIssue, Severity, VazaEntry } from \"../types.js\";\n\n/**\n * Check for duplicate slugs across all entries.\n */\nexport function checkDuplicateSlugs(\n entries: VazaEntry[],\n severity: Severity = \"error\",\n): IntegrityIssue[] {\n const issues: IntegrityIssue[] = [];\n const seen = new Map<string, number>();\n\n for (const entry of entries) {\n const count = seen.get(entry.slug) ?? 0;\n seen.set(entry.slug, count + 1);\n }\n\n for (const [slug, count] of seen) {\n if (count > 1) {\n issues.push({\n type: \"duplicate-slug\",\n severity,\n message: `Slug \"${slug}\" is used by ${count} entries`,\n slug,\n });\n }\n }\n\n return issues;\n}\n","import type {\n IntegrityConfig,\n IntegrityIssue,\n Severity,\n VazaEntry,\n} from \"../types.js\";\n\n/**\n * Parse a maxAge string like \"6m\", \"1y\", \"90d\" into milliseconds.\n */\nfunction parseMaxAge(maxAge: string): number {\n const match = maxAge.match(/^(\\d+)(d|m|y)$/);\n if (!match) {\n throw new Error(\n `Invalid maxAge format: \"${maxAge}\". Use \"90d\", \"6m\", or \"1y\".`,\n );\n }\n\n const value = Number.parseInt(match[1], 10);\n const unit = match[2];\n\n const MS_PER_DAY = 86_400_000;\n\n switch (unit) {\n case \"d\":\n return value * MS_PER_DAY;\n case \"m\":\n return value * 30 * MS_PER_DAY;\n case \"y\":\n return value * 365 * MS_PER_DAY;\n default:\n return value * MS_PER_DAY;\n }\n}\n\n/**\n * Check if any entry's updateDate (or publishDate) is older than maxAge.\n */\nexport function checkFreshness(\n entries: VazaEntry[],\n config: IntegrityConfig,\n): IntegrityIssue[] {\n const issues: IntegrityIssue[] = [];\n\n const maxAgeStr = config.freshness?.maxAge ?? \"6m\";\n const severity: Severity = config.freshness?.severity ?? \"warn\";\n\n if (severity === \"off\") return issues;\n\n const maxAgeMs = parseMaxAge(maxAgeStr);\n const now = Date.now();\n\n for (const entry of entries) {\n const lastDate = entry.updateDate ?? entry.publishDate;\n const age = now - lastDate.getTime();\n\n if (age > maxAgeMs) {\n const daysOld = Math.floor(age / 86_400_000);\n issues.push({\n type: \"stale-content\",\n severity,\n message: `Content is ${daysOld} days old (max age: ${maxAgeStr})`,\n slug: entry.slug,\n });\n }\n }\n\n return issues;\n}\n","import type { IntegrityConfig, IntegrityIssue, VazaEntry } from \"../types.js\";\n\n/**\n * Check title length and description length against configured maximums.\n */\nexport function checkMetaLength(\n entries: VazaEntry[],\n config: IntegrityConfig,\n): IntegrityIssue[] {\n const issues: IntegrityIssue[] = [];\n\n const titleMax = config.metaTitleLength?.max ?? 60;\n const titleSeverity = config.metaTitleLength?.severity ?? \"warn\";\n\n const descMax = config.metaDescriptionLength?.max ?? 120;\n const descSeverity = config.metaDescriptionLength?.severity ?? \"warn\";\n\n for (const entry of entries) {\n if (titleSeverity !== \"off\" && entry.title.length > titleMax) {\n issues.push({\n type: \"meta-title-length\",\n severity: titleSeverity,\n message: `Title is ${entry.title.length} chars (max ${titleMax}): \"${entry.title}\"`,\n slug: entry.slug,\n });\n }\n\n if (descSeverity !== \"off\" && entry.description.length > descMax) {\n issues.push({\n type: \"meta-description-length\",\n severity: descSeverity,\n message: `Description is ${entry.description.length} chars (max ${descMax})`,\n slug: entry.slug,\n });\n }\n }\n\n return issues;\n}\n","import type { IntegrityIssue, Severity, VazaEntry } from \"../types.js\";\n\n/**\n * Check if any entry has zero inbound internal links from other entries.\n */\nexport function checkOrphanPages(\n entries: VazaEntry[],\n severity: Severity = \"warn\",\n): IntegrityIssue[] {\n const issues: IntegrityIssue[] = [];\n\n // Build a set of slugs that are linked to by at least one other entry\n const linkedSlugs = new Set<string>();\n const linkRegex = /\\[([^\\]]*)\\]\\(\\/([^)]*)\\)/g;\n\n for (const entry of entries) {\n linkRegex.lastIndex = 0;\n let match: RegExpExecArray | null;\n\n while ((match = linkRegex.exec(entry.body)) !== null) {\n const rawPath = match[2];\n const cleanPath = rawPath.split(/[?#]/)[0].replace(/\\/$/, \"\");\n const segments = cleanPath.split(\"/\").filter(Boolean);\n const lastSegment = segments[segments.length - 1];\n\n // Add both the full path and last segment as potential targets\n linkedSlugs.add(cleanPath);\n if (lastSegment !== undefined) {\n linkedSlugs.add(lastSegment);\n }\n }\n }\n\n // Check which entries have no inbound links\n for (const entry of entries) {\n if (!linkedSlugs.has(entry.slug)) {\n issues.push({\n type: \"orphan-page\",\n severity,\n message: `No other entry links to \"${entry.slug}\"`,\n slug: entry.slug,\n });\n }\n }\n\n return issues;\n}\n","import type {\n IntegrityConfig,\n IntegrityIssue,\n IntegrityReport,\n VazaConfig,\n VazaEntry,\n} from \"../types.js\";\nimport { checkAltText } from \"./alt-text.js\";\nimport { checkBrokenLinks } from \"./broken-links.js\";\nimport { checkDuplicateContent } from \"./duplicate-content.js\";\nimport { checkDuplicateSlugs } from \"./duplicate-slugs.js\";\nimport { checkFreshness } from \"./freshness.js\";\nimport { checkMetaLength } from \"./meta-length.js\";\nimport { checkOrphanPages } from \"./orphan-pages.js\";\n\n/**\n * Run all integrity checks based on config severity settings.\n * Checks with severity \"off\" are skipped entirely.\n */\nexport function checkIntegrity(\n entries: VazaEntry[],\n config: VazaConfig,\n): IntegrityReport {\n const ic: IntegrityConfig = config.integrity ?? {};\n const issues: IntegrityIssue[] = [];\n\n // Broken links\n if (ic.brokenLinks !== \"off\") {\n issues.push(...checkBrokenLinks(entries, ic.brokenLinks ?? \"warn\"));\n }\n\n // Meta length (has its own internal severity handling)\n const titleOff = ic.metaTitleLength?.severity === \"off\";\n const descOff = ic.metaDescriptionLength?.severity === \"off\";\n if (!titleOff || !descOff) {\n issues.push(...checkMetaLength(entries, ic));\n }\n\n // Alt text\n if (ic.altText !== \"off\") {\n issues.push(...checkAltText(entries, ic.altText ?? \"warn\"));\n }\n\n // Duplicate slugs\n if (ic.duplicateSlugs !== \"off\") {\n issues.push(...checkDuplicateSlugs(entries, ic.duplicateSlugs ?? \"error\"));\n }\n\n // Duplicate content\n if (ic.duplicateContent !== \"off\") {\n issues.push(\n ...checkDuplicateContent(entries, ic.duplicateContent ?? \"warn\"),\n );\n }\n\n // Orphan pages\n if (ic.orphanPages !== \"off\") {\n issues.push(...checkOrphanPages(entries, ic.orphanPages ?? \"warn\"));\n }\n\n // Freshness\n if (ic.freshness?.severity !== \"off\") {\n issues.push(...checkFreshness(entries, ic));\n }\n\n // Compute summary\n let errors = 0;\n let warnings = 0;\n for (const issue of issues) {\n if (issue.severity === \"error\") errors++;\n else if (issue.severity === \"warn\") warnings++;\n }\n\n return { issues, summary: { errors, warnings } };\n}\n\nexport { checkAltText } from \"./alt-text.js\";\nexport { checkBrokenLinks } from \"./broken-links.js\";\nexport { checkDuplicateContent } from \"./duplicate-content.js\";\nexport { checkDuplicateSlugs } from \"./duplicate-slugs.js\";\nexport { checkFreshness } from \"./freshness.js\";\nexport { checkMetaLength } from \"./meta-length.js\";\nexport { checkOrphanPages } from \"./orphan-pages.js\";\n","import sharp from \"sharp\";\n\n/**\n * Generate a tiny blurred placeholder image and return it as a base64 data URL.\n * Returns undefined if the image cannot be read.\n */\nexport async function generateBlurPlaceholder(\n imagePath: string,\n): Promise<string | undefined> {\n try {\n const buffer = await sharp(imagePath)\n .resize(8, 8, { fit: \"inside\" })\n .blur()\n .png()\n .toBuffer();\n\n return `data:image/png;base64,${buffer.toString(\"base64\")}`;\n } catch {\n return undefined;\n }\n}\n","import { EXCERPT_MAX_LENGTH } from \"../constants.js\";\n\n/**\n * Strip common markdown syntax from text.\n */\nfunction stripMarkdown(text: string): string {\n return (\n text\n // Remove code blocks\n .replace(/```[\\s\\S]*?```/g, \"\")\n // Remove images \n .replace(/!\\[.*?\\]\\(.*?\\)/g, \"\")\n // Remove links but keep text [text](url)\n .replace(/\\[([^\\]]*)\\]\\(.*?\\)/g, \"$1\")\n // Remove headings (## etc.)\n .replace(/^#{1,6}\\s+/gm, \"\")\n // Remove bold/italic markers\n .replace(/\\*{1,3}(.*?)\\*{1,3}/g, \"$1\")\n .replace(/_{1,3}(.*?)_{1,3}/g, \"$1\")\n // Remove inline code\n .replace(/`([^`]*)`/g, \"$1\")\n // Remove blockquotes\n .replace(/^>\\s+/gm, \"\")\n // Remove horizontal rules\n .replace(/^[-*_]{3,}\\s*$/gm, \"\")\n // Remove HTML tags\n .replace(/<[^>]+>/g, \"\")\n // Collapse whitespace\n .replace(/\\s+/g, \" \")\n .trim()\n );\n}\n\n/**\n * Extract the first ~maxLength characters from body text,\n * trimmed at a word boundary. Strips markdown syntax.\n */\nexport function generateExcerpt(\n body: string,\n maxLength = EXCERPT_MAX_LENGTH,\n): string {\n const clean = stripMarkdown(body);\n\n if (clean.length <= maxLength) {\n return clean;\n }\n\n const truncated = clean.slice(0, maxLength);\n const lastSpace = truncated.lastIndexOf(\" \");\n\n // If no space found, hard-truncate at maxLength\n if (lastSpace === -1) {\n return truncated.slice(0, maxLength);\n }\n\n return truncated.slice(0, lastSpace);\n}\n","import { generateExcerpt } from \"./excerpt.js\";\n\n/**\n * Generate a meta description. If an excerpt is provided, use it.\n * Otherwise, generate one from the body text.\n */\nexport function generateDescription(body: string, excerpt?: string): string {\n return excerpt ?? generateExcerpt(body);\n}\n","import sharp from \"sharp\";\n\n/**\n * Read the width and height of an image file using sharp.\n * Returns undefined if the image cannot be read.\n */\nexport async function getImageDimensions(\n imagePath: string,\n): Promise<{ width: number; height: number } | undefined> {\n try {\n const metadata = await sharp(imagePath).metadata();\n if (metadata.width && metadata.height) {\n return { width: metadata.width, height: metadata.height };\n }\n return undefined;\n } catch {\n return undefined;\n }\n}\n","/**\n * Count the number of words in a body of text.\n */\nexport function calcWordCount(body: string): number {\n return body.trim().split(/\\s+/).filter(Boolean).length;\n}\n","import { WORDS_PER_MINUTE } from \"../constants.js\";\nimport { calcWordCount } from \"./word-count.js\";\n\n/**\n * Calculate estimated reading time in minutes from body text.\n */\nexport function calcReadingTime(body: string): number {\n const words = calcWordCount(body);\n return Math.max(1, Math.ceil(words / WORDS_PER_MINUTE));\n}\n","/**\n * Generate a URL slug from a title string.\n * Lowercases, replaces spaces with hyphens, removes special characters,\n * and trims leading/trailing hyphens.\n */\nexport function generateSlug(title: string): string {\n return title\n .toLowerCase()\n .replace(/\\s+/g, \"-\")\n .replace(/[^a-z0-9-]/g, \"\")\n .replace(/-+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n","import type { TocItem } from \"../types.js\";\n\n/**\n * Generate a URL-friendly anchor slug from heading text.\n */\nfunction toAnchorSlug(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .replace(/-+/g, \"-\")\n .replace(/^-|-$/g, \"\");\n}\n\n/**\n * Extract headings (## and ###) from markdown body and generate\n * a table of contents with anchor slugs.\n */\nexport function extractToc(body: string): TocItem[] {\n const headingRegex = /^(#{2,3})\\s+(.+)$/gm;\n const items: TocItem[] = [];\n let match: RegExpExecArray | null;\n\n while ((match = headingRegex.exec(body)) !== null) {\n const depth = match[1].length;\n const text = match[2].trim();\n items.push({\n depth,\n text,\n slug: toAnchorSlug(text),\n });\n }\n\n return items;\n}\n","import type { PartialVazaEntry, VazaEntry } from \"../types.js\";\nimport { generateBlurPlaceholder } from \"./blur-placeholder.js\";\nimport { generateDescription } from \"./description.js\";\nimport { generateExcerpt } from \"./excerpt.js\";\nimport { getImageDimensions } from \"./image-dimensions.js\";\nimport { calcReadingTime } from \"./reading-time.js\";\nimport { generateSlug } from \"./slug.js\";\nimport { extractToc } from \"./toc.js\";\nimport { calcWordCount } from \"./word-count.js\";\n\nexport {\n calcReadingTime,\n calcWordCount,\n extractToc,\n generateBlurPlaceholder,\n generateDescription,\n generateExcerpt,\n generateSlug,\n getImageDimensions,\n};\n\n/**\n * Convert a date value (string or Date) to a Date object.\n */\nfunction toDate(value: Date | string | undefined): Date | undefined {\n if (value === undefined) return undefined;\n if (value instanceof Date) return value;\n return new Date(value);\n}\n\n/**\n * Normalize a partial entry into a full VazaEntry.\n * For each field, keeps the existing value if present, otherwise generates it.\n */\nexport async function normalize(partial: PartialVazaEntry): Promise<VazaEntry> {\n const { body, title } = partial;\n\n const slug = partial.slug || generateSlug(title);\n const wordCount = partial.wordCount ?? calcWordCount(body);\n const readingTime = partial.readingTime ?? calcReadingTime(body);\n const excerpt = partial.excerpt ?? generateExcerpt(body);\n const toc = partial.toc ?? extractToc(body);\n const description = partial.description ?? generateDescription(body, excerpt);\n const publishDate = toDate(partial.publishDate) ?? new Date();\n const updateDate = toDate(partial.updateDate);\n const eventDate = toDate(partial.eventDate);\n\n // Process image blur placeholder and dimensions if needed\n const image = partial.image ? { ...partial.image } : undefined;\n\n if (image?.src) {\n if (!image.blurDataURL) {\n image.blurDataURL = await generateBlurPlaceholder(image.src);\n }\n if (!image.width || !image.height) {\n const dims = await getImageDimensions(image.src);\n if (dims) {\n image.width = dims.width;\n image.height = dims.height;\n }\n }\n }\n\n const entry: VazaEntry = {\n slug,\n title,\n body,\n description,\n publishDate,\n readingTime,\n wordCount,\n excerpt,\n toc,\n ...(updateDate && { updateDate }),\n ...(image && { image: image as VazaEntry[\"image\"] }),\n ...(partial.tags && { tags: partial.tags }),\n ...(partial.category && { category: partial.category }),\n ...(partial.author && { author: partial.author as VazaEntry[\"author\"] }),\n ...(partial.relatedEntries && { relatedEntries: partial.relatedEntries }),\n ...(partial.canonical && { canonical: partial.canonical }),\n ...(partial.noindex !== undefined && { noindex: partial.noindex }),\n ...(partial.jsonLdType && { jsonLdType: partial.jsonLdType }),\n ...(partial.faqs && { faqs: partial.faqs }),\n ...(partial.steps && { steps: partial.steps }),\n ...(partial.price && { price: partial.price }),\n ...(partial.ingredients && { ingredients: partial.ingredients }),\n ...(partial.cookTime && { cookTime: partial.cookTime }),\n ...(eventDate && { eventDate }),\n ...(partial.eventLocation && { eventLocation: partial.eventLocation }),\n };\n\n return entry;\n}\n","import { generateJsonLd } from \"./generate/json-ld/index.js\";\nimport { generateOgImages } from \"./generate/og-images.js\";\nimport { detectRedirects } from \"./generate/redirects.js\";\nimport { generateRss } from \"./generate/rss.js\";\nimport { generateSitemap } from \"./generate/sitemap.js\";\nimport { generateTaxonomy } from \"./generate/taxonomy.js\";\nimport { checkIntegrity } from \"./integrity/index.js\";\nimport { logger, setLogLevel } from \"./logger.js\";\nimport { normalize } from \"./normalize/index.js\";\nimport type {\n PartialVazaEntry,\n VazaConfig,\n VazaEntry,\n VazaOutput,\n} from \"./types.js\";\n\nexport async function processCollections(\n config: VazaConfig,\n): Promise<VazaOutput> {\n if (config.logLevel) {\n setLogLevel(config.logLevel);\n }\n\n const allEntries: VazaEntry[] = [];\n\n // Collect and normalize entries from all collections\n for (const [name, collection] of Object.entries(config.collections)) {\n logger.debug(`Processing collection: ${name}`);\n const rawEntries = await collection.entries();\n const partialEntries: PartialVazaEntry[] = rawEntries.map(collection.map);\n\n for (const partial of partialEntries) {\n const normalized = await normalize(partial);\n normalized._collection = name;\n normalized._basePath = collection.basePath;\n allEntries.push(normalized);\n }\n logger.debug(`Collection \"${name}\": ${rawEntries.length} entries`);\n }\n\n logger.info(`Normalized ${allEntries.length} entries`);\n\n // Generate all SEO outputs in parallel\n const [sitemap, rss, jsonLd, ogImagePaths, taxonomy, redirects, integrity] =\n await Promise.all([\n config.sitemap?.enabled !== false\n ? generateSitemap(allEntries, config)\n : Promise.resolve([]),\n config.rss?.enabled !== false\n ? generateRss(allEntries, config)\n : Promise.resolve([]),\n config.jsonLd?.enabled !== false\n ? generateJsonLd(allEntries, config)\n : Promise.resolve({}),\n config.ogImages?.enabled !== false\n ? generateOgImages(allEntries, config)\n : Promise.resolve({}),\n config.taxonomy?.enabled !== false\n ? generateTaxonomy(allEntries, config)\n : Promise.resolve({ tags: {}, categories: {} }),\n config.redirects?.enabled !== false\n ? detectRedirects(allEntries, config)\n : Promise.resolve([]),\n checkIntegrity(allEntries, config),\n ]);\n\n // Print integrity report to terminal\n if (integrity.summary.errors > 0 || integrity.summary.warnings > 0) {\n printIntegrityReport(integrity);\n }\n\n return {\n entries: allEntries,\n sitemap,\n rss,\n jsonLd,\n ogImagePaths,\n taxonomy,\n redirects,\n integrity,\n };\n}\n\nfunction printIntegrityReport(report: VazaOutput[\"integrity\"]): void {\n const { issues, summary } = report;\n\n logger.info(\"\");\n logger.info(\"Integrity Report\");\n logger.info(\"─\".repeat(50));\n\n for (const issue of issues) {\n const location = issue.slug ? ` (${issue.slug})` : \"\";\n if (issue.severity === \"error\") {\n logger.error(`${issue.message}${location}`);\n } else {\n logger.warn(`${issue.message}${location}`);\n }\n }\n\n logger.info(\"─\".repeat(50));\n logger.info(`${summary.errors} error(s), ${summary.warnings} warning(s)`);\n\n if (summary.errors > 0) {\n throw new Error(\n `[vaza-content] Build failed: ${summary.errors} integrity error(s) found.`,\n );\n }\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/generate/json-ld/article.ts
|
|
2
2
|
function generateArticleSchema(entry, site) {
|
|
3
|
-
if (!entry.title || !entry.publishDate
|
|
3
|
+
if (!entry.title || !entry.publishDate) {
|
|
4
4
|
return null;
|
|
5
5
|
}
|
|
6
6
|
const schema = {
|
|
@@ -9,17 +9,19 @@ function generateArticleSchema(entry, site) {
|
|
|
9
9
|
headline: entry.title,
|
|
10
10
|
description: entry.description || entry.excerpt,
|
|
11
11
|
datePublished: entry.publishDate.toISOString(),
|
|
12
|
-
author: {
|
|
13
|
-
"@type": "Person",
|
|
14
|
-
name: entry.author.name,
|
|
15
|
-
...entry.author.url && { url: entry.author.url }
|
|
16
|
-
},
|
|
17
12
|
publisher: {
|
|
18
13
|
"@type": "Organization",
|
|
19
14
|
name: site.name,
|
|
20
15
|
...site.url && { url: site.url }
|
|
21
16
|
}
|
|
22
17
|
};
|
|
18
|
+
if (entry.author) {
|
|
19
|
+
schema.author = {
|
|
20
|
+
"@type": "Person",
|
|
21
|
+
name: entry.author.name,
|
|
22
|
+
...entry.author.url && { url: entry.author.url }
|
|
23
|
+
};
|
|
24
|
+
}
|
|
23
25
|
if (entry.updateDate) {
|
|
24
26
|
schema.dateModified = entry.updateDate.toISOString();
|
|
25
27
|
}
|
|
@@ -256,7 +258,7 @@ import {
|
|
|
256
258
|
statSync,
|
|
257
259
|
writeFileSync
|
|
258
260
|
} from "fs";
|
|
259
|
-
import { join } from "path";
|
|
261
|
+
import { dirname, join } from "path";
|
|
260
262
|
|
|
261
263
|
// src/constants.ts
|
|
262
264
|
var WORDS_PER_MINUTE = 200;
|
|
@@ -379,7 +381,7 @@ async function generateOgImages(entries, config) {
|
|
|
379
381
|
});
|
|
380
382
|
const pngData = resvg.render();
|
|
381
383
|
const pngBuffer = pngData.asPng();
|
|
382
|
-
const parentDir =
|
|
384
|
+
const parentDir = dirname(outputPath);
|
|
383
385
|
if (!existsSync(parentDir)) {
|
|
384
386
|
mkdirSync(parentDir, { recursive: true });
|
|
385
387
|
}
|
|
@@ -455,7 +457,7 @@ async function loadTemplate(template) {
|
|
|
455
457
|
// src/generate/redirects.ts
|
|
456
458
|
import { execSync } from "child_process";
|
|
457
459
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
458
|
-
import { basename, dirname, join as join2 } from "path";
|
|
460
|
+
import { basename, dirname as dirname2, join as join2 } from "path";
|
|
459
461
|
|
|
460
462
|
// src/utils/levenshtein.ts
|
|
461
463
|
function levenshtein(a, b) {
|
|
@@ -607,7 +609,7 @@ function detectManifestChanges(entries) {
|
|
|
607
609
|
logger.warn("Corrupted slug manifest, will be overwritten");
|
|
608
610
|
}
|
|
609
611
|
}
|
|
610
|
-
const dir =
|
|
612
|
+
const dir = dirname2(manifestPath);
|
|
611
613
|
if (!existsSync2(dir)) {
|
|
612
614
|
mkdirSync2(dir, { recursive: true });
|
|
613
615
|
}
|
|
@@ -1261,4 +1263,4 @@ export {
|
|
|
1261
1263
|
normalize2 as normalize,
|
|
1262
1264
|
processCollections
|
|
1263
1265
|
};
|
|
1264
|
-
//# sourceMappingURL=chunk-
|
|
1266
|
+
//# sourceMappingURL=chunk-OKXBDPYF.js.map
|