doccupine 0.0.59 → 0.0.60

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 (55) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +264 -12
  3. package/dist/templates/app/layout.d.ts +34 -1
  4. package/dist/templates/app/layout.js +98 -34
  5. package/dist/templates/components/Chat.d.ts +1 -1
  6. package/dist/templates/components/Chat.js +9 -15
  7. package/dist/templates/components/DocsSideBar.d.ts +1 -1
  8. package/dist/templates/components/DocsSideBar.js +14 -12
  9. package/dist/templates/components/MDXComponents.d.ts +1 -1
  10. package/dist/templates/components/MDXComponents.js +21 -0
  11. package/dist/templates/components/SectionNavProvider.d.ts +1 -0
  12. package/dist/templates/components/SectionNavProvider.js +102 -0
  13. package/dist/templates/components/SideBar.d.ts +1 -1
  14. package/dist/templates/components/SideBar.js +7 -2
  15. package/dist/templates/components/layout/ActionBar.d.ts +1 -1
  16. package/dist/templates/components/layout/ActionBar.js +17 -75
  17. package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
  18. package/dist/templates/components/layout/DocsComponents.js +50 -11
  19. package/dist/templates/components/layout/Footer.d.ts +1 -1
  20. package/dist/templates/components/layout/Footer.js +3 -3
  21. package/dist/templates/components/layout/GlobalStyles.d.ts +1 -1
  22. package/dist/templates/components/layout/GlobalStyles.js +5 -2
  23. package/dist/templates/components/layout/Header.d.ts +1 -1
  24. package/dist/templates/components/layout/Header.js +82 -45
  25. package/dist/templates/components/layout/SectionBar.d.ts +1 -0
  26. package/dist/templates/components/layout/SectionBar.js +92 -0
  27. package/dist/templates/components/layout/StaticLinks.d.ts +1 -1
  28. package/dist/templates/components/layout/StaticLinks.js +11 -30
  29. package/dist/templates/mdx/ai-assistant.mdx.d.ts +1 -1
  30. package/dist/templates/mdx/ai-assistant.mdx.js +1 -1
  31. package/dist/templates/mdx/commands.mdx.d.ts +1 -1
  32. package/dist/templates/mdx/commands.mdx.js +3 -1
  33. package/dist/templates/mdx/deployment.mdx.d.ts +1 -1
  34. package/dist/templates/mdx/deployment.mdx.js +21 -6
  35. package/dist/templates/mdx/fonts.mdx.d.ts +1 -1
  36. package/dist/templates/mdx/fonts.mdx.js +9 -2
  37. package/dist/templates/mdx/footer-links.mdx.d.ts +1 -0
  38. package/dist/templates/mdx/footer-links.mdx.js +45 -0
  39. package/dist/templates/mdx/globals.mdx.d.ts +1 -1
  40. package/dist/templates/mdx/globals.mdx.js +6 -2
  41. package/dist/templates/mdx/links.mdx.d.ts +1 -1
  42. package/dist/templates/mdx/links.mdx.js +1 -1
  43. package/dist/templates/mdx/media-and-assets.mdx.d.ts +1 -1
  44. package/dist/templates/mdx/media-and-assets.mdx.js +6 -3
  45. package/dist/templates/mdx/model-context-protocol.mdx.d.ts +1 -1
  46. package/dist/templates/mdx/model-context-protocol.mdx.js +1 -1
  47. package/dist/templates/mdx/navigation.mdx.d.ts +1 -1
  48. package/dist/templates/mdx/navigation.mdx.js +43 -7
  49. package/dist/templates/mdx/sections.mdx.d.ts +1 -0
  50. package/dist/templates/mdx/sections.mdx.js +194 -0
  51. package/dist/templates/mdx/theme.mdx.d.ts +1 -1
  52. package/dist/templates/mdx/theme.mdx.js +7 -4
  53. package/dist/templates/utils/orderNavItems.d.ts +1 -1
  54. package/dist/templates/utils/orderNavItems.js +1 -0
  55. package/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
  export declare function generateSlug(filePath: string): string;
3
+ export declare function getFullSlug(pageSlug: string, sectionSlug: string): string;
3
4
  export declare function escapeTemplateContent(content: string): string;
package/dist/index.js CHANGED
@@ -27,7 +27,9 @@ import { clickOutsideTemplate } from "./templates/components/ClickOutside.js";
27
27
  import { docsTemplate } from "./templates/components/Docs.js";
28
28
  import { docsSideBarTemplate } from "./templates/components/DocsSideBar.js";
29
29
  import { mdxComponentsTemplate } from "./templates/components/MDXComponents.js";
30
+ import { sectionNavProviderTemplate } from "./templates/components/SectionNavProvider.js";
30
31
  import { sideBarTemplate } from "./templates/components/SideBar.js";
