@heyo-sh/create-heyo-docs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/deployments/cloudflare/astro/package.json +12 -0
  3. package/deployments/cloudflare/astro/platform.astro.ts +4 -0
  4. package/deployments/cloudflare/astro/wrangler.jsonc +6 -0
  5. package/deployments/cloudflare/next/open-next.config.ts +3 -0
  6. package/deployments/cloudflare/next/package.json +13 -0
  7. package/deployments/cloudflare/next/wrangler.jsonc +11 -0
  8. package/deployments/cloudflare/react-router/app/entry.server.tsx +48 -0
  9. package/deployments/cloudflare/react-router/package.json +21 -0
  10. package/deployments/cloudflare/react-router/platform.vite.ts +6 -0
  11. package/deployments/cloudflare/react-router/react-router.config.ts +20 -0
  12. package/deployments/cloudflare/react-router/workers/app.ts +12 -0
  13. package/deployments/cloudflare/react-router/wrangler.jsonc +10 -0
  14. package/deployments/vercel/astro/package.json +11 -0
  15. package/deployments/vercel/astro/platform.astro.ts +4 -0
  16. package/deployments/vercel/astro/vercel.json +4 -0
  17. package/deployments/vercel/next/package.json +8 -0
  18. package/deployments/vercel/next/vercel.json +3 -0
  19. package/deployments/vercel/react-router/package.json +12 -0
  20. package/deployments/vercel/react-router/platform.vite.ts +4 -0
  21. package/deployments/vercel/react-router/react-router.config.ts +22 -0
  22. package/deployments/vercel/react-router/vercel.json +4 -0
  23. package/dist/index.d.ts +13 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +328 -0
  26. package/dist/package-manager.d.ts +6 -0
  27. package/dist/package-manager.d.ts.map +1 -0
  28. package/dist/project-name.d.ts +3 -0
  29. package/dist/project-name.d.ts.map +1 -0
  30. package/dist/scaffold.d.ts +5 -0
  31. package/dist/scaffold.d.ts.map +1 -0
  32. package/dist/types.d.ts +19 -0
  33. package/dist/types.d.ts.map +1 -0
  34. package/dist/utils.d.ts +8 -0
  35. package/dist/utils.d.ts.map +1 -0
  36. package/package.json +36 -0
  37. package/templates/astro/astro.config.ts +15 -0
  38. package/templates/astro/components.json +25 -0
  39. package/templates/astro/content/getting-started.mdx +16 -0
  40. package/templates/astro/content/index.mdx +22 -0
  41. package/templates/astro/content/mdx-components.mdx +44 -0
  42. package/templates/astro/heyo-docs.config.ts +24 -0
  43. package/templates/astro/package.json +34 -0
  44. package/templates/astro/platform.astro.ts +4 -0
  45. package/templates/astro/public/apple-touch-icon.png +0 -0
  46. package/templates/astro/public/favicon-96x96.png +0 -0
  47. package/templates/astro/public/favicon.ico +0 -0
  48. package/templates/astro/public/favicon.svg +1 -0
  49. package/templates/astro/public/logo.svg +12 -0
  50. package/templates/astro/public/site.webmanifest +21 -0
  51. package/templates/astro/public/web-app-manifest-192x192.png +0 -0
  52. package/templates/astro/public/web-app-manifest-512x512.png +0 -0
  53. package/templates/astro/src/components/docs-app.tsx +165 -0
  54. package/templates/astro/src/components/theme-provider.tsx +106 -0
  55. package/templates/astro/src/env.d.ts +2 -0
  56. package/templates/astro/src/heyo-docs-icons.tsx +79 -0
  57. package/templates/astro/src/layouts/docs-layout.astro +70 -0
  58. package/templates/astro/src/lib/docs.ts +31 -0
  59. package/templates/astro/src/lib/openapi-request.ts +160 -0
  60. package/templates/astro/src/lib/seo.ts +221 -0
  61. package/templates/astro/src/lib/theme.ts +74 -0
  62. package/templates/astro/src/pages/[...slug].astro +50 -0
  63. package/templates/astro/src/pages/[...slug].md.ts +28 -0
  64. package/templates/astro/src/pages/heyo-docs-internal/openapi-request.ts +8 -0
  65. package/templates/astro/src/pages/llms-full.txt.ts +13 -0
  66. package/templates/astro/src/pages/llms.txt.ts +13 -0
  67. package/templates/astro/src/pages/robots.txt.ts +19 -0
  68. package/templates/astro/src/pages/rss.xml.ts +13 -0
  69. package/templates/astro/src/pages/sitemap.xml.ts +19 -0
  70. package/templates/astro/src/styles/app.css +8 -0
  71. package/templates/astro/src/virtual.d.ts +19 -0
  72. package/templates/astro/tsconfig.json +12 -0
  73. package/templates/next/app/[[...slug]]/page.tsx +59 -0
  74. package/templates/next/app/app.css +8 -0
  75. package/templates/next/app/components/docs-app.tsx +139 -0
  76. package/templates/next/app/components/theme-provider.tsx +93 -0
  77. package/templates/next/app/error.tsx +8 -0
  78. package/templates/next/app/heyo-docs-icons.tsx +70 -0
  79. package/templates/next/app/heyo-docs-internal/markdown/[[...slug]]/route.ts +26 -0
  80. package/templates/next/app/heyo-docs-internal/openapi-request/route.ts +5 -0
  81. package/templates/next/app/layout.tsx +79 -0
  82. package/templates/next/app/lib/docs.ts +43 -0
  83. package/templates/next/app/lib/openapi-request.ts +147 -0
  84. package/templates/next/app/lib/seo.ts +189 -0
  85. package/templates/next/app/lib/theme.ts +61 -0
  86. package/templates/next/app/llms-full.txt/route.ts +10 -0
  87. package/templates/next/app/llms.txt/route.ts +10 -0
  88. package/templates/next/app/not-found.tsx +13 -0
  89. package/templates/next/app/robots.txt/route.ts +16 -0
  90. package/templates/next/app/rss.xml/route.ts +10 -0
  91. package/templates/next/app/sitemap.xml/route.ts +14 -0
  92. package/templates/next/components.json +20 -0
  93. package/templates/next/content/getting-started.mdx +16 -0
  94. package/templates/next/content/index.mdx +22 -0
  95. package/templates/next/content/mdx-components.mdx +41 -0
  96. package/templates/next/heyo-docs.config.ts +24 -0
  97. package/templates/next/mdx.d.ts +6 -0
  98. package/templates/next/next-env.d.ts +7 -0
  99. package/templates/next/next.config.ts +23 -0
  100. package/templates/next/package.json +40 -0
  101. package/templates/next/postcss.config.mjs +5 -0
  102. package/templates/next/public/apple-touch-icon.png +0 -0
  103. package/templates/next/public/favicon-96x96.png +0 -0
  104. package/templates/next/public/favicon.ico +0 -0
  105. package/templates/next/public/favicon.svg +1 -0
  106. package/templates/next/public/logo.svg +12 -0
  107. package/templates/next/public/site.webmanifest +21 -0
  108. package/templates/next/public/web-app-manifest-192x192.png +0 -0
  109. package/templates/next/public/web-app-manifest-512x512.png +0 -0
  110. package/templates/next/scripts/generate-heyo-docs.ts +50 -0
  111. package/templates/next/tsconfig.json +27 -0
  112. package/templates/react-router/app/app.css +8 -0
  113. package/templates/react-router/app/components/theme-provider.tsx +106 -0
  114. package/templates/react-router/app/heyo-docs-icons.tsx +71 -0
  115. package/templates/react-router/app/lib/theme.ts +74 -0
  116. package/templates/react-router/app/root.tsx +140 -0
  117. package/templates/react-router/app/routes/docs.tsx +179 -0
  118. package/templates/react-router/app/routes/home.tsx +1 -0
  119. package/templates/react-router/app/routes/llms-full.ts +13 -0
  120. package/templates/react-router/app/routes/llms.ts +13 -0
  121. package/templates/react-router/app/routes/markdown.ts +25 -0
  122. package/templates/react-router/app/routes/openapi-request.ts +159 -0
  123. package/templates/react-router/app/routes/page.tsx +1 -0
  124. package/templates/react-router/app/routes/robots.ts +19 -0
  125. package/templates/react-router/app/routes/rss.ts +13 -0
  126. package/templates/react-router/app/routes/sitemap.ts +18 -0
  127. package/templates/react-router/app/routes.ts +13 -0
  128. package/templates/react-router/app/virtual.d.ts +19 -0
  129. package/templates/react-router/components.json +25 -0
  130. package/templates/react-router/content/getting-started.mdx +16 -0
  131. package/templates/react-router/content/index.mdx +22 -0
  132. package/templates/react-router/content/mdx-components.mdx +293 -0
  133. package/templates/react-router/heyo-docs.config.ts +24 -0
  134. package/templates/react-router/package.json +36 -0
  135. package/templates/react-router/platform.vite.ts +2 -0
  136. package/templates/react-router/public/apple-touch-icon.png +0 -0
  137. package/templates/react-router/public/favicon-96x96.png +0 -0
  138. package/templates/react-router/public/favicon.ico +0 -0
  139. package/templates/react-router/public/favicon.svg +1 -0
  140. package/templates/react-router/public/logo.svg +12 -0
  141. package/templates/react-router/public/site.webmanifest +21 -0
  142. package/templates/react-router/public/web-app-manifest-192x192.png +0 -0
  143. package/templates/react-router/public/web-app-manifest-512x512.png +0 -0
  144. package/templates/react-router/react-router.config.ts +20 -0
  145. package/templates/react-router/tsconfig.json +20 -0
  146. package/templates/react-router/vite.config.ts +17 -0
