@lastbrain/app 2.0.31 → 2.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/analytics/registry.d.ts +7 -0
  2. package/dist/analytics/registry.d.ts.map +1 -0
  3. package/dist/analytics/registry.js +11 -0
  4. package/dist/auth/useAuthSession.d.ts.map +1 -1
  5. package/dist/auth/useAuthSession.js +85 -1
  6. package/dist/cli.js +19 -3
  7. package/dist/components/LanguageSwitcher.d.ts.map +1 -1
  8. package/dist/components/LanguageSwitcher.js +89 -5
  9. package/dist/config/version.d.ts.map +1 -1
  10. package/dist/config/version.js +30 -19
  11. package/dist/i18n/useLink.d.ts.map +1 -1
  12. package/dist/i18n/useLink.js +15 -0
  13. package/dist/index.d.ts +3 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +4 -0
  16. package/dist/layouts/AdminLayoutWithSidebar.d.ts +3 -1
  17. package/dist/layouts/AdminLayoutWithSidebar.d.ts.map +1 -1
  18. package/dist/layouts/AdminLayoutWithSidebar.js +2 -2
  19. package/dist/layouts/AppProviders.d.ts +7 -1
  20. package/dist/layouts/AppProviders.d.ts.map +1 -1
  21. package/dist/layouts/AppProviders.js +24 -3
  22. package/dist/layouts/AuthLayout.js +1 -1
  23. package/dist/layouts/PublicLayout.js +1 -1
  24. package/dist/layouts/RootLayout.d.ts.map +1 -1
  25. package/dist/scripts/init-app.d.ts.map +1 -1
  26. package/dist/scripts/init-app.js +301 -138
  27. package/dist/scripts/module-build.d.ts.map +1 -1
  28. package/dist/scripts/module-build.js +402 -67
  29. package/dist/scripts/module-create.d.ts.map +1 -1
  30. package/dist/scripts/module-create.js +227 -10
  31. package/dist/scripts/sitemap-flat-generator.d.ts +39 -0
  32. package/dist/scripts/sitemap-flat-generator.d.ts.map +1 -0
  33. package/dist/scripts/sitemap-flat-generator.js +231 -0
  34. package/dist/scripts/sitemap-manifest-generator.d.ts +59 -0
  35. package/dist/scripts/sitemap-manifest-generator.d.ts.map +1 -0
  36. package/dist/scripts/sitemap-manifest-generator.js +290 -0
  37. package/dist/sitemap/manifest.d.ts +8 -0
  38. package/dist/sitemap/manifest.d.ts.map +1 -0
  39. package/dist/sitemap/manifest.js +6 -0
  40. package/dist/styles.css +2 -2
  41. package/dist/templates/AuthGuidePage.js +2 -0
  42. package/dist/templates/DefaultDoc.d.ts.map +1 -1
  43. package/dist/templates/DefaultDoc.js +9 -5
  44. package/dist/templates/DocPage.d.ts.map +1 -1
  45. package/dist/templates/DocPage.js +40 -0
  46. package/dist/templates/MigrationsGuidePage.js +2 -0
  47. package/dist/templates/ModuleGuidePage.d.ts.map +1 -1
  48. package/dist/templates/ModuleGuidePage.js +4 -1
  49. package/dist/templates/SimpleHomePage.js +2 -0
  50. package/package.json +11 -4
  51. package/src/analytics/registry.ts +14 -0
  52. package/src/auth/useAuthSession.ts +91 -1
  53. package/src/cli.ts +19 -3
  54. package/src/components/LanguageSwitcher.tsx +113 -23
  55. package/src/config/version.ts +30 -19
  56. package/src/i18n/useLink.ts +15 -0
  57. package/src/index.ts +17 -0
  58. package/src/layouts/AdminLayoutWithSidebar.tsx +4 -0
  59. package/src/layouts/AppProviders.tsx +66 -8
  60. package/src/layouts/AuthLayout.tsx +1 -1
  61. package/src/layouts/PublicLayout.tsx +1 -1
  62. package/src/layouts/RootLayout.tsx +0 -1
  63. package/src/scripts/init-app.ts +360 -149
  64. package/src/scripts/module-build.ts +458 -72
  65. package/src/scripts/module-create.ts +260 -10
  66. package/src/scripts/sitemap-flat-generator.ts +313 -0
  67. package/src/scripts/sitemap-manifest-generator.ts +476 -0
  68. package/src/sitemap/manifest.ts +17 -0
  69. package/src/templates/AuthGuidePage.tsx +1 -1
  70. package/src/templates/DefaultDoc.tsx +397 -6
  71. package/src/templates/DocPage.tsx +40 -0
  72. package/src/templates/MigrationsGuidePage.tsx +1 -1
  73. package/src/templates/ModuleGuidePage.tsx +3 -2
  74. package/src/templates/SimpleHomePage.tsx +1 -1
