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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +264 -12
- package/dist/templates/app/layout.d.ts +34 -1
- package/dist/templates/app/layout.js +98 -34
- package/dist/templates/components/Chat.d.ts +1 -1
- package/dist/templates/components/Chat.js +9 -15
- package/dist/templates/components/DocsSideBar.d.ts +1 -1
- package/dist/templates/components/DocsSideBar.js +14 -12
- package/dist/templates/components/MDXComponents.d.ts +1 -1
- package/dist/templates/components/MDXComponents.js +21 -0
- package/dist/templates/components/SectionNavProvider.d.ts +1 -0
- package/dist/templates/components/SectionNavProvider.js +102 -0
- package/dist/templates/components/SideBar.d.ts +1 -1
- package/dist/templates/components/SideBar.js +7 -2
- package/dist/templates/components/layout/ActionBar.d.ts +1 -1
- package/dist/templates/components/layout/ActionBar.js +17 -75
- package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
- package/dist/templates/components/layout/DocsComponents.js +50 -11
- package/dist/templates/components/layout/Footer.d.ts +1 -1
- package/dist/templates/components/layout/Footer.js +3 -3
- package/dist/templates/components/layout/GlobalStyles.d.ts +1 -1
- package/dist/templates/components/layout/GlobalStyles.js +5 -2
- package/dist/templates/components/layout/Header.d.ts +1 -1
- package/dist/templates/components/layout/Header.js +82 -45
- package/dist/templates/components/layout/SectionBar.d.ts +1 -0
- package/dist/templates/components/layout/SectionBar.js +92 -0
- package/dist/templates/components/layout/StaticLinks.d.ts +1 -1
- package/dist/templates/components/layout/StaticLinks.js +11 -30
- package/dist/templates/mdx/ai-assistant.mdx.d.ts +1 -1
- package/dist/templates/mdx/ai-assistant.mdx.js +1 -1
- package/dist/templates/mdx/commands.mdx.d.ts +1 -1
- package/dist/templates/mdx/commands.mdx.js +3 -1
- package/dist/templates/mdx/deployment.mdx.d.ts +1 -1
- package/dist/templates/mdx/deployment.mdx.js +21 -6
- package/dist/templates/mdx/fonts.mdx.d.ts +1 -1
- package/dist/templates/mdx/fonts.mdx.js +9 -2
- package/dist/templates/mdx/footer-links.mdx.d.ts +1 -0
- package/dist/templates/mdx/footer-links.mdx.js +45 -0
- package/dist/templates/mdx/globals.mdx.d.ts +1 -1
- package/dist/templates/mdx/globals.mdx.js +6 -2
- package/dist/templates/mdx/links.mdx.d.ts +1 -1
- package/dist/templates/mdx/links.mdx.js +1 -1
- package/dist/templates/mdx/media-and-assets.mdx.d.ts +1 -1
- package/dist/templates/mdx/media-and-assets.mdx.js +6 -3
- package/dist/templates/mdx/model-context-protocol.mdx.d.ts +1 -1
- package/dist/templates/mdx/model-context-protocol.mdx.js +1 -1
- package/dist/templates/mdx/navigation.mdx.d.ts +1 -1
- package/dist/templates/mdx/navigation.mdx.js +43 -7
- package/dist/templates/mdx/sections.mdx.d.ts +1 -0
- package/dist/templates/mdx/sections.mdx.js +194 -0
- package/dist/templates/mdx/theme.mdx.d.ts +1 -1
- package/dist/templates/mdx/theme.mdx.js +7 -4
- package/dist/templates/utils/orderNavItems.d.ts +1 -1
- package/dist/templates/utils/orderNavItems.js +1 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
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 {
|
|
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
|
-
|
|
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":
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
631
|
-
const
|
|
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
|
-
|
|
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
|
|
1
|
+
function formatObjectArray(items) {
|
|
2
2
|
const MAX_WIDTH = 80;
|
|
3
|
-
const
|
|
3
|
+
const formatted = items.map((item) => {
|
|
4
4
|
const lines = [" {"];
|
|
5
|
-
const entries = Object.entries(
|
|
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" +
|
|
20
|
+
return "[\n" + formatted.join("\n") + "\n]";
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
src: ${
|
|
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
|
-
|
|
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 = ${
|
|
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 =
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
<
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
};
|