@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
@@ -1,6 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createRequire } from "node:module";
4
+ import { generateManifestBasedSitemaps } from "./sitemap-manifest-generator.js";
5
+ const LOCALE_MAP = {
6
+ fr: "fr_FR",
7
+ en: "en_US",
8
+ es: "es_ES",
9
+ };
4
10
  // Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
5
11
  const projectRoot = process.env.PROJECT_ROOT || process.cwd();
6
12
  // Si on est dans une app, monter jusqu'à la racine du monorepo
@@ -186,7 +192,7 @@ function buildPage(moduleConfig, page) {
186
192
  page.path.includes("users/[id]") &&
187
193
  page.componentExport === "UserPage";
188
194
  // Détecter les pages légales qui utilisent cookies (privacy, terms, returns)
189
- const isLegalPage = page.section === "public" &&
195
+ const _isLegalPage = page.section === "public" &&
190
196
  (page.path.includes("privacy") ||
191
197
  page.path.includes("terms") ||
192
198
  page.path.includes("returns"));
@@ -196,6 +202,7 @@ function buildPage(moduleConfig, page) {
196
202
  // On importe directement depuis app/config au lieu de passer via props
197
203
  content = `// GENERATED BY LASTBRAIN MODULE BUILD
198
204
  import { UserDetailPage } from "${moduleConfig.moduleName}";
205
+ import { logger } from "@lastbrain/core";
199
206
 
200
207
  interface UserPageProps { params: Promise<{ id: string }> }
201
208
 
@@ -205,7 +212,7 @@ async function getModuleUserTabs() {
205
212
  const { moduleUserTabs } = await import("../../../../../../config/user-tabs");
206
213
  return moduleUserTabs || [];
207
214
  } catch (e) {
208
- console.warn("[user-detail-wrapper] erreur chargement user-tabs", e);
215
+ logger.warn("[user-detail-wrapper] erreur chargement user-tabs", e);
209
216
  return [];
210
217
  }
211
218
  }
@@ -228,17 +235,6 @@ const ${page.componentExport} = dynamic(
228
235
  { ssr: false }
229
236
  );
230
237
 
231
- export default function ${wrapperName}${hasDynamicParams ? "(props: Record<string, unknown>)" : "()"} {
232
- return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
233
- }
234
- `;
235
- }
236
- else if (isLegalPage) {
237
- content = `// GENERATED BY LASTBRAIN MODULE BUILD
238
- import { ${page.componentExport} } from "${moduleConfig.moduleName}";
239
-
240
- export const dynamic = 'force-dynamic';
241
-
242
238
  export default function ${wrapperName}${hasDynamicParams ? "(props: Record<string, unknown>)" : "()"} {
243
239
  return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
244
240
  }
@@ -256,7 +252,7 @@ export default function ${wrapperName}${hasDynamicParams ? "(props: Record<strin
256
252
  .filter((seg) => seg.startsWith("[") && seg.endsWith("]"))
257
253
  .map((seg) => seg.slice(1, -1)); // Enlève les []
258
254
  // Détecter si le module path lui-même a des params dynamiques (pas seulement [lang])
259
- const moduleHasDynamicParams = segments.some((seg) => seg.startsWith("[") && seg.endsWith("]"));
255
+ const _moduleHasDynamicParams = segments.some((seg) => seg.startsWith("[") && seg.endsWith("]"));
260
256
  // Générer le type des params si nécessaire
261
257
  const paramsType = dynamicParamNames.length > 0
262
258
  ? `{ ${dynamicParamNames.map((name) => `${name}: string`).join("; ")} }`
@@ -437,6 +433,7 @@ export interface MenuItem {
437
433
  shortcut?: string;
438
434
  shortcutDisplay?: string;
439
435
  type?: 'text' | 'icon' | 'textIcon';
436
+ keyboardOnly?: boolean;
440
437
  position?: 'center' | 'end';
441
438
  component?: React.ComponentType<any>;
442
439
  componentExport?: string;
@@ -537,7 +534,7 @@ function copyModuleMigrations(moduleConfigs) {
537
534
  fs.writeFileSync(modulesJsonPath, JSON.stringify(modulesConfig, null, 2));
538
535
  }
539
536
  function generateDocsPage(moduleConfigs) {
540
- const docsDir = path.join(appDirectory, "docs");
537
+ const docsDir = path.join(appDirectory, "[lang]", "(public)", "docs");
541
538
  ensureDirectory(docsDir);
542
539
  const docsPagePath = path.join(docsDir, "page.tsx");
543
540
  // Charger tous les modules depuis modules.json (actifs ET inactifs)
@@ -557,13 +554,13 @@ function generateDocsPage(moduleConfigs) {
557
554
  const moduleConfigurations = [];
558
555
  allModules.forEach((moduleEntry) => {
559
556
  const moduleName = moduleEntry.package;
560
- // Extraire le nom du module sans le scope et sans "module-"
557
+ // Extraire le nom du module sans le scope et sans "-pro"
561
558
  // Ex: @lastbrain/module-auth -> auth
562
- // Ex: @lastbrain-labs/module-recipes-pro -> recipes
559
+ // Ex: @lastbrain-labs/module-metrics-pro -> metrics
563
560
  const moduleId = moduleName
564
561
  .replace("@lastbrain-labs/module-", "")
565
562
  .replace("@lastbrain/module-", "")
566
- .replace(/-pro$/, ""); // Retirer le suffix -pro pour avoir le nom de base
563
+ .replace(/-pro$/, ""); // Retirer le suffixe -pro
567
564
  const docComponentName = `${toPascalCase(moduleId)}ModuleDoc`;
568
565
  // Trouver la config du module pour obtenir la description
569
566
  const moduleConfig = moduleConfigs.find((mc) => mc.moduleName === moduleName);
@@ -606,6 +603,43 @@ ${moduleConfigurations.join(",\n")}
606
603
  if (isDebugMode) {
607
604
  console.log(`📚 Generated docs page: ${docsPagePath}`);
608
605
  }
606
+ // Generate docs layout with translation support
607
+ const docsLayoutPath = path.join(docsDir, "layout.tsx");
608
+ const docsLayoutContent = `// Auto-generated docs layout with translation support
609
+ import { loadTranslations } from "@lastbrain/app/i18n/server-lang";
610
+ import { ClientLayout } from "../../../../components/ClientLayout";
611
+ import { headers } from "next/headers";
612
+
613
+ // Extract language from request headers
614
+ async function getLanguageFromRequest(): Promise<string> {
615
+ const headersList = await headers();
616
+ const acceptLanguage = headersList.get("accept-language") || "";
617
+ // Detect if French is requested
618
+ if (acceptLanguage.toLowerCase().includes("fr")) {
619
+ return "fr";
620
+ }
621
+ return "en";
622
+ }
623
+
624
+ export default async function DocsLayout({
625
+ children,
626
+ }: {
627
+ children: React.ReactNode;
628
+ }) {
629
+ const lang = await getLanguageFromRequest();
630
+ const translations = await loadTranslations(lang);
631
+
632
+ return (
633
+ <ClientLayout lang={lang} translations={translations}>
634
+ <div className="min-h-screen pt-12">{children}</div>
635
+ </ClientLayout>
636
+ );
637
+ }
638
+ `;
639
+ fs.writeFileSync(docsLayoutPath, docsLayoutContent);
640
+ if (isDebugMode) {
641
+ console.log(`📚 Generated docs layout: ${docsLayoutPath}`);
642
+ }
609
643
  }
610
644
  function buildGroupedApi(apis, routePath) {
611
645
  const segments = routePath.replace(/^\/+/, "").split("/").filter(Boolean);
@@ -657,7 +691,8 @@ function cleanGeneratedFiles() {
657
691
  "page.tsx", // Page racine seulement
658
692
  "admin/page.tsx", // Page admin racine
659
693
  "admin/layout.tsx", // Layout admin racine
660
- "docs/page.tsx", // Page docs générée
694
+ "[lang]/(public)/docs/page.tsx", // Page docs générée
695
+ "[lang]/(public)/docs/layout.tsx", // Layout docs généré
661
696
  // Middleware et autres fichiers core
662
697
  "middleware.ts",
663
698
  // Dossiers de lib et config
@@ -727,7 +762,14 @@ function cleanGeneratedFiles() {
727
762
  }
728
763
  };
729
764
  // Nettoyer les dossiers de sections
730
- const sectionsToClean = ["(public)", "auth", "admin", "api", "[lang]"];
765
+ const sectionsToClean = [
766
+ "(public)",
767
+ "auth",
768
+ "admin",
769
+ "api",
770
+ "[lang]",
771
+ "sitemap",
772
+ ];
731
773
  sectionsToClean.forEach((section) => {
732
774
  const sectionPath = path.join(appDirectory, section);
733
775
  if (fs.existsSync(sectionPath)) {
@@ -796,7 +838,7 @@ function cleanOldRoutesWithoutLang() {
796
838
  }
797
839
  });
798
840
  }
799
- function generateAppAside() {
841
+ function _generateAppAside() {
800
842
  const targetPath = path.join(appDirectory, "components", "AppAside.tsx");
801
843
  const templateContent = `"use client";
802
844
 
@@ -828,9 +870,11 @@ export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
828
870
  console.error(`❌ Error generating AppAside component: ${error}`);
829
871
  }
830
872
  }
