@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,15 +1,23 @@
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
+ import type { ModuleBuildConfig } from "../index.js";
4
6
 
5
7
  import type {
6
8
  ModuleApiConfig,
7
- ModuleBuildConfig,
8
9
  ModuleMenuItemConfig,
9
10
  ModulePageConfig,
10
11
  ModuleSection,
11
12
  ModuleRealtimeConfig,
12
- } from "@lastbrain/core";
13
+ ModuleSitemapConfig,
14
+ } from "../index.js";
15
+
16
+ const LOCALE_MAP: Record<string, string> = {
17
+ fr: "fr_FR",
18
+ en: "en_US",
19
+ es: "es_ES",
20
+ };
13
21
 
14
22
  // Utiliser PROJECT_ROOT si défini (pour pnpm --filter), sinon process.cwd()
15
23
  const projectRoot = process.env.PROJECT_ROOT || process.cwd();
@@ -248,7 +256,7 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
248
256
  page.componentExport === "UserPage";
249
257
 
250
258
  // Détecter les pages légales qui utilisent cookies (privacy, terms, returns)
251
- const isLegalPage =
259
+ const _isLegalPage =
252
260
  page.section === "public" &&
253
261
  (page.path.includes("privacy") ||
254
262
  page.path.includes("terms") ||
@@ -261,6 +269,7 @@ function buildPage(moduleConfig: ModuleBuildConfig, page: ModulePageConfig) {
261
269
  // On importe directement depuis app/config au lieu de passer via props
262
270
  content = `// GENERATED BY LASTBRAIN MODULE BUILD
263
271
  import { UserDetailPage } from "${moduleConfig.moduleName}";
272
+ import { logger } from "@lastbrain/core";
264
273
 
265
274
  interface UserPageProps { params: Promise<{ id: string }> }
266
275
 
@@ -270,7 +279,7 @@ async function getModuleUserTabs() {
270
279
  const { moduleUserTabs } = await import("../../../../../../config/user-tabs");
271
280
  return moduleUserTabs || [];
272
281
  } catch (e) {
273
- console.warn("[user-detail-wrapper] erreur chargement user-tabs", e);
282
+ logger.warn("[user-detail-wrapper] erreur chargement user-tabs", e);
274
283
  return [];
275
284
  }
276
285
  }
@@ -292,16 +301,6 @@ const ${page.componentExport} = dynamic(
292
301
  { ssr: false }
293
302
  );
294
303
 
295
- export default function ${wrapperName}${hasDynamicParams ? "(props: Record<string, unknown>)" : "()"} {
296
- return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
297
- }
298
- `;
299
- } else if (isLegalPage) {
300
- content = `// GENERATED BY LASTBRAIN MODULE BUILD
301
- import { ${page.componentExport} } from "${moduleConfig.moduleName}";
302
-
303
- export const dynamic = 'force-dynamic';
304
-
305
304
  export default function ${wrapperName}${hasDynamicParams ? "(props: Record<string, unknown>)" : "()"} {
306
305
  return <${page.componentExport} ${hasDynamicParams ? "{...props}" : ""} />;
307
306
  }
@@ -320,7 +319,7 @@ export default function ${wrapperName}${hasDynamicParams ? "(props: Record<strin
320
319
  .map((seg) => seg.slice(1, -1)); // Enlève les []
321
320
 
322
321
  // Détecter si le module path lui-même a des params dynamiques (pas seulement [lang])
323
- const moduleHasDynamicParams = segments.some(
322
+ const _moduleHasDynamicParams = segments.some(
324
323
  (seg) => seg.startsWith("[") && seg.endsWith("]")
325
324
  );
326
325
 
@@ -549,6 +548,7 @@ export interface MenuItem {
549
548
  shortcut?: string;
550
549
  shortcutDisplay?: string;
551
550
  type?: 'text' | 'icon' | 'textIcon';
551
+ keyboardOnly?: boolean;
552
552
  position?: 'center' | 'end';
553
553
  component?: React.ComponentType<any>;
554
554
  componentExport?: string;
@@ -686,7 +686,7 @@ function copyModuleMigrations(moduleConfigs: ModuleBuildConfig[]) {
686
686
  }
687
687
 
688
688
  function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
689
- const docsDir = path.join(appDirectory, "docs");
689
+ const docsDir = path.join(appDirectory, "[lang]", "(public)", "docs");
690
690
  ensureDirectory(docsDir);
691
691
 
692
692
  const docsPagePath = path.join(docsDir, "page.tsx");
@@ -714,13 +714,13 @@ function generateDocsPage(moduleConfigs: ModuleBuildConfig[]) {
714
714
 
715
715
  allModules.forEach((moduleEntry) => {
716
716
  const moduleName = moduleEntry.package;
717
- // Extraire le nom du module sans le scope et sans "module-"
717
+ // Extraire le nom du module sans le scope et sans "-pro"
718
718
  // Ex: @lastbrain/module-auth -> auth
719
- // Ex: @lastbrain-labs/module-recipes-pro -> recipes
719
+ // Ex: @lastbrain-labs/module-metrics-pro -> metrics
720
720
  const moduleId = moduleName
721
721
  .replace("@lastbrain-labs/module-", "")
722
722
  .replace("@lastbrain/module-", "")
723
- .replace(/-pro$/, ""); // Retirer le suffix -pro pour avoir le nom de base
723
+ .replace(/-pro$/, ""); // Retirer le suffixe -pro
724
724
 
725
725
  const docComponentName = `${toPascalCase(moduleId)}ModuleDoc`;
726
726
 
@@ -772,6 +772,45 @@ ${moduleConfigurations.join(",\n")}
772
772
  if (isDebugMode) {
773
773
  console.log(`📚 Generated docs page: ${docsPagePath}`);
774
774
  }
775
+
776
+ // Generate docs layout with translation support
777
+ const docsLayoutPath = path.join(docsDir, "layout.tsx");
778
+ const docsLayoutContent = `// Auto-generated docs layout with translation support
779
+ import { loadTranslations } from "@lastbrain/app/i18n/server-lang";
780
+ import { ClientLayout } from "../../../../components/ClientLayout";
781
+ import { headers } from "next/headers";
782
+
783
+ // Extract language from request headers
784
+ async function getLanguageFromRequest(): Promise<string> {
785
+ const headersList = await headers();
786
+ const acceptLanguage = headersList.get("accept-language") || "";
787
+ // Detect if French is requested
788
+ if (acceptLanguage.toLowerCase().includes("fr")) {
789
+ return "fr";
790
+ }
791
+ return "en";
792
+ }
793
+
794
+ export default async function DocsLayout({
795
+ children,
796
+ }: {
797
+ children: React.ReactNode;
798
+ }) {
799
+ const lang = await getLanguageFromRequest();
800
+ const translations = await loadTranslations(lang);
801
+
802
+ return (
803
+ <ClientLayout lang={lang} translations={translations}>
804
+ <div className="min-h-screen pt-12">{children}</div>
805
+ </ClientLayout>
806
+ );
807
+ }
808
+ `;
809
+
810
+ fs.writeFileSync(docsLayoutPath, docsLayoutContent);
811
+ if (isDebugMode) {
812
+ console.log(`📚 Generated docs layout: ${docsLayoutPath}`);
813
+ }
775
814
  }
776
815
 
777
816
  function buildGroupedApi(
@@ -838,7 +877,8 @@ function cleanGeneratedFiles() {
838
877
  "page.tsx", // Page racine seulement
839
878
  "admin/page.tsx", // Page admin racine
840
879
  "admin/layout.tsx", // Layout admin racine
841
- "docs/page.tsx", // Page docs générée
880
+ "[lang]/(public)/docs/page.tsx", // Page docs générée
881
+ "[lang]/(public)/docs/layout.tsx", // Layout docs généré
842
882
  // Middleware et autres fichiers core
843
883
  "middleware.ts",
844
884
  // Dossiers de lib et config
@@ -917,7 +957,14 @@ function cleanGeneratedFiles() {
917
957
  };
918
958
 
919
959
  // Nettoyer les dossiers de sections
920
- const sectionsToClean = ["(public)", "auth", "admin", "api", "[lang]"];
960
+ const sectionsToClean = [
961
+ "(public)",
962
+ "auth",
963
+ "admin",
964
+ "api",
965
+ "[lang]",
966
+ "sitemap",
967
+ ];
921
968
  sectionsToClean.forEach((section) => {
922
969
  const sectionPath = path.join(appDirectory, section);
923
970
  if (fs.existsSync(sectionPath)) {
@@ -993,7 +1040,7 @@ function cleanOldRoutesWithoutLang() {
993
1040
  }
994
1041
  });
995
1042
  }
996
- function generateAppAside() {
1043
+ function _generateAppAside() {
997
1044
  const targetPath = path.join(appDirectory, "components", "AppAside.tsx");
998
1045
  const templateContent = `"use client";
999
1046
 
@@ -1026,10 +1073,15 @@ export function AppAside({ className = "", isVisible = true }: AppAsideProps) {
1026
1073
  }
1027
1074
  }
1028
1075
 
1029
- function generateLayouts() {
1030
- // Générer ClientLayout wrapper client
1076
+ function generateLayouts(moduleConfigs: ModuleBuildConfig[]) {
1077
+ // Vérifier si le module billing-pro est actif
1078
+ const hasBillingModule = moduleConfigs.some(
1079
+ (config) => config.moduleName === "@lastbrain-labs/module-billing-pro"
1080
+ );
1081
+
1082
+ // Générer ClientLayout wrapper client (dans components/ racine)
1031
1083
  const clientLayoutPath = path.join(
1032
- appDirectory,
1084
+ projectRoot,
1033
1085
  "components",
1034
1086
  "ClientLayout.tsx"
1035
1087
  );
@@ -1085,21 +1137,33 @@ export function ClientLayout({
1085
1137
  console.error(`❌ Error generating ClientLayout component: ${error}`);
1086
1138
  }
1087
1139
 
1088
- // Générer AppProviders wrapper client
1140
+ // Générer AppProviders wrapper client (dans components/ racine)
1089
1141
  const appProvidersPath = path.join(
1090
- appDirectory,
1142
+ projectRoot,
1091
1143
  "components",
1092
1144
  "AppProviders.tsx"
1093
1145
  );
1146
+
1147
+ // Import conditionnel de EntitlementsProvider
1148
+ const entitlementsImport = hasBillingModule
1149
+ ? `import { EntitlementsProvider } from "@lastbrain-labs/module-billing-pro";`
1150
+ : "";
1151
+
1152
+ // Prop conditionnel pour EntitlementsProviderComponent
1153
+ const entitlementsProp = hasBillingModule
1154
+ ? `EntitlementsProviderComponent={EntitlementsProvider}`
1155
+ : "";
1156
+
1094
1157
  const appProvidersContent = `"use client";
1095
1158
 
1096
1159
  import { AppProviders as BaseAppProviders } from "@lastbrain/app";
1097
- import { realtimeConfig } from "../../config/realtime";
1160
+ ${entitlementsImport}
1161
+ import { realtimeConfig } from "../config/realtime";
1098
1162
  import type { ReactNode } from "react";
1099
1163
  import type { Language } from "@lastbrain/core";
1100
1164
 
1101
1165
  export function AppProviders({
1102
- children,
1166
+ children,
1103
1167
  lang = "fr",
1104
1168
  translations = {},
1105
1169
  availableLanguages = ["fr", "en"],
@@ -1115,6 +1179,7 @@ export function AppProviders({
1115
1179
  lang={lang}
1116
1180
  translations={translations}
1117
1181
  availableLanguages={availableLanguages}
1182
+ ${entitlementsProp}
1118
1183
  >
1119
1184
  {children}
1120
1185
  </BaseAppProviders>
@@ -1131,19 +1196,26 @@ export function AppProviders({
1131
1196
  console.error(`❌ Error generating AppProviders component: ${error}`);
1132
1197
  }
1133
1198
 
1134
- // Générer AppHeader component
1135
- const appHeaderPath = path.join(appDirectory, "components", "AppHeader.tsx");
1199
+ // Générer AppHeader component (dans components/ racine)
1200
+ const appHeaderPath = path.join(projectRoot, "components", "AppHeader.tsx");
1136
1201
  const appHeaderContent = `"use client";
1137
1202
 
1138
1203
  import { Header } from "@lastbrain/ui";
1139
- import { LanguageSwitcher } from "@lastbrain/app";
1140
- import { menuConfig } from "../../config/menu";
1141
- import { LOCALE_CONFIG } from "../../config/locales.generated";
1204
+ import { LanguageSwitcher, useAuth } from "@lastbrain/app";
1205
+ import { menuConfig } from "../config/menu";
1206
+ import { menuIgnored } from "../config/menu-ignored";
1207
+ import { LOCALE_CONFIG } from "../config/locales.generated";
1142
1208
 
1143
1209
  export function AppHeader() {
1210
+ const { user, loading, isSuperAdmin } = useAuth();
1211
+
1144
1212
  return (
1145
1213
  <Header
1214
+ user={user}
1215
+ isSuperAdmin={isSuperAdmin}
1146
1216
  menuConfig={menuConfig}
1217
+ accountMenu={menuConfig.account}
1218
+ menuIgnored={menuIgnored}
1147
1219
  languageSwitcher={
1148
1220
  <LanguageSwitcher
1149
1221
  variant="minimal"
@@ -1212,7 +1284,7 @@ export default async function LangLayout({
1212
1284
 
1213
1285
  return (
1214
1286
  <ClientLayout lang={validLang} translations={translations} availableLanguages={[...LOCALE_CONFIG.languages]}>
1215
- <div className="min-h-screen pt-16">
1287
+ <div className="min-h-screen ">
1216
1288
  {children}
1217
1289
  </div>
1218
1290
  </ClientLayout>
@@ -1243,6 +1315,7 @@ export default async function LangLayout({
1243
1315
 
1244
1316
  import { AuthLayoutWithSidebar, langHref, useLanguage } from "@lastbrain/app";
1245
1317
  import { menuConfig as fullMenuConfig } from "../../../config/menu";
1318
+ import { menuIgnored } from "../../../config/menu-ignored";
1246
1319
 
1247
1320
  export default function SectionLayout({
1248
1321
  children,
@@ -1263,7 +1336,7 @@ export default function SectionLayout({
1263
1336
  };
1264
1337
 
1265
1338
  return (
1266
- <AuthLayoutWithSidebar menuConfig={menuConfig}>
1339
+ <AuthLayoutWithSidebar menuConfig={menuConfig} menuIgnored={menuIgnored}>
1267
1340
  {children}
1268
1341
  </AuthLayoutWithSidebar>
1269
1342
  );
@@ -1279,6 +1352,39 @@ export default function SectionLayout({
1279
1352
  console.error(`❌ Error generating auth layout: ${error}`);
1280
1353
  }
1281
1354
 
1355
+ // Générer layout public avec footer
1356
+ const publicLayoutPath = path.join(
1357
+ appDirectory,
1358
+ "[lang]",
1359
+ "(public)",
1360
+ "layout.tsx"
1361
+ );
1362
+ const publicLayoutContent = `"use client";
1363
+
1364
+ import type React from "react";
1365
+ import { PublicLayout } from "@lastbrain/app";
1366
+ import { footerConfig } from "../../../config/footer";
1367
+
1368
+ export default function PublicSectionLayout({
1369
+ children,
1370
+ }: {
1371
+ children: React.ReactNode;
1372
+ }) {
1373
+ return <PublicLayout footerConfig={footerConfig}>
1374
+ {children}
1375
+ </PublicLayout>;
1376
+ }`;
1377
+
1378
+ try {
1379
+ writeScaffoldFile(
1380
+ publicLayoutPath,
1381
+ publicLayoutContent,
1382
+ "public layout with footer"
1383
+ );
1384
+ } catch (error) {
1385
+ console.error(`❌ Error generating public layout: ${error}`);
1386
+ }
1387
+
1282
1388
  // Générer layout admin avec sidebar
1283
1389
  const adminLayoutPath = path.join(
1284
1390
  appDirectory,
@@ -1290,6 +1396,7 @@ export default function SectionLayout({
1290
1396
 
1291
1397
  import { AdminLayoutWithSidebar, langHref, useLanguage } from "@lastbrain/app";
1292
1398
  import { menuConfig as fullMenuConfig } from "../../../config/menu";
1399
+ import { menuIgnored } from "../../../config/menu-ignored";
1293
1400
 
1294
1401
  export default function AdminLayout({
1295
1402
  children,
@@ -1310,7 +1417,7 @@ export default function AdminLayout({
1310
1417
  };
1311
1418
 
1312
1419
  return (
1313
- <AdminLayoutWithSidebar menuConfig={menuConfig}>
1420
+ <AdminLayoutWithSidebar menuConfig={menuConfig} menuIgnored={menuIgnored}>
1314
1421
  {children}
1315
1422
  </AdminLayoutWithSidebar>
1316
1423
  );
@@ -1414,10 +1521,12 @@ async function generateUserTabsConfig(moduleConfigs: ModuleBuildConfig[]) {
1414
1521
  } else {
1415
1522
  // Générer les imports statiques (Next/dynamic pour chaque composant)
1416
1523
  const importsForApp = userTabsConfigs
1417
- .map(
1418
- (tab) =>
1419
- `const ${tab.componentExport} = dynamic(() => import("${tab.moduleName}").then(mod => ({ default: mod.${tab.componentExport} })), { ssr: true });`
1420
- )
1524
+ .map((tab) => {
1525
+ // UserTokenTab est un Client Component, utiliser ssr: false
1526
+ const ssrMode =
1527
+ tab.componentExport === "UserTokenTab" ? "false" : "true";
1528
+ return `const ${tab.componentExport} = dynamic(() => import("${tab.moduleName}").then(mod => ({ default: mod.${tab.componentExport} })), { ssr: ${ssrMode} });`;
1529
+ })
1421
1530
  .join("\n");
1422
1531
 
1423
1532
  // Générer le tableau des tabs
@@ -1815,7 +1924,6 @@ export async function GET(
1815
1924
  .createSignedUrl(storagePath, 3600); // 1 heure
1816
1925
 
1817
1926
  if (error) {
1818
- console.error(\`[storage] Error creating signed URL for public image:\`, error);
1819
1927
  return new NextResponse("Not found", { status: 404 });
1820
1928
  }
1821
1929
 
@@ -1836,16 +1944,23 @@ export async function GET(
1836
1944
 
1837
1945
  // Cas spécial: si le chemin commence par /product/ ou /recipe/,
1838
1946
  // c'est une image avec format court qui nécessite le préfixe userId
1947
+ // Sinon le chemin contient déjà le userId
1839
1948
  let actualStoragePath = storagePath;
1840
-
1949
+ const pathParts = storagePath.split("/");
1841
1950
 
1842
- actualStoragePath = \`\${user.id}/\${storagePath}\`;
1951
+ // Vérifier si le premier segment est un UUID (userId déjà présent)
1952
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1953
+ const firstSegmentIsUuid = uuidRegex.test(pathParts[0]);
1843
1954
 
1955
+ if (!firstSegmentIsUuid) {
1956
+ // Le chemin ne contient pas le userId, on l'ajoute
1957
+ actualStoragePath = \`\${user.id}/\${storagePath}\`;
1958
+ }
1844
1959
 
1845
1960
  // Vérifier que l'utilisateur a accès à cette image
1846
1961
  // Format: {userId}/recipe/{recipeId}/{filename} ou {userId}/product/{productId}/{filename}
1847
- const pathParts = actualStoragePath.split("/");
1848
- const pathUserId = pathParts[0];
1962
+ const actualPathParts = actualStoragePath.split("/");
1963
+ const pathUserId = actualPathParts[0];
1849
1964
 
1850
1965
  if (pathUserId !== user.id) {
1851
1966
  return new NextResponse("Forbidden", { status: 403 });
@@ -1854,17 +1969,31 @@ export async function GET(
1854
1969
  // Créer une URL signée pour l'image privée
1855
1970
  const { data, error } = await supabase.storage
1856
1971
  .from(bucket)
1857
- .createSignedUrl(actualStoragePath, 3600); // 1 heure
1972
+ .createSignedUrl(actualStoragePath, 60); // 1 minute seulement
1858
1973
 
1859
1974
  if (error) {
1860
- console.error(\`[storage] Error creating signed URL:\`, error);
1861
1975
  return new NextResponse("Not found", { status: 404 });
1862
1976
  }
1863
1977
 
1864
- // Rediriger vers l'URL signée
1865
- return NextResponse.redirect(data.signedUrl);
1978
+ // Télécharger l'image et la retourner avec des headers no-cache
1979
+ const imageResponse = await fetch(data.signedUrl);
1980
+ if (!imageResponse.ok) {
1981
+ return new NextResponse("Not found", { status: 404 });
1982
+ }
1983
+
1984
+ const imageBuffer = await imageResponse.arrayBuffer();
1985
+ const contentType = imageResponse.headers.get("content-type") || "image/jpeg";
1986
+
1987
+ return new NextResponse(imageBuffer, {
1988
+ status: 200,
1989
+ headers: {
1990
+ "Content-Type": contentType,
1991
+ "Cache-Control": "no-cache, no-store, must-revalidate",
1992
+ "Pragma": "no-cache",
1993
+ "Expires": "0",
1994
+ },
1995
+ });
1866
1996
  } catch (error) {
1867
- console.error("[storage] Error:", error);
1868
1997
  return new NextResponse("Internal server error", { status: 500 });
1869
1998
  }
1870
1999
  }
@@ -1956,6 +2085,255 @@ export const footerConfig: FooterConfig = {
1956
2085
  }
1957
2086
  }
1958
2087
 
2088
+ /**
2089
+ * Génère les routes sitemap à partir des configurations des modules
2090
+ */
2091
+ async function generateSitemapRoutes(moduleConfigs: ModuleBuildConfig[]) {
2092
+ try {
2093
+ // Collecter tous les sitemaps de tous les modules
2094
+ const allSitemaps: Array<{
2095
+ moduleConfig: ModuleBuildConfig;
2096
+ sitemap: ModuleSitemapConfig;
2097
+ }> = [];
2098
+
2099
+ moduleConfigs.forEach((config) => {
2100
+ if (config.sitemaps && config.sitemaps.length > 0) {
2101
+ config.sitemaps.forEach((sitemap) => {
2102
+ allSitemaps.push({ moduleConfig: config, sitemap });
2103
+ });
2104
+ }
2105
+ });
2106
+
2107
+ if (allSitemaps.length === 0) {
2108
+ if (isDebugMode) {
2109
+ console.log("⏭️ No sitemaps found, skipping sitemap generation");
2110
+ }
2111
+ return;
2112
+ }
2113
+
2114
+ // Détecter les sitemaps index des modules (sitemap.xml ou sitemap-index)
2115
+ const moduleSitemapIndexes: string[] = [];
2116
+
2117
+ // Renommer les /sitemap.xml en /sitemap-{moduleName}.xml pour éviter les conflits
2118
+ allSitemaps.forEach(({ moduleConfig, sitemap }) => {
2119
+ if (
2120
+ sitemap.path === "/sitemap.xml" ||
2121
+ sitemap.path.endsWith("-index.xml")
2122
+ ) {
2123
+ // Extraire le nom court du module (ex: module-blog -> blog)
2124
+ const moduleShortName = moduleConfig.moduleName
2125
+ .replace("@lastbrain/", "")
2126
+ .replace("@lastbrain-labs/", "")
2127
+ .replace("module-", "")
2128
+ .replace("module-core-", "");
2129
+
2130
+ // Renommer le path
2131
+ sitemap.path = `/sitemap-${moduleShortName}.xml`;
2132
+ moduleSitemapIndexes.push(sitemap.path);
2133
+
2134
+ if (isDebugMode) {
2135
+ console.log(
2136
+ `🔄 Renamed sitemap from /sitemap.xml to ${sitemap.path} for ${moduleConfig.moduleName}`
2137
+ );
2138
+ }
2139
+ }
2140
+ });
2141
+
2142
+ // Générer chaque route sitemap
2143
+ for (const { moduleConfig, sitemap } of allSitemaps) {
2144
+ const sitemapPath = sitemap.path;
2145
+
2146
+ // Déterminer le chemin du fichier route.ts
2147
+ let routeDir: string;
2148
+ if (sitemapPath === "/sitemap.xml") {
2149
+ // Route principale sitemap.xml à la racine de app
2150
+ routeDir = path.join(appDirectory, "sitemap.xml");
2151
+ } else {
2152
+ // Sous-sitemaps (ex: /sitemap/recipes.xml -> app/sitemap/recipes.xml)
2153
+ const segments = sitemapPath
2154
+ .replace(/^\//, "")
2155
+ .replace(/\.xml$/, ".xml")
2156
+ .split("/");
2157
+ routeDir = path.join(appDirectory, ...segments);
2158
+ }
2159
+
2160
+ const routeFilePath = path.join(routeDir, "route.ts");
2161
+ ensureDirectory(routeDir);
2162
+
2163
+ // Déterminer le type de sitemap et générer le code approprié
2164
+ const entryPoint = sitemap.entryPoint;
2165
+ let content: string;
2166
+
2167
+ if (entryPoint === "sitemap/index") {
2168
+ // Sitemap index principal
2169
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Sitemap Index Route
2170
+ // Module: ${moduleConfig.moduleName}
2171
+ // Path: ${sitemapPath}
2172
+ import { generateSitemapIndex } from "${moduleConfig.moduleName}/sitemap";
2173
+
2174
+ export async function GET(): Promise<Response> {
2175
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
2176
+
2177
+ // Liste des sous-sitemaps
2178
+ const sitemapPaths = [
2179
+ "/sitemap/pages.xml",
2180
+ "/sitemap/recipes.xml",
2181
+ "/sitemap/chefs.xml",
2182
+ ];
2183
+
2184
+ const xml = generateSitemapIndex(baseUrl, sitemapPaths);
2185
+
2186
+ return new Response(xml, {
2187
+ headers: {
2188
+ "Content-Type": "application/xml",
2189
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
2190
+ },
2191
+ });
2192
+ }
2193
+ `;
2194
+ } else if (entryPoint === "sitemap/pages") {
2195
+ // Sitemap des pages statiques
2196
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Static Pages Sitemap Route
2197
+ // Module: ${moduleConfig.moduleName}
2198
+ // Path: ${sitemapPath}
2199
+ import { generateStaticPagesSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
2200
+
2201
+ export async function GET(): Promise<Response> {
2202
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
2203
+
2204
+ const sitemap = await generateStaticPagesSitemap(baseUrl);
2205
+ const xml = generateSitemapXml(sitemap);
2206
+
2207
+ return new Response(xml, {
2208
+ headers: {
2209
+ "Content-Type": "application/xml",
2210
+ "Cache-Control": "public, max-age=86400, s-maxage=86400",
2211
+ },
2212
+ });
2213
+ }
2214
+ `;
2215
+ } else if (entryPoint === "sitemap/recipes") {
2216
+ // Sitemap des recettes
2217
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Recipes Sitemap Route
2218
+ // Module: ${moduleConfig.moduleName}
2219
+ // Path: ${sitemapPath}
2220
+ import { generateRecipesSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
2221
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
2222
+
2223
+ export async function GET(): Promise<Response> {
2224
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
2225
+ const supabase = await getSupabaseServiceClient();
2226
+
2227
+ const sitemap = await generateRecipesSitemap(baseUrl, supabase);
2228
+ const xml = generateSitemapXml(sitemap);
2229
+
2230
+ return new Response(xml, {
2231
+ headers: {
2232
+ "Content-Type": "application/xml",
2233
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
2234
+ },
2235
+ });
2236
+ }
2237
+ `;
2238
+ } else if (entryPoint === "sitemap/chefs") {
2239
+ // Sitemap des chefs
2240
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Chefs Sitemap Route
2241
+ // Module: ${moduleConfig.moduleName}
2242
+ // Path: ${sitemapPath}
2243
+ import { generateChefsSitemap, generateSitemapXml } from "${moduleConfig.moduleName}/sitemap";
2244
+ import { getSupabaseServiceClient } from "@lastbrain/core/server";
2245
+
2246
+ export async function GET(): Promise<Response> {
2247
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
2248
+ const supabase = await getSupabaseServiceClient();
2249
+
2250
+ const sitemap = await generateChefsSitemap(baseUrl, supabase);
2251
+ const xml = generateSitemapXml(sitemap);
2252
+
2253
+ return new Response(xml, {
2254
+ headers: {
2255
+ "Content-Type": "application/xml",
2256
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
2257
+ },
2258
+ });
2259
+ }
2260
+ `;
2261
+ } else {
2262
+ // Sitemap générique - export direct du handler
2263
+ content = `// GENERATED BY LASTBRAIN MODULE BUILD - Sitemap route
2264
+ // Module: ${moduleConfig.moduleName}
2265
+ // Path: ${sitemapPath}
2266
+ export { ${sitemap.handlerExport} } from "${moduleConfig.moduleName}/${sitemap.entryPoint}";
2267
+ `;
2268
+ }
2269
+
2270
+ fs.writeFileSync(routeFilePath, content);
2271
+
2272
+ if (isDebugMode) {
2273
+ console.log(`🗺️ Generated sitemap route: ${routeFilePath}`);
2274
+ }
2275
+ }
2276
+
2277
+ // Générer le sitemap.xml global qui agrège tous les sitemaps des modules
2278
+ if (moduleSitemapIndexes.length > 0) {
2279
+ const globalSitemapDir = path.join(appDirectory, "sitemap.xml");
2280
+ const globalSitemapFile = path.join(globalSitemapDir, "route.ts");
2281
+ ensureDirectory(globalSitemapDir);
2282
+
2283
+ const sitemapUrls = moduleSitemapIndexes
2284
+ .map((path) => {
2285
+ return ` {
2286
+ url: \`\${baseUrl}${path}\`,
2287
+ lastModified: new Date(),
2288
+ }`;
2289
+ })
2290
+ .join(",\n");
2291
+
2292
+ const globalSitemapContent = `// GENERATED BY LASTBRAIN MODULE BUILD - Global Sitemap Index
2293
+ // This file aggregates all module sitemaps
2294
+ // Module sitemaps: ${moduleSitemapIndexes.join(", ")}
2295
+
2296
+ export async function GET(): Promise<Response> {
2297
+ const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || (process.env.VERCEL_URL ? \`https://\${process.env.VERCEL_URL}\` : "https://example.com");
2298
+
2299
+ const sitemapIndexXml = \`<?xml version="1.0" encoding="UTF-8"?>
2300
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
2301
+ ${moduleSitemapIndexes
2302
+ .map(
2303
+ (path) => ` <sitemap>
2304
+ <loc>\${baseUrl}${path}</loc>
2305
+ <lastmod>\${new Date().toISOString()}</lastmod>
2306
+ </sitemap>`
2307
+ )
2308
+ .join("\n")}
2309
+ </sitemapindex>\`;
2310
+
2311
+ return new Response(sitemapIndexXml, {
2312
+ headers: {
2313
+ "Content-Type": "application/xml",
2314
+ "Cache-Control": "public, max-age=3600, s-maxage=3600",
2315
+ },
2316
+ });
2317
+ }
2318
+ `;
2319
+
2320
+ fs.writeFileSync(globalSitemapFile, globalSitemapContent);
2321
+
2322
+ if (isDebugMode) {
2323
+ console.log(
2324
+ `🌍 Generated global sitemap.xml aggregating ${moduleSitemapIndexes.length} module sitemap(s)`
2325
+ );
2326
+ }
2327
+ }
2328
+
2329
+ if (isDebugMode) {
2330
+ console.log(`✅ Generated ${allSitemaps.length} sitemap route(s)`);
2331
+ }
2332
+ } catch (error) {
2333
+ console.error("❌ Error generating sitemap routes:", error);
2334
+ }
2335
+ }
2336
+
1959
2337
  /**
1960
2338
  * Génère les fichiers i18n en concaténant les traductions des modules
1961
2339
  * et en incluant les traductions personnalisées de i18n/default
@@ -1964,22 +2342,6 @@ async function generateLocaleConfig() {
1964
2342
  const appI18nDir = path.join(projectRoot, "i18n");
1965
2343
  const appConfigDir = path.join(projectRoot, "config");
1966
2344
 
1967
- // Mapping standard des codes de langue vers les locales
1968
- const LOCALE_MAP: Record<string, string> = {
1969
- fr: "fr_FR",
1970
- en: "en_US",
1971
- es: "es_ES",
1972
- de: "de_DE",
1973
- it: "it_IT",
1974
- pt: "pt_PT",
1975
- nl: "nl_NL",
1976
- ru: "ru_RU",
1977
- ja: "ja_JP",
1978
- zh: "zh_CN",
1979
- ar: "ar_SA",
1980
- hi: "hi_IN",
1981
- };
1982
-
1983
2345
  try {
1984
2346
  // Scanner les fichiers i18n disponibles
1985
2347
  let languages: string[] = ["fr"]; // Fallback par défaut
@@ -2255,7 +2617,7 @@ function generateHomePage(moduleConfigs: ModuleBuildConfig[]) {
2255
2617
  const content = `// GENERATED BY LASTBRAIN MODULE BUILD
2256
2618
  // Homepage from ${homePageModule.moduleName}
2257
2619
  import { ${importComponentName} as ModuleHomePage${homePageConfig.metadataExport ? `, ${metadataImportAlias}` : ""} } from "${importPath}";
2258
- import { footerConfig } from "../../../config/footer";
2620
+
2259
2621
  import { LOCALE_CONFIG } from "../../../config/locales.generated";
2260
2622
  ${
2261
2623
  homePageConfig.metadataExport
@@ -2264,8 +2626,12 @@ ${
2264
2626
  }`
2265
2627
  : ""
2266
2628
  }
2267
- export default function ${wrapperComponentName}() {
2268
- return <ModuleHomePage footerConfig={footerConfig} localeConfig={LOCALE_CONFIG} />;
2629
+ export default function ${wrapperComponentName}({
2630
+ params,
2631
+ }: {
2632
+ params: Promise<{ lang: string }>;
2633
+ }) {
2634
+ return <ModuleHomePage params={params} localeConfig={LOCALE_CONFIG} />;
2269
2635
  }
2270
2636
  `;
2271
2637
 
@@ -2342,8 +2708,9 @@ export async function runModuleBuild() {
2342
2708
  dumpNavigation();
2343
2709
  generateMenuConfig(moduleConfigs);
2344
2710
  generateDocsPage(moduleConfigs);
2345
- generateAppAside();
2346
- generateLayouts();
2711
+ // Note: AppAside, AppHeader, etc. sont maintenant générés dans components/ à la racine
2712
+ // et non plus dupliqués dans app/components/
2713
+ generateLayouts(moduleConfigs);
2347
2714
  copyModuleMigrations(moduleConfigs);
2348
2715
 
2349
2716
  // Générer la page d'accueil "/" si un module la définit
@@ -2388,6 +2755,25 @@ export async function runModuleBuild() {
2388
2755
  }
2389
2756
  await generateFooterConfig(moduleConfigs);
2390
2757
 
2758
+ // Générer les routes sitemap (ancien système)
2759
+ if (isDebugMode) {
2760
+ console.log("🗺️ Generating sitemap routes (legacy)...");
2761
+ }
2762
+ //await generateSitemapRoutes(moduleConfigs);
2763
+
2764
+ // Générer les sitemaps basés sur manifests (nouveau système)
2765
+ if (isDebugMode) {
2766
+ console.log("🗺️ Generating manifest-based sitemaps...");
2767
+ }
2768
+ const availableLanguages = Object.keys(LOCALE_MAP);
2769
+ await generateManifestBasedSitemaps(
2770
+ appDirectory,
2771
+ moduleConfigs,
2772
+ availableLanguages,
2773
+ projectRequire,
2774
+ isDebugMode
2775
+ );
2776
+
2391
2777
  // Générer les fichiers i18n
2392
2778
  if (isDebugMode) {
2393
2779
  console.log("🌍 Building i18n files...");