@@ -50,13 +50,14 @@ function parsePagesList(input: string): string[] {
50
50
 
51
51
  /**
52
52
  * Slugifie un nom de page: minuscules, tirets, alphanum uniquement
53
+ * Préserve les crochets [] pour les routes dynamiques et les slashes / pour les sous-chemins
53
54
  */
54
55
  function slugifyPageName(name: string): string {
55
56
  return name
56
57
  .trim()
57
58
  .toLowerCase()
58
59
  .replace(/[\s_]+/g, "-")
59
- .replace(/[^a-z0-9-]/g, "")
60
+ .replace(/[^a-z0-9\-/[\]]/g, "") // Préserve -, /, [, ]
60
61
  .replace(/--+/g, "-")
61
62
  .replace(/^-+|-+$/g, "");
62
63
  }
@@ -121,7 +122,7 @@ function generatePackageJson(
121
122
  isPro: boolean = false
122
123
  ): string {
123
124
  const versions = getLastBrainPackageVersions(rootDir);
124
- const moduleNameOnly = slug.replace("module-", "").replace("-pro", "");
125
+ const moduleNameOnly = slug.replace("module-", "");
125
126
  const buildConfigExport = `./${moduleNameOnly}.build.config`;
126
127
  const packageJson: Record<string, unknown> = {
127
128
  name: moduleName,
@@ -403,7 +404,11 @@ function toPascalCase(value: string): string {
403
404
  .join("");
404
405
  }
405
406
 
406
- function generateIndexTs(pages: PageConfig[], moduleNameOnly: string): string {
407
+ function generateIndexTs(
408
+ pages: PageConfig[],
409
+ moduleNameOnly: string,
410
+ hasPublicPages: boolean
411
+ ): string {
407
412
  // Grouper les pages par nom pour détecter les doublons
408
413
  const pagesByName = new Map<string, PageConfig[]>();
409
414
  for (const page of pages) {
@@ -430,6 +435,11 @@ function generateIndexTs(pages: PageConfig[], moduleNameOnly: string): string {
430
435
  const cleanedNameOnly = moduleNameOnly.replace(/-pro$/, "");
431
436
  const moduleAlias = toPascalCase(cleanedNameOnly);
432
437
 
438
+ // Export du sitemap manifest si le module a des pages publiques
439
+ const sitemapExport = hasPublicPages
440
+ ? `\n// Sitemap configuration\nexport { sitemapManifest } from "./sitemap/manifest";\n`
441
+ : "";
442
+
433
443
  return `// Client Components
434
444
  ${exports.join("\n")}
435
445
 
@@ -438,7 +448,7 @@ export { Doc } from "./components/Doc";
438
448
  export { Doc as ${moduleAlias}ModuleDoc } from "./components/Doc";
439
449
 
440
450
  // Configuration de build
441
- export { default as buildConfig } from "./${moduleNameOnly}.build.config";
451
+ export { default as buildConfig } from "./${moduleNameOnly}.build.config";${sitemapExport}
442
452
  `;
443
453
  }
444
454
 
@@ -1419,6 +1429,184 @@ export function findWorkspaceRoot(): string {
1419
1429
  );
1420
1430
  }
1421
1431
 
1432
+ /**
1433
+ * Génère le fichier manifest.ts pour les sitemaps
1434
+ */
1435
+ function generateSitemapManifest(moduleName: string): string {
1436
+ return `/**
1437
+ * Types pour le système de sitemaps multi-modules
1438
+ */
1439
+ export type SitemapChildKind = "static" | "reexport";
1440
+
1441
+ export interface SitemapChild {
1442
+ id: string;
1443
+ path: string;
1444
+ kind: SitemapChildKind;
1445
+ handler?: string;
1446
+ paging?: {
1447
+ pageSize: number;
1448
+ };
1449
+ }
1450
+
1451
+ export interface SitemapManifest {
1452
+ module: string;
1453
+ enabled: boolean;
1454
+ includePublicPagesFromBuildConfig: boolean;
1455
+ children: SitemapChild[];
1456
+ }
1457
+
1458
+ /**
1459
+ * Manifest des sitemaps du module ${moduleName}
1460
+ * Déclare tous les sitemaps enfants à générer
1461
+ *
1462
+ * Configuration:
1463
+ * - enabled: Active/désactive le module dans les sitemaps globaux
1464
+ * - includePublicPagesFromBuildConfig: Inclut automatiquement les pages publiques du build.config
1465
+ * - children: Liste des sitemaps à générer
1466
+ *
1467
+ * Types de sitemaps:
1468
+ * 1. "static": Pages statiques générées depuis build.config (sans paramètres dynamiques)
1469
+ * 2. "reexport": Sitemaps dynamiques avec handler personnalisé (avec paramètres :lang, :type, etc.)
1470
+ */
1471
+ export const sitemapManifest: SitemapManifest = {
1472
+ module: "${moduleName}",
1473
+ enabled: true,
1474
+ includePublicPagesFromBuildConfig: true,
1475
+ children: [
1476
+ // Pages statiques (générées automatiquement depuis build.config)
1477
+ {
1478
+ id: "static",
1479
+ path: ":lang/static.xml",
1480
+ kind: "static",
1481
+ },
1482
+
1483
+ // EXEMPLE: Sitemap dynamique avec handler personnalisé
1484
+ // Décommentez et adaptez selon vos besoins:
1485
+
1486
+ // {
1487
+ // id: "dynamic-content",
1488
+ // path: ":lang/content.xml",
1489
+ // kind: "reexport",
1490
+ // handler: "${moduleName}/sitemap/handlers/content",
1491
+ // },
1492
+ ],
1493
+ } as const;
1494
+ `;
1495
+ }
1496
+
1497
+ /**
1498
+ * Génère un exemple de handler de sitemap dynamique
1499
+ */
1500
+ function generateSitemapHandler(moduleName: string): string {
1501
+ return `/**
1502
+ * EXEMPLE: Handler dynamique pour sitemap
1503
+ *
1504
+ * Ce fichier montre comment créer un sitemap dynamique qui génère
1505
+ * des URLs à partir de données (base de données, API, liste statique, etc.)
1506
+ *
1507
+ * Pour activer ce handler:
1508
+ * 1. Décommentez l'entrée correspondante dans manifest.ts
1509
+ * 2. Adaptez la logique ci-dessous à vos besoins
1510
+ * 3. Rebuild le module: pnpm --filter ${moduleName} build
1511
+ */
1512
+
1513
+ import { LOCALE_MAP } from "@lastbrain/core";
1514
+ // Pour récupérer des données depuis Supabase:
1515
+ // import { getSupabaseServiceClient } from "@lastbrain/core/server";
1516
+
1517
+ /**
1518
+ * Handler GET pour générer le sitemap XML
1519
+ *
1520
+ * @param _request - Requête HTTP (non utilisée ici)
1521
+ * @param context - Contient les paramètres dynamiques de la route (:lang, :type, etc.)
1522
+ * @returns Response avec le XML du sitemap
1523
+ */
1524
+ export async function GET(
1525
+ _request: Request,
1526
+ context: { params: Promise<{ lang: string }> }
1527
+ ): Promise<Response> {
1528
+ const params = await context.params;
1529
+ const { lang } = params;
1530
+
1531
+ // Validation de la langue
1532
+ if (!LOCALE_MAP[lang]) {
1533
+ return new Response("Invalid language", { status: 404 });
1534
+ }
1535
+
1536
+ // Configuration de l'URL de base
1537
+ const baseUrl =
1538
+ process.env.NEXT_PUBLIC_SITE_URL ||
1539
+ (process.env.VERCEL_URL
1540
+ ? \`https://\${process.env.VERCEL_URL}\`
1541
+ : "https://example.com");
1542
+
1543
+ try {
1544
+ // EXEMPLE 1: Liste statique d'URLs
1545
+ const staticItems = [
1546
+ { slug: "item-1", updatedAt: new Date().toISOString() },
1547
+ { slug: "item-2", updatedAt: new Date().toISOString() },
1548
+ { slug: "item-3", updatedAt: new Date().toISOString() },
1549
+ ];
1550
+
1551
+ // EXEMPLE 2: Récupération depuis Supabase (décommentez si besoin)
1552
+ /*
1553
+ const supabase = getSupabaseServiceClient();
1554
+ const { data: items, error } = await supabase
1555
+ .from("your_table")
1556
+ .select("slug, updated_at")
1557
+ .eq("status", "published")
1558
+ .order("updated_at", { ascending: false })
1559
+ .limit(1000);
1560
+
1561
+ if (error) {
1562
+ console.error("Sitemap error:", error);
1563
+ return new Response(JSON.stringify({ error: error.message }), {
1564
+ status: 500,
1565
+ headers: { "Content-Type": "application/json" },
1566
+ });
1567
+ }
1568
+ */
1569
+
1570
+ // Génération des URLs pour le sitemap
1571
+ const urls = staticItems.map((item) => ({
1572
+ url: \`\${baseUrl}/\${lang}/your-section/\${item.slug}\`,
1573
+ lastmod: item.updatedAt,
1574
+ changefreq: "weekly",
1575
+ priority: 0.7,
1576
+ }));
1577
+
1578
+ // Construction du XML
1579
+ const urlEntries = urls
1580
+ .map(
1581
+ (entry) => \` <url>
1582
+ <loc>\${entry.url}</loc>
1583
+ <lastmod>\${entry.lastmod}</lastmod>
1584
+ <changefreq>\${entry.changefreq}</changefreq>
1585
+ <priority>\${entry.priority}</priority>
1586
+ </url>\`
1587
+ )
1588
+ .join("\\n");
1589
+
1590
+ const xml = \`<?xml version="1.0" encoding="UTF-8"?>
1591
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1592
+ \${urlEntries}
1593
+ </urlset>\`;
1594
+
1595
+ // Retour de la réponse XML
1596
+ return new Response(xml, {
1597
+ headers: {
1598
+ "Content-Type": "application/xml",
1599
+ "Cache-Control": "public, max-age=3600",
1600
+ },
1601
+ });
1602
+ } catch (err) {
1603
+ console.error("Sitemap generation error:", err);
1604
+ return new Response("Internal error", { status: 500 });
1605
+ }
1606
+ }
1607
+ `;
1608
+ }
1609
+
1422
1610
  /**
1423
1611
  * Crée la structure du module
1424
1612
  */
@@ -1464,11 +1652,14 @@ export async function createModuleStructure(
1464
1652
  generateBuildConfig(config)
1465
1653
  );
1466
1654
 
1655
+ // Détecter si le module a des pages publiques (pour l'export sitemap)
1656
+ const hasPublicPages = config.pages.some((p) => p.section === "public");
1657
+
1467
1658
  // Créer index.ts
1468
1659
  console.log(chalk.yellow(" 📄 src/index.ts"));
1469
1660
  await fs.writeFile(
1470
1661
  path.join(moduleDir, "src", "index.ts"),
1471
- generateIndexTs(config.pages, moduleNameOnly)
1662
+ generateIndexTs(config.pages, moduleNameOnly, hasPublicPages)
1472
1663
  );
1473
1664
 
1474
1665
  // Note: server.ts n'est plus généré pour éviter les conflits d'exports
@@ -1494,24 +1685,48 @@ export async function createModuleStructure(
1494
1685
  }
1495
1686
 
1496
1687
  for (const page of config.pages) {
1497
- const pagePath = path.join(moduleDir, "src", "web", page.section);
1688
+ // Extraire le chemin du répertoire et le nom de base de la page
1689
+ const pageNameParts = page.name.split("/");
1690
+ const pageBaseName = pageNameParts[pageNameParts.length - 1];
1691
+
1692
+ // Créer le chemin complet incluant les sous-répertoires avec leurs noms originaux
1693
+ // Garder les noms tels quels (ex: "apis", "[slug]" etc.) pour les répertoires
1694
+ const subdirParts = pageNameParts.slice(0, -1);
1695
+ const pagePath =
1696
+ subdirParts.length > 0
1697
+ ? path.join(moduleDir, "src", "web", page.section, ...subdirParts)
1698
+ : path.join(moduleDir, "src", "web", page.section);
1699
+
1498
1700
  await fs.ensureDir(pagePath);
1499
1701
 
1500
- const baseName = page.name
1702
+ // Le nom de fichier suit la convention Next.js : garder les crochets
1703
+ // apis/[slug] → apis/[slug].tsx
1704
+ // apis/[id]/versions → apis/[id]/versions.tsx
1705
+ const fileName = `${pageBaseName}.tsx`;
1706
+
1707
+ // Pour le nom du composant React, on enlève les crochets
1708
+ // [slug] → Slug, [id] → Id
1709
+ const cleanedBaseName = pageBaseName.replace(/[[\]]/g, "");
1710
+
1711
+ const baseName = cleanedBaseName
1501
1712
  .split("-")
1502
1713
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1503
1714
  .join("");
1504
1715
 
1505
- // Si le même nom de page existe dans plusieurs sections, ajouter la section au nom du fichier
1716
+ // Si le même nom de page existe dans plusieurs sections, ajouter la section au nom du composant
1506
1717
  const duplicates = pagesByName.get(page.name) || [];
1507
1718
  const hasDuplicates = duplicates.length > 1;
1508
1719
  const componentNameWithSection = hasDuplicates
1509
1720
  ? `${baseName}${toPascalCase(page.section)}`
1510
1721
  : baseName;
1511
1722
 
1512
- const fileName = `${componentNameWithSection}Page.tsx`;
1723
+ // Afficher le chemin complet
1724
+ const fullDisplayPath =
1725
+ subdirParts.length > 0
1726
+ ? `src/web/${page.section}/${subdirParts.join("/")}/${fileName}`
1727
+ : `src/web/${page.section}/${fileName}`;
1728
+ console.log(chalk.yellow(` 📄 ${fullDisplayPath}`));
1513
1729
 
1514
- console.log(chalk.yellow(` 📄 src/web/${page.section}/${fileName}`));
1515
1730
  await fs.writeFile(
1516
1731
  path.join(pagePath, fileName),
1517
1732
  generatePageComponent(page.name, page.section, componentNameWithSection)
@@ -1567,6 +1782,41 @@ export async function createModuleStructure(
1567
1782
  );
1568
1783
  }
1569
1784
 
1785
+ // Créer les fichiers sitemap si le module a des pages publiques
1786
+ if (hasPublicPages) {
1787
+ console.log(chalk.blue("\n🗺️ Création des fichiers sitemap..."));
1788
+
1789
+ const sitemapDir = path.join(moduleDir, "src", "sitemap");
1790
+ const handlersDir = path.join(sitemapDir, "handlers");
1791
+ await fs.ensureDir(handlersDir);
1792
+
1793
+ // manifest.ts
1794
+ console.log(chalk.yellow(" 📄 src/sitemap/manifest.ts"));
1795
+ await fs.writeFile(
1796
+ path.join(sitemapDir, "manifest.ts"),
1797
+ generateSitemapManifest(config.slug.replace("module-", ""))
1798
+ );
1799
+
1800
+ // Exemple de handler dynamique (commenté par défaut)
1801
+ console.log(
1802
+ chalk.yellow(" 📄 src/sitemap/handlers/example.ts (commented)")
1803
+ );
1804
+ await fs.writeFile(
1805
+ path.join(handlersDir, "example.ts"),
1806
+ generateSitemapHandler(config.moduleName)
1807
+ );
1808
+
1809
+ console.log(chalk.green(" ✓ Fichiers sitemap créés"));
1810
+ console.log(
1811
+ chalk.gray(" → Éditez manifest.ts pour personnaliser vos sitemaps")
1812
+ );
1813
+ console.log(
1814
+ chalk.gray(
1815
+ " → Décommentez handlers/example.ts pour ajouter des sitemaps dynamiques"
1816
+ )
1817
+ );
1818
+ }
1819
+
1570
1820
  // Générer la documentation du module
1571
1821
  console.log(chalk.blue("\n📝 Génération de la documentation..."));
1572
1822
  await generateModuleReadme(config, moduleDir);
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Générateur de sitemap PLAT (pas d'imbrication d'index)
3
+ *
4
+ * PRINCIPE:
5
+ * - /sitemap.xml = unique index racine
6
+ * - /sitemap/{module}/{lang}/{type}.xml = sitemaps de contenu (urlset)
7
+ * - PAS d'index intermédiaire /sitemap/{module}/sitemap.xml
8
+ *
9
+ * Conforme aux recommandations Google Search Console :
10
+ * "Un sitemap index ne doit pas référencer un autre sitemap index"
11
+ */
12
+
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import type { ModuleBuildConfig } from "../index.js";
16
+
17
+ interface SitemapEntry {
18
+ path: string;
19
+ module: string;
20
+ kind: string; // "static", "dynamic", etc.
21
+ }
22
+
23
+ interface MenuIgnored {
24
+ public: { title: string; path: string }[];
25
+ auth: { title: string; path: string }[];
26
+ }
27
+
28
+ /**
29
+ * Charge le fichier menu-ignored.ts de l'app si disponible
30
+ */
31
+ function loadMenuIgnored(appDirectory: string): string[] {
32
+ try {
33
+ const menuIgnoredPath = path.join(
34
+ appDirectory,
35
+ "..",
36
+ "config",
37
+ "menu-ignored.ts"
38
+ );
39
+ if (!fs.existsSync(menuIgnoredPath)) {
40
+ return [];
41
+ }
42
+
43
+ const content = fs.readFileSync(menuIgnoredPath, "utf-8");
44
+ const ignoredPaths: string[] = [];
45
+
46
+ // Parser simple pour extraire les paths du fichier
47
+ const pathMatches = content.matchAll(/path:\s*["']([^"']+)["']/g);
48
+ for (const match of pathMatches) {
49
+ ignoredPaths.push(match[1]);
50
+ }
51
+
52
+ return ignoredPaths;
53
+ } catch (error) {
54
+ return [];
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Vérifie si un sitemap correspond à une URL ignorée
60
+ */
61
+ function isSitemapIgnored(
62
+ sitemapPath: string,
63
+ ignoredPaths: string[]
64
+ ): boolean {
65
+ // Extraire le chemin de base du sitemap (sans /sitemap/{module}/{lang}/)
66
+ // Ex: /sitemap/recipes-pro/fr/static.xml contient les pages comme /fr/...
67
+
68
+ for (const ignoredPath of ignoredPaths) {
69
+ // Si le sitemap contient une page ignorée, on l'exclut
70
+ // Note: cette logique peut être affinée selon vos besoins
71
+ if (sitemapPath.includes("static.xml")) {
72
+ // Les sitemaps statiques peuvent contenir des pages ignorées
73
+ // On garde tous les sitemaps statiques pour l'instant
74
+ // car ils peuvent contenir d'autres pages non ignorées
75
+ continue;
76
+ }
77
+ }
78
+
79
+ return false;
80
+ }
81
+
82
+ /**
83
+ * Collecte tous les sitemaps enfants (urlset) de tous les modules
84
+ * Sans créer d'index intermédiaires
85
+ */
86
+ export function collectAllContentSitemaps(
87
+ moduleManifests: Array<{
88
+ config: ModuleBuildConfig;
89
+ manifest: any;
90
+ }>,
91
+ languages: string[],
92
+ appDirectory?: string
93
+ ): SitemapEntry[] {
94
+ const entries: SitemapEntry[] = [];
95
+
96
+ for (const { manifest } of moduleManifests) {
97
+ if (!manifest.enabled) continue;
98
+
99
+ // Exclure le module auth (routes bloquées pour l'exploration)
100
+ if (manifest.module === "auth") continue;
101
+
102
+ for (const child of manifest.children || []) {
103
+ if (child.kind === "static") {
104
+ // Générer une entrée par langue
105
+ for (const lang of languages) {
106
+ const childPath = child.path.replace(":lang", lang);
107
+ const fullPath = childPath.startsWith("/")
108
+ ? childPath
109
+ : `/${childPath}`;
110
+
111
+ // Ajouter le préfixe du module pour éviter les conflits
112
+ const modulePath = `/sitemap/${manifest.module}${fullPath}`;
113
+
114
+ entries.push({
115
+ path: modulePath,
116
+ module: manifest.module,
117
+ kind: child.kind,
118
+ });
119
+ }
120
+ } else if (child.kind === "reexport") {
121
+ // Sitemap dynamique - gérer les patterns :lang et :type
122
+
123
+ // Vérifier si le path contient d'autres paramètres que :lang
124
+ // (comme :type, :id, etc.) - ces sitemaps ne doivent PAS être dans l'index
125
+ if (
126
+ child.path.includes(":") &&
127
+ child.path.replace(":lang", "").includes(":")
128
+ ) {
129
+ // Skip les sitemaps avec des paramètres autres que :lang
130
+ // Exemple: :lang/:type/categories.xml contient :type qui n'est pas géré ici
131
+ continue;
132
+ }
133
+
134
+ if (child.path.includes(":lang")) {
135
+ // Générer pour chaque langue
136
+ for (const lang of languages) {
137
+ const childPath = child.path.replace(":lang", lang);
138
+ const fullPath = childPath.startsWith("/")
139
+ ? childPath
140
+ : `/${childPath}`;
141
+
142
+ // Ajouter le préfixe du module
143
+ const modulePath = `/sitemap/${manifest.module}${fullPath}`;
144
+
145
+ entries.push({
146
+ path: modulePath,
147
+ module: manifest.module,
148
+ kind: child.kind,
149
+ });
150
+ }
151
+ } else {
152
+ // Pas de paramètre langue - path statique
153
+ const fullPath = child.path.startsWith("/")
154
+ ? child.path
155
+ : `/${child.path}`;
156
+
157
+ // Ajouter le préfixe du module
158
+ const modulePath = `/sitemap/${manifest.module}${fullPath}`;
159
+
160
+ entries.push({
161
+ path: modulePath,
162
+ module: manifest.module,
163
+ kind: child.kind,
164
+ });
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ // Filtrer les sitemaps selon menu-ignored.ts si appDirectory est fourni
171
+ if (appDirectory) {
172
+ const ignoredPaths = loadMenuIgnored(appDirectory);
173
+ if (ignoredPaths.length > 0) {
174
+ // Pour l'instant, on ne filtre pas au niveau du sitemap index
175
+ // car les sitemaps peuvent contenir un mix de pages ignorées et non ignorées
176
+ // Le filtrage devrait idéalement se faire au niveau des handlers de sitemap individuels
177
+ }
178
+ }
179
+
180
+ return entries;
181
+ }
182
+
183
+ /**
184
+ * Génère le sitemap.xml racine PLAT qui liste directement tous les sitemaps de contenu
185
+ * SANS passer par des index intermédiaires
186
+ */
187
+ export function generateFlatGlobalSitemapIndex(
188
+ appDirectory: string,
189
+ entries: SitemapEntry[],
190
+ isDebugMode: boolean
191
+ ) {
192
+ const globalSitemapDir = path.join(appDirectory, "sitemap.xml");
193
+ const globalSitemapFile = path.join(globalSitemapDir, "route.ts");
194
+
195
+ if (!fs.existsSync(globalSitemapDir)) {
196
+ fs.mkdirSync(globalSitemapDir, { recursive: true });
197
+ }
198
+
199
+ // Générer les entrées XML
200
+ const sitemapEntries = entries
201
+ .map(
202
+ (entry) => ` <sitemap>
203
+ <loc>\${baseUrl}${entry.path}</loc>
204
+ <lastmod>\${new Date().toISOString()}</lastmod>
205
+ </sitemap>`
206
+ )
207
+ .join("\n");
208
+
209
+ const content = `// GENERATED BY LASTBRAIN MODULE BUILD - FLAT Global Sitemap Index
210
+ // Lists ALL content sitemaps directly (NO nested indexes)
211
+ // Total: ${entries.length} sitemap(s)
212
+ // Modules: ${Array.from(new Set(entries.map((e) => e.module))).join(", ")}
213
+
214
+ export async function GET(): Promise<Response> {
215
+ const baseUrl =
216
+ process.env.NEXT_PUBLIC_SITE_URL ||
217
+ (process.env.VERCEL_URL
218
+ ? \`https://\${process.env.VERCEL_URL}\`
219
+ : "https://example.com");
220
+
221
+ const xml = \`<?xml version="1.0" encoding="UTF-8"?>
222
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
223
+ ${sitemapEntries}
224
+ </sitemapindex>\`;
225
+
226
+ return new Response(xml, {
227
+ headers: {
228
+ "Content-Type": "application/xml",
229
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
230
+ },
231
+ });
232
+ }
233
+ `;
234
+
235
+ fs.writeFileSync(globalSitemapFile, content);
236
+
237
+ if (isDebugMode) {
238
+ console.log(
239
+ `🌍 Generated FLAT global sitemap index with ${entries.length} content sitemaps`
240
+ );
241
+ console.log(
242
+ ` Modules: ${Array.from(new Set(entries.map((e) => e.module))).join(", ")}`
243
+ );
244
+ }
245
+
246
+ return globalSitemapFile;
247
+ }
248
+
249
+ /**
250
+ * Valide qu'aucun sitemap de contenu ne contient de référence circulaire ou imbrication
251
+ */
252
+ export function validateNoNestedIndexes(
253
+ appDirectory: string,
254
+ entries: SitemapEntry[],
255
+ isDebugMode: boolean
256
+ ): { valid: boolean; errors: string[] } {
257
+ const errors: string[] = [];
258
+
259
+ // Vérifier qu'aucune entrée ne pointe vers un sitemap.xml (qui serait un index)
260
+ for (const entry of entries) {
261
+ if (entry.path.endsWith("/sitemap.xml")) {
262
+ errors.push(
263
+ `❌ NESTED INDEX DETECTED: ${entry.path} (module: ${entry.module})`
264
+ );
265
+ }
266
+
267
+ // Vérifier qu'aucune entrée ne pointe vers la racine /sitemap.xml
268
+ if (entry.path === "/sitemap.xml") {
269
+ errors.push(
270
+ `❌ CIRCULAR REFERENCE: ${entry.path} points to root sitemap (module: ${entry.module})`
271
+ );
272
+ }
273
+ }
274
+
275
+ // Vérifier les fichiers générés pour s'assurer qu'ils sont bien des urlset et pas des sitemapindex
276
+ for (const entry of entries) {
277
+ const segments = entry.path
278
+ .replace(/^\//, "")
279
+ .replace(/\.xml$/, ".xml")
280
+ .split("/");
281
+ const routeDir = path.join(appDirectory, ...segments);
282
+ const routeFile = path.join(routeDir, "route.ts");
283
+
284
+ if (fs.existsSync(routeFile)) {
285
+ const content = fs.readFileSync(routeFile, "utf-8");
286
+
287
+ // Vérifier si le fichier contient "sitemapindex" (mauvais)
288
+ if (content.includes("<sitemapindex")) {
289
+ errors.push(
290
+ `❌ FILE IS INDEX NOT URLSET: ${entry.path} contains <sitemapindex> (module: ${entry.module})`
291
+ );
292
+ }
293
+
294
+ // Vérifier s'il contient bien "urlset" (bon)
295
+ if (!content.includes("<urlset") && !content.includes("sitemapindex")) {
296
+ // Peut être un re-export, c'est OK
297
+ if (isDebugMode) {
298
+ console.log(` ℹ️ ${entry.path} is a re-export (OK)`);
299
+ }
300
+ }
301
+ }
302
+ }
303
+
304
+ if (errors.length > 0 && isDebugMode) {
305
+ console.error("\n🚨 SITEMAP VALIDATION ERRORS:");
306
+ errors.forEach((err) => console.error(err));
307
+ }
308
+
309
+ return {
310
+ valid: errors.length === 0,
311
+ errors,
312
+ };
313
+ }