doccupine 0.0.88 → 0.0.90

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 (160) hide show
  1. package/README.md +15 -2
  2. package/dist/index.js +206 -5
  3. package/dist/lib/layout.js +38 -24
  4. package/dist/lib/metadata.d.ts +30 -0
  5. package/dist/lib/metadata.js +98 -1
  6. package/dist/templates/app/theme.d.ts +1 -1
  7. package/dist/templates/app/theme.js +84 -19
  8. package/dist/templates/components/Chat.d.ts +1 -1
  9. package/dist/templates/components/Chat.js +26 -27
  10. package/dist/templates/components/SearchModalContent.d.ts +1 -1
  11. package/dist/templates/components/SearchModalContent.js +12 -6
  12. package/dist/templates/components/SideBar.d.ts +1 -1
  13. package/dist/templates/components/SideBar.js +3 -1
  14. package/dist/templates/components/layout/Accordion.d.ts +1 -1
  15. package/dist/templates/components/layout/Accordion.js +2 -1
  16. package/dist/templates/components/layout/ActionBar.d.ts +1 -1
  17. package/dist/templates/components/layout/ActionBar.js +4 -6
  18. package/dist/templates/components/layout/Button.d.ts +1 -1
  19. package/dist/templates/components/layout/Button.js +19 -0
  20. package/dist/templates/components/layout/Callout.d.ts +1 -1
  21. package/dist/templates/components/layout/Callout.js +75 -20
  22. package/dist/templates/components/layout/Card.d.ts +1 -1
  23. package/dist/templates/components/layout/Card.js +2 -1
  24. package/dist/templates/components/layout/CherryThemeProvider.d.ts +1 -1
  25. package/dist/templates/components/layout/CherryThemeProvider.js +6 -12
  26. package/dist/templates/components/layout/ClientThemeProvider.d.ts +1 -1
  27. package/dist/templates/components/layout/ClientThemeProvider.js +45 -40
  28. package/dist/templates/components/layout/Code.d.ts +1 -1
  29. package/dist/templates/components/layout/Code.js +223 -255
  30. package/dist/templates/components/layout/ColorSwatch.d.ts +1 -1
  31. package/dist/templates/components/layout/ColorSwatch.js +2 -2
  32. package/dist/templates/components/layout/Columns.d.ts +1 -1
  33. package/dist/templates/components/layout/Columns.js +1 -1
  34. package/dist/templates/components/layout/DemoTheme.d.ts +1 -1
  35. package/dist/templates/components/layout/DemoTheme.js +65 -167
  36. package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
  37. package/dist/templates/components/layout/DocsComponents.js +13 -19
  38. package/dist/templates/components/layout/Field.d.ts +1 -1
  39. package/dist/templates/components/layout/Field.js +6 -4
  40. package/dist/templates/components/layout/Footer.d.ts +1 -1
  41. package/dist/templates/components/layout/Footer.js +2 -3
  42. package/dist/templates/components/layout/GlobalStyles.d.ts +1 -1
  43. package/dist/templates/components/layout/GlobalStyles.js +63 -10
  44. package/dist/templates/components/layout/Header.d.ts +1 -1
  45. package/dist/templates/components/layout/Header.js +14 -11
  46. package/dist/templates/components/layout/SharedStyles.d.ts +1 -1
  47. package/dist/templates/components/layout/SharedStyles.js +4 -5
  48. package/dist/templates/components/layout/StaticLinks.d.ts +1 -1
  49. package/dist/templates/components/layout/StaticLinks.js +4 -6
  50. package/dist/templates/components/layout/Steps.d.ts +1 -1
  51. package/dist/templates/components/layout/Steps.js +3 -3
  52. package/dist/templates/components/layout/Tabs.d.ts +1 -1
  53. package/dist/templates/components/layout/Tabs.js +5 -2
  54. package/dist/templates/components/layout/ThemeToggle.d.ts +1 -1
  55. package/dist/templates/components/layout/ThemeToggle.js +17 -19
  56. package/dist/templates/components/layout/Typography.d.ts +1 -1
  57. package/dist/templates/components/layout/Typography.js +1 -1
  58. package/dist/templates/components/layout/Update.d.ts +1 -1
  59. package/dist/templates/components/layout/Update.js +4 -3
  60. package/dist/templates/llms/llmsFull.d.ts +12 -0
  61. package/dist/templates/llms/llmsFull.js +59 -0
  62. package/dist/templates/llms/llmsIndex.d.ts +9 -0
  63. package/dist/templates/llms/llmsIndex.js +105 -0
  64. package/dist/templates/llms/llmsPage.d.ts +2 -0
  65. package/dist/templates/llms/llmsPage.js +20 -0
  66. package/dist/templates/mdx/accordion.mdx.d.ts +1 -1
  67. package/dist/templates/mdx/accordion.mdx.js +21 -16
  68. package/dist/templates/mdx/ai-assistant.mdx.d.ts +1 -1
  69. package/dist/templates/mdx/ai-assistant.mdx.js +22 -5
  70. package/dist/templates/mdx/analytics.mdx.d.ts +1 -1
  71. package/dist/templates/mdx/analytics.mdx.js +15 -4
  72. package/dist/templates/mdx/buttons.mdx.d.ts +1 -1
  73. package/dist/templates/mdx/buttons.mdx.js +10 -2
  74. package/dist/templates/mdx/callouts.mdx.d.ts +1 -1
  75. package/dist/templates/mdx/callouts.mdx.js +10 -17
  76. package/dist/templates/mdx/cards.mdx.d.ts +1 -1
  77. package/dist/templates/mdx/cards.mdx.js +10 -5
  78. package/dist/templates/mdx/code.mdx.d.ts +1 -1
  79. package/dist/templates/mdx/code.mdx.js +7 -3
  80. package/dist/templates/mdx/color-swatches.mdx.d.ts +1 -1
  81. package/dist/templates/mdx/color-swatches.mdx.js +7 -4
  82. package/dist/templates/mdx/columns.mdx.d.ts +1 -1
  83. package/dist/templates/mdx/columns.mdx.js +3 -0
  84. package/dist/templates/mdx/commands.mdx.d.ts +1 -1
  85. package/dist/templates/mdx/commands.mdx.js +7 -4
  86. package/dist/templates/mdx/components.mdx.d.ts +1 -1
  87. package/dist/templates/mdx/components.mdx.js +1 -0
  88. package/dist/templates/mdx/deployment-and-hosting.mdx.d.ts +1 -1
  89. package/dist/templates/mdx/deployment-and-hosting.mdx.js +6 -0
  90. package/dist/templates/mdx/fields.mdx.d.ts +1 -1
  91. package/dist/templates/mdx/fields.mdx.js +3 -0
  92. package/dist/templates/mdx/fonts.mdx.d.ts +1 -1
  93. package/dist/templates/mdx/fonts.mdx.js +13 -2
  94. package/dist/templates/mdx/footer-links.mdx.d.ts +1 -1
  95. package/dist/templates/mdx/footer-links.mdx.js +5 -0
  96. package/dist/templates/mdx/globals.mdx.d.ts +1 -1
  97. package/dist/templates/mdx/globals.mdx.js +16 -13
  98. package/dist/templates/mdx/headers-and-text.mdx.d.ts +1 -1
  99. package/dist/templates/mdx/headers-and-text.mdx.js +22 -2
  100. package/dist/templates/mdx/icons.mdx.d.ts +1 -1
  101. package/dist/templates/mdx/icons.mdx.js +3 -0
  102. package/dist/templates/mdx/image-and-embeds.mdx.d.ts +1 -1
  103. package/dist/templates/mdx/image-and-embeds.mdx.js +19 -10
  104. package/dist/templates/mdx/index.mdx.d.ts +1 -1
  105. package/dist/templates/mdx/index.mdx.js +2 -2
  106. package/dist/templates/mdx/lists-and-tables.mdx.d.ts +1 -1
  107. package/dist/templates/mdx/lists-and-tables.mdx.js +8 -2
  108. package/dist/templates/mdx/media-and-assets.mdx.d.ts +1 -1
  109. package/dist/templates/mdx/media-and-assets.mdx.js +14 -5
  110. package/dist/templates/mdx/model-context-protocol.mdx.d.ts +1 -1
  111. package/dist/templates/mdx/model-context-protocol.mdx.js +31 -15
  112. package/dist/templates/mdx/navigation.mdx.d.ts +1 -1
  113. package/dist/templates/mdx/navigation.mdx.js +9 -0
  114. package/dist/templates/mdx/platform/ai-assistant.mdx.d.ts +1 -1
  115. package/dist/templates/mdx/platform/ai-assistant.mdx.js +7 -0
  116. package/dist/templates/mdx/platform/analytics.mdx.d.ts +1 -1
  117. package/dist/templates/mdx/platform/analytics.mdx.js +7 -0
  118. package/dist/templates/mdx/platform/billing.mdx.d.ts +1 -1
  119. package/dist/templates/mdx/platform/billing.mdx.js +8 -0
  120. package/dist/templates/mdx/platform/build-and-deploy.mdx.d.ts +1 -1
  121. package/dist/templates/mdx/platform/build-and-deploy.mdx.js +6 -0
  122. package/dist/templates/mdx/platform/creating-a-project.mdx.d.ts +1 -1
  123. package/dist/templates/mdx/platform/creating-a-project.mdx.js +7 -0
  124. package/dist/templates/mdx/platform/custom-domains.mdx.d.ts +1 -1
  125. package/dist/templates/mdx/platform/custom-domains.mdx.js +5 -0
  126. package/dist/templates/mdx/platform/external-links.mdx.d.ts +1 -1
  127. package/dist/templates/mdx/platform/external-links.mdx.js +5 -0
  128. package/dist/templates/mdx/platform/file-editor.mdx.d.ts +1 -1
  129. package/dist/templates/mdx/platform/file-editor.mdx.js +7 -0
  130. package/dist/templates/mdx/platform/fonts-settings.mdx.d.ts +1 -1
  131. package/dist/templates/mdx/platform/fonts-settings.mdx.js +5 -0
  132. package/dist/templates/mdx/platform/index.mdx.d.ts +1 -1
  133. package/dist/templates/mdx/platform/index.mdx.js +5 -0
  134. package/dist/templates/mdx/platform/navigation-settings.mdx.d.ts +1 -1
  135. package/dist/templates/mdx/platform/navigation-settings.mdx.js +20 -4
  136. package/dist/templates/mdx/platform/project-settings.mdx.d.ts +1 -1
  137. package/dist/templates/mdx/platform/project-settings.mdx.js +4 -0
  138. package/dist/templates/mdx/platform/publishing.mdx.d.ts +1 -1
  139. package/dist/templates/mdx/platform/publishing.mdx.js +6 -0
  140. package/dist/templates/mdx/platform/site-settings.mdx.d.ts +1 -1
  141. package/dist/templates/mdx/platform/site-settings.mdx.js +13 -1
  142. package/dist/templates/mdx/platform/team-members.mdx.d.ts +1 -1
  143. package/dist/templates/mdx/platform/team-members.mdx.js +8 -0
  144. package/dist/templates/mdx/platform/theme-settings.mdx.d.ts +1 -1
  145. package/dist/templates/mdx/platform/theme-settings.mdx.js +7 -0
  146. package/dist/templates/mdx/sections.mdx.d.ts +1 -1
  147. package/dist/templates/mdx/sections.mdx.js +22 -1
  148. package/dist/templates/mdx/steps.mdx.d.ts +1 -1
  149. package/dist/templates/mdx/steps.mdx.js +7 -5
  150. package/dist/templates/mdx/tabs.mdx.d.ts +1 -1
  151. package/dist/templates/mdx/tabs.mdx.js +7 -2
  152. package/dist/templates/mdx/theme.mdx.d.ts +1 -1
  153. package/dist/templates/mdx/theme.mdx.js +10 -0
  154. package/dist/templates/mdx/update.mdx.d.ts +1 -1
  155. package/dist/templates/mdx/update.mdx.js +17 -14
  156. package/dist/templates/package.js +14 -15
  157. package/dist/templates/pnpmWorkspace.d.ts +1 -0
  158. package/dist/templates/pnpmWorkspace.js +7 -0
  159. package/dist/templates/proxy.js +14 -20
  160. package/package.json +6 -7
