vaza-content 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/astro/index.cjs +75 -0
- package/dist/adapters/astro/index.cjs.map +1 -0
- package/dist/adapters/astro/index.d.cts +23 -0
- package/dist/adapters/astro/index.d.ts +23 -0
- package/dist/adapters/astro/index.js +75 -0
- package/dist/adapters/astro/index.js.map +1 -0
- package/dist/adapters/next/index.cjs +82 -0
- package/dist/adapters/next/index.cjs.map +1 -0
- package/dist/adapters/next/index.d.cts +17 -0
- package/dist/adapters/next/index.d.ts +17 -0
- package/dist/adapters/next/index.js +82 -0
- package/dist/adapters/next/index.js.map +1 -0
- package/dist/adapters/nuxt/index.cjs +70 -0
- package/dist/adapters/nuxt/index.cjs.map +1 -0
- package/dist/adapters/nuxt/index.d.cts +18 -0
- package/dist/adapters/nuxt/index.d.ts +18 -0
- package/dist/adapters/nuxt/index.js +70 -0
- package/dist/adapters/nuxt/index.js.map +1 -0
- package/dist/adapters/sveltekit/index.cjs +78 -0
- package/dist/adapters/sveltekit/index.cjs.map +1 -0
- package/dist/adapters/sveltekit/index.d.cts +19 -0
- package/dist/adapters/sveltekit/index.d.ts +19 -0
- package/dist/adapters/sveltekit/index.js +78 -0
- package/dist/adapters/sveltekit/index.js.map +1 -0
- package/dist/blog-7EEJJG26.js +106 -0
- package/dist/blog-7EEJJG26.js.map +1 -0
- package/dist/blog-Z3R5GOMP.cjs +106 -0
- package/dist/blog-Z3R5GOMP.cjs.map +1 -0
- package/dist/chunk-7VCRDESM.cjs +1050 -0
- package/dist/chunk-7VCRDESM.cjs.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-JEQ2X3Z6.cjs +11 -0
- package/dist/chunk-JEQ2X3Z6.cjs.map +1 -0
- package/dist/chunk-L4VH2NSA.js +1050 -0
- package/dist/chunk-L4VH2NSA.js.map +1 -0
- package/dist/cli/index.cjs +256 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +256 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/dark-6E36AKLN.js +70 -0
- package/dist/dark-6E36AKLN.js.map +1 -0
- package/dist/dark-EX2GRAYK.cjs +70 -0
- package/dist/dark-EX2GRAYK.cjs.map +1 -0
- package/dist/index.cjs +97 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +70 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/minimal-D2PRAVG6.js +54 -0
- package/dist/minimal-D2PRAVG6.js.map +1 -0
- package/dist/minimal-RHOK4XEZ.cjs +54 -0
- package/dist/minimal-RHOK4XEZ.cjs.map +1 -0
- package/dist/process-FYZZ5QAG.js +8 -0
- package/dist/process-FYZZ5QAG.js.map +1 -0
- package/dist/process-XEGGGY6S.cjs +8 -0
- package/dist/process-XEGGGY6S.cjs.map +1 -0
- package/dist/product-NCUW3U72.js +97 -0
- package/dist/product-NCUW3U72.js.map +1 -0
- package/dist/product-OT3XYMWD.cjs +97 -0
- package/dist/product-OT3XYMWD.cjs.map +1 -0
- package/dist/types-Bkue7DeN.d.cts +247 -0
- package/dist/types-Bkue7DeN.d.ts +247 -0
- package/package.json +94 -0
|
@@ -0,0 +1,1050 @@
|
|
|
1
|
+
// src/normalize/blur-placeholder.ts
|
|
2
|
+
import sharp from "sharp";
|
|
3
|
+
async function generateBlurPlaceholder(imagePath) {
|
|
4
|
+
try {
|
|
5
|
+
const buffer = await sharp(imagePath).resize(8, 8, { fit: "inside" }).blur().png().toBuffer();
|
|
6
|
+
return `data:image/png;base64,${buffer.toString("base64")}`;
|
|
7
|
+
} catch {
|
|
8
|
+
return void 0;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/normalize/excerpt.ts
|
|
13
|
+
function stripMarkdown(text) {
|
|
14
|
+
return text.replace(/!\[.*?\]\(.*?\)/g, "").replace(/\[([^\]]*)\]\(.*?\)/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/\*{1,3}(.*?)\*{1,3}/g, "$1").replace(/_{1,3}(.*?)_{1,3}/g, "$1").replace(/`([^`]*)`/g, "$1").replace(/^>\s+/gm, "").replace(/^[-*_]{3,}\s*$/gm, "").replace(/\s+/g, " ").trim();
|
|
15
|
+
}
|
|
16
|
+
function generateExcerpt(body, maxLength = 160) {
|
|
17
|
+
const clean = stripMarkdown(body);
|
|
18
|
+
if (clean.length <= maxLength) {
|
|
19
|
+
return clean;
|
|
20
|
+
}
|
|
21
|
+
const truncated = clean.slice(0, maxLength);
|
|
22
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
23
|
+
if (lastSpace === -1) {
|
|
24
|
+
return truncated;
|
|
25
|
+
}
|
|
26
|
+
return truncated.slice(0, lastSpace);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/normalize/description.ts
|
|
30
|
+
function generateDescription(body, excerpt) {
|
|
31
|
+
return excerpt ?? generateExcerpt(body);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/normalize/image-dimensions.ts
|
|
35
|
+
import sharp2 from "sharp";
|
|
36
|
+
async function getImageDimensions(imagePath) {
|
|
37
|
+
try {
|
|
38
|
+
const metadata = await sharp2(imagePath).metadata();
|
|
39
|
+
if (metadata.width && metadata.height) {
|
|
40
|
+
return { width: metadata.width, height: metadata.height };
|
|
41
|
+
}
|
|
42
|
+
return void 0;
|
|
43
|
+
} catch {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/normalize/word-count.ts
|
|
49
|
+
function calcWordCount(body) {
|
|
50
|
+
return body.trim().split(/\s+/).filter(Boolean).length;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/normalize/reading-time.ts
|
|
54
|
+
function calcReadingTime(body) {
|
|
55
|
+
const words = calcWordCount(body);
|
|
56
|
+
return Math.ceil(words / 200);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/normalize/slug.ts
|
|
60
|
+
function generateSlug(title) {
|
|
61
|
+
return title.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/normalize/toc.ts
|
|
65
|
+
function toAnchorSlug(text) {
|
|
66
|
+
return text.toLowerCase().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
67
|
+
}
|
|
68
|
+
function extractToc(body) {
|
|
69
|
+
const headingRegex = /^(#{2,3})\s+(.+)$/gm;
|
|
70
|
+
const items = [];
|
|
71
|
+
let match;
|
|
72
|
+
while ((match = headingRegex.exec(body)) !== null) {
|
|
73
|
+
const depth = match[1].length;
|
|
74
|
+
const text = match[2].trim();
|
|
75
|
+
items.push({
|
|
76
|
+
depth,
|
|
77
|
+
text,
|
|
78
|
+
slug: toAnchorSlug(text)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return items;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/normalize/index.ts
|
|
85
|
+
function toDate(value) {
|
|
86
|
+
if (value === void 0) return void 0;
|
|
87
|
+
if (value instanceof Date) return value;
|
|
88
|
+
return new Date(value);
|
|
89
|
+
}
|
|
90
|
+
async function normalize(partial) {
|
|
91
|
+
const { body, title } = partial;
|
|
92
|
+
const slug = partial.slug || generateSlug(title);
|
|
93
|
+
const wordCount = partial.wordCount ?? calcWordCount(body);
|
|
94
|
+
const readingTime = partial.readingTime ?? calcReadingTime(body);
|
|
95
|
+
const excerpt = partial.excerpt ?? generateExcerpt(body);
|
|
96
|
+
const toc = partial.toc ?? extractToc(body);
|
|
97
|
+
const description = partial.description ?? generateDescription(body, excerpt);
|
|
98
|
+
const publishDate = toDate(partial.publishDate) ?? /* @__PURE__ */ new Date();
|
|
99
|
+
const updateDate = toDate(partial.updateDate);
|
|
100
|
+
const eventDate = toDate(partial.eventDate);
|
|
101
|
+
let image = partial.image ? { ...partial.image } : void 0;
|
|
102
|
+
if (image?.src) {
|
|
103
|
+
if (!image.blurDataURL) {
|
|
104
|
+
image.blurDataURL = await generateBlurPlaceholder(image.src);
|
|
105
|
+
}
|
|
106
|
+
if (!image.width || !image.height) {
|
|
107
|
+
const dims = await getImageDimensions(image.src);
|
|
108
|
+
if (dims) {
|
|
109
|
+
image.width = dims.width;
|
|
110
|
+
image.height = dims.height;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const entry = {
|
|
115
|
+
slug,
|
|
116
|
+
title,
|
|
117
|
+
body,
|
|
118
|
+
description,
|
|
119
|
+
publishDate,
|
|
120
|
+
readingTime,
|
|
121
|
+
wordCount,
|
|
122
|
+
excerpt,
|
|
123
|
+
toc,
|
|
124
|
+
...updateDate && { updateDate },
|
|
125
|
+
...image && { image },
|
|
126
|
+
...partial.tags && { tags: partial.tags },
|
|
127
|
+
...partial.category && { category: partial.category },
|
|
128
|
+
...partial.author && { author: partial.author },
|
|
129
|
+
...partial.relatedEntries && { relatedEntries: partial.relatedEntries },
|
|
130
|
+
...partial.canonical && { canonical: partial.canonical },
|
|
131
|
+
...partial.noindex !== void 0 && { noindex: partial.noindex },
|
|
132
|
+
...partial.jsonLdType && { jsonLdType: partial.jsonLdType },
|
|
133
|
+
...partial.faqs && { faqs: partial.faqs },
|
|
134
|
+
...partial.steps && { steps: partial.steps },
|
|
135
|
+
...partial.price && { price: partial.price },
|
|
136
|
+
...partial.ingredients && { ingredients: partial.ingredients },
|
|
137
|
+
...partial.cookTime && { cookTime: partial.cookTime },
|
|
138
|
+
...eventDate && { eventDate },
|
|
139
|
+
...partial.eventLocation && { eventLocation: partial.eventLocation }
|
|
140
|
+
};
|
|
141
|
+
return entry;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/generate/sitemap.ts
|
|
145
|
+
function generateSitemap(entries, config) {
|
|
146
|
+
const siteUrl = config.site.url.replace(/\/$/, "");
|
|
147
|
+
const defaultChangeFreq = config.sitemap?.changeFrequency ?? "weekly";
|
|
148
|
+
const defaultPriority = config.sitemap?.priority ?? 0.7;
|
|
149
|
+
const sitemapEntries = [];
|
|
150
|
+
for (const [, collection] of Object.entries(config.collections)) {
|
|
151
|
+
const basePath = collection.basePath.replace(/\/$/, "");
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const loc = `${siteUrl}${basePath}/${entry.slug}`;
|
|
154
|
+
const lastmod = (entry.updateDate ?? entry.publishDate).toISOString();
|
|
155
|
+
const sitemapEntry = {
|
|
156
|
+
loc,
|
|
157
|
+
lastmod,
|
|
158
|
+
changefreq: defaultChangeFreq,
|
|
159
|
+
priority: defaultPriority
|
|
160
|
+
};
|
|
161
|
+
if (entry.image) {
|
|
162
|
+
sitemapEntry.images = [
|
|
163
|
+
{
|
|
164
|
+
loc: entry.image.src.startsWith("http") ? entry.image.src : `${siteUrl}${entry.image.src}`,
|
|
165
|
+
title: entry.image.alt
|
|
166
|
+
}
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
sitemapEntries.push(sitemapEntry);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return sitemapEntries;
|
|
173
|
+
}
|
|
174
|
+
function renderSitemapXml(entries) {
|
|
175
|
+
const hasImages = entries.some((e) => e.images && e.images.length > 0);
|
|
176
|
+
const lines = [
|
|
177
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
178
|
+
`<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"${hasImages ? ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"' : ""}>`
|
|
179
|
+
];
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
lines.push(" <url>");
|
|
182
|
+
lines.push(` <loc>${escapeXml(entry.loc)}</loc>`);
|
|
183
|
+
if (entry.lastmod) {
|
|
184
|
+
lines.push(` <lastmod>${entry.lastmod}</lastmod>`);
|
|
185
|
+
}
|
|
186
|
+
if (entry.changefreq) {
|
|
187
|
+
lines.push(` <changefreq>${entry.changefreq}</changefreq>`);
|
|
188
|
+
}
|
|
189
|
+
if (entry.priority !== void 0) {
|
|
190
|
+
lines.push(` <priority>${entry.priority}</priority>`);
|
|
191
|
+
}
|
|
192
|
+
if (entry.images) {
|
|
193
|
+
for (const img of entry.images) {
|
|
194
|
+
lines.push(" <image:image>");
|
|
195
|
+
lines.push(` <image:loc>${escapeXml(img.loc)}</image:loc>`);
|
|
196
|
+
if (img.title) {
|
|
197
|
+
lines.push(
|
|
198
|
+
` <image:title>${escapeXml(img.title)}</image:title>`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
lines.push(" </image:image>");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
lines.push(" </url>");
|
|
205
|
+
}
|
|
206
|
+
lines.push("</urlset>");
|
|
207
|
+
return lines.join("\n");
|
|
208
|
+
}
|
|
209
|
+
function escapeXml(str) {
|
|
210
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/generate/rss.ts
|
|
214
|
+
function generateRss(entries, config) {
|
|
215
|
+
const siteUrl = config.site.url.replace(/\/$/, "");
|
|
216
|
+
const limit = config.rss?.limit ?? 20;
|
|
217
|
+
const sorted = [...entries].sort(
|
|
218
|
+
(a, b) => b.publishDate.getTime() - a.publishDate.getTime()
|
|
219
|
+
);
|
|
220
|
+
const limited = sorted.slice(0, limit);
|
|
221
|
+
const items = [];
|
|
222
|
+
for (const [, collection] of Object.entries(config.collections)) {
|
|
223
|
+
const basePath = collection.basePath.replace(/\/$/, "");
|
|
224
|
+
for (const entry of limited) {
|
|
225
|
+
const link = `${siteUrl}${basePath}/${entry.slug}`;
|
|
226
|
+
items.push({
|
|
227
|
+
title: entry.title,
|
|
228
|
+
link,
|
|
229
|
+
description: entry.description || entry.excerpt,
|
|
230
|
+
pubDate: entry.publishDate.toUTCString(),
|
|
231
|
+
guid: link,
|
|
232
|
+
author: entry.author?.name
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return items;
|
|
237
|
+
}
|
|
238
|
+
function renderRssXml(items, config) {
|
|
239
|
+
const siteUrl = config.site.url.replace(/\/$/, "");
|
|
240
|
+
const lines = [
|
|
241
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
242
|
+
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
|
|
243
|
+
" <channel>",
|
|
244
|
+
` <title>${escapeXml2(config.site.name)}</title>`,
|
|
245
|
+
` <link>${escapeXml2(siteUrl)}</link>`,
|
|
246
|
+
` <description>${escapeXml2(config.site.description ?? "")}</description>`,
|
|
247
|
+
` <language>${config.site.language ?? "en"}</language>`,
|
|
248
|
+
` <lastBuildDate>${(/* @__PURE__ */ new Date()).toUTCString()}</lastBuildDate>`
|
|
249
|
+
];
|
|
250
|
+
if (config.rss?.path) {
|
|
251
|
+
lines.push(
|
|
252
|
+
` <atom:link href="${escapeXml2(siteUrl + config.rss.path)}" rel="self" type="application/rss+xml" />`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
for (const item of items) {
|
|
256
|
+
lines.push(" <item>");
|
|
257
|
+
lines.push(` <title>${escapeXml2(item.title)}</title>`);
|
|
258
|
+
lines.push(` <link>${escapeXml2(item.link)}</link>`);
|
|
259
|
+
lines.push(
|
|
260
|
+
` <description>${escapeXml2(item.description)}</description>`
|
|
261
|
+
);
|
|
262
|
+
lines.push(` <pubDate>${item.pubDate}</pubDate>`);
|
|
263
|
+
lines.push(` <guid isPermaLink="true">${escapeXml2(item.guid)}</guid>`);
|
|
264
|
+
if (item.author) {
|
|
265
|
+
lines.push(` <author>${escapeXml2(item.author)}</author>`);
|
|
266
|
+
}
|
|
267
|
+
lines.push(" </item>");
|
|
268
|
+
}
|
|
269
|
+
lines.push(" </channel>");
|
|
270
|
+
lines.push("</rss>");
|
|
271
|
+
return lines.join("\n");
|
|
272
|
+
}
|
|
273
|
+
function escapeXml2(str) {
|
|
274
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/generate/json-ld/article.ts
|
|
278
|
+
function generateArticleSchema(entry, site) {
|
|
279
|
+
if (!entry.title || !entry.publishDate || !entry.author) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
const schema = {
|
|
283
|
+
"@context": "https://schema.org",
|
|
284
|
+
"@type": entry.jsonLdType === "Article" ? "Article" : "BlogPosting",
|
|
285
|
+
headline: entry.title,
|
|
286
|
+
description: entry.description || entry.excerpt,
|
|
287
|
+
datePublished: entry.publishDate.toISOString(),
|
|
288
|
+
author: {
|
|
289
|
+
"@type": "Person",
|
|
290
|
+
name: entry.author.name,
|
|
291
|
+
...entry.author.url && { url: entry.author.url }
|
|
292
|
+
},
|
|
293
|
+
publisher: {
|
|
294
|
+
"@type": "Organization",
|
|
295
|
+
name: site.name,
|
|
296
|
+
...site.url && { url: site.url }
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
if (entry.updateDate) {
|
|
300
|
+
schema.dateModified = entry.updateDate.toISOString();
|
|
301
|
+
}
|
|
302
|
+
if (entry.image) {
|
|
303
|
+
schema.image = {
|
|
304
|
+
"@type": "ImageObject",
|
|
305
|
+
url: entry.image.src.startsWith("http") ? entry.image.src : `${site.url.replace(/\/$/, "")}${entry.image.src}`,
|
|
306
|
+
...entry.image.width && { width: entry.image.width },
|
|
307
|
+
...entry.image.height && { height: entry.image.height }
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (entry.wordCount) {
|
|
311
|
+
schema.wordCount = entry.wordCount;
|
|
312
|
+
}
|
|
313
|
+
return schema;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/generate/json-ld/faq.ts
|
|
317
|
+
function generateFaqSchema(entry, _site) {
|
|
318
|
+
if (!entry.faqs || entry.faqs.length === 0) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
"@context": "https://schema.org",
|
|
323
|
+
"@type": "FAQPage",
|
|
324
|
+
mainEntity: entry.faqs.map((faq) => ({
|
|
325
|
+
"@type": "Question",
|
|
326
|
+
name: faq.question,
|
|
327
|
+
acceptedAnswer: {
|
|
328
|
+
"@type": "Answer",
|
|
329
|
+
text: faq.answer
|
|
330
|
+
}
|
|
331
|
+
}))
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/generate/breadcrumbs.ts
|
|
336
|
+
function generateBreadcrumbs(slug, title, siteUrl, basePath) {
|
|
337
|
+
const url = siteUrl.replace(/\/$/, "");
|
|
338
|
+
const path = basePath.replace(/\/$/, "");
|
|
339
|
+
const items = [{ name: "Home", url: `${url}/` }];
|
|
340
|
+
if (path) {
|
|
341
|
+
const pathSegments = path.split("/").filter(Boolean);
|
|
342
|
+
let accumulated = url;
|
|
343
|
+
for (const segment of pathSegments) {
|
|
344
|
+
accumulated += `/${segment}`;
|
|
345
|
+
items.push({
|
|
346
|
+
name: segment.charAt(0).toUpperCase() + segment.slice(1),
|
|
347
|
+
url: `${accumulated}/`
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const slugSegments = slug.split("/").filter(Boolean);
|
|
352
|
+
if (slugSegments.length > 1) {
|
|
353
|
+
let accumulated = `${url}${path}`;
|
|
354
|
+
for (let i = 0; i < slugSegments.length - 1; i++) {
|
|
355
|
+
accumulated += `/${slugSegments[i]}`;
|
|
356
|
+
const segmentName = slugSegments[i].replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
357
|
+
items.push({
|
|
358
|
+
name: segmentName,
|
|
359
|
+
url: `${accumulated}/`
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
items.push({
|
|
364
|
+
name: title,
|
|
365
|
+
url: `${url}${path}/${slug}`
|
|
366
|
+
});
|
|
367
|
+
return items;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/generate/json-ld/breadcrumb.ts
|
|
371
|
+
function generateBreadcrumbSchema(entry, site, basePath) {
|
|
372
|
+
const siteUrl = site.url.replace(/\/$/, "");
|
|
373
|
+
const crumbs = generateBreadcrumbs(entry.slug, entry.title, siteUrl, basePath);
|
|
374
|
+
return {
|
|
375
|
+
"@context": "https://schema.org",
|
|
376
|
+
"@type": "BreadcrumbList",
|
|
377
|
+
itemListElement: crumbs.map((crumb, index) => ({
|
|
378
|
+
"@type": "ListItem",
|
|
379
|
+
position: index + 1,
|
|
380
|
+
name: crumb.name,
|
|
381
|
+
item: crumb.url
|
|
382
|
+
}))
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/generate/json-ld/how-to.ts
|
|
387
|
+
function generateHowToSchema(entry, _site) {
|
|
388
|
+
if (!entry.steps || entry.steps.length === 0) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
"@context": "https://schema.org",
|
|
393
|
+
"@type": "HowTo",
|
|
394
|
+
name: entry.title,
|
|
395
|
+
description: entry.description || entry.excerpt,
|
|
396
|
+
step: entry.steps.map((step, index) => ({
|
|
397
|
+
"@type": "HowToStep",
|
|
398
|
+
position: index + 1,
|
|
399
|
+
name: step.name,
|
|
400
|
+
text: step.text
|
|
401
|
+
}))
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// src/generate/json-ld/product.ts
|
|
406
|
+
function generateProductSchema(entry, site) {
|
|
407
|
+
if (!entry.price) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
const schema = {
|
|
411
|
+
"@context": "https://schema.org",
|
|
412
|
+
"@type": "Product",
|
|
413
|
+
name: entry.title,
|
|
414
|
+
description: entry.description || entry.excerpt,
|
|
415
|
+
offers: {
|
|
416
|
+
"@type": "Offer",
|
|
417
|
+
price: entry.price.amount,
|
|
418
|
+
priceCurrency: entry.price.currency,
|
|
419
|
+
availability: "https://schema.org/InStock"
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
if (entry.image) {
|
|
423
|
+
schema.image = entry.image.src.startsWith("http") ? entry.image.src : `${site.url.replace(/\/$/, "")}${entry.image.src}`;
|
|
424
|
+
}
|
|
425
|
+
return schema;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/generate/json-ld/recipe.ts
|
|
429
|
+
function generateRecipeSchema(entry, _site) {
|
|
430
|
+
if (!entry.ingredients || entry.ingredients.length === 0) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
const schema = {
|
|
434
|
+
"@context": "https://schema.org",
|
|
435
|
+
"@type": "Recipe",
|
|
436
|
+
name: entry.title,
|
|
437
|
+
description: entry.description || entry.excerpt,
|
|
438
|
+
recipeIngredient: entry.ingredients
|
|
439
|
+
};
|
|
440
|
+
if (entry.cookTime) {
|
|
441
|
+
schema.cookTime = entry.cookTime;
|
|
442
|
+
}
|
|
443
|
+
if (entry.author) {
|
|
444
|
+
schema.author = {
|
|
445
|
+
"@type": "Person",
|
|
446
|
+
name: entry.author.name
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (entry.image) {
|
|
450
|
+
schema.image = entry.image.src;
|
|
451
|
+
}
|
|
452
|
+
return schema;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/generate/json-ld/event.ts
|
|
456
|
+
function generateEventSchema(entry, site) {
|
|
457
|
+
if (!entry.eventDate) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const schema = {
|
|
461
|
+
"@context": "https://schema.org",
|
|
462
|
+
"@type": "Event",
|
|
463
|
+
name: entry.title,
|
|
464
|
+
description: entry.description || entry.excerpt,
|
|
465
|
+
startDate: entry.eventDate.toISOString(),
|
|
466
|
+
organizer: {
|
|
467
|
+
"@type": "Organization",
|
|
468
|
+
name: site.name,
|
|
469
|
+
url: site.url
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
if (entry.eventLocation) {
|
|
473
|
+
schema.location = {
|
|
474
|
+
"@type": "Place",
|
|
475
|
+
name: entry.eventLocation
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
if (entry.image) {
|
|
479
|
+
schema.image = entry.image.src.startsWith("http") ? entry.image.src : `${site.url.replace(/\/$/, "")}${entry.image.src}`;
|
|
480
|
+
}
|
|
481
|
+
return schema;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/generate/json-ld/index.ts
|
|
485
|
+
var builtinGenerators = [
|
|
486
|
+
(entry, site) => generateArticleSchema(entry, site),
|
|
487
|
+
(entry, site) => generateFaqSchema(entry, site),
|
|
488
|
+
(entry, site, basePath) => generateBreadcrumbSchema(entry, site, basePath),
|
|
489
|
+
(entry, site) => generateHowToSchema(entry, site),
|
|
490
|
+
(entry, site) => generateProductSchema(entry, site),
|
|
491
|
+
(entry, site) => generateRecipeSchema(entry, site),
|
|
492
|
+
(entry, site) => generateEventSchema(entry, site)
|
|
493
|
+
];
|
|
494
|
+
function generateJsonLd(entries, config) {
|
|
495
|
+
const result = {};
|
|
496
|
+
const site = config.site;
|
|
497
|
+
const collectionKeys = Object.keys(config.collections);
|
|
498
|
+
const basePath = collectionKeys.length > 0 ? config.collections[collectionKeys[0]].basePath : "";
|
|
499
|
+
const customGenerators = [];
|
|
500
|
+
if (config.jsonLd?.customSchemas) {
|
|
501
|
+
for (const [, generator] of Object.entries(config.jsonLd.customSchemas)) {
|
|
502
|
+
customGenerators.push((entry, s) => generator(entry, s));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const allGenerators = [...builtinGenerators, ...customGenerators];
|
|
506
|
+
for (const entry of entries) {
|
|
507
|
+
const schemas = [];
|
|
508
|
+
for (const generator of allGenerators) {
|
|
509
|
+
const schema = generator(entry, site, basePath);
|
|
510
|
+
if (schema) {
|
|
511
|
+
schemas.push(schema);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (schemas.length > 0) {
|
|
515
|
+
result[entry.slug] = schemas;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/generate/og-images.ts
|
|
522
|
+
import { writeFileSync, existsSync, mkdirSync, statSync } from "fs";
|
|
523
|
+
import { join } from "path";
|
|
524
|
+
async function generateOgImages(entries, config) {
|
|
525
|
+
const ogConfig = config.ogImages;
|
|
526
|
+
if (!ogConfig?.enabled) return {};
|
|
527
|
+
const outputDir = ogConfig.outputDir ?? join(process.cwd(), "public", "og");
|
|
528
|
+
const template = ogConfig.template ?? "minimal";
|
|
529
|
+
if (!existsSync(outputDir)) {
|
|
530
|
+
mkdirSync(outputDir, { recursive: true });
|
|
531
|
+
}
|
|
532
|
+
const satori = await import("satori").then((m) => m.default ?? m);
|
|
533
|
+
const { Resvg } = await import("@resvg/resvg-js");
|
|
534
|
+
const templateRenderer = ogConfig.render ?? await loadTemplate(template);
|
|
535
|
+
const results = {};
|
|
536
|
+
for (const entry of entries) {
|
|
537
|
+
const outputPath = join(outputDir, `${entry.slug}.png`);
|
|
538
|
+
if (existsSync(outputPath)) {
|
|
539
|
+
const stat = statSync(outputPath);
|
|
540
|
+
const entryDate = entry.updateDate ?? entry.publishDate;
|
|
541
|
+
if (stat.mtime >= entryDate) {
|
|
542
|
+
results[entry.slug] = outputPath;
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const element = templateRenderer(entry, ogConfig);
|
|
547
|
+
const svg = await satori(element, {
|
|
548
|
+
width: 1200,
|
|
549
|
+
height: 630,
|
|
550
|
+
fonts: [
|
|
551
|
+
{
|
|
552
|
+
name: "sans-serif",
|
|
553
|
+
data: new ArrayBuffer(0),
|
|
554
|
+
weight: 400,
|
|
555
|
+
style: "normal"
|
|
556
|
+
}
|
|
557
|
+
]
|
|
558
|
+
});
|
|
559
|
+
const resvg = new Resvg(svg, {
|
|
560
|
+
fitTo: { mode: "width", value: 1200 }
|
|
561
|
+
});
|
|
562
|
+
const pngData = resvg.render();
|
|
563
|
+
const pngBuffer = pngData.asPng();
|
|
564
|
+
const parentDir = join(outputPath, "..");
|
|
565
|
+
if (!existsSync(parentDir)) {
|
|
566
|
+
mkdirSync(parentDir, { recursive: true });
|
|
567
|
+
}
|
|
568
|
+
writeFileSync(outputPath, pngBuffer);
|
|
569
|
+
results[entry.slug] = outputPath;
|
|
570
|
+
}
|
|
571
|
+
return results;
|
|
572
|
+
}
|
|
573
|
+
async function loadTemplate(template) {
|
|
574
|
+
switch (template) {
|
|
575
|
+
case "blog": {
|
|
576
|
+
const mod = await import("./blog-7EEJJG26.js");
|
|
577
|
+
return mod.blogTemplate;
|
|
578
|
+
}
|
|
579
|
+
case "product": {
|
|
580
|
+
const mod = await import("./product-NCUW3U72.js");
|
|
581
|
+
return mod.productTemplate;
|
|
582
|
+
}
|
|
583
|
+
case "dark": {
|
|
584
|
+
const mod = await import("./dark-6E36AKLN.js");
|
|
585
|
+
return mod.darkTemplate;
|
|
586
|
+
}
|
|
587
|
+
case "minimal":
|
|
588
|
+
default: {
|
|
589
|
+
const mod = await import("./minimal-D2PRAVG6.js");
|
|
590
|
+
return mod.minimalTemplate;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/generate/taxonomy.ts
|
|
596
|
+
function generateTaxonomy(entries, _config) {
|
|
597
|
+
const tags = {};
|
|
598
|
+
const categories = {};
|
|
599
|
+
for (const entry of entries) {
|
|
600
|
+
if (entry.tags) {
|
|
601
|
+
for (const tag of entry.tags) {
|
|
602
|
+
const normalized = tag.toLowerCase().trim();
|
|
603
|
+
if (!tags[normalized]) {
|
|
604
|
+
tags[normalized] = [];
|
|
605
|
+
}
|
|
606
|
+
tags[normalized].push(entry.slug);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (entry.category) {
|
|
610
|
+
const normalized = entry.category.toLowerCase().trim();
|
|
611
|
+
if (!categories[normalized]) {
|
|
612
|
+
categories[normalized] = [];
|
|
613
|
+
}
|
|
614
|
+
categories[normalized].push(entry.slug);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return { tags, categories };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// src/generate/redirects.ts
|
|
621
|
+
import { execSync } from "child_process";
|
|
622
|
+
import { readFileSync, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
|
|
623
|
+
import { join as join2, dirname } from "path";
|
|
624
|
+
var MANIFEST_DIR = ".vaza-content";
|
|
625
|
+
var MANIFEST_FILE = "slugs.json";
|
|
626
|
+
async function detectRedirects(entries, config) {
|
|
627
|
+
const strategy = config.redirects?.strategy ?? "both";
|
|
628
|
+
const redirects = [];
|
|
629
|
+
if (strategy === "git" || strategy === "both") {
|
|
630
|
+
const gitRedirects = detectGitRenames();
|
|
631
|
+
redirects.push(...gitRedirects);
|
|
632
|
+
}
|
|
633
|
+
if (strategy === "manifest" || strategy === "both") {
|
|
634
|
+
const manifestRedirects = detectManifestChanges(entries);
|
|
635
|
+
redirects.push(...manifestRedirects);
|
|
636
|
+
}
|
|
637
|
+
const seen = /* @__PURE__ */ new Set();
|
|
638
|
+
const unique = [];
|
|
639
|
+
for (const r of redirects) {
|
|
640
|
+
if (!seen.has(r.from)) {
|
|
641
|
+
seen.add(r.from);
|
|
642
|
+
unique.push(r);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return unique;
|
|
646
|
+
}
|
|
647
|
+
function detectGitRenames() {
|
|
648
|
+
const redirects = [];
|
|
649
|
+
try {
|
|
650
|
+
const output = execSync(
|
|
651
|
+
"git log --all --diff-filter=R --summary --format=",
|
|
652
|
+
{
|
|
653
|
+
encoding: "utf-8",
|
|
654
|
+
timeout: 1e4
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
const renameRegex = /rename\s+(.+?)\{(.+?)\s*=>\s*(.+?)\}\s*\((\d+)%\)/g;
|
|
658
|
+
const simpleRenameRegex = /rename\s+(.+?)\s+=>\s+(.+?)\s+\((\d+)%\)/g;
|
|
659
|
+
let match;
|
|
660
|
+
match = renameRegex.exec(output);
|
|
661
|
+
while (match) {
|
|
662
|
+
const prefix = match[1];
|
|
663
|
+
const oldPart = match[2].trim();
|
|
664
|
+
const newPart = match[3].trim();
|
|
665
|
+
const oldPath = `${prefix}${oldPart}`;
|
|
666
|
+
const newPath = `${prefix}${newPart}`;
|
|
667
|
+
const oldSlug = extractSlug(oldPath);
|
|
668
|
+
const newSlug = extractSlug(newPath);
|
|
669
|
+
if (oldSlug && newSlug && oldSlug !== newSlug) {
|
|
670
|
+
redirects.push({
|
|
671
|
+
from: oldSlug,
|
|
672
|
+
to: newSlug,
|
|
673
|
+
status: 301
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
match = renameRegex.exec(output);
|
|
677
|
+
}
|
|
678
|
+
match = simpleRenameRegex.exec(output);
|
|
679
|
+
while (match) {
|
|
680
|
+
const oldPath = match[1].trim();
|
|
681
|
+
const newPath = match[2].trim();
|
|
682
|
+
const oldSlug = extractSlug(oldPath);
|
|
683
|
+
const newSlug = extractSlug(newPath);
|
|
684
|
+
if (oldSlug && newSlug && oldSlug !== newSlug) {
|
|
685
|
+
redirects.push({
|
|
686
|
+
from: oldSlug,
|
|
687
|
+
to: newSlug,
|
|
688
|
+
status: 301
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
match = simpleRenameRegex.exec(output);
|
|
692
|
+
}
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
return redirects;
|
|
696
|
+
}
|
|
697
|
+
function detectManifestChanges(entries) {
|
|
698
|
+
const redirects = [];
|
|
699
|
+
const manifestPath = join2(process.cwd(), MANIFEST_DIR, MANIFEST_FILE);
|
|
700
|
+
const currentSlugs = entries.map((e) => e.slug).sort();
|
|
701
|
+
if (existsSync2(manifestPath)) {
|
|
702
|
+
try {
|
|
703
|
+
const raw = readFileSync(manifestPath, "utf-8");
|
|
704
|
+
const previousSlugs = JSON.parse(raw);
|
|
705
|
+
const currentSet = new Set(currentSlugs);
|
|
706
|
+
const removedSlugs = previousSlugs.filter((s) => !currentSet.has(s));
|
|
707
|
+
const previousSet = new Set(previousSlugs);
|
|
708
|
+
const addedSlugs = currentSlugs.filter((s) => !previousSet.has(s));
|
|
709
|
+
if (removedSlugs.length === 1 && addedSlugs.length === 1) {
|
|
710
|
+
redirects.push({
|
|
711
|
+
from: removedSlugs[0],
|
|
712
|
+
to: addedSlugs[0],
|
|
713
|
+
status: 301
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const dir = dirname(manifestPath);
|
|
720
|
+
if (!existsSync2(dir)) {
|
|
721
|
+
mkdirSync2(dir, { recursive: true });
|
|
722
|
+
}
|
|
723
|
+
writeFileSync2(manifestPath, JSON.stringify(currentSlugs, null, 2));
|
|
724
|
+
return redirects;
|
|
725
|
+
}
|
|
726
|
+
function extractSlug(filePath) {
|
|
727
|
+
const name = filePath.split("/").pop();
|
|
728
|
+
if (!name) return null;
|
|
729
|
+
return name.replace(/\.(mdx?|tsx?|jsx?)$/, "");
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// src/integrity/alt-text.ts
|
|
733
|
+
function checkAltText(entries, severity = "warn") {
|
|
734
|
+
const issues = [];
|
|
735
|
+
const emptyAltRegex = /!\[\s*\]\([^)]+\)/g;
|
|
736
|
+
for (const entry of entries) {
|
|
737
|
+
if (entry.image && !entry.image.alt?.trim()) {
|
|
738
|
+
issues.push({
|
|
739
|
+
type: "missing-alt-text",
|
|
740
|
+
severity,
|
|
741
|
+
message: `Featured image is missing alt text (src: "${entry.image.src}")`,
|
|
742
|
+
slug: entry.slug
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
emptyAltRegex.lastIndex = 0;
|
|
746
|
+
let match;
|
|
747
|
+
while ((match = emptyAltRegex.exec(entry.body)) !== null) {
|
|
748
|
+
issues.push({
|
|
749
|
+
type: "missing-alt-text",
|
|
750
|
+
severity,
|
|
751
|
+
message: `Markdown image with empty alt text: ${match[0]}`,
|
|
752
|
+
slug: entry.slug
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return issues;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/integrity/broken-links.ts
|
|
760
|
+
function checkBrokenLinks(entries, severity = "warn") {
|
|
761
|
+
const slugs = new Set(entries.map((e) => e.slug));
|
|
762
|
+
const issues = [];
|
|
763
|
+
const linkRegex = /\[([^\]]*)\]\(\/([^)]*)\)/g;
|
|
764
|
+
for (const entry of entries) {
|
|
765
|
+
let match;
|
|
766
|
+
linkRegex.lastIndex = 0;
|
|
767
|
+
while ((match = linkRegex.exec(entry.body)) !== null) {
|
|
768
|
+
const rawPath = match[2];
|
|
769
|
+
const cleanPath = rawPath.split(/[?#]/)[0].replace(/\/$/, "");
|
|
770
|
+
const segments = cleanPath.split("/").filter(Boolean);
|
|
771
|
+
const lastSegment = segments[segments.length - 1];
|
|
772
|
+
const found = slugs.has(cleanPath) || lastSegment !== void 0 && slugs.has(lastSegment);
|
|
773
|
+
if (!found) {
|
|
774
|
+
issues.push({
|
|
775
|
+
type: "broken-link",
|
|
776
|
+
severity,
|
|
777
|
+
message: `Broken internal link to "/${rawPath}"`,
|
|
778
|
+
slug: entry.slug
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return issues;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/integrity/duplicate-content.ts
|
|
787
|
+
function normalize2(title) {
|
|
788
|
+
return title.toLowerCase().replace(/[^\w\s]/g, "").replace(/\s+/g, " ").trim();
|
|
789
|
+
}
|
|
790
|
+
function checkDuplicateContent(entries, severity = "warn") {
|
|
791
|
+
const issues = [];
|
|
792
|
+
const seen = /* @__PURE__ */ new Map();
|
|
793
|
+
for (const entry of entries) {
|
|
794
|
+
const key = normalize2(entry.title);
|
|
795
|
+
if (!key) continue;
|
|
796
|
+
const existing = seen.get(key);
|
|
797
|
+
if (existing) {
|
|
798
|
+
existing.push(entry.slug);
|
|
799
|
+
} else {
|
|
800
|
+
seen.set(key, [entry.slug]);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
for (const [, slugs] of seen) {
|
|
804
|
+
if (slugs.length > 1) {
|
|
805
|
+
issues.push({
|
|
806
|
+
type: "duplicate-content",
|
|
807
|
+
severity,
|
|
808
|
+
message: `Entries have near-identical titles: ${slugs.join(", ")}`,
|
|
809
|
+
slug: slugs[0]
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return issues;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// src/integrity/duplicate-slugs.ts
|
|
817
|
+
function checkDuplicateSlugs(entries, severity = "error") {
|
|
818
|
+
const issues = [];
|
|
819
|
+
const seen = /* @__PURE__ */ new Map();
|
|
820
|
+
for (const entry of entries) {
|
|
821
|
+
const count = seen.get(entry.slug) ?? 0;
|
|
822
|
+
seen.set(entry.slug, count + 1);
|
|
823
|
+
}
|
|
824
|
+
for (const [slug, count] of seen) {
|
|
825
|
+
if (count > 1) {
|
|
826
|
+
issues.push({
|
|
827
|
+
type: "duplicate-slug",
|
|
828
|
+
severity,
|
|
829
|
+
message: `Slug "${slug}" is used by ${count} entries`,
|
|
830
|
+
slug
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return issues;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/integrity/freshness.ts
|
|
838
|
+
function parseMaxAge(maxAge) {
|
|
839
|
+
const match = maxAge.match(/^(\d+)(d|m|y)$/);
|
|
840
|
+
if (!match) {
|
|
841
|
+
throw new Error(
|
|
842
|
+
`Invalid maxAge format: "${maxAge}". Use "90d", "6m", or "1y".`
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
const value = Number.parseInt(match[1], 10);
|
|
846
|
+
const unit = match[2];
|
|
847
|
+
const MS_PER_DAY = 864e5;
|
|
848
|
+
switch (unit) {
|
|
849
|
+
case "d":
|
|
850
|
+
return value * MS_PER_DAY;
|
|
851
|
+
case "m":
|
|
852
|
+
return value * 30 * MS_PER_DAY;
|
|
853
|
+
case "y":
|
|
854
|
+
return value * 365 * MS_PER_DAY;
|
|
855
|
+
default:
|
|
856
|
+
return value * MS_PER_DAY;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function checkFreshness(entries, config) {
|
|
860
|
+
const issues = [];
|
|
861
|
+
const maxAgeStr = config.freshness?.maxAge ?? "6m";
|
|
862
|
+
const severity = config.freshness?.severity ?? "warn";
|
|
863
|
+
if (severity === "off") return issues;
|
|
864
|
+
const maxAgeMs = parseMaxAge(maxAgeStr);
|
|
865
|
+
const now = Date.now();
|
|
866
|
+
for (const entry of entries) {
|
|
867
|
+
const lastDate = entry.updateDate ?? entry.publishDate;
|
|
868
|
+
const age = now - lastDate.getTime();
|
|
869
|
+
if (age > maxAgeMs) {
|
|
870
|
+
const daysOld = Math.floor(age / 864e5);
|
|
871
|
+
issues.push({
|
|
872
|
+
type: "stale-content",
|
|
873
|
+
severity,
|
|
874
|
+
message: `Content is ${daysOld} days old (max age: ${maxAgeStr})`,
|
|
875
|
+
slug: entry.slug
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return issues;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/integrity/meta-length.ts
|
|
883
|
+
function checkMetaLength(entries, config) {
|
|
884
|
+
const issues = [];
|
|
885
|
+
const titleMax = config.metaTitleLength?.max ?? 60;
|
|
886
|
+
const titleSeverity = config.metaTitleLength?.severity ?? "warn";
|
|
887
|
+
const descMax = config.metaDescriptionLength?.max ?? 120;
|
|
888
|
+
const descSeverity = config.metaDescriptionLength?.severity ?? "warn";
|
|
889
|
+
for (const entry of entries) {
|
|
890
|
+
if (titleSeverity !== "off" && entry.title.length > titleMax) {
|
|
891
|
+
issues.push({
|
|
892
|
+
type: "meta-title-length",
|
|
893
|
+
severity: titleSeverity,
|
|
894
|
+
message: `Title is ${entry.title.length} chars (max ${titleMax}): "${entry.title}"`,
|
|
895
|
+
slug: entry.slug
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
if (descSeverity !== "off" && entry.description.length > descMax) {
|
|
899
|
+
issues.push({
|
|
900
|
+
type: "meta-description-length",
|
|
901
|
+
severity: descSeverity,
|
|
902
|
+
message: `Description is ${entry.description.length} chars (max ${descMax})`,
|
|
903
|
+
slug: entry.slug
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
return issues;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/integrity/orphan-pages.ts
|
|
911
|
+
function checkOrphanPages(entries, severity = "warn") {
|
|
912
|
+
const issues = [];
|
|
913
|
+
const linkedSlugs = /* @__PURE__ */ new Set();
|
|
914
|
+
const linkRegex = /\[([^\]]*)\]\(\/([^)]*)\)/g;
|
|
915
|
+
for (const entry of entries) {
|
|
916
|
+
linkRegex.lastIndex = 0;
|
|
917
|
+
let match;
|
|
918
|
+
while ((match = linkRegex.exec(entry.body)) !== null) {
|
|
919
|
+
const rawPath = match[2];
|
|
920
|
+
const cleanPath = rawPath.split(/[?#]/)[0].replace(/\/$/, "");
|
|
921
|
+
const segments = cleanPath.split("/").filter(Boolean);
|
|
922
|
+
const lastSegment = segments[segments.length - 1];
|
|
923
|
+
linkedSlugs.add(cleanPath);
|
|
924
|
+
if (lastSegment !== void 0) {
|
|
925
|
+
linkedSlugs.add(lastSegment);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
for (const entry of entries) {
|
|
930
|
+
if (!linkedSlugs.has(entry.slug)) {
|
|
931
|
+
issues.push({
|
|
932
|
+
type: "orphan-page",
|
|
933
|
+
severity,
|
|
934
|
+
message: `No other entry links to "${entry.slug}"`,
|
|
935
|
+
slug: entry.slug
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return issues;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/integrity/index.ts
|
|
943
|
+
function checkIntegrity(entries, config) {
|
|
944
|
+
const ic = config.integrity ?? {};
|
|
945
|
+
const issues = [];
|
|
946
|
+
if (ic.brokenLinks !== "off") {
|
|
947
|
+
issues.push(...checkBrokenLinks(entries, ic.brokenLinks ?? "warn"));
|
|
948
|
+
}
|
|
949
|
+
const titleOff = ic.metaTitleLength?.severity === "off";
|
|
950
|
+
const descOff = ic.metaDescriptionLength?.severity === "off";
|
|
951
|
+
if (!titleOff || !descOff) {
|
|
952
|
+
issues.push(...checkMetaLength(entries, ic));
|
|
953
|
+
}
|
|
954
|
+
if (ic.altText !== "off") {
|
|
955
|
+
issues.push(...checkAltText(entries, ic.altText ?? "warn"));
|
|
956
|
+
}
|
|
957
|
+
if (ic.duplicateSlugs !== "off") {
|
|
958
|
+
issues.push(...checkDuplicateSlugs(entries, ic.duplicateSlugs ?? "error"));
|
|
959
|
+
}
|
|
960
|
+
if (ic.duplicateContent !== "off") {
|
|
961
|
+
issues.push(
|
|
962
|
+
...checkDuplicateContent(entries, ic.duplicateContent ?? "warn")
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
if (ic.orphanPages !== "off") {
|
|
966
|
+
issues.push(...checkOrphanPages(entries, ic.orphanPages ?? "warn"));
|
|
967
|
+
}
|
|
968
|
+
if (ic.freshness?.severity !== "off") {
|
|
969
|
+
issues.push(...checkFreshness(entries, ic));
|
|
970
|
+
}
|
|
971
|
+
let errors = 0;
|
|
972
|
+
let warnings = 0;
|
|
973
|
+
for (const issue of issues) {
|
|
974
|
+
if (issue.severity === "error") errors++;
|
|
975
|
+
else if (issue.severity === "warn") warnings++;
|
|
976
|
+
}
|
|
977
|
+
return { issues, summary: { errors, warnings } };
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/process.ts
|
|
981
|
+
async function processCollections(config) {
|
|
982
|
+
const allEntries = [];
|
|
983
|
+
for (const [name, collection] of Object.entries(config.collections)) {
|
|
984
|
+
const rawEntries = await collection.entries();
|
|
985
|
+
const partialEntries = rawEntries.map(collection.map);
|
|
986
|
+
for (const partial of partialEntries) {
|
|
987
|
+
const normalized = await normalize(partial);
|
|
988
|
+
allEntries.push(normalized);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const [sitemap, rss, jsonLd, ogImagePaths, taxonomy, redirects, integrity] = await Promise.all([
|
|
992
|
+
config.sitemap?.enabled !== false ? generateSitemap(allEntries, config) : Promise.resolve([]),
|
|
993
|
+
config.rss?.enabled !== false ? generateRss(allEntries, config) : Promise.resolve([]),
|
|
994
|
+
config.jsonLd?.enabled !== false ? generateJsonLd(allEntries, config) : Promise.resolve({}),
|
|
995
|
+
config.ogImages?.enabled !== false ? generateOgImages(allEntries, config) : Promise.resolve({}),
|
|
996
|
+
config.taxonomy?.enabled !== false ? generateTaxonomy(allEntries, config) : Promise.resolve({ tags: {}, categories: {} }),
|
|
997
|
+
config.redirects?.enabled !== false ? detectRedirects(allEntries, config) : Promise.resolve([]),
|
|
998
|
+
checkIntegrity(allEntries, config)
|
|
999
|
+
]);
|
|
1000
|
+
if (integrity.summary.errors > 0 || integrity.summary.warnings > 0) {
|
|
1001
|
+
printIntegrityReport(integrity);
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
entries: allEntries,
|
|
1005
|
+
sitemap,
|
|
1006
|
+
rss,
|
|
1007
|
+
jsonLd,
|
|
1008
|
+
ogImagePaths,
|
|
1009
|
+
taxonomy,
|
|
1010
|
+
redirects,
|
|
1011
|
+
integrity
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
function printIntegrityReport(report) {
|
|
1015
|
+
const { issues, summary } = report;
|
|
1016
|
+
console.log("\n[vaza-content] Integrity Report");
|
|
1017
|
+
console.log("\u2500".repeat(50));
|
|
1018
|
+
for (const issue of issues) {
|
|
1019
|
+
const icon = issue.severity === "error" ? "\u2716" : "\u26A0";
|
|
1020
|
+
const prefix = issue.severity === "error" ? "ERROR" : "WARN";
|
|
1021
|
+
const location = issue.slug ? ` (${issue.slug})` : "";
|
|
1022
|
+
console.log(` ${icon} [${prefix}] ${issue.message}${location}`);
|
|
1023
|
+
}
|
|
1024
|
+
console.log("\u2500".repeat(50));
|
|
1025
|
+
console.log(
|
|
1026
|
+
` ${summary.errors} error(s), ${summary.warnings} warning(s)
|
|
1027
|
+
`
|
|
1028
|
+
);
|
|
1029
|
+
if (summary.errors > 0) {
|
|
1030
|
+
throw new Error(
|
|
1031
|
+
`[vaza-content] Build failed: ${summary.errors} integrity error(s) found.`
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
export {
|
|
1037
|
+
normalize,
|
|
1038
|
+
generateSitemap,
|
|
1039
|
+
renderSitemapXml,
|
|
1040
|
+
generateRss,
|
|
1041
|
+
renderRssXml,
|
|
1042
|
+
generateBreadcrumbs,
|
|
1043
|
+
generateJsonLd,
|
|
1044
|
+
generateOgImages,
|
|
1045
|
+
generateTaxonomy,
|
|
1046
|
+
detectRedirects,
|
|
1047
|
+
checkIntegrity,
|
|
1048
|
+
processCollections
|
|
1049
|
+
};
|
|
1050
|
+
//# sourceMappingURL=chunk-L4VH2NSA.js.map
|