@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.
- package/dist/analytics/registry.d.ts +7 -0
- package/dist/analytics/registry.d.ts.map +1 -0
- package/dist/analytics/registry.js +11 -0
- package/dist/auth/useAuthSession.d.ts.map +1 -1
- package/dist/auth/useAuthSession.js +85 -1
- package/dist/cli.js +19 -3
- package/dist/components/LanguageSwitcher.d.ts.map +1 -1
- package/dist/components/LanguageSwitcher.js +89 -5
- package/dist/config/version.d.ts.map +1 -1
- package/dist/config/version.js +30 -19
- package/dist/i18n/useLink.d.ts.map +1 -1
- package/dist/i18n/useLink.js +15 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/layouts/AdminLayoutWithSidebar.d.ts +3 -1
- package/dist/layouts/AdminLayoutWithSidebar.d.ts.map +1 -1
- package/dist/layouts/AdminLayoutWithSidebar.js +2 -2
- package/dist/layouts/AppProviders.d.ts +7 -1
- package/dist/layouts/AppProviders.d.ts.map +1 -1
- package/dist/layouts/AppProviders.js +24 -3
- package/dist/layouts/AuthLayout.js +1 -1
- package/dist/layouts/PublicLayout.js +1 -1
- package/dist/layouts/RootLayout.d.ts.map +1 -1
- package/dist/scripts/init-app.d.ts.map +1 -1
- package/dist/scripts/init-app.js +301 -138
- package/dist/scripts/module-build.d.ts.map +1 -1
- package/dist/scripts/module-build.js +402 -67
- package/dist/scripts/module-create.d.ts.map +1 -1
- package/dist/scripts/module-create.js +227 -10
- package/dist/scripts/sitemap-flat-generator.d.ts +39 -0
- package/dist/scripts/sitemap-flat-generator.d.ts.map +1 -0
- package/dist/scripts/sitemap-flat-generator.js +231 -0
- package/dist/scripts/sitemap-manifest-generator.d.ts +59 -0
- package/dist/scripts/sitemap-manifest-generator.d.ts.map +1 -0
- package/dist/scripts/sitemap-manifest-generator.js +290 -0
- package/dist/sitemap/manifest.d.ts +8 -0
- package/dist/sitemap/manifest.d.ts.map +1 -0
- package/dist/sitemap/manifest.js +6 -0
- package/dist/styles.css +2 -2
- package/dist/templates/AuthGuidePage.js +2 -0
- package/dist/templates/DefaultDoc.d.ts.map +1 -1
- package/dist/templates/DefaultDoc.js +9 -5
- package/dist/templates/DocPage.d.ts.map +1 -1
- package/dist/templates/DocPage.js +40 -0
- package/dist/templates/MigrationsGuidePage.js +2 -0
- package/dist/templates/ModuleGuidePage.d.ts.map +1 -1
- package/dist/templates/ModuleGuidePage.js +4 -1
- package/dist/templates/SimpleHomePage.js +2 -0
- package/package.json +11 -4
- package/src/analytics/registry.ts +14 -0
- package/src/auth/useAuthSession.ts +91 -1
- package/src/cli.ts +19 -3
- package/src/components/LanguageSwitcher.tsx +113 -23
- package/src/config/version.ts +30 -19
- package/src/i18n/useLink.ts +15 -0
- package/src/index.ts +17 -0
- package/src/layouts/AdminLayoutWithSidebar.tsx +4 -0
- package/src/layouts/AppProviders.tsx +66 -8
- package/src/layouts/AuthLayout.tsx +1 -1
- package/src/layouts/PublicLayout.tsx +1 -1
- package/src/layouts/RootLayout.tsx +0 -1
- package/src/scripts/init-app.ts +360 -149
- package/src/scripts/module-build.ts +458 -72
- package/src/scripts/module-create.ts +260 -10
- package/src/scripts/sitemap-flat-generator.ts +313 -0
- package/src/scripts/sitemap-manifest-generator.ts +476 -0
- package/src/sitemap/manifest.ts +17 -0
- package/src/templates/AuthGuidePage.tsx +1 -1
- package/src/templates/DefaultDoc.tsx +397 -6
- package/src/templates/DocPage.tsx +40 -0
- package/src/templates/MigrationsGuidePage.tsx +1 -1
- package/src/templates/ModuleGuidePage.tsx +3 -2
- 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
|
|
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-", "")
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|