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.
- package/README.md +15 -2
- package/dist/index.js +206 -5
- package/dist/lib/layout.js +38 -24
- package/dist/lib/metadata.d.ts +30 -0
- package/dist/lib/metadata.js +98 -1
- 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 +19 -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 +2 -3
- 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/llms/llmsFull.d.ts +12 -0
- package/dist/templates/llms/llmsFull.js +59 -0
- package/dist/templates/llms/llmsIndex.d.ts +9 -0
- package/dist/templates/llms/llmsIndex.js +105 -0
- package/dist/templates/llms/llmsPage.d.ts +2 -0
- package/dist/templates/llms/llmsPage.js +20 -0
- package/dist/templates/mdx/accordion.mdx.d.ts +1 -1
- package/dist/templates/mdx/accordion.mdx.js +21 -16
- package/dist/templates/mdx/ai-assistant.mdx.d.ts +1 -1
- package/dist/templates/mdx/ai-assistant.mdx.js +22 -5
- package/dist/templates/mdx/analytics.mdx.d.ts +1 -1
- package/dist/templates/mdx/analytics.mdx.js +15 -4
- package/dist/templates/mdx/buttons.mdx.d.ts +1 -1
- package/dist/templates/mdx/buttons.mdx.js +10 -2
- package/dist/templates/mdx/callouts.mdx.d.ts +1 -1
- package/dist/templates/mdx/callouts.mdx.js +10 -17
- package/dist/templates/mdx/cards.mdx.d.ts +1 -1
- package/dist/templates/mdx/cards.mdx.js +10 -5
- package/dist/templates/mdx/code.mdx.d.ts +1 -1
- package/dist/templates/mdx/code.mdx.js +7 -3
- package/dist/templates/mdx/color-swatches.mdx.d.ts +1 -1
- package/dist/templates/mdx/color-swatches.mdx.js +7 -4
- package/dist/templates/mdx/columns.mdx.d.ts +1 -1
- package/dist/templates/mdx/columns.mdx.js +3 -0
- package/dist/templates/mdx/commands.mdx.d.ts +1 -1
- package/dist/templates/mdx/commands.mdx.js +7 -4
- package/dist/templates/mdx/components.mdx.d.ts +1 -1
- package/dist/templates/mdx/components.mdx.js +1 -0
- package/dist/templates/mdx/deployment-and-hosting.mdx.d.ts +1 -1
- package/dist/templates/mdx/deployment-and-hosting.mdx.js +6 -0
- package/dist/templates/mdx/fields.mdx.d.ts +1 -1
- package/dist/templates/mdx/fields.mdx.js +3 -0
- package/dist/templates/mdx/fonts.mdx.d.ts +1 -1
- package/dist/templates/mdx/fonts.mdx.js +13 -2
- package/dist/templates/mdx/footer-links.mdx.d.ts +1 -1
- package/dist/templates/mdx/footer-links.mdx.js +5 -0
- package/dist/templates/mdx/globals.mdx.d.ts +1 -1
- package/dist/templates/mdx/globals.mdx.js +16 -13
- package/dist/templates/mdx/headers-and-text.mdx.d.ts +1 -1
- package/dist/templates/mdx/headers-and-text.mdx.js +22 -2
- package/dist/templates/mdx/icons.mdx.d.ts +1 -1
- package/dist/templates/mdx/icons.mdx.js +3 -0
- package/dist/templates/mdx/image-and-embeds.mdx.d.ts +1 -1
- package/dist/templates/mdx/image-and-embeds.mdx.js +19 -10
- package/dist/templates/mdx/index.mdx.d.ts +1 -1
- package/dist/templates/mdx/index.mdx.js +2 -2
- package/dist/templates/mdx/lists-and-tables.mdx.d.ts +1 -1
- package/dist/templates/mdx/lists-and-tables.mdx.js +8 -2
- package/dist/templates/mdx/media-and-assets.mdx.d.ts +1 -1
- package/dist/templates/mdx/media-and-assets.mdx.js +14 -5
- package/dist/templates/mdx/model-context-protocol.mdx.d.ts +1 -1
- package/dist/templates/mdx/model-context-protocol.mdx.js +31 -15
- package/dist/templates/mdx/navigation.mdx.d.ts +1 -1
- package/dist/templates/mdx/navigation.mdx.js +9 -0
- package/dist/templates/mdx/platform/ai-assistant.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/ai-assistant.mdx.js +7 -0
- package/dist/templates/mdx/platform/analytics.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/analytics.mdx.js +7 -0
- package/dist/templates/mdx/platform/billing.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/billing.mdx.js +8 -0
- package/dist/templates/mdx/platform/build-and-deploy.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/build-and-deploy.mdx.js +6 -0
- package/dist/templates/mdx/platform/creating-a-project.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/creating-a-project.mdx.js +7 -0
- package/dist/templates/mdx/platform/custom-domains.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/custom-domains.mdx.js +5 -0
- package/dist/templates/mdx/platform/external-links.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/external-links.mdx.js +5 -0
- package/dist/templates/mdx/platform/file-editor.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/file-editor.mdx.js +7 -0
- package/dist/templates/mdx/platform/fonts-settings.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/fonts-settings.mdx.js +5 -0
- package/dist/templates/mdx/platform/index.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/index.mdx.js +5 -0
- package/dist/templates/mdx/platform/navigation-settings.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/navigation-settings.mdx.js +20 -4
- package/dist/templates/mdx/platform/project-settings.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/project-settings.mdx.js +4 -0
- package/dist/templates/mdx/platform/publishing.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/publishing.mdx.js +6 -0
- package/dist/templates/mdx/platform/site-settings.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/site-settings.mdx.js +13 -1
- package/dist/templates/mdx/platform/team-members.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/team-members.mdx.js +8 -0
- package/dist/templates/mdx/platform/theme-settings.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/theme-settings.mdx.js +7 -0
- package/dist/templates/mdx/sections.mdx.d.ts +1 -1
- package/dist/templates/mdx/sections.mdx.js +22 -1
- package/dist/templates/mdx/steps.mdx.d.ts +1 -1
- package/dist/templates/mdx/steps.mdx.js +7 -5
- package/dist/templates/mdx/tabs.mdx.d.ts +1 -1
- package/dist/templates/mdx/tabs.mdx.js +7 -2
- package/dist/templates/mdx/theme.mdx.d.ts +1 -1
- package/dist/templates/mdx/theme.mdx.js +10 -0
- package/dist/templates/mdx/update.mdx.d.ts +1 -1
- package/dist/templates/mdx/update.mdx.js +17 -14
- package/dist/templates/package.js +14 -15
- package/dist/templates/pnpmWorkspace.d.ts +1 -0
- package/dist/templates/pnpmWorkspace.js +7 -0
- package/dist/templates/proxy.js +14 -20
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
1206
|
+
stdio: "inherit",
|
|
1006
1207
|
});
|
|
1007
1208
|
await new Promise((resolve, reject) => {
|
|
1008
1209
|
install.on("close", (code) => {
|
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}\`,
|