doccupine 0.0.87 → 0.0.89

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 (78) hide show
  1. package/README.md +15 -2
  2. package/dist/index.js +177 -4
  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/lib/structures.js +0 -2
  7. package/dist/lib/types.d.ts +1 -0
  8. package/dist/templates/app/robots.d.ts +1 -1
  9. package/dist/templates/app/robots.js +28 -1
  10. package/dist/templates/app/sitemap.d.ts +7 -0
  11. package/dist/templates/app/sitemap.js +56 -0
  12. package/dist/templates/app/theme.d.ts +1 -1
  13. package/dist/templates/app/theme.js +84 -19
  14. package/dist/templates/components/Chat.d.ts +1 -1
  15. package/dist/templates/components/Chat.js +26 -27
  16. package/dist/templates/components/SearchModalContent.d.ts +1 -1
  17. package/dist/templates/components/SearchModalContent.js +12 -6
  18. package/dist/templates/components/SideBar.d.ts +1 -1
  19. package/dist/templates/components/SideBar.js +3 -1
  20. package/dist/templates/components/layout/Accordion.d.ts +1 -1
  21. package/dist/templates/components/layout/Accordion.js +2 -1
  22. package/dist/templates/components/layout/ActionBar.d.ts +1 -1
  23. package/dist/templates/components/layout/ActionBar.js +4 -6
  24. package/dist/templates/components/layout/Button.d.ts +1 -1
  25. package/dist/templates/components/layout/Button.js +10 -0
  26. package/dist/templates/components/layout/Callout.d.ts +1 -1
  27. package/dist/templates/components/layout/Callout.js +75 -20
  28. package/dist/templates/components/layout/Card.d.ts +1 -1
  29. package/dist/templates/components/layout/Card.js +2 -1
  30. package/dist/templates/components/layout/CherryThemeProvider.d.ts +1 -1
  31. package/dist/templates/components/layout/CherryThemeProvider.js +6 -12
  32. package/dist/templates/components/layout/ClientThemeProvider.d.ts +1 -1
  33. package/dist/templates/components/layout/ClientThemeProvider.js +45 -40
  34. package/dist/templates/components/layout/Code.d.ts +1 -1
  35. package/dist/templates/components/layout/Code.js +223 -255
  36. package/dist/templates/components/layout/ColorSwatch.d.ts +1 -1
  37. package/dist/templates/components/layout/ColorSwatch.js +2 -2
  38. package/dist/templates/components/layout/Columns.d.ts +1 -1
  39. package/dist/templates/components/layout/Columns.js +1 -1
  40. package/dist/templates/components/layout/DemoTheme.d.ts +1 -1
  41. package/dist/templates/components/layout/DemoTheme.js +65 -167
  42. package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
  43. package/dist/templates/components/layout/DocsComponents.js +13 -19
  44. package/dist/templates/components/layout/Field.d.ts +1 -1
  45. package/dist/templates/components/layout/Field.js +6 -4
  46. package/dist/templates/components/layout/Footer.d.ts +1 -1
  47. package/dist/templates/components/layout/Footer.js +1 -2
  48. package/dist/templates/components/layout/GlobalStyles.d.ts +1 -1
  49. package/dist/templates/components/layout/GlobalStyles.js +63 -10
  50. package/dist/templates/components/layout/Header.d.ts +1 -1
  51. package/dist/templates/components/layout/Header.js +14 -11
  52. package/dist/templates/components/layout/SharedStyles.d.ts +1 -1
  53. package/dist/templates/components/layout/SharedStyles.js +4 -5
  54. package/dist/templates/components/layout/StaticLinks.d.ts +1 -1
  55. package/dist/templates/components/layout/StaticLinks.js +4 -6
  56. package/dist/templates/components/layout/Steps.d.ts +1 -1
  57. package/dist/templates/components/layout/Steps.js +3 -3
  58. package/dist/templates/components/layout/Tabs.d.ts +1 -1
  59. package/dist/templates/components/layout/Tabs.js +5 -2
  60. package/dist/templates/components/layout/ThemeToggle.d.ts +1 -1
  61. package/dist/templates/components/layout/ThemeToggle.js +17 -19
  62. package/dist/templates/components/layout/Typography.d.ts +1 -1
  63. package/dist/templates/components/layout/Typography.js +1 -1
  64. package/dist/templates/components/layout/Update.d.ts +1 -1
  65. package/dist/templates/components/layout/Update.js +4 -3
  66. package/dist/templates/env.example.d.ts +1 -1
  67. package/dist/templates/env.example.js +5 -1
  68. package/dist/templates/mdx/deployment-and-hosting.mdx.d.ts +1 -1
  69. package/dist/templates/mdx/deployment-and-hosting.mdx.js +19 -0
  70. package/dist/templates/mdx/globals.mdx.d.ts +1 -1
  71. package/dist/templates/mdx/globals.mdx.js +3 -1
  72. package/dist/templates/mdx/platform/site-settings.mdx.d.ts +1 -1
  73. package/dist/templates/mdx/platform/site-settings.mdx.js +5 -1
  74. package/dist/templates/package.js +17 -18
  75. package/dist/templates/proxy.js +14 -20
  76. package/dist/templates/utils/config.d.ts +1 -1
  77. package/dist/templates/utils/config.js +1 -0
  78. package/package.json +8 -8
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,9 +10,11 @@ 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
15
  import { proxyTemplate } from "./templates/proxy.js";