package/README.md CHANGED
@@ -129,7 +129,7 @@ Place these JSON files in your project root (where you run `doccupine`). They ar
129
129
  | File | Purpose |
130
130
  | ----------------- | --------------------------------------------------------------------------------------------------------- |
131
131
  | `doccupine.json` | CLI config (watchDir, outputDir, port). Auto-generated on first run. |
132
- | `config.json` | Site metadata: `name`, `description`, `icon`, `image` URL |
132
+ | `config.json` | Site metadata: `name`, `description`, `icon`, `image`, `url` (public site URL for sitemap/robots) |
133
133
  | `theme.json` | Theme overrides for [cherry-styled-components](https://github.com/cherry-design-system/styled-components) |
134
134
  | `navigation.json` | Manual navigation structure (overrides auto-generated) |
135
135
  | `links.json` | Static header/footer links |
@@ -139,7 +139,20 @@ Place these JSON files in your project root (where you run `doccupine`). They ar
139
139
 
140
140
  ## Public Directory
141
141
 
142
- Place static assets (images, favicons, `robots.txt`, etc.) in a `public/` directory at your project root. Doccupine copies it to the generated Next.js app on startup and watches for changes, so added, modified, or deleted files are synced automatically.
142
+ Place static assets (images, favicons, etc.) in a `public/` directory at your project root. Doccupine copies it to the generated Next.js app on startup and watches for changes, so added, modified, or deleted files are synced automatically.
143
+
144
+ ## Sitemap and robots.txt
145
+
146
+ Doccupine generates `robots.ts` automatically for every site. When you set a `url` in `config.json`, it also generates `sitemap.ts` covering every page (across all sections) and links the sitemap from `robots.txt`.
147
+
148
+ ```json
149
+ {
150
+ "name": "My Docs",
151
+ "url": "https://docs.example.com"
152
+ }
153
+ ```
154
+
155
+ You can override the URL at deploy time by setting the `NEXT_PUBLIC_SITE_URL` environment variable. When no URL is configured (neither in `config.json` nor via env), the sitemap is skipped and `robots.txt` is emitted without a sitemap reference.
143
156
 
144
157
  ## AI Chat Setup
145
158
 
package/dist/index.js CHANGED
@@ -10,11 +10,15 @@ import { appStructure, startingDocsStructure } from "./lib/structures.js";
10
10
  import { layoutTemplate } from "./lib/layout.js";
11
11
  import { ConfigManager } from "./lib/config-manager.js";
12
12
  import { findAvailablePort, generateSlug, getFullSlug, escapeTemplateContent, } from "./lib/utils.js";
13
- import { generateMetadataBlock, generateRuntimeOnlyMetadataBlock, } from "./lib/metadata.js";
13
+ import { generateMetadataBlock, generateRuntimeOnlyMetadataBlock, generateJsonLdScript, } from "./lib/metadata.js";
14
14
  import { nextConfigTemplate } from "./templates/next.config.js";
15
+ import { pnpmWorkspaceTemplate } from "./templates/pnpmWorkspace.js";
15
16
  import { proxyTemplate } from "./templates/proxy.js";
16
17
  import { robotsTemplate } from "./templates/app/robots.js";
17
18
  import { sitemapTemplate } from "./templates/app/sitemap.js";
19
+ import { llmsIndexTemplate } from "./templates/llms/llmsIndex.js";
20
+ import { llmsFullTemplate, } from "./templates/llms/llmsFull.js";
21
+ import { llmsPageTemplate } from "./templates/llms/llmsPage.js";
18
22
  export { generateSlug, getFullSlug, escapeTemplateContent, } from "./lib/utils.js";
19
23
  const __filename = fileURLToPath(import.meta.url);
20
24
  const __dirname = path.dirname(__filename);
@@ -43,6 +47,8 @@ class MDXToNextJSGenerator {
43
47
  sectionsConfig = null;
44
48
  /** Guards against recursive reprocessing when maybeUpdateSections() triggers processAllMDXFiles() */
45
49
  isReprocessing = false;
50
+ /** Tracks per-page .md files written under public/ so we can clean up stale ones on rename/delete */
51
+ generatedLlmsPagePaths = new Set();
46
52
  constructor(watchDir, outputDir) {
47
53
  this.watchDir = path.resolve(watchDir);
48
54
  this.outputDir = path.resolve(outputDir);
@@ -78,6 +84,7 @@ class MDXToNextJSGenerator {
78
84
  const structure = {
79
85
  ...appStructure,
80
86
  "next.config.ts": nextConfigTemplate(this.analyticsConfig),
87
+ "pnpm-workspace.yaml": pnpmWorkspaceTemplate,
81
88
  "proxy.ts": proxyTemplate(this.analyticsConfig),
82
89
  "analytics.json": `{}\n`,
83
90
  "config.json": `{}\n`,
@@ -94,6 +101,7 @@ class MDXToNextJSGenerator {
94
101
  await fs.writeFile(fullPath, String(await content), "utf8");
95
102
  }
96
103
  await this.updateSitemap();
104
+ await this.updateLlmsFiles();
97
105
  }
98
106
  async createStartingDocs() {
99
107
  const structure = startingDocsStructure;
@@ -334,6 +342,7 @@ class MDXToNextJSGenerator {
334
342
  if (fileName === "config.json") {
335
343
  await this.updateSitemap();
336
344
  await this.updateRobots();
345
+ await this.updateLlmsFiles();
337
346
  }
338
347
  }
339
348
  catch (error) {
@@ -356,6 +365,7 @@ class MDXToNextJSGenerator {
356
365
  if (fileName === "config.json") {
357
366
  await this.updateSitemap();
358
367
  await this.updateRobots();
368
+ await this.updateLlmsFiles();
359
369
  }
360
370
  }
361
371
  catch (error) {
@@ -681,6 +691,7 @@ class MDXToNextJSGenerator {
681
691
  await this.updatePagesIndex();
682
692
  await this.updateRootLayout();
683
693
  await this.updateSitemap();
694
+ await this.updateLlmsFiles();
684
695
  await this.generateSectionIndexPages();
685
696
  console.log(chalk.green(`✅ Generated page for: ${filePath}`));
686
697
  await this.maybeUpdateSections();
@@ -705,6 +716,7 @@ class MDXToNextJSGenerator {
705
716
  await this.updatePagesIndex();
706
717
  await this.updateRootLayout();
707
718
  await this.updateSitemap();
719
+ await this.updateLlmsFiles();
708
720
  console.log(chalk.green(`✅ Removed page for: ${filePath}`));
709
721
  await this.maybeUpdateSections();
710
722
  }
@@ -786,6 +798,20 @@ export default function SectionIndex() {
786
798
  description: fm.description,
787
799
  icon: fm.icon,
788
800
  image: fm.image,
801
+ canonicalPath: mdxFile.slug,
802
+ });
803
+ const jsonLd = generateJsonLdScript({
804
+ kind: "article",
805
+ canonicalPath: mdxFile.slug,
806
+ title: fm.title,
807
+ description: fm.description,
808
+ date: typeof fm.date === "string" ? fm.date : undefined,
809
+ updated: typeof fm.updated === "string"
810
+ ? fm.updated
811
+ : typeof fm.date === "string"
812
+ ? fm.date
813
+ : undefined,
814
+ image: fm.image,
789
815
  });
790
816
  const pageContent = `import { Metadata } from "next";
791
817
  import { Docs } from "@/components/Docs";
@@ -795,8 +821,21 @@ const content = \`${escapeTemplateContent(mdxFile.content)}\`;
795
821
 
796
822
  ${metadataBlock}
797
823
 
824
+ // Doc pages have no per-request data: theme resolves client-side via the
825
+ // "dark" class on <html> (set before paint by the theme-init blocking
826
+ // script). Static rendering lets every response come from the edge cache.
827
+ export const dynamic = "force-static";
828
+ export const revalidate = false;
829
+
798
830
  export default function Page() {
799
- return <Docs content={content} />;
831
+ ${jsonLd.declarations}
832
+
833
+ return (
834
+ <>
835
+ ${jsonLd.element}
836
+ <Docs content={content} />
837
+ </>
838
+ );
800
839
  }
801
840
  `;
802
841
  const pagePath = path.join(this.outputDir, "app", mdxFile.slug, "page.tsx");
@@ -818,6 +857,10 @@ export default function Page() {
818
857
  icon: frontmatter.icon,
819
858
  image: frontmatter.image,
820
859
  name: frontmatter.name,
860
+ date: typeof frontmatter.date === "string" ? frontmatter.date : undefined,
861
+ updated: typeof frontmatter.updated === "string"
862
+ ? frontmatter.updated
863
+ : undefined,
821
864
  };
822
865
  break;
823
866
  }
@@ -831,8 +874,18 @@ export default function Page() {
831
874
  description: indexMDX.description || undefined,
832
875
  icon: indexMDX.icon,
833
876
  image: indexMDX.image,
877
+ canonicalPath: "",
834
878
  })
835
879
  : generateRuntimeOnlyMetadataBlock();
880
+ const homeJsonLd = generateJsonLdScript({
881
+ kind: "homepage",
882
+ canonicalPath: "",
883
+ title: indexMDX?.title,
884
+ description: indexMDX?.description || undefined,
885
+ date: indexMDX?.date,
886
+ updated: indexMDX?.updated ?? indexMDX?.date,
887
+ image: indexMDX?.image,
888
+ });
836
889
  const indexContent = `import { Metadata } from "next";
837
890
  import { Docs } from "@/components/Docs";
838
891
  import { config } from "@/utils/config";
@@ -841,8 +894,18 @@ ${indexMDX ? `const content = \`${escapeTemplateContent(indexMDX.content)}\`;` :
841
894
 
842
895
  ${metadataBlock}
843
896
 
897
+ export const dynamic = "force-static";
898
+ export const revalidate = false;
899
+
844
900
  export default function Home() {
845
- return <Docs content={content} />;
901
+ ${homeJsonLd.declarations}
902
+
903
+ return (
904
+ <>
905
+ ${homeJsonLd.element}
906
+ <Docs content={content} />
907
+ </>
908
+ );
846
909
  }
847
910
  `;
848
911
  await fs.writeFile(path.join(this.outputDir, "app", "page.tsx"), indexContent, "utf8");
@@ -856,6 +919,20 @@ export default function Home() {
856
919
  description: frontmatter.description || undefined,
857
920
  icon: frontmatter.icon,
858
921
  image: frontmatter.image,
922
+ canonicalPath: sectionSlug,
923
+ });
924
+ const sectionJsonLd = generateJsonLdScript({
925
+ kind: "article",
926
+ canonicalPath: sectionSlug,
927
+ title: frontmatter.title,
928
+ description: frontmatter.description,
929
+ date: typeof frontmatter.date === "string" ? frontmatter.date : undefined,
930
+ updated: typeof frontmatter.updated === "string"
931
+ ? frontmatter.updated
932
+ : typeof frontmatter.date === "string"
933
+ ? frontmatter.date
934
+ : undefined,
935
+ image: frontmatter.image,
859
936
  });
860
937
  const indexContent = `import { Metadata } from "next";
861
938
  import { Docs } from "@/components/Docs";
@@ -865,8 +942,18 @@ const content = \`${escapeTemplateContent(mdxContent)}\`;
865
942
 
866
943
  ${metadataBlock}
867
944
 
945
+ export const dynamic = "force-static";
946
+ export const revalidate = false;
947
+
868
948
  export default function Page() {
869
- return <Docs content={content} />;
949
+ ${sectionJsonLd.declarations}
950
+
951
+ return (
952
+ <>
953
+ ${sectionJsonLd.element}
954
+ <Docs content={content} />
955
+ </>
956
+ );
870
957
  }
871
958
  `;
872
959
  const pagePath = path.join(this.outputDir, "app", sectionSlug, "page.tsx");
@@ -943,6 +1030,120 @@ export default function Page() {
943
1030
  ? `🤖 Regenerated robots.ts with sitemap link`
944
1031
  : `🤖 Regenerated robots.ts (no sitemap link)`));
945
1032
  }
1033
+ async loadSiteMetadata() {
1034
+ const configPath = path.join(this.rootDir, "config.json");
1035
+ let url = null;
1036
+ let name = "Documentation";
1037
+ let description = "";
1038
+ try {
1039
+ if (await fs.pathExists(configPath)) {
1040
+ const content = await fs.readFile(configPath, "utf8");
1041
+ const parsed = JSON.parse(content);
1042
+ if (typeof parsed.url === "string" && parsed.url.trim() !== "") {
1043
+ url = parsed.url.trim().replace(/\/$/, "");
1044
+ }
1045
+ if (typeof parsed.name === "string" && parsed.name.trim() !== "") {
1046
+ name = parsed.name.trim();
1047
+ }
1048
+ else if (typeof parsed.title === "string" &&
1049
+ parsed.title.trim() !== "") {
1050
+ name = parsed.title.trim();
1051
+ }
1052
+ if (typeof parsed.description === "string" &&
1053
+ parsed.description.trim() !== "") {
1054
+ description = parsed.description.trim();
1055
+ }
1056
+ }
1057
+ }
1058
+ catch (error) {
1059
+ console.warn(chalk.yellow("⚠️ Error reading config.json for llms metadata"), error);
1060
+ }
1061
+ return { url, name, description };
1062
+ }
1063
+ async readPageWithBody(page) {
1064
+ const fullPath = path.join(this.watchDir, page.path);
1065
+ const raw = await fs.readFile(fullPath, "utf8");
1066
+ const { content: body } = matter(raw);
1067
+ return { ...page, body };
1068
+ }
1069
+ llmsManifestPath() {
1070
+ return path.join(this.outputDir, ".doccupine-llms-manifest.json");
1071
+ }
1072
+ async readLlmsManifest() {
1073
+ const manifestPath = this.llmsManifestPath();
1074
+ try {
1075
+ if (await fs.pathExists(manifestPath)) {
1076
+ const raw = await fs.readFile(manifestPath, "utf8");
1077
+ const parsed = JSON.parse(raw);
1078
+ if (Array.isArray(parsed.pageFiles)) {
1079
+ return new Set(parsed.pageFiles.filter((entry) => typeof entry === "string"));
1080
+ }
1081
+ }
1082
+ }
1083
+ catch {
1084
+ // ignore corrupted manifest
1085
+ }
1086
+ return new Set();
1087
+ }
1088
+ async writeLlmsManifest(pageFiles) {
1089
+ const manifestPath = this.llmsManifestPath();
1090
+ const payload = { pageFiles: Array.from(pageFiles).sort() };
1091
+ const json = JSON.stringify(payload, null, 2) + "\n";
1092
+ await fs.writeFile(manifestPath, json, "utf8");
1093
+ }
1094
+ async updateLlmsFiles() {
1095
+ const publicDir = path.join(this.outputDir, "public");
1096
+ await fs.ensureDir(publicDir);
1097
+ const { url: baseUrl, name, description } = await this.loadSiteMetadata();
1098
+ const pages = await this.buildAllPagesMeta();
1099
+ const pagesWithBodies = await Promise.all(pages.map((page) => this.readPageWithBody(page)));
1100
+ const indexContent = llmsIndexTemplate({
1101
+ siteName: name,
1102
+ siteDescription: description,
1103
+ baseUrl,
1104
+ pages,
1105
+ sectionsConfig: this.sectionsConfig,
1106
+ });
1107
+ const fullContent = llmsFullTemplate({
1108
+ siteName: name,
1109
+ siteDescription: description,
1110
+ baseUrl,
1111
+ pages: pagesWithBodies,
1112
+ sectionsConfig: this.sectionsConfig,
1113
+ });
1114
+ await fs.writeFile(path.join(publicDir, "llms.txt"), indexContent, "utf8");
1115
+ await fs.writeFile(path.join(publicDir, "llms-full.txt"), fullContent, "utf8");
1116
+ const nextRelativePaths = new Set();
1117
+ await Promise.all(pagesWithBodies.map(async (page) => {
1118
+ if (page.slug === "")
1119
+ return;
1120
+ const relPath = `${page.slug}.md`;
1121
+ const targetPath = path.join(publicDir, relPath);
1122
+ await fs.ensureDir(path.dirname(targetPath));
1123
+ await fs.writeFile(targetPath, llmsPageTemplate(page, baseUrl), "utf8");
1124
+ nextRelativePaths.add(relPath);
1125
+ }));
1126
+ const previousRelativePaths = new Set([
1127
+ ...this.generatedLlmsPagePaths,
1128
+ ...(await this.readLlmsManifest()),
1129
+ ]);
1130
+ for (const stale of previousRelativePaths) {
1131
+ if (!nextRelativePaths.has(stale)) {
1132
+ try {
1133
+ const stalePath = path.join(publicDir, stale);
1134
+ if (await fs.pathExists(stalePath)) {
1135
+ await fs.remove(stalePath);
1136
+ }
1137
+ }
1138
+ catch {
1139
+ // ignore
1140
+ }
1141
+ }
1142
+ }
1143
+ this.generatedLlmsPagePaths = nextRelativePaths;
1144
+ await this.writeLlmsManifest(nextRelativePaths);
1145
+ console.log(chalk.green(`🤖 Generated llms.txt and llms-full.txt with ${pages.length} page(s)${baseUrl ? ` using ${baseUrl}` : " (relative URLs)"}`));
1146
+ }
946
1147
  async stop() {
947
1148
  if (this.watcher) {
948
1149
  await this.watcher.close();
@@ -1002,7 +1203,7 @@ program
1002
1203
  console.log(chalk.blue(`📦 Using ${packageManager}...`));
1003
1204
  const install = spawn(packageManager, ["install"], {
1004
1205
  cwd: config.outputDir,
1005
- stdio: "pipe",
1206
+ stdio: "inherit",
1006
1207
  });
1007
1208
  await new Promise((resolve, reject) => {
1008
1209
  install.on("close", (code) => {
@@ -48,9 +48,8 @@ ${a} >`
48
48
  return `import type { Metadata } from "next";
49
49
  ${isGoogleFont(fontConfig) ? `import { ${fontConfig.googleFont.fontName} } from "next/font/google";` : isLocalFont(fontConfig) ? 'import localFont from "next/font/local";' : 'import { Inter } from "next/font/google";'}
50
50
  import dynamic from "next/dynamic";
51
- import Script from "next/script";
52
51
  import { StyledComponentsRegistry } from "cherry-styled-components";
53
- import { theme, themeDark } from "@/app/theme";
52
+ import { theme } from "@/app/theme";
54
53
  import { CherryThemeProvider } from "@/components/layout/CherryThemeProvider";
55
54
  import { ChtProvider } from "@/components/Chat";
56
55
  import { SearchProvider } from "@/components/SearchDocs";
@@ -90,7 +89,18 @@ ${isGoogleFont(fontConfig)
90
89
  });`
91
90
  : 'const font = Inter({ subsets: ["latin"] });'}
92
91
 
92
+ function resolveSiteUrl(): URL | undefined {
93
+ const raw = process.env.NEXT_PUBLIC_SITE_URL ?? config.url;
94
+ if (!raw || typeof raw !== "string") return undefined;
95
+ try {
96
+ return new URL(raw);
97
+ } catch {
98
+ return undefined;
99
+ }
100
+ }
101
+
93
102
  export const metadata: Metadata = {
103
+ metadataBase: resolveSiteUrl(),
94
104
  title: config.name || "${DEFAULT_SITE_NAME}",
95
105
  description:
96
106
  config.description ||
@@ -118,24 +128,26 @@ ${hasSections
118
128
  const pages: PagesProps[] = doccupinePages;
119
129
 
120
130
  return (
121
- <html lang="en">
131
+ <html lang="en" suppressHydrationWarning>
122
132
  <head>
123
- {/* Prevents dark-mode FOUC on Safari/Firefox. These browsers don't support
124
- Sec-CH-Prefers-Color-Scheme (handled by middleware for Chrome), so on
125
- a first visit this blocking script detects prefers-color-scheme, sets
126
- the theme cookie, and hides the body until router.refresh() re-renders
127
- with the correct theme (see ClientThemeProvider). */}
128
- <Script
129
- id="theme-init"
130
- strategy="beforeInteractive"
133
+ {/* Resolves dark mode before first paint by adding the "dark" class
134
+ to <html> when needed. CSS variables in GlobalStyles flip values
135
+ on :root vs :root.dark, so the right palette renders without a
136
+ React roundtrip. Inlined as a plain <script> (not next/script) so
137
+ it ships in the SSR HTML and runs synchronously before paint —
138
+ next/script with beforeInteractive is async in App Router and
139
+ would still show a flash. suppressHydrationWarning on <html>
140
+ tells React the class/colorScheme attributes are intentionally
141
+ different between server (no class) and client (after script). */}
142
+ <script
131
143
  dangerouslySetInnerHTML={{
132
- __html: \`(function(){try{var c=document.cookie.split(";").find(function(s){return s.trim().startsWith("theme=")});if(!c){var d=window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches;document.cookie="theme="+(d?"dark":"light")+";path=/;max-age=31536000;SameSite=Lax";if(d){var s=document.createElement("style");s.id="__theme-init";s.textContent="html{background:#000!important;color-scheme:dark}body{visibility:hidden}";document.head.appendChild(s)}}}catch(e){}})();\`,
144
+ __html: \`(function(){try{var c=document.cookie.split(";").map(function(s){return s.trim();}).find(function(s){return s.indexOf("theme=")===0;});var v=c?c.split("=")[1]:null;var d=v?v==="dark":(window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches);if(!v){document.cookie="theme="+(d?"dark":"light")+";path=/;max-age=31536000;SameSite=Lax";}if(d){document.documentElement.classList.add("dark");document.documentElement.style.colorScheme="dark";}else{document.documentElement.style.colorScheme="light";}}catch(e){}})();\`,
133
145
  }}
134
146
  />
135
147
  </head>
136
148
  <body className={font.className}>
137
149
  <StyledComponentsRegistry>
138
- ${analyticsEnabled ? " <PostHogProvider>\n" : ""}${a} <CherryThemeProvider theme={theme} themeDark={themeDark}>
150
+ ${analyticsEnabled ? " <PostHogProvider>\n" : ""}${a} <CherryThemeProvider theme={theme}>
139
151
  ${a} ${chtOpen}
140
152
  ${a} <SearchProvider pages={pages} sections={doccupineSections}>
141
153
  ${a} <Header>
@@ -181,24 +193,26 @@ ${analyticsEnabled ? " </PostHogProvider>\n" : ""} </StyledCompo
181
193
  const defaultResults = transformPagesToGroupedStructure(defaultPages);
182
194
 
183
195
  return (
184
- <html lang="en">
196
+ <html lang="en" suppressHydrationWarning>
185
197
  <head>
186
- {/* Prevents dark-mode FOUC on Safari/Firefox. These browsers don't support
187
- Sec-CH-Prefers-Color-Scheme (handled by middleware for Chrome), so on
188
- a first visit this blocking script detects prefers-color-scheme, sets
189
- the theme cookie, and hides the body until router.refresh() re-renders
190
- with the correct theme (see ClientThemeProvider). */}
191
- <Script
192
- id="theme-init"
193
- strategy="beforeInteractive"
198
+ {/* Resolves dark mode before first paint by adding the "dark" class
199
+ to <html> when needed. CSS variables in GlobalStyles flip values
200
+ on :root vs :root.dark, so the right palette renders without a
201
+ React roundtrip. Inlined as a plain <script> (not next/script) so
202
+ it ships in the SSR HTML and runs synchronously before paint —
203
+ next/script with beforeInteractive is async in App Router and
204
+ would still show a flash. suppressHydrationWarning on <html>
205
+ tells React the class/colorScheme attributes are intentionally
206
+ different between server (no class) and client (after script). */}
207
+ <script
194
208
  dangerouslySetInnerHTML={{
195
- __html: \`(function(){try{var c=document.cookie.split(";").find(function(s){return s.trim().startsWith("theme=")});if(!c){var d=window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches;document.cookie="theme="+(d?"dark":"light")+";path=/;max-age=31536000;SameSite=Lax";if(d){var s=document.createElement("style");s.id="__theme-init";s.textContent="html{background:#000!important;color-scheme:dark}body{visibility:hidden}";document.head.appendChild(s)}}}catch(e){}})();\`,
209
+ __html: \`(function(){try{var c=document.cookie.split(";").map(function(s){return s.trim();}).find(function(s){return s.indexOf("theme=")===0;});var v=c?c.split("=")[1]:null;var d=v?v==="dark":(window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches);if(!v){document.cookie="theme="+(d?"dark":"light")+";path=/;max-age=31536000;SameSite=Lax";}if(d){document.documentElement.classList.add("dark");document.documentElement.style.colorScheme="dark";}else{document.documentElement.style.colorScheme="light";}}catch(e){}})();\`,
196
210
  }}
197
211
  />
198
212
  </head>
199
213
  <body className={font.className}>
200
214
  <StyledComponentsRegistry>
201
- ${analyticsEnabled ? " <PostHogProvider>\n" : ""}${a} <CherryThemeProvider theme={theme} themeDark={themeDark}>
215
+ ${analyticsEnabled ? " <PostHogProvider>\n" : ""}${a} <CherryThemeProvider theme={theme}>
202
216
  ${a} ${chtOpen}
203
217
  ${a} <SearchProvider pages={pages}>
204
218
  ${a} <Header />
@@ -1,3 +1,28 @@
1
+ export type JsonLdKind = "article" | "homepage";
2
+ export interface JsonLdOptions {
3
+ kind: JsonLdKind;
4
+ /** Page slug ("" for homepage). Used to build the absolute URL with config.url. */
5
+ canonicalPath: string;
6
+ title?: string;
7
+ description?: string;
8
+ date?: string;
9
+ /** Optional last-modified date; defaults to date when present. */
10
+ updated?: string;
11
+ image?: string;
12
+ }
13
+ /**
14
+ * Returns a code snippet that:
15
+ * - declares a `jsonLd` object at runtime using the site's config,
16
+ * - and renders a single `<script type="application/ld+json">` element.
17
+ *
18
+ * The snippet is meant to be inlined inside a generated page component.
19
+ * It emits a TechArticle on every doc page; the homepage additionally
20
+ * emits a graph that includes an Organization entity for entity recognition.
21
+ */
22
+ export declare function generateJsonLdScript(opts: JsonLdOptions): {
23
+ declarations: string;
24
+ element: string;
25
+ };
1
26
  export interface MetadataOptions {
2
27
  title?: string;
3
28
  titleFallback: string;
@@ -6,6 +31,11 @@ export interface MetadataOptions {
6
31
  description?: string;
7
32
  icon?: string;
8
33
  image?: string;
34
+ /**
35
+ * Canonical path for this page, e.g. "" for the homepage or "components".
36
+ * Resolves against `metadataBase` defined in the root layout.
37
+ */
38
+ canonicalPath?: string;
9
39
  }
10
40
  export declare function generateMetadataBlock(opts: MetadataOptions): string;
11
41
  export declare function generateRuntimeOnlyMetadataBlock(): string;
@@ -1,4 +1,91 @@
1
1
  import { DEFAULT_FAVICON, DEFAULT_META_DESCRIPTION, DEFAULT_OG_IMAGE, DEFAULT_SITE_NAME, } from "./constants.js";
2
+ /**
3
+ * Returns a code snippet that:
4
+ * - declares a `jsonLd` object at runtime using the site's config,
5
+ * - and renders a single `<script type="application/ld+json">` element.
6
+ *
7
+ * The snippet is meant to be inlined inside a generated page component.
8
+ * It emits a TechArticle on every doc page; the homepage additionally
9
+ * emits a graph that includes an Organization entity for entity recognition.
10
+ */
11
+ export function generateJsonLdScript(opts) {
12
+ const safePath = opts.canonicalPath.replace(/^\/+/, "");
13
+ const titleLiteral = JSON.stringify(opts.title ?? DEFAULT_SITE_NAME);
14
+ const descLiteral = JSON.stringify(opts.description ?? DEFAULT_META_DESCRIPTION);
15
+ const dateLiteral = opts.date ? JSON.stringify(opts.date) : "undefined";
16
+ const updatedLiteral = opts.updated
17
+ ? JSON.stringify(opts.updated)
18
+ : opts.date
19
+ ? JSON.stringify(opts.date)
20
+ : "undefined";
21
+ const defaultFaviconLiteral = JSON.stringify(DEFAULT_FAVICON);
22
+ const faviconLine = opts.image
23
+ ? `const faviconUrl =
24
+ ${JSON.stringify(opts.image)} ||
25
+ config.icon ||
26
+ ${defaultFaviconLiteral};`
27
+ : `const faviconUrl = config.icon || ${defaultFaviconLiteral};`;
28
+ // Indent 10 + "description: ".length(13) + ",".length(1) = 24. Prettier
29
+ // wraps the value to the next line (indent 12) once total exceeds 80.
30
+ const descriptionLine = descLiteral.length > 56
31
+ ? `description:
32
+ ${descLiteral},`
33
+ : `description: ${descLiteral},`;
34
+ const pathLiteral = JSON.stringify(safePath);
35
+ const homepageGraph = opts.kind === "homepage"
36
+ ? `,
37
+ {
38
+ "@type": "Organization",
39
+ name: siteName,
40
+ url: baseUrl ?? undefined,
41
+ logo: faviconUrl,
42
+ }`
43
+ : "";
44
+ const declarations = `const __jsonLdBaseUrl = (() => {
45
+ const raw =
46
+ typeof process !== "undefined"
47
+ ? process.env.NEXT_PUBLIC_SITE_URL
48
+ : undefined;
49
+ const fromEnv = typeof raw === "string" && raw.trim() !== "" ? raw : null;
50
+ const fromConfig =
51
+ typeof config.url === "string" && config.url.trim() !== ""
52
+ ? config.url
53
+ : null;
54
+ return (fromEnv ?? fromConfig)?.replace(/\\/$/, "") ?? null;
55
+ })();
56
+ const __jsonLd = (() => {
57
+ const baseUrl = __jsonLdBaseUrl;
58
+ const path = ${pathLiteral};
59
+ const url = baseUrl ? (path ? \`\${baseUrl}/\${path}\` : baseUrl) : undefined;
60
+ const siteName = config.name || ${JSON.stringify(DEFAULT_SITE_NAME)};
61
+ ${faviconLine}
62
+ return {
63
+ "@context": "https://schema.org",
64
+ "@graph": [
65
+ {
66
+ "@type": "TechArticle",
67
+ headline: ${titleLiteral},
68
+ ${descriptionLine}
69
+ url,
70
+ mainEntityOfPage: url,
71
+ datePublished: ${dateLiteral},
72
+ dateModified: ${updatedLiteral},
73
+ author: { "@type": "Organization", name: siteName },
74
+ publisher: {
75
+ "@type": "Organization",
76
+ name: siteName,
77
+ logo: { "@type": "ImageObject", url: faviconUrl },
78
+ },
79
+ }${homepageGraph},
80
+ ],
81
+ };
82
+ })();`;
83
+ const element = `<script
84
+ type="application/ld+json"
85
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(__jsonLd) }}
86
+ />`;
87
+ return { declarations, element };
88
+ }
2
89
  function buildFieldExpression(staticValue, configKey, defaultValue) {
3
90
  if (staticValue)
4
91
  return staticValue;
@@ -18,15 +105,24 @@ function buildTitleExpression(opts) {
18
105
  : `\${config.name ? config.name + " -" : "${DEFAULT_SITE_NAME} -"}`;
19
106
  return `${prefix} ${title}`;
20
107
  }
108
+ function buildCanonicalLine(canonicalPath) {
109
+ if (canonicalPath === undefined)
110
+ return "";
111
+ const safePath = canonicalPath.replace(/^\/+/, "");
112
+ // Relative path resolves against `metadataBase` set in the root layout.
113
+ // Empty string means the homepage canonical equals the base URL itself.
114
+ return `\n alternates: { canonical: ${JSON.stringify("/" + safePath)} },`;
115
+ }
21
116
  export function generateMetadataBlock(opts) {
22
117
  const title = buildTitleExpression(opts);
23
118
  const desc = buildFieldExpression(opts.description, "description", DEFAULT_META_DESCRIPTION);
24
119
  const icon = buildFieldExpression(opts.icon, "icon", DEFAULT_FAVICON);
25
120
  const image = buildFieldExpression(opts.image, "image", DEFAULT_OG_IMAGE);
121
+ const canonical = buildCanonicalLine(opts.canonicalPath);
26
122
  return `export const metadata: Metadata = {
27
123
  title: \`${title}\`,
28
124
  description: \`${desc}\`,
29
- icons: \`${icon}\`,
125
+ icons: \`${icon}\`,${canonical}
30
126
  openGraph: {
31
127
  title: \`${title}\`,
32
128
  description: \`${desc}\`,
@@ -43,6 +139,7 @@ export function generateRuntimeOnlyMetadataBlock() {
43
139
  title: \`${title}\`,
44
140
  description: \`${desc}\`,
45
141
  icons: \`${icon}\`,
142
+ alternates: { canonical: "/" },
46
143
  openGraph: {
47
144
  title: \`${title}\`,
48
145
  description: \`${desc}\`,