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.
- package/README.md +15 -2
- package/dist/index.js +177 -4
- package/dist/lib/layout.js +38 -24
- package/dist/lib/metadata.d.ts +30 -0
- package/dist/lib/metadata.js +98 -1
- package/dist/lib/structures.js +0 -2
- package/dist/lib/types.d.ts +1 -0
- package/dist/templates/app/robots.d.ts +1 -1
- package/dist/templates/app/robots.js +28 -1
- package/dist/templates/app/sitemap.d.ts +7 -0
- package/dist/templates/app/sitemap.js +56 -0
- package/dist/templates/app/theme.d.ts +1 -1
- package/dist/templates/app/theme.js +84 -19
- package/dist/templates/components/Chat.d.ts +1 -1
- package/dist/templates/components/Chat.js +26 -27
- package/dist/templates/components/SearchModalContent.d.ts +1 -1
- package/dist/templates/components/SearchModalContent.js +12 -6
- package/dist/templates/components/SideBar.d.ts +1 -1
- package/dist/templates/components/SideBar.js +3 -1
- package/dist/templates/components/layout/Accordion.d.ts +1 -1
- package/dist/templates/components/layout/Accordion.js +2 -1
- package/dist/templates/components/layout/ActionBar.d.ts +1 -1
- package/dist/templates/components/layout/ActionBar.js +4 -6
- package/dist/templates/components/layout/Button.d.ts +1 -1
- package/dist/templates/components/layout/Button.js +10 -0
- package/dist/templates/components/layout/Callout.d.ts +1 -1
- package/dist/templates/components/layout/Callout.js +75 -20
- package/dist/templates/components/layout/Card.d.ts +1 -1
- package/dist/templates/components/layout/Card.js +2 -1
- package/dist/templates/components/layout/CherryThemeProvider.d.ts +1 -1
- package/dist/templates/components/layout/CherryThemeProvider.js +6 -12
- package/dist/templates/components/layout/ClientThemeProvider.d.ts +1 -1
- package/dist/templates/components/layout/ClientThemeProvider.js +45 -40
- package/dist/templates/components/layout/Code.d.ts +1 -1
- package/dist/templates/components/layout/Code.js +223 -255
- package/dist/templates/components/layout/ColorSwatch.d.ts +1 -1
- package/dist/templates/components/layout/ColorSwatch.js +2 -2
- package/dist/templates/components/layout/Columns.d.ts +1 -1
- package/dist/templates/components/layout/Columns.js +1 -1
- package/dist/templates/components/layout/DemoTheme.d.ts +1 -1
- package/dist/templates/components/layout/DemoTheme.js +65 -167
- package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
- package/dist/templates/components/layout/DocsComponents.js +13 -19
- package/dist/templates/components/layout/Field.d.ts +1 -1
- package/dist/templates/components/layout/Field.js +6 -4
- package/dist/templates/components/layout/Footer.d.ts +1 -1
- package/dist/templates/components/layout/Footer.js +1 -2
- package/dist/templates/components/layout/GlobalStyles.d.ts +1 -1
- package/dist/templates/components/layout/GlobalStyles.js +63 -10
- package/dist/templates/components/layout/Header.d.ts +1 -1
- package/dist/templates/components/layout/Header.js +14 -11
- package/dist/templates/components/layout/SharedStyles.d.ts +1 -1
- package/dist/templates/components/layout/SharedStyles.js +4 -5
- package/dist/templates/components/layout/StaticLinks.d.ts +1 -1
- package/dist/templates/components/layout/StaticLinks.js +4 -6
- package/dist/templates/components/layout/Steps.d.ts +1 -1
- package/dist/templates/components/layout/Steps.js +3 -3
- package/dist/templates/components/layout/Tabs.d.ts +1 -1
- package/dist/templates/components/layout/Tabs.js +5 -2
- package/dist/templates/components/layout/ThemeToggle.d.ts +1 -1
- package/dist/templates/components/layout/ThemeToggle.js +17 -19
- package/dist/templates/components/layout/Typography.d.ts +1 -1
- package/dist/templates/components/layout/Typography.js +1 -1
- package/dist/templates/components/layout/Update.d.ts +1 -1
- package/dist/templates/components/layout/Update.js +4 -3
- package/dist/templates/env.example.d.ts +1 -1
- package/dist/templates/env.example.js +5 -1
- package/dist/templates/mdx/deployment-and-hosting.mdx.d.ts +1 -1
- package/dist/templates/mdx/deployment-and-hosting.mdx.js +19 -0
- package/dist/templates/mdx/globals.mdx.d.ts +1 -1
- package/dist/templates/mdx/globals.mdx.js +3 -1
- package/dist/templates/mdx/platform/site-settings.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/site-settings.mdx.js +5 -1
- package/dist/templates/package.js +17 -18
- package/dist/templates/proxy.js +14 -20
- package/dist/templates/utils/config.d.ts +1 -1
- package/dist/templates/utils/config.js +1 -0
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
package/dist/lib/layout.js
CHANGED
|
@@ -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
|
|
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
|
-
{/*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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(";").
|
|
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}
|
|
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
|
-
{/*
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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(";").
|
|
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}
|
|
215
|
+
${analyticsEnabled ? " <PostHogProvider>\n" : ""}${a} <CherryThemeProvider theme={theme}>
|
|
202
216
|
${a} ${chtOpen}
|
|
203
217
|
${a} <SearchProvider pages={pages}>
|
|
204
218
|
${a} <Header />
|
package/dist/lib/metadata.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/metadata.js
CHANGED
|
@@ -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}\`,
|
package/dist/lib/structures.js
CHANGED
|
@@ -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,
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const robotsTemplate
|
|
1
|
+
export declare const robotsTemplate: (hasSiteUrl: boolean) => string;
|