16
+ import { robotsTemplate } from "./templates/app/robots.js";
17
+ import { sitemapTemplate } from "./templates/app/sitemap.js";
16
18
  export { generateSlug, getFullSlug, escapeTemplateContent, } from "./lib/utils.js";
17
19
  const __filename = fileURLToPath(import.meta.url);
18
20
  const __dirname = path.dirname(__filename);
@@ -72,6 +74,7 @@ class MDXToNextJSGenerator {
72
74
  console.log(chalk.white(" npm install && npm run dev"));
73
75
  }
74
76
  async createNextJSStructure() {
77
+ const siteUrl = await this.loadSiteUrl();
75
78
  const structure = {
76
79
  ...appStructure,
77
80
  "next.config.ts": nextConfigTemplate(this.analyticsConfig),
@@ -82,6 +85,7 @@ class MDXToNextJSGenerator {
82
85
  "navigation.json": `[]\n`,
83
86
  "sections.json": `[]\n`,
84
87
  "theme.json": `{}\n`,
88
+ "app/robots.ts": robotsTemplate(siteUrl !== null),
85
89
  "app/layout.tsx": this.generateRootLayout(),
86
90
  };
87
91
  for (const [filePath, content] of Object.entries(structure)) {
@@ -89,6 +93,7 @@ class MDXToNextJSGenerator {
89
93
  await fs.ensureDir(path.dirname(fullPath));
90
94
  await fs.writeFile(fullPath, String(await content), "utf8");
91
95
  }
96
+ await this.updateSitemap();
92
97
  }
93
98
  async createStartingDocs() {
94
99
  const structure = startingDocsStructure;
@@ -326,6 +331,10 @@ class MDXToNextJSGenerator {
326
331
  if (fileName === "sections.json") {
327
332
  await this.reloadSections();
328
333
  }
334
+ if (fileName === "config.json") {
335
+ await this.updateSitemap();
336
+ await this.updateRobots();
337
+ }
329
338
  }
330
339
  catch (error) {
331
340
  console.error(chalk.red(`❌ Error copying ${fileName}:`), error);
@@ -344,6 +353,10 @@ class MDXToNextJSGenerator {
344
353
  if (fileName === "sections.json") {
345
354
  await this.reloadSections();
346
355
  }
356
+ if (fileName === "config.json") {
357
+ await this.updateSitemap();
358
+ await this.updateRobots();
359
+ }
347
360
  }
348
361
  catch (error) {
349
362
  console.error(chalk.red(`❌ Error removing ${fileName}:`), error);
@@ -607,6 +620,22 @@ class MDXToNextJSGenerator {
607
620
  const { data: frontmatter } = matter(content);
608
621
  const { sectionSlug, pageSlug } = this.determineSectionForFile(file, frontmatter);
609
622
  const fullSlug = getFullSlug(pageSlug, sectionSlug);
623
+ let lastModified;
624
+ if (frontmatter.date) {
625
+ const parsed = new Date(frontmatter.date);
626
+ if (!Number.isNaN(parsed.getTime())) {
627
+ lastModified = parsed.toISOString();
628
+ }
629
+ }
630
+ if (!lastModified) {
631
+ try {
632
+ const stats = await fs.stat(fullPath);
633
+ lastModified = stats.mtime.toISOString();
634
+ }
635
+ catch {
636
+ // ignore
637
+ }
638
+ }
610
639
  return {
611
640
  slug: fullSlug,
612
641
  title: frontmatter.title || "Untitled",
@@ -617,6 +646,7 @@ class MDXToNextJSGenerator {
617
646
  categoryOrder: frontmatter.categoryOrder || 0,
618
647
  order: frontmatter.order || 0,
619
648
  section: sectionSlug,
649
+ lastModified,
620
650
  };
621
651
  }
622
652
  async buildAllPagesMeta() {
@@ -650,6 +680,7 @@ class MDXToNextJSGenerator {
650
680
  }
651
681
  await this.updatePagesIndex();
652
682
  await this.updateRootLayout();
683
+ await this.updateSitemap();
653
684
  await this.generateSectionIndexPages();
654
685
  console.log(chalk.green(`✅ Generated page for: ${filePath}`));
655
686
  await this.maybeUpdateSections();
@@ -673,6 +704,7 @@ class MDXToNextJSGenerator {
673
704
  }
674
705
  await this.updatePagesIndex();
675
706
  await this.updateRootLayout();
707
+ await this.updateSitemap();
676
708
  console.log(chalk.green(`✅ Removed page for: ${filePath}`));
677
709
  await this.maybeUpdateSections();
678
710
  }
@@ -754,6 +786,20 @@ export default function SectionIndex() {
754
786
  description: fm.description,
755
787
  icon: fm.icon,
756
788
  image: fm.image,
789
+ canonicalPath: mdxFile.slug,
790
+ });
791
+ const jsonLd = generateJsonLdScript({
792
+ kind: "article",
793
+ canonicalPath: mdxFile.slug,
794
+ title: fm.title,
795
+ description: fm.description,
796
+ date: typeof fm.date === "string" ? fm.date : undefined,
797
+ updated: typeof fm.updated === "string"
798
+ ? fm.updated
799
+ : typeof fm.date === "string"
800
+ ? fm.date
801
+ : undefined,
802
+ image: fm.image,
757
803
  });
758
804
  const pageContent = `import { Metadata } from "next";
759
805
  import { Docs } from "@/components/Docs";
@@ -763,8 +809,21 @@ const content = \`${escapeTemplateContent(mdxFile.content)}\`;
763
809
 
764
810
  ${metadataBlock}
765
811
 
812
+ // Doc pages have no per-request data: theme resolves client-side via the
813
+ // "dark" class on <html> (set before paint by the theme-init blocking
814
+ // script). Static rendering lets every response come from the edge cache.
815
+ export const dynamic = "force-static";
816
+ export const revalidate = false;
817
+
766
818
  export default function Page() {
767
- return <Docs content={content} />;
819
+ ${jsonLd.declarations}
820
+
821
+ return (
822
+ <>
823
+ ${jsonLd.element}
824
+ <Docs content={content} />
825
+ </>
826
+ );
768
827
  }
769
828
  `;
770
829
  const pagePath = path.join(this.outputDir, "app", mdxFile.slug, "page.tsx");
@@ -786,6 +845,10 @@ export default function Page() {
786
845
  icon: frontmatter.icon,
787
846
  image: frontmatter.image,
788
847
  name: frontmatter.name,
848
+ date: typeof frontmatter.date === "string" ? frontmatter.date : undefined,
849
+ updated: typeof frontmatter.updated === "string"
850
+ ? frontmatter.updated
851
+ : undefined,
789
852
  };
790
853
  break;
791
854
  }
@@ -799,8 +862,18 @@ export default function Page() {
799
862
  description: indexMDX.description || undefined,
800
863
  icon: indexMDX.icon,
801
864
  image: indexMDX.image,
865
+ canonicalPath: "",
802
866
  })
803
867
  : generateRuntimeOnlyMetadataBlock();
868
+ const homeJsonLd = generateJsonLdScript({
869
+ kind: "homepage",
870
+ canonicalPath: "",
871
+ title: indexMDX?.title,
872
+ description: indexMDX?.description || undefined,
873
+ date: indexMDX?.date,
874
+ updated: indexMDX?.updated ?? indexMDX?.date,
875
+ image: indexMDX?.image,
876
+ });
804
877
  const indexContent = `import { Metadata } from "next";
805
878
  import { Docs } from "@/components/Docs";
806
879
  import { config } from "@/utils/config";
@@ -809,8 +882,18 @@ ${indexMDX ? `const content = \`${escapeTemplateContent(indexMDX.content)}\`;` :
809
882
 
810
883
  ${metadataBlock}
811
884
 
885
+ export const dynamic = "force-static";
886
+ export const revalidate = false;
887
+
812
888
  export default function Home() {
813
- return <Docs content={content} />;
889
+ ${homeJsonLd.declarations}
890
+
891
+ return (
892
+ <>
893
+ ${homeJsonLd.element}
894
+ <Docs content={content} />
895
+ </>
896
+ );
814
897
  }
815
898
  `;
816
899
  await fs.writeFile(path.join(this.outputDir, "app", "page.tsx"), indexContent, "utf8");
@@ -824,6 +907,20 @@ export default function Home() {
824
907
  description: frontmatter.description || undefined,
825
908
  icon: frontmatter.icon,
826
909
  image: frontmatter.image,
910
+ canonicalPath: sectionSlug,
911
+ });
912
+ const sectionJsonLd = generateJsonLdScript({
913
+ kind: "article",
914
+ canonicalPath: sectionSlug,
915
+ title: frontmatter.title,
916
+ description: frontmatter.description,
917
+ date: typeof frontmatter.date === "string" ? frontmatter.date : undefined,
918
+ updated: typeof frontmatter.updated === "string"
919
+ ? frontmatter.updated
920
+ : typeof frontmatter.date === "string"
921
+ ? frontmatter.date
922
+ : undefined,
923
+ image: frontmatter.image,
827
924
  });
828
925
  const indexContent = `import { Metadata } from "next";
829
926
  import { Docs } from "@/components/Docs";
@@ -833,8 +930,18 @@ const content = \`${escapeTemplateContent(mdxContent)}\`;
833
930
 
834
931
  ${metadataBlock}
835
932
 
933
+ export const dynamic = "force-static";
934
+ export const revalidate = false;
935
+
836
936
  export default function Page() {
837
- return <Docs content={content} />;
937
+ ${sectionJsonLd.declarations}
938
+
939
+ return (
940
+ <>
941
+ ${sectionJsonLd.element}
942
+ <Docs content={content} />
943
+ </>
944
+ );
838
945
  }
839
946
  `;
840
947
  const pagePath = path.join(this.outputDir, "app", sectionSlug, "page.tsx");
@@ -845,6 +952,72 @@ export default function Page() {
845
952
  const layoutContent = await this.generateRootLayout();
846
953
  await fs.writeFile(path.join(this.outputDir, "app", "layout.tsx"), layoutContent, "utf8");
847
954
  }
955
+ async loadSiteUrl() {
956
+ const configPath = path.join(this.rootDir, "config.json");
957
+ try {
958
+ if (await fs.pathExists(configPath)) {
959
+ const content = await fs.readFile(configPath, "utf8");
960
+ const parsed = JSON.parse(content);
961
+ if (typeof parsed.url === "string" && parsed.url.trim() !== "") {
962
+ return parsed.url.trim().replace(/\/$/, "");
963
+ }
964
+ }
965
+ }
966
+ catch (error) {
967
+ console.warn(chalk.yellow("⚠️ Error reading config.json"), error);
968
+ }
969
+ return null;
970
+ }
971
+ buildSitemapEntries(pages) {
972
+ const sectionSlugs = new Set((this.sectionsConfig || [])
973
+ .map((s) => s.slug)
974
+ .filter((s) => typeof s === "string" && s !== ""));
975
+ const entries = pages.map((page) => {
976
+ let priority = 0.5;
977
+ if (page.slug === "") {
978
+ priority = 1.0;
979
+ }
980
+ else if (sectionSlugs.has(page.slug)) {
981
+ priority = 0.8;
982
+ }
983
+ return {
984
+ slug: page.slug,
985
+ lastModified: page.lastModified,
986
+ changeFrequency: "weekly",
987
+ priority,
988
+ };
989
+ });
990
+ if (!entries.some((entry) => entry.slug === "")) {
991
+ entries.unshift({
992
+ slug: "",
993
+ changeFrequency: "weekly",
994
+ priority: 1.0,
995
+ });
996
+ }
997
+ return entries;
998
+ }
999
+ async updateSitemap() {
1000
+ const sitemapPath = path.join(this.outputDir, "app", "sitemap.ts");
1001
+ const siteUrl = await this.loadSiteUrl();
1002
+ if (!siteUrl) {
1003
+ if (await fs.pathExists(sitemapPath)) {
1004
+ await fs.remove(sitemapPath);
1005
+ console.log(chalk.yellow("🗑️ Removed sitemap.ts (no site URL configured)"));
1006
+ }
1007
+ return;
1008
+ }
1009
+ const pages = await this.buildAllPagesMeta();
1010
+ const entries = this.buildSitemapEntries(pages);
1011
+ await fs.writeFile(sitemapPath, sitemapTemplate(entries), "utf8");
1012
+ console.log(chalk.green(`🗺️ Generated sitemap.ts with ${entries.length} page(s) using ${siteUrl}`));
1013
+ }
1014
+ async updateRobots() {
1015
+ const siteUrl = await this.loadSiteUrl();
1016
+ await fs.writeFile(path.join(this.outputDir, "app", "robots.ts"), robotsTemplate(siteUrl !== null), "utf8");
1017
+ console.log(chalk.green(siteUrl
1018
+ ? `🤖 Regenerated robots.ts with sitemap link`
1019
+ : `🤖 Regenerated robots.ts (no sitemap link)`));
1020
+ }
848
1021
  async stop() {
849
1022
  if (this.watcher) {
850
1023
  await this.watcher.close();
@@ -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}\`,
@@ -10,7 +10,6 @@ import { ragRoutesTemplate } from "../templates/app/api/rag/route.js";
10
10
  import { searchRoutesTemplate } from "../templates/app/api/search/route.js";
11
11
  import { routesTemplate } from "../templates/app/api/theme/routes.js";
12
12
  import { notFoundTemplate } from "../templates/app/not-found.js";
13
- import { robotsTemplate } from "../templates/app/robots.js";
14
13
  import { themeTemplate } from "../templates/app/theme.js";
15
14
  import { chatTemplate } from "../templates/components/Chat.js";
16
15
  import { clickOutsideTemplate } from "../templates/components/ClickOutside.js";
@@ -121,7 +120,6 @@ export const appStructure = {
121
120
  "package.json": packageJsonTemplate,
122
121
  "tsconfig.json": tsconfigTemplate,
123
122
  "app/not-found.tsx": notFoundTemplate,
124
- "app/robots.ts": robotsTemplate,
125
123
  "app/theme.ts": themeTemplate,
126
124
  "app/api/mcp/route.ts": mcpRoutesTemplate,
127
125
  "app/api/rag/route.ts": ragRoutesTemplate,
@@ -14,6 +14,7 @@ export interface PageMeta {
14
14
  categoryOrder: number;
15
15
  order: number;
16
16
  section: string;
17
+ lastModified?: string;
17
18
  }
18
19
  export interface SectionConfig {
19
20
  label: string;
@@ -1 +1 @@
1
- export declare const robotsTemplate = "import type { MetadataRoute } from \"next\";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: {\n userAgent: \"*\",\n allow: \"/\",\n },\n };\n}\n";
1
+ export declare const robotsTemplate: (hasSiteUrl: boolean) => string;