32
+ import { sectionBarTemplate } from "./templates/components/layout/SectionBar.js";
31
33
  import { accordionTemplate } from "./templates/components/layout/Accordion.js";
32
34
  import { actionBarTemplate } from "./templates/components/layout/ActionBar.js";
33
35
  import { buttonTemplate } from "./templates/components/layout/Button.js";
@@ -82,11 +84,12 @@ import { headersAndTextMdxTemplate } from "./templates/mdx/headers-and-text.mdx.
82
84
  import { iconsMdxTemplate } from "./templates/mdx/icons.mdx.js";
83
85
  import { imageAndEmbedsMdxTemplate } from "./templates/mdx/image-and-embeds.mdx.js";
84
86
  import { indexMdxTemplate } from "./templates/mdx/index.mdx.js";
85
- import { linksMdxTemplate } from "./templates/mdx/links.mdx.js";
87
+ import { footerLinksMdxTemplate } from "./templates/mdx/footer-links.mdx.js";
86
88
  import { listAndTablesMdxTemplate } from "./templates/mdx/list-and-tables.mdx.js";
87
89
  import { mediaAndAssetsMdxTemplate } from "./templates/mdx/media-and-assets.mdx.js";
88
90
  import { mcpMdxTemplate } from "./templates/mdx/model-context-protocol.mdx.js";
89
91
  import { navigationMdxTemplate } from "./templates/mdx/navigation.mdx.js";
92
+ import { sectionsMdxTemplate } from "./templates/mdx/sections.mdx.js";
90
93
  import { stepsMdxTemplate } from "./templates/mdx/steps.mdx.js";
91
94
  import { tabsMdxTemplate } from "./templates/mdx/tabs.mdx.js";
92
95
  import { themeMdxTemplate } from "./templates/mdx/theme.mdx.js";
@@ -107,11 +110,23 @@ export function generateSlug(filePath) {
107
110
  if (filePath === "index.mdx" || filePath === "./index.mdx") {
108
111
  return "";
109
112
  }
110
- return filePath
113
+ const normalized = filePath
111
114
  .replace(/\.mdx$/, "")
112
115
  .replace(/\\/g, "/")
113
116
  .replace(/[^a-zA-Z0-9\/\-_]/g, "-")
114
117
  .toLowerCase();
118
+ // Strip trailing /index for subdirectory index files
119
+ if (normalized.endsWith("/index")) {
120
+ return normalized.slice(0, -"/index".length);
121
+ }
122
+ return normalized;
123
+ }
124
+ export function getFullSlug(pageSlug, sectionSlug) {
125
+ if (!sectionSlug)
126
+ return pageSlug;
127
+ if (pageSlug === "")
128
+ return sectionSlug;
129
+ return `${sectionSlug}/${pageSlug}`;
115
130
  }
