@zoyth/simple-site-framework 1.0.2 → 1.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/index.mjs CHANGED
@@ -351,6 +351,168 @@ function getPolicyLocales(slug, contentDir = "src/content/policies") {
351
351
  return locales2;
352
352
  }
353
353
 
354
+ // src/lib/content/blog.ts
355
+ import fs2 from "fs";
356
+ import path2 from "path";
357
+ import { compileMDX as compileMDX2 } from "next-mdx-remote/rsc";
358
+ import rehypeSlug2 from "rehype-slug";
359
+ function resolveDir(contentDir) {
360
+ return path2.isAbsolute(contentDir) ? contentDir : path2.join(process.cwd(), contentDir);
361
+ }
362
+ var REQUIRED_FIELDS = ["title", "excerpt", "author", "date", "tags"];
363
+ async function loadBlogPost(slug, locale, contentDir = "src/content/blog") {
364
+ const dir = resolveDir(contentDir);
365
+ const filePath = path2.join(dir, `${slug}.${locale}.md`);
366
+ if (!fs2.existsSync(filePath)) {
367
+ throw new Error(
368
+ `Blog post file not found: ${slug}.${locale}.md in ${contentDir}
369
+ Looking for: ${filePath}`
370
+ );
371
+ }
372
+ const source = fs2.readFileSync(filePath, "utf8");
373
+ const { content, frontmatter } = await compileMDX2({
374
+ source,
375
+ options: {
376
+ parseFrontmatter: true,
377
+ mdxOptions: {
378
+ rehypePlugins: [rehypeSlug2]
379
+ }
380
+ }
381
+ });
382
+ for (const field of REQUIRED_FIELDS) {
383
+ if (!frontmatter[field]) {
384
+ throw new Error(
385
+ `Blog post ${slug}.${locale}.md is missing required frontmatter field: ${field}`
386
+ );
387
+ }
388
+ }
389
+ return {
390
+ content,
391
+ metadata: frontmatter,
392
+ slug,
393
+ locale
394
+ };
395
+ }
396
+ function getBlogPostSlugs(contentDir = "src/content/blog") {
397
+ const dir = resolveDir(contentDir);
398
+ if (!fs2.existsSync(dir)) {
399
+ console.warn(`Blog directory not found: ${dir}`);
400
+ return [];
401
+ }
402
+ const files = fs2.readdirSync(dir);
403
+ const slugs = Array.from(
404
+ new Set(
405
+ files.filter((file) => file.endsWith(".md") || file.endsWith(".mdx")).map((file) => file.replace(/\.[a-z]{2}(-[A-Z]{2})?\.mdx?$/, ""))
406
+ )
407
+ );
408
+ return slugs;
409
+ }
410
+ async function getAllBlogPosts(locale, contentDir = "src/content/blog") {
411
+ const slugs = getBlogPostSlugs(contentDir);
412
+ const posts = await Promise.all(
413
+ slugs.map(async (slug) => {
414
+ try {
415
+ const post = await loadBlogPost(slug, locale, contentDir);
416
+ return {
417
+ slug: post.slug,
418
+ locale: post.locale,
419
+ metadata: post.metadata
420
+ };
421
+ } catch (error) {
422
+ console.warn(`Skipping ${slug} for locale ${locale}:`, error);
423
+ return null;
424
+ }
425
+ })
426
+ );
427
+ return posts.filter((p) => p !== null).sort((a, b) => new Date(b.metadata.date).getTime() - new Date(a.metadata.date).getTime());
428
+ }
429
+ function getBlogPostLocales(slug, contentDir = "src/content/blog") {
430
+ const dir = resolveDir(contentDir);
431
+ if (!fs2.existsSync(dir)) {
432
+ return [];
433
+ }
434
+ const files = fs2.readdirSync(dir);
435
+ const locales2 = files.filter((file) => {
436
+ const pattern = new RegExp(`^${slug}\\.[a-z]{2}(-[A-Z]{2})?\\.mdx?$`);
437
+ return pattern.test(file);
438
+ }).map((file) => {
439
+ const match = file.match(/\.([a-z]{2}(-[A-Z]{2})?)\.mdx?$/);
440
+ return match ? match[1] : null;
441
+ }).filter((locale) => locale !== null);
442
+ return locales2;
443
+ }
444
+ async function getBlogPostsByTag(tag, locale, contentDir = "src/content/blog") {
445
+ const posts = await getAllBlogPosts(locale, contentDir);
446
+ return posts.filter((p) => p.metadata.tags.includes(tag));
447
+ }
448
+ async function getFeaturedBlogPosts(locale, contentDir = "src/content/blog") {
449
+ const posts = await getAllBlogPosts(locale, contentDir);
450
+ return posts.filter((p) => p.metadata.featured === true);
451
+ }
452
+ async function getRelatedBlogPosts(slug, locale, count = 3, contentDir = "src/content/blog") {
453
+ const allPosts = await getAllBlogPosts(locale, contentDir);
454
+ const sourcePost = allPosts.find((p) => p.slug === slug);
455
+ if (!sourcePost) return [];
456
+ const sourceTags = new Set(sourcePost.metadata.tags);
457
+ const candidates = allPosts.filter((p) => p.slug !== slug);
458
+ const scored = candidates.map((post) => ({
459
+ post,
460
+ sharedTags: post.metadata.tags.filter((t) => sourceTags.has(t)).length
461
+ }));
462
+ return scored.filter((s) => s.sharedTags > 0).sort((a, b) => {
463
+ if (b.sharedTags !== a.sharedTags) return b.sharedTags - a.sharedTags;
464
+ return new Date(b.post.metadata.date).getTime() - new Date(a.post.metadata.date).getTime();
465
+ }).slice(0, count).map((s) => s.post);
466
+ }
467
+ async function getAllTags(locale, contentDir = "src/content/blog") {
468
+ const posts = await getAllBlogPosts(locale, contentDir);
469
+ const counts = /* @__PURE__ */ new Map();
470
+ for (const post of posts) {
471
+ for (const tag of post.metadata.tags) {
472
+ counts.set(tag, (counts.get(tag) ?? 0) + 1);
473
+ }
474
+ }
475
+ return Array.from(counts.entries()).map(([tag, count]) => ({ tag, count })).sort((a, b) => b.count - a.count);
476
+ }
477
+
478
+ // src/lib/content/rss.ts
479
+ var MAX_ITEMS = 20;
480
+ async function generateBlogRssFeed(options) {
481
+ const { siteUrl, siteName, locale, contentDir } = options;
482
+ const description = typeof options.description === "string" ? options.description : getLocalizedString(options.description, locale);
483
+ const posts = await getAllBlogPosts(locale, contentDir);
484
+ const items = posts.slice(0, MAX_ITEMS);
485
+ const itemsXml = items.map((post) => {
486
+ const link = `${siteUrl}/${locale}/blog/${post.slug}`;
487
+ const pubDate = (/* @__PURE__ */ new Date(`${post.metadata.date}T12:00:00Z`)).toUTCString();
488
+ const categories = post.metadata.tags.map((tag) => ` <category>${escapeXml(tag)}</category>`).join("\n");
489
+ return ` <item>
490
+ <title>${escapeXml(post.metadata.title)}</title>
491
+ <description>${escapeXml(post.metadata.excerpt)}</description>
492
+ <link>${escapeXml(link)}</link>
493
+ <guid>${escapeXml(link)}</guid>
494
+ <pubDate>${pubDate}</pubDate>
495
+ <author>${escapeXml(post.metadata.author)}</author>
496
+ ${categories}
497
+ </item>`;
498
+ }).join("\n");
499
+ return `<?xml version="1.0" encoding="UTF-8"?>
500
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
501
+ <channel>
502
+ <title>${escapeXml(siteName)}</title>
503
+ <description>${escapeXml(description)}</description>
504
+ <link>${escapeXml(siteUrl)}</link>
505
+ <language>${escapeXml(locale)}</language>
506
+ <lastBuildDate>${(/* @__PURE__ */ new Date()).toUTCString()}</lastBuildDate>
507
+ <atom:link href="${escapeXml(siteUrl)}/${escapeXml(locale)}/blog/feed.xml" rel="self" type="application/rss+xml"/>
508
+ ${itemsXml}
509
+ </channel>
510
+ </rss>`;
511
+ }
512
+ function escapeXml(str) {
513
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
514
+ }
515
+
354
516
  // src/lib/navigation/utils.ts