831
- function generateLayouts() {
832
- // Générer ClientLayout wrapper client
833
- const clientLayoutPath = path.join(appDirectory, "components", "ClientLayout.tsx");
873
+ function generateLayouts(moduleConfigs) {
874
+ // Vérifier si le module billing-pro est actif
875
+ const hasBillingModule = moduleConfigs.some((config) => config.moduleName === "@lastbrain-labs/module-billing-pro");
876
+ // Générer ClientLayout wrapper client (dans components/ racine)
877
+ const clientLayoutPath = path.join(projectRoot, "components", "ClientLayout.tsx");
834
878
  const clientLayoutContent = `"use client";
835
879
 
836
880
  import { HeroUIProvider } from "@heroui/system";
@@ -878,17 +922,26 @@ export function ClientLayout({
878
922
  catch (error) {
879
923
  console.error(`❌ Error generating ClientLayout component: ${error}`);
880
924
  }
881
- // Générer AppProviders wrapper client
882
- const appProvidersPath = path.join(appDirectory, "components", "AppProviders.tsx");
925
+ // Générer AppProviders wrapper client (dans components/ racine)
926
+ const appProvidersPath = path.join(projectRoot, "components", "AppProviders.tsx");
927
+ // Import conditionnel de EntitlementsProvider
928
+ const entitlementsImport = hasBillingModule
929
+ ? `import { EntitlementsProvider } from "@lastbrain-labs/module-billing-pro";`
930
+ : "";
931
+ // Prop conditionnel pour EntitlementsProviderComponent
932
+ const entitlementsProp = hasBillingModule
933
+ ? `EntitlementsProviderComponent={EntitlementsProvider}`
934
+ : "";
883
935
  const appProvidersContent = `"use client";
884
936
 
885
937
  import { AppProviders as BaseAppProviders } from "@lastbrain/app";
886
- import { realtimeConfig } from "../../config/realtime";
938
+ ${entitlementsImport}
939
+ import { realtimeConfig } from "../config/realtime";
887
940
  import type { ReactNode } from "react";
888
941
  import type { Language } from "@lastbrain/core";
889
942
 
890
943
  export function AppProviders({
891
- children,
944
+ children,
892
945
  lang = "fr",
893
946
  translations = {},
894
947
  availableLanguages = ["fr", "en"],
@@ -904,6 +957,7 @@ export function AppProviders({
904
957
  lang={lang}
905
958
  translations={translations}
906
959
  availableLanguages={availableLanguages}
960
+ ${entitlementsProp}
907
961
  >
908
962
  {children}
909
963
  </BaseAppProviders>
@@ -915,19 +969,26 @@ export function AppProviders({
915
969
  catch (error) {
916
970
  console.error(`❌ Error generating AppProviders component: ${error}`);
917
971
  }
918
- // Générer AppHeader component
919
- const appHeaderPath = path.join(appDirectory, "components", "AppHeader.tsx");
972
+ // Générer AppHeader component (dans components/ racine)
973
+ const appHeaderPath = path.join(projectRoot, "components", "AppHeader.tsx");
920
974
  const appHeaderContent = `"use client";
921
975
 
922
976
  import { Header } from "@lastbrain/ui";
923
- import { LanguageSwitcher } from "@lastbrain/app";
924
- import { menuConfig } from "../../config/menu";
925
- import { LOCALE_CONFIG } from "../../config/locales.generated";
977
+ import { LanguageSwitcher, useAuth } from "@lastbrain/app";
978
+ import { menuConfig } from "../config/menu";
979
+ import { menuIgnored } from "../config/menu-ignored";
980
+ import { LOCALE_CONFIG } from "../config/locales.generated";
926
981
 
927
982
  export function AppHeader() {
983
+ const { user, loading, isSuperAdmin } = useAuth();
984
+
928
985
  return (
929
986
  <Header
987
+ user={user}
988
+ isSuperAdmin={isSuperAdmin}
930
989
  menuConfig={menuConfig}
990
+ accountMenu={menuConfig.account}
991
+ menuIgnored={menuIgnored}
931
992
  languageSwitcher={
932
993
  <LanguageSwitcher
933
994
  variant="minimal"
@@ -995,7 +1056,7 @@ export default async function LangLayout({
995
1056
 
996
1057
  return (
997
1058
  <ClientLayout lang={validLang} translations={translations} availableLanguages={[...LOCALE_CONFIG.languages]}>
998
- <div className="min-h-screen pt-16">
1059
+ <div className="min-h-screen ">
999
1060
  {children}
1000
1061
  </div>
1001
1062
  </ClientLayout>
@@ -1021,6 +1082,7 @@ export default async function LangLayout({
1021
1082
 
1022
1083
  import { AuthLayoutWithSidebar, langHref, useLanguage } from "@lastbrain/app";
1023
1084
  import { menuConfig as fullMenuConfig } from "../../../config/menu";
1085
+ import { menuIgnored } from "../../../config/menu-ignored";
1024
1086
 
1025
1087
  export default function SectionLayout({
1026
1088
  children,
@@ -1041,7 +1103,7 @@ export default function SectionLayout({
1041
1103
  };
1042
1104
 
1043
1105
  return (
1044
- <AuthLayoutWithSidebar menuConfig={menuConfig}>
1106
+ <AuthLayoutWithSidebar menuConfig={menuConfig} menuIgnored={menuIgnored}>
1045
1107
  {children}
1046
1108
  </AuthLayoutWithSidebar>
1047
1109
  );
@@ -1052,12 +1114,36 @@ export default function SectionLayout({
1052
1114
  catch (error) {
1053
1115
  console.error(`❌ Error generating auth layout: ${error}`);
1054
1116
  }
1117
+ // Générer layout public avec footer
1118
+ const publicLayoutPath = path.join(appDirectory, "[lang]", "(public)", "layout.tsx");
1119
+ const publicLayoutContent = `"use client";
1120
+
1121
+ import type React from "react";
1122
+ import { PublicLayout } from "@lastbrain/app";
1123
+ import { footerConfig } from "../../../config/footer";
1124
+
1125
+ export default function PublicSectionLayout({
1126
+ children,
1127
+ }: {
1128
+ children: React.ReactNode;
1129
+ }) {
1130
+ return <PublicLayout footerConfig={footerConfig}>
1131
+ {children}
1132
+ </PublicLayout>;
1133
+ }`;
1134
+ try {
1135
+ writeScaffoldFile(publicLayoutPath, publicLayoutContent, "public layout with footer");
1136
+ }
1137
+ catch (error) {
1138
+ console.error(`❌ Error generating public layout: ${error}`);
1139
+ }
1055
1140
  // Générer layout admin avec sidebar
1056
1141
  const adminLayoutPath = path.join(appDirectory, "[lang]", "admin", "layout.tsx");
1057
1142
  const adminLayoutContent = `"use client";
1058
1143
 
1059
1144
  import { AdminLayoutWithSidebar, langHref, useLanguage } from "@lastbrain/app";
1060
1145
  import { menuConfig as fullMenuConfig } from "../../../config/menu";
1146
+ import { menuIgnored } from "../../../config/menu-ignored";
1061
1147
 
1062
1148
  export default function AdminLayout({
1063
1149
  children,
@@ -1078,7 +1164,7 @@ export default function AdminLayout({
1078
1164
  };
1079
1165
 
1080
1166
  return (
1081
- <AdminLayoutWithSidebar menuConfig={menuConfig}>
1167
+ <AdminLayoutWithSidebar menuConfig={menuConfig} menuIgnored={menuIgnored}>
1082
1168
  {children}
1083
1169
  </AdminLayoutWithSidebar>
1084
1170
  );
@@ -1158,7 +1244,11 @@ async function generateUserTabsConfig(moduleConfigs) {
1158
1244
  else {
1159
1245
  // Générer les imports statiques (Next/dynamic pour chaque composant)
1160
1246
  const importsForApp = userTabsConfigs
1161
- .map((tab) => `const ${tab.componentExport} = dynamic(() => import("${tab.moduleName}").then(mod => ({ default: mod.${tab.componentExport} })), { ssr: true });`)
1247
+ .map((tab) => {
1248
+ // UserTokenTab est un Client Component, utiliser ssr: false
1249
+ const ssrMode = tab.componentExport === "UserTokenTab" ? "false" : "true";
1250
+ return `const ${tab.componentExport} = dynamic(() => import("${tab.moduleName}").then(mod => ({ default: mod.${tab.componentExport} })), { ssr: ${ssrMode} });`;
1251
+ })
1162
1252
  .join("\n");
1163
1253
  // Générer le tableau des tabs
1164
1254
  const tabsArray = userTabsConfigs
@@ -1500,7 +1590,6 @@ export async function GET(
1500
1590
  .createSignedUrl(storagePath, 3600); // 1 heure
1501
1591
 
1502
1592
  if (error) {
1503
- console.error(\`[storage] Error creating signed URL for public image:\`, error);
1504
1593
  return new NextResponse("Not found", { status: 404 });
1505
1594
  }
1506
1595
 
@@ -1521,16 +1610,23 @@ export async function GET(
1521
1610
 
1522
1611
  // Cas spécial: si le chemin commence par /product/ ou /recipe/,
1523
1612
  // c'est une image avec format court qui nécessite le préfixe userId
1613
+ // Sinon le chemin contient déjà le userId
1524
1614
  let actualStoragePath = storagePath;
1525
-
1615
+ const pathParts = storagePath.split("/");
1526
1616
 
1527
- actualStoragePath = \`\${user.id}/\${storagePath}\`;
1617
+ // Vérifier si le premier segment est un UUID (userId déjà présent)
1618
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1619
+ const firstSegmentIsUuid = uuidRegex.test(pathParts[0]);
1528
1620
 
1621
+ if (!firstSegmentIsUuid) {
1622
+ // Le chemin ne contient pas le userId, on l'ajoute
1623
+ actualStoragePath = \`\${user.id}/\${storagePath}\`;
1624
+ }
1529
1625
 
1530
1626
  // Vérifier que l'utilisateur a accès à cette image
1531
1627
  // Format: {userId}/recipe/{recipeId}/{filename} ou {userId}/product/{productId}/{filename}
1532
- const pathParts = actualStoragePath.split("/");
1533
- const pathUserId = pathParts[0];
1628
+ const actualPathParts = actualStoragePath.split("/");
1629
+ const pathUserId = actualPathParts[0];
1534
1630
 
1535
1631
  if (pathUserId !== user.id) {
1536
1632
  return new NextResponse("Forbidden", { status: 403 });
@@ -1539,17 +1635,31 @@ export async function GET(
1539
1635
  // Créer une URL signée pour l'image privée
1540
1636
  const { data, error } = await supabase.storage
1541
1637
  .from(bucket)
1542
- .createSignedUrl(actualStoragePath, 3600); // 1 heure
1638
+ .createSignedUrl(actualStoragePath, 60); // 1 minute seulement
1543
1639
 
1544
1640
  if (error) {
1545
- console.error(\`[storage] Error creating signed URL:\`, error);
1546
1641
  return new NextResponse("Not found", { status: 404 });
1547
1642
  }
1548
1643
 
1549
- // Rediriger vers l'URL signée
1550
- return NextResponse.redirect(data.signedUrl);
1644
+ // Télécharger l'image et la retourner avec des headers no-cache
1645
+ const imageResponse = await fetch(data.signedUrl);
1646
+ if (!imageResponse.ok) {
1647
+ return new NextResponse("Not found", { status: 404 });
1648
+ }
1649
+
1650
+ const imageBuffer = await imageResponse.arrayBuffer();
1651
+ const contentType = imageResponse.headers.get("content-type") || "image/jpeg";
1652
+
1653
+ return new NextResponse(imageBuffer, {
1654
+ status: 200,
1655
+ headers: {
1656
+ "Content-Type": contentType,
1657
+ "Cache-Control": "no-cache, no-store, must-revalidate",
1658
+ "Pragma": "no-cache",
1659
+ "Expires": "0",
1660
+ },
1661
+ });
1551
1662
  } catch (error) {
1552
- console.error("[storage] Error:", error);
1553
1663
  return new NextResponse("Internal server error", { status: 500 });
1554
1664
  }
1555
1665
  }
@@ -1616,6 +1726,230 @@ export const footerConfig: FooterConfig = {
1616
1726
  console.error("❌ Error generating footer config:", error);
1617
1727
  }
1618
1728
  }
1729
+ /**
1730
+ * Génère les routes sitemap à partir des configurations des modules
1731
+ */
1732
+ async function generateSitemapRoutes(moduleConfigs) {
1733
+ try {
1734
+ // Collecter tous les sitemaps de tous les modules
1735
+ const allSitemaps = [];
1736
+ moduleConfigs.forEach((config) => {
1737
+ if (config.sitemaps && config.sitemaps.length > 0) {
1738
+ config.sitemaps.forEach((sitemap) => {
1739
+ allSitemaps.push({ moduleConfig: config, sitemap });
1740
+ });
1741
+ }
1742
+ });
1743
+ if (allSitemaps.length === 0) {
1744
+ if (isDebugMode) {
1745
+ console.log("⏭️ No sitemaps found, skipping sitemap generation");
1746
+ }
1747
+ return;
1748
+ }
1749
+ // Détecter les sitemaps index des modules (sitemap.xml ou sitemap-index)
1750
+ const moduleSitemapIndexes = [];
1751
+ // Renommer les /sitemap.xml en /sitemap-{moduleName}.xml pour éviter les conflits
1752
+ allSitemaps.forEach(({ moduleConfig, sitemap }) => {
1753
+ if (sitemap.path === "/sitemap.xml" ||
1754
+ sitemap.path.endsWith("-index.xml")) {
1755
+ // Extraire le nom court du module (ex: module-blog -> blog)
1756
+ const moduleShortName = moduleConfig.moduleName
1757
+ .replace("@lastbrain/", "")
1758
+ .replace("@lastbrain-labs/", "")
1759
+ .replace("module-", "")
1760
+ .replace("module-core-", "");
1761
+ // Renommer le path
1762
+ sitemap.path = `/sitemap-${moduleShortName}.xml`;
1763
+ moduleSitemapIndexes.push(sitemap.path);
1764
+ if (isDebugMode) {
1765
+ console.log(`🔄 Renamed sitemap from /sitemap.xml to ${sitemap.path} for ${moduleConfig.moduleName}`);
1766
+ }
1767
+ }
1768
+ });
1769
+ // Générer chaque route sitemap
1770
+ for (const { moduleConfig, sitemap } of allSitemaps) {
1771
+ const sitemapPath = sitemap.path;
1772
+ // Déterminer le chemin du fichier route.ts
1773
+ let routeDir;
1774
+ if (sitemapPath === "/sitemap.xml") {
1775
+ // Route principale sitemap.xml à la racine de app
1776
+ routeDir = path.join(appDirectory, "sitemap.xml");
1777
+ }
1778
+ else {
1779
+ // Sous-sitemaps (ex: /sitemap/recipes.xml -> app/sitemap/recipes.xml)
1780
+ const segments = sitemapPath
1781
+ .replace(/^\//, "")
1782
+ .replace(/\.xml$/, ".xml")
1783
+ .split("/");
1784
+ routeDir = path.join(appDirectory, ...segments);
1785
+ }
1786
+ const routeFilePath = path.join(routeDir, "route.ts");
1787
+ ensureDirectory(routeDir);
1788
+ // Déterminer le type de sitemap et générer le code approprié
1789
+ const entryPoint = sitemap.entryPoint;
1790
+ let content;
1791
+ if (entryPoint === "sitemap/index") {
1792
+ // Sitemap index principal
1793
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Sitemap Index Route
1794
+ // Module: ${moduleConfig.moduleName}
1795
+ // Path: ${sitemapPath}
1796
+ import { generateSitemapIndex } from "${moduleConfig.moduleName}/sitemap";
1797
+
1798
+ export async function GET(): Promise<Response> {
1799
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
1800
+
1801
+ // Liste des sous-sitemaps
1802
+ const sitemapPaths = [
1803
+ "/sitemap/pages.xml",
1804
+ "/sitemap/recipes.xml",
1805
+ "/sitemap/chefs.xml",
1806
+ ];
1807
+
1808
+ const xml = generateSitemapIndex(baseUrl, sitemapPaths);
1809
+
1810
+ return new Response(xml, {
1811
+ headers: {
1812
+ "Content-Type": "application/xml",
1813
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
1814
+ },
1815
+ });
1816
+ }
1817
+ `;
1818
+ }
1819
+ else if (entryPoint === "sitemap/pages") {
1820
+ // Sitemap des pages statiques
1821
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Static Pages Sitemap Route
1822
+ // Module: ${moduleConfig.moduleName}
1823
+ // Path: ${sitemapPath}
1824
+ import { generateStaticPagesSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
1825
+
1826
+ export async function GET(): Promise<Response> {
1827
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
1828
+
1829
+ const sitemap = await generateStaticPagesSitemap(baseUrl);
1830
+ const xml = generateSitemapXml(sitemap);
1831
+
1832
+ return new Response(xml, {
1833
+ headers: {
1834
+ "Content-Type": "application/xml",
1835
+ "Cache-Control": "public, max-age=86400, s-maxage=86400",
1836
+ },
1837
+ });
1838
+ }
1839
+ `;
1840
+ }
1841
+ else if (entryPoint === "sitemap/recipes") {
1842
+ // Sitemap des recettes
1843
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Recipes Sitemap Route
1844
+ // Module: ${moduleConfig.moduleName}
1845
+ // Path: ${sitemapPath}
1846
+ import { generateRecipesSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
1847
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
1848
+
1849
+ export async function GET(): Promise<Response> {
1850
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
1851
+ const supabase = await getSupabaseServiceClient();
1852
+
1853
+ const sitemap = await generateRecipesSitemap(baseUrl, supabase);
1854
+ const xml = generateSitemapXml(sitemap);
1855
+
1856
+ return new Response(xml, {
1857
+ headers: {
1858
+ "Content-Type": "application/xml",
1859
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
1860
+ },
1861
+ });
1862
+ }
1863
+ `;
1864
+ }
1865
+ else if (entryPoint === "sitemap/chefs") {
1866
+ // Sitemap des chefs
1867
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Chefs Sitemap Route
1868
+ // Module: ${moduleConfig.moduleName}
1869
+ // Path: ${sitemapPath}
1870
+ import { generateChefsSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
1871
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
1872
+
1873
+ export async function GET(): Promise<Response> {
1874
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
1875
+ const supabase = await getSupabaseServiceClient();
1876
+
1877
+ const sitemap = await generateChefsSitemap(baseUrl, supabase);
1878
+ const xml = generateSitemapXml(sitemap);
1879
+
1880
+ return new Response(xml, {
1881
+ headers: {
1882
+ "Content-Type": "application/xml",
1883
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
1884
+ },
1885
+ });
1886
+ }
1887
+ `;
1888
+ }
1889
+ else {
1890
+ // Sitemap générique - export direct du handler
1891
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Sitemap route
1892
+ // Module: ${moduleConfig.moduleName}
1893
+ // Path: ${sitemapPath}
1894
+ export { ${sitemap.handlerExport} } from "${moduleConfig.moduleName}/${sitemap.entryPoint}";
1895
+ `;
1896
+ }
1897
+ fs.writeFileSync(routeFilePath, content);
1898
+ if (isDebugMode) {
1899
+ console.log(`🗺️ Generated sitemap route: ${routeFilePath}`);
1900
+ }
1901
+ }
1902
+ // Générer le sitemap.xml global qui agrège tous les sitemaps des modules
1903
+ if (moduleSitemapIndexes.length > 0) {
1904
+ const globalSitemapDir = path.join(appDirectory, "sitemap.xml");
1905
+ const globalSitemapFile = path.join(globalSitemapDir, "route.ts");
1906
+ ensureDirectory(globalSitemapDir);
1907
+ const sitemapUrls = moduleSitemapIndexes
1908
+ .map((path) => {
1909
+ return ` {
1910
+ url: \`\${baseUrl}${path}\`,
1911
+ lastModified: new Date(),
1912
+ }`;
1913
+ })
1914
+ .join(",\n");
1915
+ const globalSitemapContent = `// GENERATED BY LASTBRAIN MODULE BUILD - Global Sitemap Index
1916
+ // This file aggregates all module sitemaps
1917
+ // Module sitemaps: ${moduleSitemapIndexes.join(", ")}
1918
+
1919
+ export async function GET(): Promise<Response> {
1920
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
1921
+
1922
+ const sitemapIndexXml = \`<?xml version="1.0" encoding="UTF-8"?>
1923
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1924
+ ${moduleSitemapIndexes
1925
+ .map((path) => ` <sitemap>
1926
+ <loc>\${baseUrl}${path}</loc>
1927
+ <lastmod>\${new Date().toISOString()}</lastmod>
1928
+ </sitemap>`)
1929
+ .join("\n")}
1930
+ </sitemapindex>\`;
1931
+
1932
+ return new Response(sitemapIndexXml, {
1933
+ headers: {
1934
+ "Content-Type": "application/xml",
1935
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
1936
+ },
1937
+ });
1938
+ }
1939
+ `;
1940
+ fs.writeFileSync(globalSitemapFile, globalSitemapContent);
1941
+ if (isDebugMode) {
1942
+ console.log(`🌍 Generated global sitemap.xml aggregating ${moduleSitemapIndexes.length} module sitemap(s)`);
1943
+ }
1944
+ }
1945
+ if (isDebugMode) {
1946
+ console.log(`✅ Generated ${allSitemaps.length} sitemap route(s)`);
1947
+ }
1948
+ }
1949
+ catch (error) {
1950
+ console.error("❌ Error generating sitemap routes:", error);
1951
+ }
1952
+ }
1619
1953
  /**
1620
1954
  * Génère les fichiers i18n en concaténant les traductions des modules
1621
1955
  * et en incluant les traductions personnalisées de i18n/default
@@ -1623,21 +1957,6 @@ export const footerConfig: FooterConfig = {
1623
1957
  async function generateLocaleConfig() {
1624
1958
  const appI18nDir = path.join(projectRoot, "i18n");
1625
1959
  const appConfigDir = path.join(projectRoot, "config");
1626
- // Mapping standard des codes de langue vers les locales
1627
- const LOCALE_MAP = {
1628
- fr: "fr_FR",
1629
- en: "en_US",
1630
- es: "es_ES",
1631
- de: "de_DE",
1632
- it: "it_IT",
1633
- pt: "pt_PT",
1634
- nl: "nl_NL",
1635
- ru: "ru_RU",
1636
- ja: "ja_JP",
1637
- zh: "zh_CN",
1638
- ar: "ar_SA",
1639
- hi: "hi_IN",
1640
- };
1641
1960
  try {
1642
1961
  // Scanner les fichiers i18n disponibles
1643
1962
  let languages = ["fr"]; // Fallback par défaut
@@ -1856,15 +2175,19 @@ function generateHomePage(moduleConfigs) {
1856
2175
  const content = `// GENERATED BY LASTBRAIN MODULE BUILD
1857
2176
  // Homepage from ${homePageModule.moduleName}
1858
2177
  import { ${importComponentName} as ModuleHomePage${homePageConfig.metadataExport ? `, ${metadataImportAlias}` : ""} } from "${importPath}";
1859
- import { footerConfig } from "../../../config/footer";
2178
+
1860
2179
  import { LOCALE_CONFIG } from "../../../config/locales.generated";
1861
2180
  ${homePageConfig.metadataExport
1862
2181
  ? `\nexport async function generateMetadata(props: any) {
1863
2182
  return ${homePageConfig.metadataExport}Original({ ...props, localeConfig: LOCALE_CONFIG });
1864
2183
  }`
1865
2184
  : ""}
1866
- export default function ${wrapperComponentName}() {
1867
- return <ModuleHomePage footerConfig={footerConfig} localeConfig={LOCALE_CONFIG} />;
2185
+ export default function ${wrapperComponentName}({
2186
+ params,
2187
+ }: {
2188
+ params: Promise<{ lang: string }>;
2189
+ }) {
2190
+ return <ModuleHomePage params={params} localeConfig={LOCALE_CONFIG} />;
1868
2191
  }
1869
2192
  `;
1870
2193
  fs.writeFileSync(homePagePath, content, "utf-8");
@@ -1923,8 +2246,9 @@ export async function runModuleBuild() {
1923
2246
  dumpNavigation();
1924
2247
  generateMenuConfig(moduleConfigs);
1925
2248
  generateDocsPage(moduleConfigs);
1926
- generateAppAside();
1927
- generateLayouts();
2249
+ // Note: AppAside, AppHeader, etc. sont maintenant générés dans components/ à la racine
2250
+ // et non plus dupliqués dans app/components/
2251
+ generateLayouts(moduleConfigs);
1928
2252
  copyModuleMigrations(moduleConfigs);
1929
2253
  // Générer la page d'accueil "/" si un module la définit
1930
2254
  if (isDebugMode) {
@@ -1961,6 +2285,17 @@ export async function runModuleBuild() {
1961
2285
  console.log("🦶 Generating footer configuration...");
1962
2286
  }
1963
2287
  await generateFooterConfig(moduleConfigs);
2288
+ // Générer les routes sitemap (ancien système)
2289
+ if (isDebugMode) {
2290
+ console.log("🗺️ Generating sitemap routes (legacy)...");
2291
+ }
2292
+ //await generateSitemapRoutes(moduleConfigs);
2293
+ // Générer les sitemaps basés sur manifests (nouveau système)
2294
+ if (isDebugMode) {
2295
+ console.log("🗺️ Generating manifest-based sitemaps...");
2296
+ }
2297
+ const availableLanguages = Object.keys(LOCALE_MAP);
2298
+ await generateManifestBasedSitemaps(appDirectory, moduleConfigs, availableLanguages, projectRequire, isDebugMode);
1964
2299
  // Générer les fichiers i18n
1965
2300
  if (isDebugMode) {
1966
2301
  console.log("🌍 Building i18n files...");