doccupine 0.0.70 → 0.0.72

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 CHANGED
@@ -135,6 +135,7 @@ Place these JSON files in your project root (where you run `doccupine`). They ar
135
135
  | `links.json` | Static header/footer links |
136
136
  | `fonts.json` | Font configuration (Google Fonts or local) |
137
137
  | `sections.json` | Section definitions for tabbed doc groups (see [Sections](#sections)) |
138
+ | `analytics.json` | Analytics provider configuration (PostHog supported) |
138
139
 
139
140
  ## Public Directory
140
141
 
package/dist/index.js CHANGED
@@ -26,6 +26,7 @@ class MDXToNextJSGenerator {
26
26
  configWatcher = null;
27
27
  fontWatcher = null;
28
28
  publicWatcher = null;
29
+ rootDirWatcher = null;
29
30
  analyticsWatcher = null;
30
31
  configFiles = [
31
32
  "theme.json",
@@ -552,27 +553,53 @@ class MDXToNextJSGenerator {
552
553
  });
553
554
  const publicDir = path.join(this.rootDir, "public");
554
555
  if (await fs.pathExists(publicDir)) {
555
- this.publicWatcher = chokidar.watch(publicDir, {
556
- persistent: true,
557
- ignoreInitial: true,
558
- });
559
- this.publicWatcher
560
- .on("add", (filePath) => {
561
- console.log(chalk.cyan(`📁 Public file added: ${path.relative(publicDir, filePath)}`));
562
- this.handlePublicFileChange(filePath);
563
- })
564
- .on("change", (filePath) => {
565
- console.log(chalk.cyan(`📁 Public file changed: ${path.relative(publicDir, filePath)}`));
566
- this.handlePublicFileChange(filePath);
567
- })
568
- .on("unlink", (filePath) => {
569
- console.log(chalk.red(`🗑️ Public file deleted: ${path.relative(publicDir, filePath)}`));
570
- this.handlePublicFileDelete(filePath);
571
- })
572
- .on("error", (error) => {
573
- console.error(chalk.red("❌ Public watcher error:"), error);
574
- });
556
+ this.setupPublicWatcher();
575
557
  }
558
+ // Watch rootDir for public directory creation
559
+ this.rootDirWatcher = chokidar.watch(this.rootDir, {
560
+ persistent: true,
561
+ ignoreInitial: true,
562
+ depth: 0,
563
+ });
564
+ this.rootDirWatcher
565
+ .on("addDir", async (dirPath) => {
566
+ if (path.basename(dirPath) === "public" &&
567
+ path.dirname(dirPath) === this.rootDir &&
568
+ !this.publicWatcher) {
569
+ console.log(chalk.cyan("📁 Public directory created"));
570
+ await this.copyPublicFiles();
571
+ this.setupPublicWatcher();
572
+ }
573
+ })
574
+ .on("error", (error) => {
575
+ console.error(chalk.red("❌ Root dir watcher error:"), error);
576
+ });
577
+ }
578
+ setupPublicWatcher() {
579
+ if (this.publicWatcher) {
580
+ return;
581
+ }
582
+ const publicDir = path.join(this.rootDir, "public");
583
+ this.publicWatcher = chokidar.watch(publicDir, {
584
+ persistent: true,
585
+ ignoreInitial: true,
586
+ });
587
+ this.publicWatcher
588
+ .on("add", (filePath) => {
589
+ console.log(chalk.cyan(`📁 Public file added: ${path.relative(publicDir, filePath)}`));
590
+ this.handlePublicFileChange(filePath);
591
+ })
592
+ .on("change", (filePath) => {
593
+ console.log(chalk.cyan(`📁 Public file changed: ${path.relative(publicDir, filePath)}`));
594
+ this.handlePublicFileChange(filePath);
595
+ })
596
+ .on("unlink", (filePath) => {
597
+ console.log(chalk.red(`🗑️ Public file deleted: ${path.relative(publicDir, filePath)}`));
598
+ this.handlePublicFileDelete(filePath);
599
+ })
600
+ .on("error", (error) => {
601
+ console.error(chalk.red("❌ Public watcher error:"), error);
602
+ });
576
603
  }
577
604
  async parseMDXFile(file) {
578
605
  const fullPath = path.join(this.watchDir, file);
@@ -839,6 +866,9 @@ export default function Page() {
839
866
  await this.publicWatcher.close();
840
867
  console.log(chalk.yellow("👋 Stopped watching for public directory changes"));
841
868
  }
869
+ if (this.rootDirWatcher) {
870
+ await this.rootDirWatcher.close();
871
+ }
842
872
  }
843
873
  }
844
874
  program
@@ -96,6 +96,7 @@ import { platformThemeSettingsMdxTemplate } from "../templates/mdx/platform/them
96
96
  import { platformNavigationSettingsMdxTemplate } from "../templates/mdx/platform/navigation-settings.mdx.js";
97
97
  import { platformFontsSettingsMdxTemplate } from "../templates/mdx/platform/fonts-settings.mdx.js";
98
98
  import { platformExternalLinksMdxTemplate } from "../templates/mdx/platform/external-links.mdx.js";
99
+ import { platformAnalyticsMdxTemplate } from "../templates/mdx/platform/analytics.mdx.js";
99
100
  import { platformAiAssistantMdxTemplate } from "../templates/mdx/platform/ai-assistant.mdx.js";
100
101
  import { platformCustomDomainsMdxTemplate } from "../templates/mdx/platform/custom-domains.mdx.js";
101
102
  import { platformBuildAndDeployMdxTemplate } from "../templates/mdx/platform/build-and-deploy.mdx.js";
@@ -203,6 +204,7 @@ export const startingDocsStructure = {
203
204
  "platform/navigation-settings.mdx": platformNavigationSettingsMdxTemplate,
204
205
  "platform/fonts-settings.mdx": platformFontsSettingsMdxTemplate,
205
206
  "platform/external-links.mdx": platformExternalLinksMdxTemplate,
207
+ "platform/analytics.mdx": platformAnalyticsMdxTemplate,
206
208
  "platform/ai-assistant.mdx": platformAiAssistantMdxTemplate,
207
209
  "platform/custom-domains.mdx": platformCustomDomainsMdxTemplate,
208
210
  "platform/build-and-deploy.mdx": platformBuildAndDeployMdxTemplate,
@@ -1 +1 @@
1
- export declare const postHogProviderTemplate = "\"use client\";\n\nimport posthog from \"posthog-js\";\nimport { PostHogProvider as PHProvider } from \"posthog-js/react\";\nimport { Suspense, useEffect, useRef, useState } from \"react\";\nimport { usePathname, useSearchParams } from \"next/navigation\";\nimport rawAnalyticsConfig from \"@/analytics.json\";\n\ninterface AnalyticsConfig {\n provider?: string;\n posthog?: {\n key?: string;\n host?: string;\n };\n}\n\nconst analyticsConfig = rawAnalyticsConfig as AnalyticsConfig;\n\nconst posthogKey =\n analyticsConfig?.provider === \"posthog\" ? analyticsConfig.posthog?.key : null;\n\nfunction PostHogInit({ onReady }: { onReady: () => void }) {\n const initRef = useRef(false);\n\n useEffect(() => {\n if (initRef.current || !posthogKey) return;\n initRef.current = true;\n\n posthog.init(posthogKey, {\n api_host: \"/ingest\",\n ui_host: analyticsConfig.posthog?.host || \"https://us.posthog.com\",\n capture_pageview: false,\n capture_pageleave: true,\n loaded: onReady,\n });\n }, [onReady]);\n\n return null;\n}\n\nfunction PostHogPageviewTracker() {\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n useEffect(() => {\n if (pathname) {\n const url = searchParams?.size\n ? `${pathname}?${searchParams.toString()}`\n : pathname;\n posthog.capture(\"$pageview\", { $current_url: url });\n }\n }, [pathname, searchParams]);\n\n return null;\n}\n\nexport function PostHogProvider({ children }: { children: React.ReactNode }) {\n const [ready, setReady] = useState(false);\n\n if (!posthogKey) {\n return <>{children}</>;\n }\n\n return (\n <PHProvider client={posthog}>\n <PostHogInit onReady={() => setReady(true)} />\n <Suspense fallback={null}>{ready && <PostHogPageviewTracker />}</Suspense>\n {children}\n </PHProvider>\n );\n}\n";
1
+ export declare const postHogProviderTemplate = "\"use client\";\n\nimport posthog from \"posthog-js\";\nimport { PostHogProvider as PHProvider } from \"@posthog/react\";\nimport { Suspense, useEffect, useRef, useState } from \"react\";\nimport { usePathname, useSearchParams } from \"next/navigation\";\nimport rawAnalyticsConfig from \"@/analytics.json\";\n\ninterface AnalyticsConfig {\n provider?: string;\n posthog?: {\n key?: string;\n host?: string;\n };\n}\n\nconst analyticsConfig = rawAnalyticsConfig as AnalyticsConfig;\n\nconst posthogKey =\n analyticsConfig?.provider === \"posthog\" ? analyticsConfig.posthog?.key : null;\n\nfunction PostHogInit({ onReady }: { onReady: () => void }) {\n const initRef = useRef(false);\n\n useEffect(() => {\n if (initRef.current || !posthogKey) return;\n initRef.current = true;\n\n posthog.init(posthogKey, {\n api_host: \"/ingest\",\n ui_host: analyticsConfig.posthog?.host || \"https://us.posthog.com\",\n capture_pageview: false,\n capture_pageleave: true,\n loaded: onReady,\n });\n }, [onReady]);\n\n return null;\n}\n\nfunction PostHogPageviewTracker() {\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n useEffect(() => {\n if (pathname) {\n const url = searchParams?.size\n ? `${pathname}?${searchParams.toString()}`\n : pathname;\n posthog.capture(\"$pageview\", { $current_url: url });\n }\n }, [pathname, searchParams]);\n\n return null;\n}\n\nexport function PostHogProvider({ children }: { children: React.ReactNode }) {\n const [ready, setReady] = useState(false);\n\n if (!posthogKey) {\n return <>{children}</>;\n }\n\n return (\n <PHProvider client={posthog}>\n <PostHogInit onReady={() => setReady(true)} />\n <Suspense fallback={null}>{ready && <PostHogPageviewTracker />}</Suspense>\n {children}\n </PHProvider>\n );\n}\n";
@@ -1,7 +1,7 @@
1
1
  export const postHogProviderTemplate = `"use client";
2
2
 
3
3
  import posthog from "posthog-js";
4
- import { PostHogProvider as PHProvider } from "posthog-js/react";
4
+ import { PostHogProvider as PHProvider } from "@posthog/react";
5
5
  import { Suspense, useEffect, useRef, useState } from "react";
6
6
  import { usePathname, useSearchParams } from "next/navigation";
7
7
  import rawAnalyticsConfig from "@/analytics.json";
@@ -1 +1 @@
1
- export declare const buttonTemplate = "\"use client\";\nimport Link from \"next/link\";\nimport styled from \"styled-components\";\nimport {\n theme as localTheme,\n ButtonProps,\n buttonStyles,\n} from \"cherry-styled-components\";\nimport { Icon } from \"@/components/layout/Icon\";\n\ninterface LinkButtonProps extends ButtonProps {\n href?: string;\n target?: \"_blank\" | \"_self\" | \"_parent\" | \"_top\";\n variant?: \"primary\" | \"secondary\" | \"tertiary\";\n size?: \"default\" | \"big\";\n outline?: boolean;\n fullWidth?: boolean;\n icon?: string;\n iconPosition?: \"left\" | \"right\";\n theme?: typeof localTheme;\n}\n\nconst StyledLinkButton = styled(Link)<LinkButtonProps>`\n ${({ theme, $variant, $size, $outline, $fullWidth, disabled }) =>\n buttonStyles(theme, $variant, $size, $outline, $fullWidth, disabled)}\n\n & svg.lucide {\n margin: auto 0;\n min-width: min-content;\n color: inherit;\n }\n`;\n\nconst ButtonBase = styled.button<ButtonProps>`\n ${({ theme, $variant, $size, $outline, $fullWidth, disabled }) =>\n buttonStyles(theme, $variant, $size, $outline, $fullWidth, disabled)}\n\n & svg.lucide {\n margin: auto 0;\n min-width: min-content;\n color: inherit;\n }\n`;\n\nfunction Button({\n variant = \"primary\",\n size,\n outline,\n fullWidth,\n icon,\n iconPosition = \"left\",\n theme: _theme = localTheme,\n href,\n ...props\n}: LinkButtonProps) {\n return href ? (\n <div>\n <StyledLinkButton\n {...props}\n href={href}\n $variant={variant}\n $size={size}\n $outline={outline}\n $fullWidth={fullWidth}\n >\n {iconPosition === \"left\" && icon && <Icon name={icon} size={16} />}\n {props.children}\n {iconPosition === \"right\" && icon && <Icon name={icon} size={16} />}\n </StyledLinkButton>\n </div>\n ) : (\n <div>\n <ButtonBase\n {...props}\n $variant={variant}\n $size={size}\n $outline={outline}\n $fullWidth={fullWidth}\n >\n {iconPosition === \"left\" && icon && <Icon name={icon} size={16} />}\n {props.children}\n {iconPosition === \"right\" && icon && <Icon name={icon} size={16} />}\n </ButtonBase>\n </div>\n );\n}\n\nexport { Button };\n";
1
+ export declare const buttonTemplate = "\"use client\";\nimport Link from \"next/link\";\nimport styled from \"styled-components\";\nimport {\n theme as localTheme,\n ButtonProps,\n buttonStyles,\n} from \"cherry-styled-components\";\nimport { Icon } from \"@/components/layout/Icon\";\n\ninterface LinkButtonProps extends ButtonProps {\n href?: string;\n target?: \"_blank\" | \"_self\" | \"_parent\" | \"_top\";\n variant?: \"primary\" | \"secondary\" | \"tertiary\";\n size?: \"default\" | \"big\";\n outline?: boolean;\n fullWidth?: boolean;\n icon?: string;\n iconPosition?: \"left\" | \"right\";\n theme?: typeof localTheme;\n}\n\nconst StyledLinkButton = styled(Link)<LinkButtonProps>`\n ${({ theme, $variant, $size, $outline, $fullWidth, disabled }) =>\n buttonStyles(theme, $variant, $size, $outline, $fullWidth, disabled)}\n\n & p {\n color: inherit;\n }\n\n & svg.lucide {\n margin: auto 0;\n min-width: min-content;\n color: inherit;\n }\n`;\n\nconst ButtonBase = styled.button<ButtonProps>`\n ${({ theme, $variant, $size, $outline, $fullWidth, disabled }) =>\n buttonStyles(theme, $variant, $size, $outline, $fullWidth, disabled)}\n\n & p {\n color: inherit;\n }\n\n & svg.lucide {\n margin: auto 0;\n min-width: min-content;\n color: inherit;\n }\n`;\n\nfunction Button({\n variant = \"primary\",\n size,\n outline,\n fullWidth,\n icon,\n iconPosition = \"left\",\n theme: _theme = localTheme,\n href,\n ...props\n}: LinkButtonProps) {\n return href ? (\n <div>\n <StyledLinkButton\n {...props}\n href={href}\n $variant={variant}\n $size={size}\n $outline={outline}\n $fullWidth={fullWidth}\n >\n {iconPosition === \"left\" && icon && <Icon name={icon} size={16} />}\n {props.children}\n {iconPosition === \"right\" && icon && <Icon name={icon} size={16} />}\n </StyledLinkButton>\n </div>\n ) : (\n <div>\n <ButtonBase\n {...props}\n $variant={variant}\n $size={size}\n $outline={outline}\n $fullWidth={fullWidth}\n >\n {iconPosition === \"left\" && icon && <Icon name={icon} size={16} />}\n {props.children}\n {iconPosition === \"right\" && icon && <Icon name={icon} size={16} />}\n </ButtonBase>\n </div>\n );\n}\n\nexport { Button };\n";
@@ -24,6 +24,10 @@ const StyledLinkButton = styled(Link)<LinkButtonProps>\`
24
24
  \${({ theme, $variant, $size, $outline, $fullWidth, disabled }) =>
25
25
  buttonStyles(theme, $variant, $size, $outline, $fullWidth, disabled)}
26
26
 
27
+ & p {
28
+ color: inherit;
29
+ }
30
+
27
31
  & svg.lucide {
28
32
  margin: auto 0;
29
33
  min-width: min-content;
@@ -35,6 +39,10 @@ const ButtonBase = styled.button<ButtonProps>\`
35
39
  \${({ theme, $variant, $size, $outline, $fullWidth, disabled }) =>
36
40
  buttonStyles(theme, $variant, $size, $outline, $fullWidth, disabled)}
37
41
 
42
+ & p {
43
+ color: inherit;
44
+ }
45
+
38
46
  & svg.lucide {
39
47
  margin: auto 0;
40
48
  min-width: min-content;
@@ -1 +1 @@
1
- export declare const globalStylesTemplate = "\"use client\";\nimport { createGlobalStyle } from \"styled-components\";\n\nconst GlobalStyles = createGlobalStyle`\nhtml,\nbody {\n margin: 0;\n padding: 0;\n min-height: 100%;\n background-color: ${({ theme }) => theme.colors.light};\n scroll-padding-top: 80px;\n}\n\nhtml:has(:target) {\n scroll-behavior: smooth;\n}\n\nbody {\n font-family: \"Inter\", sans-serif;\n -moz-osx-font-smoothing: grayscale;\n -webkit-text-size-adjust: 100%;\n -webkit-font-smoothing: antialiased;\n}\n\n:root {\n interpolate-size: allow-keywords;\n}\n\n* {\n box-sizing: border-box;\n min-width: 0;\n}\n\nhr {\n border: none;\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n margin: 10px 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace;\n}\n\npre,\ncode,\nkbd,\nsamp,\nblockquote,\np,\na,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nul li,\nol li {\n margin: 0;\n padding: 0;\n color: ${({ theme }) => theme.colors.dark};\n}\n\na {\n color: ${({ theme }) => (theme.isDark ? theme.colors.dark : theme.colors.primary)};\n}\n\nol,\nul {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\nfigure {\n margin: 0;\n}\n\nfieldset {\n appearance: none;\n border: none;\n}\n\nbutton,\ninput,\na,\nimg,\nsvg,\nsvg * {\n transition: all 0.3s ease;\n}\n\nstrong,\nb {\n font-weight: 700;\n}\n\n.full-width {\n width: 100%;\n}`;\n\nexport { GlobalStyles };\n";
1
+ export declare const globalStylesTemplate = "\"use client\";\nimport { createGlobalStyle } from \"styled-components\";\n\nconst GlobalStyles = createGlobalStyle`\nhtml,\nbody {\n margin: 0;\n padding: 0;\n min-height: 100%;\n background-color: ${({ theme }) => theme.colors.light};\n scroll-padding-top: 80px;\n}\n\nhtml:has(:target) {\n scroll-behavior: smooth;\n}\n\nbody {\n font-family: \"Inter\", sans-serif;\n -moz-osx-font-smoothing: grayscale;\n -webkit-text-size-adjust: 100%;\n -webkit-font-smoothing: antialiased;\n}\n\n:root {\n interpolate-size: allow-keywords;\n}\n\n* {\n box-sizing: border-box;\n min-width: 0;\n}\n\nhr {\n border: none;\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n margin: 10px 0;\n}\n\npre,\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace;\n}\n\npre,\ncode,\nkbd,\nsamp,\nblockquote,\np,\na,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nul li,\nol li {\n margin: 0;\n padding: 0;\n color: ${({ theme }) => theme.colors.dark};\n}\n\na {\n color: ${({ theme }) => (theme.isDark ? theme.colors.dark : theme.colors.primary)};\n}\n\nol,\nul {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\nfigure {\n margin: 0;\n}\n\nfieldset {\n appearance: none;\n border: none;\n}\n\nbutton,\ninput,\na,\nimg,\nsvg,\nsvg * {\n transition: all 0.3s ease;\n}\n\nstrong,\nb {\n font-weight: 700;\n}\n\n.full-width {\n width: 100%;\n}\n\n.light-only {\n ${({ theme }) => theme.isDark && \"display: none !important;\"}\n}\n\n.dark-only {\n ${({ theme }) => !theme.isDark && \"display: none !important;\"}\n}`;\n\nexport { GlobalStyles };\n";
@@ -100,6 +100,14 @@ b {
100
100
 
101
101
  .full-width {
102
102
  width: 100%;
103
+ }
104
+
105
+ .light-only {
106
+ \${({ theme }) => theme.isDark && "display: none !important;"}
107
+ }
108
+
109
+ .dark-only {
110
+ \${({ theme }) => !theme.isDark && "display: none !important;"}
103
111
  }\`;
104
112
 
105
113
  export { GlobalStyles };
@@ -1 +1 @@
1
- export declare const imageAndEmbedsMdxTemplate = "---\ntitle: \"Images and embeds\"\ndescription: \"Enrich your documentation with visuals, videos, and interactive embeds.\"\ndate: \"2026-02-19\"\ncategory: \"Components\"\ncategoryOrder: 1\norder: 8\n---\n# Images and embeds\nEnrich your documentation with visuals, videos, and interactive embeds.\n\nDisplay images, embed video content, or add interactive frames via iframes to supplement your docs.\n\n![Demo Image](https://docs.doccupine.com/demo.png)\n\n## Images\nImages enhance documentation with context, illustration, or decorative visual cues.\n\n### Basic Image Syntax\nInclude an image in Markdown using the syntax below:\n\n```md\n![Alt text](https://docs.doccupine.com/demo.png)\n```\n\n<Callout type=\"note\">\n Use clear, descriptive alt text for accessibility and better SEO. Alt text should describe the image\u2019s appearance or content.\n</Callout>\n\n### HTML image embeds\nEmbed images in your Markdown content using HTML syntax.\n\n```md\n<img src=\"https://docs.doccupine.com/demo.png\" alt=\"Alt text\">\n```\n\n## Videos\nVideos add a dynamic element to your documentation, engaging your audience and providing a more immersive experience.\n\n### YouTube Embed\nTo embed a YouTube video, use the following syntax:\n\n```html\n<iframe\n className=\"aspect-video\"\n src=\"https://www.youtube.com/embed/ResP_eVPYQo\"\n title=\"YouTube video player\"\n frameBorder=\"0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n></iframe>\n```\n\n<iframe\n className=\"aspect-video\"\n src=\"https://www.youtube.com/embed/ResP_eVPYQo\"\n title=\"YouTube video player\"\n frameBorder=\"0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n></iframe>\n\n### Self-hosted videos\nServe up your own video content using the `<video>` tag:\n\n```html\n<video\n controls\n className=\"aspect-video\"\n src=\"https://samplelib.com/lib/preview/mp4/sample-20s.mp4\"\n></video>\n```\n\n<video\n controls\n className=\"aspect-video\"\n src=\"https://samplelib.com/lib/preview/mp4/sample-20s.mp4\"\n></video>\n\n\n#### Autoplay and Looping\nFor demonstration videos that loop or start automatically, add attributes as shown:\n\n```html\n<video\n controls\n className=\"aspect-video\"\n src=\"https://samplelib.com/lib/preview/mp4/sample-20s.mp4\"\n autoPlay\n muted\n loop\n playsInline\n></video>\n```\n";
1
+ export declare const imageAndEmbedsMdxTemplate = "---\ntitle: \"Images and embeds\"\ndescription: \"Enrich your documentation with visuals, videos, and interactive embeds.\"\ndate: \"2026-02-19\"\ncategory: \"Components\"\ncategoryOrder: 1\norder: 8\n---\n# Images and embeds\nEnrich your documentation with visuals, videos, and interactive embeds.\n\nDisplay images, embed video content, or add interactive frames via iframes to supplement your docs.\n\n![Demo Image](https://docs.doccupine.com/demo.png)\n\n## Images\nImages enhance documentation with context, illustration, or decorative visual cues.\n\n### Basic Image Syntax\nInclude an image in Markdown using the syntax below:\n\n```md\n![Alt text](https://docs.doccupine.com/demo.png)\n```\n\n<Callout type=\"note\">\n Use clear, descriptive alt text for accessibility and better SEO. Alt text should describe the image\u2019s appearance or content.\n</Callout>\n\n### HTML image embeds\nEmbed images in your Markdown content using HTML syntax.\n\n```md\n<img src=\"https://docs.doccupine.com/demo.png\" alt=\"Alt text\">\n```\n\n### Theme-aware images\nShow different images depending on whether the user is in light or dark mode. Add the `light-only` or `dark-only` className to display an image exclusively in that theme.\n\n```md\n<img className=\"light-only\" src=\"/images/diagram-light.png\" alt=\"Diagram\">\n<img className=\"dark-only\" src=\"/images/diagram-dark.png\" alt=\"Diagram\">\n```\n\n<img className=\"light-only\" src=\"https://docs.doccupine.com/demo.png\" alt=\"This image is only visible in light mode\">\n<img className=\"dark-only\" src=\"https://docs.doccupine.com/demo.png\" alt=\"This image is only visible in dark mode\" style={{ filter: \"invert(1)\" }}>\n\n<Callout type=\"note\">\n The `light-only` and `dark-only` classes work on any element, not just images. You can use them on videos, iframes, or wrapper divs too.\n</Callout>\n\n## Videos\nVideos add a dynamic element to your documentation, engaging your audience and providing a more immersive experience.\n\n### YouTube Embed\nTo embed a YouTube video, use the following syntax:\n\n```html\n<iframe\n className=\"aspect-video\"\n src=\"https://www.youtube.com/embed/ResP_eVPYQo\"\n title=\"YouTube video player\"\n frameBorder=\"0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n></iframe>\n```\n\n<iframe\n className=\"aspect-video\"\n src=\"https://www.youtube.com/embed/ResP_eVPYQo\"\n title=\"YouTube video player\"\n frameBorder=\"0\"\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\"\n allowFullScreen\n></iframe>\n\n### Self-hosted videos\nServe up your own video content using the `<video>` tag:\n\n```html\n<video\n controls\n className=\"aspect-video\"\n src=\"https://samplelib.com/lib/preview/mp4/sample-20s.mp4\"\n></video>\n```\n\n<video\n controls\n className=\"aspect-video\"\n src=\"https://samplelib.com/lib/preview/mp4/sample-20s.mp4\"\n></video>\n\n\n#### Autoplay and Looping\nFor demonstration videos that loop or start automatically, add attributes as shown:\n\n```html\n<video\n controls\n className=\"aspect-video\"\n src=\"https://samplelib.com/lib/preview/mp4/sample-20s.mp4\"\n autoPlay\n muted\n loop\n playsInline\n></video>\n```\n";
@@ -34,6 +34,21 @@ Embed images in your Markdown content using HTML syntax.
34
34
  <img src="https://docs.doccupine.com/demo.png" alt="Alt text">
35
35
  \`\`\`
36
36
 
37
+ ### Theme-aware images
38
+ Show different images depending on whether the user is in light or dark mode. Add the \`light-only\` or \`dark-only\` className to display an image exclusively in that theme.
39
+
40
+ \`\`\`md
41
+ <img className="light-only" src="/images/diagram-light.png" alt="Diagram">
42
+ <img className="dark-only" src="/images/diagram-dark.png" alt="Diagram">
43
+ \`\`\`
44
+
45
+ <img className="light-only" src="https://docs.doccupine.com/demo.png" alt="This image is only visible in light mode">
46
+ <img className="dark-only" src="https://docs.doccupine.com/demo.png" alt="This image is only visible in dark mode" style={{ filter: "invert(1)" }}>
47
+
48
+ <Callout type="note">
49
+ The \`light-only\` and \`dark-only\` classes work on any element, not just images. You can use them on videos, iframes, or wrapper divs too.
50
+ </Callout>
51
+
37
52
  ## Videos
38
53
  Videos add a dynamic element to your documentation, engaging your audience and providing a more immersive experience.
39
54
 
@@ -0,0 +1 @@
1
+ export declare const platformAnalyticsMdxTemplate = "---\ntitle: \"Analytics\"\ndescription: \"Enable PostHog analytics to track page views on your documentation site.\"\ndate: \"2026-02-24\"\ncategory: \"Configuration\"\ncategoryOrder: 2\norder: 3\nsection: \"Platform\"\n---\n# Analytics\nThe Analytics settings page lets you add PostHog analytics to your documentation site. Page views are tracked client-side and proxied through your own domain for privacy - no data is sent directly to PostHog.\n\n## Enabling analytics\nUse the **Enable Analytics** toggle to turn tracking on or off. When disabled, no tracking code is added to your site.\n\n## Configuration\n\n### PostHog Project API Key\nYour project API key from PostHog (starts with `phc_`). This is a public identifier and is safe to commit to your repository.\n\nTo find your key:\n1. Log in to [PostHog](https://posthog.com).\n2. Open your project settings.\n3. Copy the **Project API Key**.\n\n### Region\nSelect the PostHog cloud region that matches your project:\n\n- **US Cloud** - `us.i.posthog.com`\n- **EU Cloud** - `eu.i.posthog.com`\n\n## How it works\nAnalytics settings are stored in `analytics.json` at the root of your repository. Here's an example:\n\n```json\n{\n \"provider\": \"posthog\",\n \"posthog\": {\n \"key\": \"phc_your_project_api_key\",\n \"host\": \"https://us.i.posthog.com\"\n }\n}\n```\n\nWhen enabled, Doccupine routes all analytics traffic through your documentation domain using Next.js rewrites. Instead of sending data directly to PostHog (which ad blockers may intercept), requests go through `/ingest` on your own domain and are proxied to PostHog.\n\n<Callout type=\"note\">\n Changes to analytics settings are staged as pending changes. Click **Publish** to commit them to your repository and trigger a deploy.\n</Callout>\n\nSee the [Analytics](/analytics) page for the full configuration reference and additional details on the privacy proxy.";
@@ -0,0 +1,51 @@
1
+ export const platformAnalyticsMdxTemplate = `---
2
+ title: "Analytics"
3
+ description: "Enable PostHog analytics to track page views on your documentation site."
4
+ date: "2026-02-24"
5
+ category: "Configuration"
6
+ categoryOrder: 2
7
+ order: 3
8
+ section: "Platform"
9
+ ---
10
+ # Analytics
11
+ The Analytics settings page lets you add PostHog analytics to your documentation site. Page views are tracked client-side and proxied through your own domain for privacy - no data is sent directly to PostHog.
12
+
13
+ ## Enabling analytics
14
+ Use the **Enable Analytics** toggle to turn tracking on or off. When disabled, no tracking code is added to your site.
15
+
16
+ ## Configuration
17
+
18
+ ### PostHog Project API Key
19
+ Your project API key from PostHog (starts with \`phc_\`). This is a public identifier and is safe to commit to your repository.
20
+
21
+ To find your key:
22
+ 1. Log in to [PostHog](https://posthog.com).
23
+ 2. Open your project settings.
24
+ 3. Copy the **Project API Key**.
25
+
26
+ ### Region
27
+ Select the PostHog cloud region that matches your project:
28
+
29
+ - **US Cloud** - \`us.i.posthog.com\`
30
+ - **EU Cloud** - \`eu.i.posthog.com\`
31
+
32
+ ## How it works
33
+ Analytics settings are stored in \`analytics.json\` at the root of your repository. Here's an example:
34
+
35
+ \`\`\`json
36
+ {
37
+ "provider": "posthog",
38
+ "posthog": {
39
+ "key": "phc_your_project_api_key",
40
+ "host": "https://us.i.posthog.com"
41
+ }
42
+ }
43
+ \`\`\`
44
+
45
+ When enabled, Doccupine routes all analytics traffic through your documentation domain using Next.js rewrites. Instead of sending data directly to PostHog (which ad blockers may intercept), requests go through \`/ingest\` on your own domain and are proxied to PostHog.
46
+
47
+ <Callout type="note">
48
+ Changes to analytics settings are staged as pending changes. Click **Publish** to commit them to your repository and trigger a deploy.
49
+ </Callout>
50
+
51
+ See the [Analytics](/analytics) page for the full configuration reference and additional details on the privacy proxy.`;
@@ -10,20 +10,21 @@ export const packageJsonTemplate = JSON.stringify({
10
10
  format: "prettier --write .",
11
11
  },
12
12
  dependencies: {
13
- "@langchain/anthropic": "^1.3.19",
14
- "@langchain/core": "^1.1.27",
15
- "@langchain/google-genai": "^2.1.20",
16
- "@langchain/openai": "^1.2.9",
13
+ "@langchain/anthropic": "^1.3.23",
14
+ "@langchain/core": "^1.1.32",
15
+ "@langchain/google-genai": "^2.1.25",
16
+ "@langchain/openai": "^1.2.13",
17
17
  "@mdx-js/react": "^3.1.1",
18
- "@modelcontextprotocol/sdk": "^1.26.0",
18
+ "@modelcontextprotocol/sdk": "^1.27.1",
19
+ "@posthog/react": "^1.8.2",
19
20
  "cherry-styled-components": "^0.1.13",
20
- langchain: "^1.2.25",
21
- "lucide-react": "^0.575.0",
21
+ langchain: "^1.2.31",
22
+ "lucide-react": "^0.577.0",
22
23
  next: "16.1.6",
23
24
  "next-mdx-remote": "^6.0.0",
24
25
  polished: "^4.3.1",
25
- "posthog-js": "^1.353.0",
26
- "posthog-node": "^5.25.0",
26
+ "posthog-js": "^1.360.1",
27
+ "posthog-node": "^5.28.1",
27
28
  react: "19.2.4",
28
29
  "react-dom": "19.2.4",
29
30
  "rehype-highlight": "^7.0.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doccupine",
3
- "version": "0.0.70",
3
+ "version": "0.0.72",
4
4
  "description": "Free and open-source documentation platform. Write MDX, get a production-ready site with AI chat, built-in components, and an MCP server - in one command.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "chalk": "^5.6.2",
38
38
  "chokidar": "^5.0.0",
39
39
  "commander": "^14.0.3",
40
- "fs-extra": "^11.3.3",
40
+ "fs-extra": "^11.3.4",
41
41
  "gray-matter": "^4.0.3",
42
42
  "next": "^16.1.6",
43
43
  "prompts": "^2.4.2",
@@ -47,7 +47,7 @@
47
47
  "devDependencies": {
48
48
  "@types/chokidar": "^2.1.7",
49
49
  "@types/fs-extra": "^11.0.4",
50
- "@types/node": "^25.3.0",
50
+ "@types/node": "^25.4.0",
51
51
  "@types/prompts": "^2.4.9",
52
52
  "prettier": "^3.8.1",
53
53
  "typescript": "^5.9.3",