116
131
  export function escapeTemplateContent(content) {
117
132
  return content
@@ -207,8 +222,12 @@ class MDXToNextJSGenerator {
207
222
  "navigation.json",
208
223
  "config.json",
209
224
  "links.json",
225
+ "sections.json",
210
226
  ];
211
227
  fontConfigFile = "fonts.json";
228
+ sectionsConfig = null;
229
+ /** Guards against recursive reprocessing when maybeUpdateSections() triggers processAllMDXFiles() */
230
+ isReprocessing = false;
212
231
  constructor(watchDir, outputDir) {
213
232
  this.watchDir = path.resolve(watchDir);
214
233
  this.outputDir = path.resolve(outputDir);
@@ -218,12 +237,17 @@ class MDXToNextJSGenerator {
218
237
  console.log(chalk.blue("🚀 Initializing MDX to Next.js generator..."));
219
238
  await fs.ensureDir(this.watchDir);
220
239
  await fs.ensureDir(this.outputDir);
240
+ this.sectionsConfig = await this.resolveSections();
241
+ if (this.sectionsConfig) {
242
+ console.log(chalk.blue(`📑 Found ${this.sectionsConfig.length} section(s): ${this.sectionsConfig.map((s) => s.label).join(", ")}`));
243
+ }
221
244
  await this.createNextJSStructure();
222
245
  await this.createStartingDocs();
223
246
  await this.copyCustomConfigFiles();
224
247
  await this.copyFontConfig();
225
248
  await this.copyPublicFiles();
226
249
  await this.processAllMDXFiles();
250
+ await this.generateSectionIndexPages();
227
251
  console.log(chalk.green("✅ Initial setup complete!"));
228
252
  console.log(chalk.cyan("💡 To start the Next.js dev server:"));
229
253
  console.log(chalk.white(` cd ${path.relative(process.cwd(), this.outputDir)}`));
@@ -239,6 +263,7 @@ class MDXToNextJSGenerator {
239
263
  "eslint.config.mjs": eslintConfigTemplate,
240
264
  "links.json": `[]`,
241
265
  "navigation.json": `[]`,
266
+ "sections.json": `[]`,
242
267
  "next.config.ts": nextConfigTemplate,
243
268
  "package.json": packageJsonTemplate,
244
269
  "proxy.ts": proxyTemplate,
@@ -268,6 +293,7 @@ class MDXToNextJSGenerator {
268
293
  "components/Docs.tsx": docsTemplate,
269
294
  "components/DocsSideBar.tsx": docsSideBarTemplate,
270
295
  "components/MDXComponents.tsx": mdxComponentsTemplate,
296
+ "components/SectionNavProvider.tsx": sectionNavProviderTemplate,
271
297
  "components/SideBar.tsx": sideBarTemplate,
272
298
  "components/layout/Accordion.tsx": accordionTemplate,
273
299
  "components/layout/ActionBar.tsx": actionBarTemplate,
@@ -281,6 +307,7 @@ class MDXToNextJSGenerator {
281
307
  "components/layout/DemoTheme.tsx": demoThemeTemplate,
282
308
  "components/layout/DocsComponents.tsx": docsComponentsTemplate,
283
309
  "components/layout/DocsNavigation.tsx": docsNavigationTemplate,
310
+ "components/layout/SectionBar.tsx": sectionBarTemplate,
284
311
  "components/layout/Field.tsx": fieldTemplate,
285
312
  "components/layout/Footer.tsx": footerTemplate,
286
313
  "components/layout/GlobalStyles.ts": globalStylesTemplate,
@@ -319,11 +346,12 @@ class MDXToNextJSGenerator {
319
346
  "icons.mdx": iconsMdxTemplate,
320
347
  "image-and-embeds.mdx": imageAndEmbedsMdxTemplate,
321
348
  "index.mdx": indexMdxTemplate,
322
- "links.mdx": linksMdxTemplate,
349
+ "footer-links.mdx": footerLinksMdxTemplate,
323
350
  "lists-and-tables.mdx": listAndTablesMdxTemplate,
324
351
  "media-and-assets.mdx": mediaAndAssetsMdxTemplate,
325
352
  "model-context-protocol.mdx": mcpMdxTemplate,
326
353
  "navigation.mdx": navigationMdxTemplate,
354
+ "sections.mdx": sectionsMdxTemplate,
327
355
  "steps.mdx": stepsMdxTemplate,
328
356
  "tabs.mdx": tabsMdxTemplate,
329
357
  "theme.mdx": themeMdxTemplate,
@@ -378,6 +406,152 @@ class MDXToNextJSGenerator {
378
406
  }
379
407
  return null;
380
408
  }
409
+ async loadSectionsConfig() {
410
+ const sectionsPath = path.join(this.rootDir, "sections.json");
411
+ try {
412
+ if (await fs.pathExists(sectionsPath)) {
413
+ const content = await fs.readFile(sectionsPath, "utf8");
414
+ const parsed = JSON.parse(content);
415
+ if (Array.isArray(parsed) && parsed.length > 0) {
416
+ return parsed;
417
+ }
418
+ }
419
+ }
420
+ catch (error) {
421
+ console.warn(chalk.yellow("⚠️ Error reading sections.json"), error);
422
+ }
423
+ return null;
424
+ }
425
+ async discoverSectionsFromFrontmatter() {
426
+ const files = await this.getAllMDXFiles();
427
+ const sectionMap = new Map();
428
+ let hasUnsectionedPages = false;
429
+ let defaultSectionLabel = "Docs";
430
+ for (const file of files) {
431
+ const fullPath = path.join(this.watchDir, file);
432
+ const content = await fs.readFile(fullPath, "utf8");
433
+ const { data: frontmatter } = matter(content);
434
+ if (frontmatter.section) {
435
+ const label = frontmatter.section;
436
+ const order = frontmatter.sectionOrder || 0;
437
+ const existing = sectionMap.get(label);
438
+ if (!existing || order < existing.order) {
439
+ sectionMap.set(label, { label, order });
440
+ }
441
+ }
442
+ else {
443
+ hasUnsectionedPages = true;
444
+ }
445
+ if ((file === "index.mdx" || file === "./index.mdx") &&
446
+ frontmatter.sectionLabel) {
447
+ defaultSectionLabel = frontmatter.sectionLabel;
448
+ }
449
+ }
450
+ if (sectionMap.size === 0)
451
+ return null;
452
+ const sorted = [...sectionMap.values()].sort((a, b) => a.order - b.order);
453
+ const sections = [];
454
+ // Implicit root entry for pages without a section field
455
+ if (hasUnsectionedPages) {
456
+ sections.push({ label: defaultSectionLabel, slug: "" });
457
+ }
458
+ for (const s of sorted) {
459
+ sections.push({
460
+ label: s.label,
461
+ slug: s.label.toLowerCase().replace(/\s+/g, "-"),
462
+ });
463
+ }
464
+ return sections;
465
+ }
466
+ async resolveSections() {
467
+ const fromFile = await this.loadSectionsConfig();
468
+ if (fromFile)
469
+ return fromFile;
470
+ return this.discoverSectionsFromFrontmatter();
471
+ }
472
+ async reloadSections() {
473
+ console.log(chalk.cyan("📑 Sections configuration changed"));
474
+ this.sectionsConfig = await this.resolveSections();
475
+ await this.processAllMDXFiles();
476
+ await this.generateSectionIndexPages();
477
+ }
478
+ async maybeUpdateSections() {
479
+ if (this.isReprocessing)
480
+ return;
481
+ // Skip if sections.json exists (explicit config takes priority)
482
+ const fromFile = await this.loadSectionsConfig();
483
+ if (fromFile)
484
+ return;
485
+ const newSections = await this.discoverSectionsFromFrontmatter();
486
+ const changed = JSON.stringify(newSections) !== JSON.stringify(this.sectionsConfig);
487
+ if (changed) {
488
+ console.log(chalk.cyan(newSections
489
+ ? `📑 Sections updated from frontmatter: ${newSections.map((s) => s.label).join(", ")}`
490
+ : "📑 Sections cleared (no section frontmatter found)"));
491
+ this.sectionsConfig = newSections;
492
+ this.isReprocessing = true;
493
+ try {
494
+ await this.processAllMDXFiles();
495
+ await this.generateSectionIndexPages();
496
+ }
497
+ finally {
498
+ this.isReprocessing = false;
499
+ }
500
+ }
501
+ }
502
+ determineSectionForFile(filePath, frontmatter) {
503
+ if (!this.sectionsConfig || this.sectionsConfig.length === 0) {
504
+ return { sectionSlug: "", pageSlug: generateSlug(filePath) };
505
+ }
506
+ const normalizedPath = filePath.replace(/\\/g, "/");
507
+ const firstDir = normalizedPath.includes("/")
508
+ ? normalizedPath.split("/")[0]
509
+ : "";
510
+ // Explicit directory matching (entries with a directory field)
511
+ for (const section of this.sectionsConfig) {
512
+ if (!section.directory)
513
+ continue;
514
+ const dirPrefix = section.directory + "/";
515
+ if (normalizedPath.startsWith(dirPrefix)) {
516
+ return {
517
+ sectionSlug: section.slug,
518
+ pageSlug: generateSlug(normalizedPath.slice(dirPrefix.length)),
519
+ };
520
+ }
521
+ }
522
+ // Directory matches section slug (auto-detect)
523
+ if (firstDir) {
524
+ const match = this.sectionsConfig.find((s) => s.slug === firstDir);
525
+ if (match) {
526
+ const pathForSlug = normalizedPath.slice(firstDir.length + 1);
527
+ return {
528
+ sectionSlug: match.slug,
529
+ pageSlug: generateSlug(pathForSlug),
530
+ };
531
+ }
532
+ }
533
+ // Frontmatter section field
534
+ if (frontmatter.section) {
535
+ const label = frontmatter.section;
536
+ const match = this.sectionsConfig.find((s) => s.label === label);
537
+ if (match) {
538
+ // Strip the directory if it matches the section slug
539
+ let pathForSlug = filePath;
540
+ if (firstDir && firstDir === match.slug) {
541
+ pathForSlug = normalizedPath.slice(firstDir.length + 1);
542
+ }
543
+ return {
544
+ sectionSlug: match.slug,
545
+ pageSlug: generateSlug(pathForSlug),
546
+ };
547
+ }
548
+ }
549
+ // No section match - page stays at root
550
+ return {
551
+ sectionSlug: "",
552
+ pageSlug: generateSlug(filePath),
553
+ };
554
+ }
381
555
  async handleConfigFileChange(filePath) {
382
556
  const fileName = path.basename(filePath);
383
557
  if (this.configFiles.includes(fileName)) {
@@ -386,6 +560,9 @@ class MDXToNextJSGenerator {
386
560
  try {
387
561
  await fs.copy(sourcePath, destPath);
388
562
  console.log(chalk.green(`📋 Updated ${fileName} in Next.js app`));
563
+ if (fileName === "sections.json") {
564
+ await this.reloadSections();
565
+ }
389
566
  }
390
567
  catch (error) {
391
568
  console.error(chalk.red(`❌ Error copying ${fileName}:`), error);
@@ -401,6 +578,9 @@ class MDXToNextJSGenerator {
401
578
  await fs.remove(destPath);
402
579
  console.log(chalk.yellow(`🗑️ Removed ${fileName} from Next.js app`));
403
580
  }
581
+ if (fileName === "sections.json") {
582
+ await this.reloadSections();
583
+ }
404
584
  }
405
585
  catch (error) {
406
586
  console.error(chalk.red(`❌ Error removing ${fileName}:`), error);
@@ -579,8 +759,10 @@ class MDXToNextJSGenerator {
579
759
  const fullPath = path.join(this.watchDir, file);
580
760
  const content = await fs.readFile(fullPath, "utf8");
581
761
  const { data: frontmatter } = matter(content);
762
+ const { sectionSlug, pageSlug } = this.determineSectionForFile(file, frontmatter);
763
+ const fullSlug = getFullSlug(pageSlug, sectionSlug);
582
764
  return {
583
- slug: this.generateSlug(file),
765
+ slug: fullSlug,
584
766
  title: frontmatter.title || "Untitled",
585
767
  description: frontmatter.description || "",
586
768
  date: frontmatter.date || null,
@@ -588,6 +770,7 @@ class MDXToNextJSGenerator {
588
770
  path: file,
589
771
  categoryOrder: frontmatter.categoryOrder || 0,
590
772
  order: frontmatter.order || 0,
773
+ section: sectionSlug,
591
774
  };
592
775
  }
593
776
  async buildAllPagesMeta() {
@@ -600,7 +783,11 @@ class MDXToNextJSGenerator {
600
783
  try {
601
784
  const content = await fs.readFile(fullPath, "utf8");
602
785
  const { data: frontmatter, content: mdxContent } = matter(content);
603
- if (filePath === "index.mdx" || filePath === "./index.mdx") {
786
+ const { sectionSlug, pageSlug } = this.determineSectionForFile(filePath, frontmatter);
787
+ const fullSlug = getFullSlug(pageSlug, sectionSlug);
788
+ const isIndex = filePath === "index.mdx" || filePath === "./index.mdx";
789
+ const isSectionIndex = this.sectionsConfig && pageSlug === "" && sectionSlug !== "";
790
+ if (isIndex) {
604
791
  console.log(chalk.blue("🏠 Updating homepage with index.mdx content"));
605
792
  }
606
793
  else {
@@ -608,13 +795,18 @@ class MDXToNextJSGenerator {
608
795
  path: filePath,
609
796
  content: mdxContent,
610
797
  frontmatter,
611
- slug: this.generateSlug(filePath),
798
+ slug: fullSlug,
612
799
  };
613
800
  await this.generatePageFromMDX(mdxFile);
614
801
  }
802
+ if (isSectionIndex) {
803
+ await this.updateSectionIndex(sectionSlug, frontmatter, mdxContent);
804
+ }
615
805
  await this.updatePagesIndex();
616
806
  await this.updateRootLayout();
807
+ await this.generateSectionIndexPages();
617
808
  console.log(chalk.green(`✅ Generated page for: ${filePath}`));
809
+ await this.maybeUpdateSections();
618
810
  }
619
811
  catch (error) {
620
812
  console.error(chalk.red(`❌ Error processing ${filePath}:`), error);
@@ -627,13 +819,16 @@ class MDXToNextJSGenerator {
627
819
  console.log(chalk.blue("🏠 Updating homepage - index.mdx deleted"));
628
820
  }
629
821
  else {
630
- const slug = this.generateSlug(filePath);
631
- const pagePath = path.join(this.outputDir, "app", slug);
822
+ // We don't have frontmatter for deleted files, so use directory-based matching
823
+ const { sectionSlug, pageSlug } = this.determineSectionForFile(filePath, {});
824
+ const fullSlug = getFullSlug(pageSlug, sectionSlug);
825
+ const pagePath = path.join(this.outputDir, "app", fullSlug);
632
826
  await fs.remove(pagePath);
633
827
  }
634
828
  await this.updatePagesIndex();
635
829
  await this.updateRootLayout();
636
830
  console.log(chalk.green(`✅ Removed page for: ${filePath}`));
831
+ await this.maybeUpdateSections();
637
832
  }
638
833
  catch (error) {
639
834
  console.error(chalk.red(`❌ Error removing page for ${filePath}:`), error);
@@ -663,13 +858,44 @@ class MDXToNextJSGenerator {
663
858
  await scanDir(this.watchDir);
664
859
  return files;
665
860
  }
666
- generateSlug(filePath) {
667
- return generateSlug(filePath);
668
- }
669
861
  async generateRootLayout() {
670
862
  const pages = await this.buildAllPagesMeta();
671
863
  const fontConfig = await this.loadFontConfig();
672
- return layoutTemplate(pages, fontConfig);
864
+ return layoutTemplate(pages, fontConfig, this.sectionsConfig);
865
+ }
866
+ async generateSectionIndexPages() {
867
+ if (!this.sectionsConfig || this.sectionsConfig.length === 0)
868
+ return;
869
+ const pages = await this.buildAllPagesMeta();
870
+ for (const section of this.sectionsConfig) {
871
+ if (section.slug === "")
872
+ continue;
873
+ // Check if a page already exists at the section root
874
+ const hasIndex = pages.some((p) => p.slug === section.slug);
875
+ if (hasIndex)
876
+ continue;
877
+ // Find the first page in this section
878
+ const sectionPages = pages
879
+ .filter((p) => p.section === section.slug)
880
+ .sort((a, b) => {
881
+ if (a.categoryOrder !== b.categoryOrder)
882
+ return a.categoryOrder - b.categoryOrder;
883
+ return a.order - b.order;
884
+ });
885
+ if (sectionPages.length === 0)
886
+ continue;
887
+ const firstPage = sectionPages[0];
888
+ const redirectContent = `import { redirect } from "next/navigation";
889
+
890
+ export default function SectionIndex() {
891
+ redirect("/${firstPage.slug}");
892
+ }
893
+ `;
894
+ const pagePath = path.join(this.outputDir, "app", section.slug, "page.tsx");
895
+ await fs.ensureDir(path.dirname(pagePath));
896
+ await fs.writeFile(pagePath, redirectContent, "utf8");
897
+ console.log(chalk.blue(`🔀 Generated section index redirect: /${section.slug} -> /${firstPage.slug}`));
898
+ }
673
899
  }
674
900
  async generatePageFromMDX(mdxFile) {
675
901
  const pageContent = `import { Metadata } from "next";
@@ -749,6 +975,32 @@ export default function Home() {
749
975
  `;
750
976
  await fs.writeFile(path.join(this.outputDir, "app", "page.tsx"), indexContent, "utf8");
751
977
  }
978
+ async updateSectionIndex(sectionSlug, frontmatter, mdxContent) {
979
+ const indexContent = `import { Metadata } from "next";
980
+ import { Docs } from "@/components/Docs";
981
+ import { config } from "@/utils/config";
982
+
983
+ const content = \`${escapeTemplateContent(mdxContent)}\`;
984
+
985
+ export const metadata: Metadata = {
986
+ title: \`\${config.name ? config.name + " -" : "Doccupine -"} ${frontmatter.title || "Section"}\`,
987
+ description: \`${frontmatter.description ? frontmatter.description : '${config.description ? config.description : "Generated with Doccupine"}'}\`,
988
+ icons: \`${frontmatter.icon ? frontmatter.icon : '\${config.icon || "https://doccupine.com/favicon.ico"}'}\`,
989
+ openGraph: {
990
+ title: \`\${config.name ? config.name + " -" : "Doccupine -"} ${frontmatter.title || "Section"}\`,
991
+ description: \`${frontmatter.description ? frontmatter.description : '${config.description ? config.description : "Generated with Doccupine"}'}\`,
992
+ images: \`${frontmatter.image ? frontmatter.image : '\${config.preview || "https://doccupine.com/preview.png"}'}\`,
993
+ },
994
+ };
995
+
996
+ export default function Page() {
997
+ return <Docs content={content} />;
998
+ }
999
+ `;
1000
+ const pagePath = path.join(this.outputDir, "app", sectionSlug, "page.tsx");
1001
+ await fs.ensureDir(path.dirname(pagePath));
1002
+ await fs.writeFile(pagePath, indexContent, "utf8");
1003
+ }
752
1004
  async updateRootLayout() {
753
1005
  const layoutContent = await this.generateRootLayout();
754
1006
  await fs.writeFile(path.join(this.outputDir, "app", "layout.tsx"), layoutContent, "utf8");
@@ -1 +1,34 @@
1
- export declare const layoutTemplate: (pages: any[], fontConfig: any) => string;
1
+ interface SectionConfig {
2
+ label: string;
3
+ slug: string;
4
+ directory?: string;
5
+ }
6
+ interface PageData {
7
+ slug: string;
8
+ title: string;
9
+ description: string;
10
+ date: string | null;
11
+ category: string;
12
+ path: string;
13
+ categoryOrder: number;
14
+ order: number;
15
+ section: string;
16
+ }
17
+ interface GoogleFontConfig {
18
+ fontName?: string;
19
+ subsets?: string[];
20
+ weight?: string | string[];
21
+ }
22
+ interface LocalFontSrc {
23
+ path: string;
24
+ weight: string;
25
+ style: string;
26
+ }
27
+ interface FontConfig {
28
+ googleFont?: GoogleFontConfig;
29
+ localFonts?: string | {
30
+ src?: LocalFontSrc[];
31
+ };
32
+ }
33
+ export declare const layoutTemplate: (pages: PageData[], fontConfig: FontConfig | null, sectionsConfig?: SectionConfig[] | null) => string;
34
+ export {};
@@ -1,8 +1,8 @@
1
- function formatPagesArray(pages) {
1
+ function formatObjectArray(items) {
2
2
  const MAX_WIDTH = 80;
3
- const items = pages.map((page) => {
3
+ const formatted = items.map((item) => {
4
4
  const lines = [" {"];
5
- const entries = Object.entries(page);
5
+ const entries = Object.entries(item);
6
6
  for (const [key, value] of entries) {
7
7
  const valueStr = JSON.stringify(value);
8
8
  const line = ` ${key}: ${valueStr},`;
@@ -17,37 +17,67 @@ function formatPagesArray(pages) {
17
17
  lines.push(" },");
18
18
  return lines.join("\n");
19
19
  });
20
- return "[\n" + items.join("\n") + "\n]";
20
+ return "[\n" + formatted.join("\n") + "\n]";
21
21
  }
22
- export const layoutTemplate = (pages, fontConfig) => `import type { Metadata } from "next";
23
- ${fontConfig?.googleFont?.fontName?.length ? `import { ${fontConfig.googleFont.fontName} } from "next/font/google";` : fontConfig?.localFonts?.length || fontConfig?.localFonts?.src?.length ? 'import localFont from "next/font/local";' : 'import { Inter } from "next/font/google";'}
22
+ function isGoogleFont(fc) {
23
+ return !!fc?.googleFont?.fontName;
24
+ }
25
+ function isLocalFont(fc) {
26
+ if (!fc?.localFonts)
27
+ return false;
28
+ if (typeof fc.localFonts === "string")
29
+ return true;
30
+ return !!fc.localFonts.src?.length;
31
+ }
32
+ function getLocalFontSrc(fc) {
33
+ if (typeof fc.localFonts === "string")
34
+ return `"${fc.localFonts}"`;
35
+ return JSON.stringify(fc.localFonts?.src, null, 2).replace(/"([^"]+)":/g, "$1:");
36
+ }
37
+ export const layoutTemplate = (pages, fontConfig, sectionsConfig = null) => {
38
+ const hasSections = sectionsConfig !== null && sectionsConfig.length > 0;
39
+ return `import type { Metadata } from "next";
40
+ ${isGoogleFont(fontConfig) ? `import { ${fontConfig.googleFont.fontName} } from "next/font/google";` : isLocalFont(fontConfig) ? 'import localFont from "next/font/local";' : 'import { Inter } from "next/font/google";'}
24
41
  import dynamic from "next/dynamic";
25
42
  import { StyledComponentsRegistry } from "cherry-styled-components";
26
43
  import { theme, themeDark } from "@/app/theme";
27
44
  import { CherryThemeProvider } from "@/components/layout/CherryThemeProvider";
28
45
  import { ChtProvider } from "@/components/Chat";
29
- import { Footer } from "@/components/layout/Footer";
30
- import { Header } from "@/components/layout/Header";
31
- import { DocsWrapper } from "@/components/layout/DocsComponents";
32
- import { SideBar } from "@/components/SideBar";
33
- import { DocsNavigation } from "@/components/layout/DocsNavigation";
46
+ ${hasSections
47
+ ? ""
48
+ : `import { Footer } from "@/components/layout/Footer";
49
+ `}import { Header } from "@/components/layout/Header";
34
50
  import {
51
+ DocsWrapper,
52
+ SectionBarProvider,
53
+ } from "@/components/layout/DocsComponents";
54
+ ${hasSections
55
+ ? ""
56
+ : `import { SideBar } from "@/components/SideBar";
57
+ import { DocsNavigation } from "@/components/layout/DocsNavigation";
58
+ `}import {
35
59
  transformPagesToGroupedStructure,
36
60
  type PagesProps,
37
61
  } from "@/utils/orderNavItems";
38
- import { StaticLinks } from "@/components/layout/StaticLinks";
39
- import { config } from "@/utils/config";
62
+ ${hasSections
63
+ ? ""
64
+ : `import { StaticLinks } from "@/components/layout/StaticLinks";
65
+ `}import { config } from "@/utils/config";
40
66
  import { verifyBrandingKey } from "@/utils/branding";
41
67
  import navigation from "@/navigation.json";
42
- const Chat = dynamic(() => import("@/components/Chat").then((mod) => mod.Chat));
68
+ ${hasSections
69
+ ? `import { SectionBar } from "@/components/layout/SectionBar";
70
+ import { SectionNavProvider } from "@/components/SectionNavProvider";
71
+ `
72
+ : ""}const Chat = dynamic(() => import("@/components/Chat").then((mod) => mod.Chat));
43
73
 
44
- ${fontConfig?.googleFont?.fontName?.length
45
- ? `const font = ${fontConfig.googleFont.fontName}({ ${[fontConfig?.googleFont?.subsets?.length ? `subsets: ${JSON.stringify(fontConfig.googleFont.subsets)}` : "", fontConfig.googleFont?.weight?.length ? `weight: ${Array.isArray(fontConfig.googleFont.weight) ? JSON.stringify(fontConfig.googleFont.weight) : `"${fontConfig.googleFont.weight}"`}` : ""].filter(Boolean).join(", ")} });`
46
- : fontConfig?.localFonts?.length || fontConfig?.localFonts?.src?.length
47
- ? `const font = localFont({
48
- src: ${fontConfig.localFonts?.src?.length ? JSON.stringify(fontConfig?.localFonts.src, null, 2).replace(/"([^"]+)":/g, "$1:") : `"${fontConfig?.localFonts}"`},
74
+ ${isGoogleFont(fontConfig)
75
+ ? `const font = ${fontConfig.googleFont.fontName}({ ${[fontConfig.googleFont.subsets?.length ? `subsets: ${JSON.stringify(fontConfig.googleFont.subsets)}` : "", fontConfig.googleFont.weight?.length ? `weight: ${Array.isArray(fontConfig.googleFont.weight) ? JSON.stringify(fontConfig.googleFont.weight) : `"${fontConfig.googleFont.weight}"`}` : ""].filter(Boolean).join(", ")} });`
76
+ : isLocalFont(fontConfig)
77
+ ? `const font = localFont({
78
+ src: ${getLocalFontSrc(fontConfig)},
49
79
  });`
50
- : 'const font = Inter({ subsets: ["latin"] });'}
80
+ : 'const font = Inter({ subsets: ["latin"] });'}
51
81
 
52
82
  export const metadata: Metadata = {
53
83
  title: config.name || "Doccupine",
@@ -64,7 +94,7 @@ export const metadata: Metadata = {
64
94
  },
65
95
  };
66
96
 
67
- const doccupinePages = ${formatPagesArray(pages)};
97
+ const doccupinePages = ${formatObjectArray(pages)};${hasSections ? `\nconst doccupineSections = ${formatObjectArray(sectionsConfig)};` : ""}
68
98
 
69
99
  export default async function RootLayout({
70
100
  children,
@@ -72,7 +102,37 @@ export default async function RootLayout({
72
102
  children: React.ReactNode;
73
103
  }>) {
74
104
  const hideBranding = verifyBrandingKey();
105
+ ${hasSections
106
+ ? `
107
+ const pages: PagesProps[] = doccupinePages;
75
108
 
109
+ return (
110
+ <html lang="en">
111
+ <body className={font.className}>
112
+ <StyledComponentsRegistry>
113
+ <CherryThemeProvider theme={theme} themeDark={themeDark}>
114
+ <ChtProvider isChatActive={process.env.LLM_PROVIDER ? true : false}>
115
+ <Header>
116
+ <SectionBar sections={doccupineSections} />
117
+ </Header>
118
+ {process.env.LLM_PROVIDER && <Chat />}
119
+ <DocsWrapper>
120
+ <SectionNavProvider
121
+ sections={doccupineSections}
122
+ allPages={pages}
123
+ hideBranding={hideBranding}
124
+ >
125
+ {children}
126
+ </SectionNavProvider>
127
+ </DocsWrapper>
128
+ </ChtProvider>
129
+ </CherryThemeProvider>
130
+ </StyledComponentsRegistry>
131
+ </body>
132
+ </html>
133
+ );
134
+ }`
135
+ : `
76
136
  const defaultPages = [
77
137
  {
78
138
  slug: "",
@@ -87,9 +147,10 @@ export default async function RootLayout({
87
147
  ];
88
148
 
89
149
  const pages: PagesProps[] = doccupinePages;
90
- const result = navigation.length
91
- ? navigation
92
- : transformPagesToGroupedStructure(pages);
150
+ const result =
151
+ Array.isArray(navigation) && navigation.length
152
+ ? navigation
153
+ : transformPagesToGroupedStructure(pages);
93
154
  const defaultResults = transformPagesToGroupedStructure(defaultPages);
94
155
 
95
156
  return (
@@ -99,21 +160,24 @@ export default async function RootLayout({
99
160
  <CherryThemeProvider theme={theme} themeDark={themeDark}>
100
161
  <ChtProvider isChatActive={process.env.LLM_PROVIDER ? true : false}>
101
162
  <Header />
102
- <StaticLinks />
103
163
  {process.env.LLM_PROVIDER && <Chat />}
104
- <DocsWrapper>
105
- <SideBar result={result.length ? result : defaultResults} />
106
- {children}
107
- <DocsNavigation
108
- result={result.length ? result : defaultResults}
109
- />
110
- <Footer hideBranding={hideBranding} />
111
- </DocsWrapper>
164
+ <SectionBarProvider hasSectionBar={false}>
165
+ <DocsWrapper>
166
+ <SideBar result={result.length ? result : defaultResults} />
167
+ {children}
168
+ <DocsNavigation
169
+ result={result.length ? result : defaultResults}
170
+ />
171
+ <StaticLinks />
172
+ <Footer hideBranding={hideBranding} />
173
+ </DocsWrapper>
174
+ </SectionBarProvider>
112
175
  </ChtProvider>
113
176
  </CherryThemeProvider>
114
177
  </StyledComponentsRegistry>
115
178
  </body>
116
179
  </html>
117
180
  );
118
- }
181
+ }`}
119
182
  `;
183
+ };