355
517
  function getNavigationString(localizedString, locale) {
356
518
  return localizedString[locale] || localizedString["en"] || "";
@@ -428,7 +590,7 @@ var defaultSlugTranslations = {
428
590
  "/anti-spam-policy": "/politique-anti-pourriel"
429
591
  }
430
592
  };
431
- function translateSlug(path2, fromLocale, toLocale, customTranslations) {
593
+ function translateSlug(path3, fromLocale, toLocale, customTranslations) {
432
594
  let configTranslations = {};
433
595
  try {
434
596
  const { getI18nConfig: getI18nConfig2 } = (init_config(), __toCommonJS(config_exports));
@@ -443,17 +605,17 @@ function translateSlug(path2, fromLocale, toLocale, customTranslations) {
443
605
  );
444
606
  const translationMap = translations[fromLocale];
445
607
  if (!translationMap) {
446
- return path2;
608
+ return path3;
447
609
  }
448
- if (translationMap[path2]) {
449
- return translationMap[path2];
610
+ if (translationMap[path3]) {
611
+ return translationMap[path3];
450
612
  }
451
613
  for (const [fromSlug, toSlug] of Object.entries(translationMap)) {
452
- if (path2.startsWith(fromSlug + "/")) {
453
- return path2.replace(fromSlug, toSlug);
614
+ if (path3.startsWith(fromSlug + "/")) {
615
+ return path3.replace(fromSlug, toSlug);
454
616
  }
455
617
  }
456
- return path2;
618
+ return path3;
457
619
  }
458
620
  function mergeTranslations(...translations) {
459
621
  const merged = {};
@@ -1282,7 +1444,7 @@ function generateSitemap(config) {
1282
1444
  const urlEntries = entries.map((entry) => {
1283
1445
  const parts = [];
1284
1446
  parts.push(`${indent}<url>${newline}`);
1285
- parts.push(`${indent}${indent}<loc>${escapeXml(entry.url)}</loc>${newline}`);
1447
+ parts.push(`${indent}${indent}<loc>${escapeXml2(entry.url)}</loc>${newline}`);
1286
1448
  if (entry.lastModified) {
1287
1449
  const date = entry.lastModified instanceof Date ? entry.lastModified.toISOString() : entry.lastModified;
1288
1450
  parts.push(`${indent}${indent}<lastmod>${date}</lastmod>${newline}`);
@@ -1297,9 +1459,9 @@ function generateSitemap(config) {
1297
1459
  if (entry.alternates && entry.alternates.length > 0) {
1298
1460
  entry.alternates.forEach((alternate) => {
1299
1461
  parts.push(
1300
- `${indent}${indent}<xhtml:link rel="alternate" hreflang="${escapeXml(
1462
+ `${indent}${indent}<xhtml:link rel="alternate" hreflang="${escapeXml2(
1301
1463
  alternate.hreflang
1302
- )}" href="${escapeXml(alternate.href)}" />${newline}`
1464
+ )}" href="${escapeXml2(alternate.href)}" />${newline}`
1303
1465
  );
1304
1466
  });
1305
1467
  }
@@ -1316,8 +1478,8 @@ function generateSitemap(config) {
1316
1478
  ].join("");
1317
1479
  return xml;
1318
1480
  }
1319
- function createMultiLanguageEntries(baseUrl, path2, locales2, defaultLocale2, options) {
1320
- const cleanPath = path2.startsWith("/") ? path2 : `/${path2}`;
1481
+ function createMultiLanguageEntries(baseUrl, path3, locales2, defaultLocale2, options) {
1482
+ const cleanPath = path3.startsWith("/") ? path3 : `/${path3}`;
1321
1483
  return locales2.map((locale) => {
1322
1484
  const alternates = locales2.map((altLocale) => ({
1323
1485
  hreflang: altLocale,
@@ -1336,15 +1498,15 @@ function createMultiLanguageEntries(baseUrl, path2, locales2, defaultLocale2, op
1336
1498
  };
1337
1499
  });
1338
1500
  }
1339
- function createSitemapEntry(baseUrl, path2, options) {
1340
- const cleanPath = path2.startsWith("/") ? path2 : `/${path2}`;
1341
- const url = path2 === "/" ? baseUrl : `${baseUrl}${cleanPath}`;
1501
+ function createSitemapEntry(baseUrl, path3, options) {
1502
+ const cleanPath = path3.startsWith("/") ? path3 : `/${path3}`;
1503
+ const url = path3 === "/" ? baseUrl : `${baseUrl}${cleanPath}`;
1342
1504
  return {
1343
1505
  url,
1344
1506
  ...options
1345
1507
  };
1346
1508
  }
1347
- function escapeXml(unsafe) {
1509
+ function escapeXml2(unsafe) {
1348
1510
  return unsafe.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1349
1511
  }
1350
1512
  function validateSitemapEntry(entry) {
@@ -1534,14 +1696,21 @@ export {
1534
1696
  formatNumber,
1535
1697
  formatRelativeTime,
1536
1698
  generateArticleMetadata,
1699
+ generateBlogRssFeed,
1537
1700
  generateDesignTokens,
1538
1701
  generateMetadata,
1539
1702
  generateSitemap,
1540
1703
  generateThemeCSS,
1704
+ getAllBlogPosts,
1541
1705
  getAllPolicies,
1706
+ getAllTags,
1542
1707
  getAlternateLocales,
1708
+ getBlogPostLocales,
1709
+ getBlogPostSlugs,
1710
+ getBlogPostsByTag,
1543
1711
  getDefaultLocale,
1544
1712
  getErrorMessage,
1713
+ getFeaturedBlogPosts,
1545
1714
  getFontConfig,
1546
1715
  getFontVariables,
1547
1716
  getI18nConfig,
@@ -1558,6 +1727,7 @@ export {
1558
1727
  getNavigationString,
1559
1728
  getPolicyLocales,
1560
1729
  getPolicySlugs,
1730
+ getRelatedBlogPosts,
1561
1731
  getRelativeTime,
1562
1732
  getRtlLocales,
1563
1733
  getTextDirection,
@@ -1566,6 +1736,7 @@ export {
1566
1736
  isLocaleDetectionEnabled,
1567
1737
  isRtlLocale,
1568
1738
  isSupportedLocale,
1739
+ loadBlogPost,
1569
1740
  loadPolicy,
1570
1741
  locales,
1571
1742
  matchLocale,