@@ -0,0 +1,139 @@
1
+ "use client";
2
+
3
+ import {
4
+ DocsApp,
5
+ openApiEndpointDataPath,
6
+ type OpenApiEndpoint,
7
+ } from "@heyo-sh/heyo-docs";
8
+ import {
9
+ createContext,
10
+ forwardRef,
11
+ useCallback,
12
+ useContext,
13
+ useEffect,
14
+ useRef,
15
+ useState,
16
+ type ComponentPropsWithoutRef,
17
+ type MouseEvent,
18
+ } from "react";
19
+
20
+ import { docsConfig, openApiEndpoints, pages } from "../_heyo-docs/content";
21
+ import config from "../../heyo-docs.config";
22
+ import { iconSet } from "../heyo-docs-icons";
23
+ import { useTheme } from "./theme-provider";
24
+
25
+ type Navigate = (href: string) => void;
26
+
27
+ const NextNavigationContext = createContext<Navigate | undefined>(undefined);
28
+
29
+ const NextDocsLink = forwardRef<
30
+ HTMLAnchorElement,
31
+ ComponentPropsWithoutRef<"a">
32
+ >(function NextDocsLink({ href, onClick, target, ...props }, ref) {
33
+ const navigate = useContext(NextNavigationContext);
34
+
35
+ const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
36
+ onClick?.(event);
37
+ if (
38
+ !navigate ||
39
+ event.defaultPrevented ||
40
+ event.button !== 0 ||
41
+ event.metaKey ||
42
+ event.altKey ||
43
+ event.ctrlKey ||
44
+ event.shiftKey ||
45
+ target
46
+ )
47
+ return;
48
+
49
+ event.preventDefault();
50
+ navigate(href ?? "/");
51
+ };
52
+
53
+ return (
54
+ <a {...props} href={href} onClick={handleClick} ref={ref} target={target} />
55
+ );
56
+ });
57
+
58
+ export function NextDocsApp({
59
+ currentOpenApiEndpoint,
60
+ pathname,
61
+ }: {
62
+ currentOpenApiEndpoint?: OpenApiEndpoint;
63
+ pathname: string;
64
+ }) {
65
+ const { mounted, resolvedTheme, setTheme } = useTheme();
66
+ const [activePathname, setActivePathname] = useState(pathname);
67
+ const [activeOpenApiEndpoint, setActiveOpenApiEndpoint] = useState(
68
+ currentOpenApiEndpoint,
69
+ );
70
+ const navigationId = useRef(0);
71
+
72
+ const navigate = useCallback(async (href: string, updateHistory = true) => {
73
+ const destination = new URL(href, window.location.href);
74
+ if (destination.origin !== window.location.origin) {
75
+ window.location.assign(href);
76
+ return;
77
+ }
78
+
79
+ const nextPathname = destination.pathname;
80
+ const indexedEndpoint = openApiEndpoints.find(
81
+ (endpoint) => endpoint.slug === nextPathname,
82
+ );
83
+ const page = pages.find((candidate) => candidate.slug === nextPathname);
84
+ if (!indexedEndpoint && !page) {
85
+ window.location.assign(href);
86
+ return;
87
+ }
88
+
89
+ const currentNavigation = ++navigationId.current;
90
+ let endpoint: OpenApiEndpoint | undefined;
91
+ if (indexedEndpoint) {
92
+ const response = await fetch(
93
+ openApiEndpointDataPath(indexedEndpoint.slug),
94
+ );
95
+ if (!response.ok) {
96
+ window.location.assign(href);
97
+ return;
98
+ }
99
+ endpoint = (await response.json()) as OpenApiEndpoint;
100
+ }
101
+ if (currentNavigation !== navigationId.current) return;
102
+
103
+ if (updateHistory) window.history.pushState({}, "", destination);
104
+ document.title =
105
+ page?.seo.title ?? `${indexedEndpoint!.title} | ${docsConfig.title}`;
106
+ setActiveOpenApiEndpoint(endpoint);
107
+ setActivePathname(nextPathname);
108
+ }, []);
109
+
110
+ useEffect(() => {
111
+ const onPopState = () => {
112
+ void navigate(window.location.href, false);
113
+ };
114
+
115
+ window.addEventListener("popstate", onPopState);
116
+ return () => window.removeEventListener("popstate", onPopState);
117
+ }, [navigate]);
118
+
119
+ return (
120
+ <NextNavigationContext.Provider value={navigate}>
121
+ <DocsApp
122
+ config={{ ...docsConfig, navigation: config.navigation }}
123
+ iconSet={iconSet}
124
+ isDark={mounted ? resolvedTheme === "dark" : undefined}
125
+ link={NextDocsLink}
126
+ onThemeToggle={
127
+ mounted
128
+ ? () => setTheme(resolvedTheme === "dark" ? "light" : "dark")
129
+ : undefined
130
+ }
131
+ currentOpenApiEndpoint={activeOpenApiEndpoint}
132
+ openApiEndpoints={openApiEndpoints}
133
+ openApiRequestUrl="/heyo-docs-internal/openapi-request"
134
+ pages={pages}
135
+ pathname={activePathname}
136
+ />
137
+ </NextNavigationContext.Provider>
138
+ );
139
+ }
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useEffect,
7
+ useState,
8
+ type ReactNode,
9
+ } from "react";
10
+
11
+ import {
12
+ applyTheme,
13
+ DEFAULT_THEME,
14
+ getStoredTheme,
15
+ type ResolvedTheme,
16
+ THEME_STORAGE_KEY,
17
+ type Theme,
18
+ } from "../lib/theme";
19
+
20
+ type ThemeProviderState = {
21
+ theme: Theme;
22
+ resolvedTheme: ResolvedTheme;
23
+ mounted: boolean;
24
+ setTheme: (theme: Theme) => void;
25
+ };
26
+
27
+ const ThemeProviderContext = createContext<ThemeProviderState | undefined>(
28
+ undefined,
29
+ );
30
+
31
+ export function ThemeProvider({
32
+ children,
33
+ defaultTheme = DEFAULT_THEME,
34
+ storageKey = THEME_STORAGE_KEY,
35
+ }: {
36
+ children: ReactNode;
37
+ defaultTheme?: Theme;
38
+ storageKey?: string;
39
+ }) {
40
+ const [theme, setThemeState] = useState<Theme>(defaultTheme);
41
+ const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>(
42
+ defaultTheme === "system" ? "light" : defaultTheme,
43
+ );
44
+ const [mounted, setMounted] = useState(false);
45
+
46
+ useEffect(() => {
47
+ const syncTheme = () => {
48
+ const nextTheme = getStoredTheme(storageKey, defaultTheme);
49
+ setThemeState(nextTheme);
50
+ setResolvedTheme(applyTheme(nextTheme));
51
+ };
52
+ syncTheme();
53
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
54
+ const onSystemThemeChange = () => syncTheme();
55
+ const onStorage = (event: StorageEvent) => {
56
+ if (event.key === null || event.key === storageKey) syncTheme();
57
+ };
58
+ mediaQuery.addEventListener("change", onSystemThemeChange);
59
+ window.addEventListener("storage", onStorage);
60
+ setMounted(true);
61
+ return () => {
62
+ mediaQuery.removeEventListener("change", onSystemThemeChange);
63
+ window.removeEventListener("storage", onStorage);
64
+ };
65
+ }, [defaultTheme, storageKey]);
66
+
67
+ return (
68
+ <ThemeProviderContext.Provider
69
+ value={{
70
+ theme,
71
+ resolvedTheme,
72
+ mounted,
73
+ setTheme: (nextTheme) => {
74
+ try {
75
+ window.localStorage.setItem(storageKey, nextTheme);
76
+ } catch {
77
+ // The requested preference still applies when storage is unavailable.
78
+ }
79
+ setThemeState(nextTheme);
80
+ setResolvedTheme(applyTheme(nextTheme));
81
+ },
82
+ }}
83
+ >
84
+ {children}
85
+ </ThemeProviderContext.Provider>
86
+ );
87
+ }
88
+
89
+ export function useTheme() {
90
+ const context = useContext(ThemeProviderContext);
91
+ if (!context) throw new Error("useTheme must be used within a ThemeProvider");
92
+ return context;
93
+ }
@@ -0,0 +1,8 @@
1
+ "use client";
2
+
3
+ import { NextDocsApp } from "./components/docs-app";
4
+
5
+ /** Retains the documentation chrome if an unexpected route error occurs. */
6
+ export default function ErrorPage() {
7
+ return <NextDocsApp pathname="/__not-found" />;
8
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ RiArrowDownLine,
3
+ RiArrowDownSLine,
4
+ RiArrowRightLine,
5
+ RiArrowRightSLine,
6
+ RiArrowUpLine,
7
+ RiBookOpenLine,
8
+ RiChatAiLine,
9
+ RiCheckboxCircleLine,
10
+ RiCheckLine,
11
+ RiCloseCircleLine,
12
+ RiCloseLine,
13
+ RiCodeBoxLine,
14
+ RiCornerDownLeftLine,
15
+ RiCursorLine,
16
+ RiExternalLinkLine,
17
+ RiFileCopyLine,
18
+ RiFileTextLine,
19
+ RiFolderLine,
20
+ RiGitForkLine,
21
+ RiGitRepositoryLine,
22
+ RiGithubLine,
23
+ RiGlobalLine,
24
+ RiInformationLine,
25
+ RiLightbulbLine,
26
+ RiLoginBoxLine,
27
+ RiMenuLine,
28
+ RiMoonLine,
29
+ RiNewsLine,
30
+ RiSearchLine,
31
+ RiStarLine,
32
+ RiSunLine,
33
+ } from "@remixicon/react";
34
+ import type { IconSet } from "@heyo-sh/heyo-docs";
35
+
36
+ /** Statically selected icons keep unused packs out of the client bundle. */
37
+ export const iconSet = {
38
+ book: RiBookOpenLine,
39
+ changelog: RiNewsLine,
40
+ code: RiCodeBoxLine,
41
+ file: RiFileTextLine,
42
+ github: RiGithubLine,
43
+ globe: RiGlobalLine,
44
+ signIn: RiLoginBoxLine,
45
+ chevronDown: RiArrowDownSLine,
46
+ chevronRight: RiArrowRightSLine,
47
+ arrowDown: RiArrowDownLine,
48
+ arrowUp: RiArrowUpLine,
49
+ arrowRight: RiArrowRightLine,
50
+ close: RiCloseLine,
51
+ cornerDownLeft: RiCornerDownLeftLine,
52
+ menu: RiMenuLine,
53
+ search: RiSearchLine,
54
+ sun: RiSunLine,
55
+ moon: RiMoonLine,
56
+ checkCircle: RiCheckboxCircleLine,
57
+ check: RiCheckLine,
58
+ closeCircle: RiCloseCircleLine,
59
+ externalLink: RiExternalLinkLine,
60
+ folder: RiFolderLine,
61
+ gitFork: RiGitForkLine,
62
+ gitRepository: RiGitRepositoryLine,
63
+ information: RiInformationLine,
64
+ lightbulb: RiLightbulbLine,
65
+ copy: RiFileCopyLine,
66
+ star: RiStarLine,
67
+ bot: RiChatAiLine,
68
+ cursor: RiCursorLine,
69
+ chat: RiChatAiLine,
70
+ } satisfies Required<IconSet>;
@@ -0,0 +1,26 @@
1
+ import {
2
+ markdownForPage,
3
+ pathnameFromMarkdownPath,
4
+ } from "@heyo-sh/heyo-docs/node";
5
+
6
+ import { markdownPages } from "../../../lib/docs";
7
+
8
+ /** Serves every documentation page as stable, frontmatter-free Markdown. */
9
+ export async function GET(
10
+ _request: Request,
11
+ { params }: { params: Promise<{ slug?: string[] }> },
12
+ ) {
13
+ const { slug } = await params;
14
+ const pagePathname = pathnameFromMarkdownPath(`/${slug?.join("/") ?? ""}`);
15
+ const page = pagePathname
16
+ ? markdownPages.find((candidate) => candidate.slug === pagePathname)
17
+ : undefined;
18
+ if (!page)
19
+ return new Response("Not Found", {
20
+ status: 404,
21
+ headers: { "content-type": "text/plain; charset=utf-8" },
22
+ });
23
+ return new Response(markdownForPage(page), {
24
+ headers: { "content-type": "text/markdown; charset=utf-8" },
25
+ });
26
+ }
@@ -0,0 +1,5 @@
1
+ import { handleOpenApiRequest } from "../../lib/openapi-request";
2
+
3
+ export async function POST(request: Request) {
4
+ return handleOpenApiRequest(request);
5
+ }
@@ -0,0 +1,79 @@
1
+ import type { Metadata } from "next";
2
+ import type { ReactNode } from "react";
3
+
4
+ import config from "../heyo-docs.config";
5
+ import "./app.css";
6
+ import { ThemeProvider } from "./components/theme-provider";
7
+ import { getThemeScript, THEME_STORAGE_KEY } from "./lib/theme";
8
+
9
+ export const metadata: Metadata = {
10
+ ...(config.siteUrl ? { metadataBase: new URL(config.siteUrl) } : {}),
11
+ title: config.title,
12
+ description: config.description,
13
+ robots: { index: true, follow: true },
14
+ alternates: {
15
+ ...(config.siteUrl ? { canonical: "/" } : {}),
16
+ ...(config.groups.some((group) => group.type === "changelog")
17
+ ? { types: { "application/rss+xml": "/rss.xml" } }
18
+ : {}),
19
+ },
20
+ manifest: "/site.webmanifest",
21
+ icons: {
22
+ icon: [
23
+ { url: "/favicon.ico", sizes: "any" },
24
+ { url: "/favicon.svg", type: "image/svg+xml" },
25
+ { url: "/favicon-96x96.png", sizes: "96x96", type: "image/png" },
26
+ ],
27
+ apple: [
28
+ { url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
29
+ ],
30
+ },
31
+ openGraph: {
32
+ type: "website",
33
+ siteName: config.title,
34
+ title: config.title,
35
+ description: config.description,
36
+ ...(config.siteUrl ? { url: config.siteUrl } : {}),
37
+ },
38
+ twitter: {
39
+ card: "summary",
40
+ title: config.title,
41
+ description: config.description,
42
+ },
43
+ };
44
+
45
+ export default function RootLayout({ children }: { children: ReactNode }) {
46
+ return (
47
+ <html lang="en" suppressHydrationWarning>
48
+ <head>
49
+ <meta name="color-scheme" content="light dark" />
50
+ <meta name="referrer" content="strict-origin-when-cross-origin" />
51
+ <script
52
+ dangerouslySetInnerHTML={{
53
+ __html: getThemeScript(THEME_STORAGE_KEY, config.mode),
54
+ }}
55
+ />
56
+ <script
57
+ type="application/ld+json"
58
+ dangerouslySetInnerHTML={{
59
+ __html: JSON.stringify({
60
+ "@context": "https://schema.org",
61
+ "@type": "WebSite",
62
+ name: config.title,
63
+ description: config.description,
64
+ ...(config.siteUrl ? { url: config.siteUrl } : {}),
65
+ }).replace(/</g, "\\u003c"),
66
+ }}
67
+ />
68
+ </head>
69
+ <body>
70
+ <ThemeProvider
71
+ defaultTheme={config.mode}
72
+ storageKey={THEME_STORAGE_KEY}
73
+ >
74
+ {children}
75
+ </ThemeProvider>
76
+ </body>
77
+ </html>
78
+ );
79
+ }
@@ -0,0 +1,43 @@
1
+ import type { DocsPage } from "@heyo-sh/heyo-docs";
2
+ import {
3
+ changelogGroupForPage,
4
+ createDocsModel,
5
+ findDocsPage,
6
+ findOpenApiEndpoint,
7
+ normaliseDocsPathname,
8
+ } from "@heyo-sh/heyo-docs/node";
9
+
10
+ import config from "../../heyo-docs.config";
11
+ import {
12
+ docsPages,
13
+ markdownPages,
14
+ openApiDocuments,
15
+ } from "../_heyo-docs/server";
16
+
17
+ /** Server-only model: MDX components stay on the client-side module boundary. */
18
+ export const docsModel = createDocsModel(
19
+ config,
20
+ docsPages as unknown as DocsPage[],
21
+ openApiDocuments,
22
+ );
23
+
24
+ export { config, markdownPages };
25
+
26
+ export function pathnameForSegments(slug: string[] | undefined): string {
27
+ return normaliseDocsPathname(slug?.join("/") ?? "/");
28
+ }
29
+
30
+ export function docsContext(pathname: string) {
31
+ const page = findDocsPage(docsModel.pages, pathname);
32
+ const endpoint = findOpenApiEndpoint(docsModel.endpoints, pathname);
33
+ return {
34
+ config,
35
+ model: docsModel,
36
+ navigation: docsModel.navigation,
37
+ page,
38
+ endpoint,
39
+ changelogGroup: page
40
+ ? changelogGroupForPage(config.groups, page, docsModel.pages)
41
+ : undefined,
42
+ };
43
+ }
@@ -0,0 +1,147 @@
1
+ import { findOpenApiEndpoint } from "@heyo-sh/heyo-docs/node";
2
+ import type { OpenApiEndpoint } from "@heyo-sh/heyo-docs";
3
+
4
+ import { docsModel } from "./docs";
5
+
6
+ interface OpenApiRequestPayload {
7
+ bearerToken: string;
8
+ body?: string;
9
+ endpointSlug: string;
10
+ parameters: Record<string, string>;
11
+ server: string;
12
+ }
13
+
14
+ /** Sends documented API calls server-side after validating the declared server. */
15
+ export async function handleOpenApiRequest(
16
+ request: Request,
17
+ ): Promise<Response> {
18
+ if (request.method !== "POST")
19
+ return new Response("Method Not Allowed", {
20
+ headers: { Allow: "POST" },
21
+ status: 405,
22
+ });
23
+ const payload = await requestPayload(request);
24
+ if (!payload)
25
+ return new Response("Invalid OpenAPI request.", { status: 400 });
26
+ const endpoint = findOpenApiEndpoint(
27
+ docsModel.endpoints,
28
+ payload.endpointSlug,
29
+ );
30
+ if (!endpoint) return new Response("Endpoint not found.", { status: 404 });
31
+ if (endpoint.servers.length && !endpoint.servers.includes(payload.server))
32
+ return new Response("API server is not declared in the OpenAPI schema.", {
33
+ status: 400,
34
+ });
35
+ if (!endpoint.servers.length && payload.server)
36
+ return new Response("This endpoint does not declare an API server.", {
37
+ status: 400,
38
+ });
39
+
40
+ try {
41
+ const response = await fetch(
42
+ requestUrl(endpoint, payload.server, payload.parameters, request.url),
43
+ {
44
+ method: endpoint.method.toUpperCase(),
45
+ headers: requestHeaders(
46
+ endpoint,
47
+ payload.parameters,
48
+ payload.bearerToken,
49
+ ),
50
+ body: payload.body,
51
+ },
52
+ );
53
+ const headers = new Headers();
54
+ const contentType = response.headers.get("content-type");
55
+ if (contentType) headers.set("content-type", contentType);
56
+ return new Response(
57
+ [204, 205, 304].includes(response.status)
58
+ ? null
59
+ : await response.arrayBuffer(),
60
+ { headers, status: response.status, statusText: response.statusText },
61
+ );
62
+ } catch {
63
+ return new Response("The API server could not be reached.", {
64
+ status: 502,
65
+ });
66
+ }
67
+ }
68
+
69
+ async function requestPayload(
70
+ request: Request,
71
+ ): Promise<OpenApiRequestPayload | undefined> {
72
+ try {
73
+ const value: unknown = await request.json();
74
+ if (!isRecord(value)) return undefined;
75
+ const endpointSlug = stringValue(value.endpointSlug);
76
+ const server = stringValue(value.server);
77
+ if (!endpointSlug || server === undefined) return undefined;
78
+ return {
79
+ endpointSlug,
80
+ server,
81
+ bearerToken: stringValue(value.bearerToken) ?? "",
82
+ body: stringValue(value.body),
83
+ parameters: stringRecord(value.parameters),
84
+ };
85
+ } catch {
86
+ return undefined;
87
+ }
88
+ }
89
+
90
+ function requestUrl(
91
+ endpoint: OpenApiEndpoint,
92
+ server: string,
93
+ parameters: Record<string, string>,
94
+ requestUrl: string,
95
+ ) {
96
+ const path = endpoint.path.replace(
97
+ /\{([^}]+)\}/g,
98
+ (match: string, name: string) => {
99
+ const value = parameters[`path:${name}`];
100
+ return value ? encodeURIComponent(value) : match;
101
+ },
102
+ );
103
+ const query = new URLSearchParams();
104
+ for (const parameter of endpoint.parameters) {
105
+ if (parameter.in !== "query") continue;
106
+ const value = parameters[`query:${parameter.name}`];
107
+ if (value) query.set(parameter.name, value);
108
+ }
109
+ const destination = `${path}${query.size ? `?${query}` : ""}`;
110
+ return server
111
+ ? `${server.replace(/\/$/, "")}${destination}`
112
+ : new URL(destination, requestUrl).toString();
113
+ }
114
+
115
+ function requestHeaders(
116
+ endpoint: OpenApiEndpoint,
117
+ parameters: Record<string, string>,
118
+ bearerToken: string,
119
+ ) {
120
+ const headers = new Headers();
121
+ for (const parameter of endpoint.parameters) {
122
+ if (parameter.in !== "header") continue;
123
+ const value = parameters[`header:${parameter.name}`];
124
+ if (value) headers.set(parameter.name, value);
125
+ }
126
+ if (endpoint.requestBody?.contentType)
127
+ headers.set("Content-Type", endpoint.requestBody.contentType);
128
+ if (bearerToken) headers.set("Authorization", `Bearer ${bearerToken}`);
129
+ return headers;
130
+ }
131
+
132
+ function isRecord(value: unknown): value is Record<string, unknown> {
133
+ return typeof value === "object" && value !== null && !Array.isArray(value);
134
+ }
135
+
136
+ function stringRecord(value: unknown): Record<string, string> {
137
+ if (!isRecord(value)) return {};
138
+ return Object.fromEntries(
139
+ Object.entries(value).filter(
140
+ (entry): entry is [string, string] => typeof entry[1] === "string",
141
+ ),
142
+ );
143
+ }
144
+
145
+ function stringValue(value: unknown): string | undefined {
146
+ return typeof value === "string" ? value : undefined;
147
+ }