@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.
- package/LICENSE +21 -0
- package/deployments/cloudflare/astro/package.json +12 -0
- package/deployments/cloudflare/astro/platform.astro.ts +4 -0
- package/deployments/cloudflare/astro/wrangler.jsonc +6 -0
- package/deployments/cloudflare/next/open-next.config.ts +3 -0
- package/deployments/cloudflare/next/package.json +13 -0
- package/deployments/cloudflare/next/wrangler.jsonc +11 -0
- package/deployments/cloudflare/react-router/app/entry.server.tsx +48 -0
- package/deployments/cloudflare/react-router/package.json +21 -0
- package/deployments/cloudflare/react-router/platform.vite.ts +6 -0
- package/deployments/cloudflare/react-router/react-router.config.ts +20 -0
- package/deployments/cloudflare/react-router/workers/app.ts +12 -0
- package/deployments/cloudflare/react-router/wrangler.jsonc +10 -0
- package/deployments/vercel/astro/package.json +11 -0
- package/deployments/vercel/astro/platform.astro.ts +4 -0
- package/deployments/vercel/astro/vercel.json +4 -0
- package/deployments/vercel/next/package.json +8 -0
- package/deployments/vercel/next/vercel.json +3 -0
- package/deployments/vercel/react-router/package.json +12 -0
- package/deployments/vercel/react-router/platform.vite.ts +4 -0
- package/deployments/vercel/react-router/react-router.config.ts +22 -0
- package/deployments/vercel/react-router/vercel.json +4 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +328 -0
- package/dist/package-manager.d.ts +6 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/project-name.d.ts +3 -0
- package/dist/project-name.d.ts.map +1 -0
- package/dist/scaffold.d.ts +5 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/types.d.ts +19 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +36 -0
- package/templates/astro/astro.config.ts +15 -0
- package/templates/astro/components.json +25 -0
- package/templates/astro/content/getting-started.mdx +16 -0
- package/templates/astro/content/index.mdx +22 -0
- package/templates/astro/content/mdx-components.mdx +44 -0
- package/templates/astro/heyo-docs.config.ts +24 -0
- package/templates/astro/package.json +34 -0
- package/templates/astro/platform.astro.ts +4 -0
- package/templates/astro/public/apple-touch-icon.png +0 -0
- package/templates/astro/public/favicon-96x96.png +0 -0
- package/templates/astro/public/favicon.ico +0 -0
- package/templates/astro/public/favicon.svg +1 -0
- package/templates/astro/public/logo.svg +12 -0
- package/templates/astro/public/site.webmanifest +21 -0
- package/templates/astro/public/web-app-manifest-192x192.png +0 -0
- package/templates/astro/public/web-app-manifest-512x512.png +0 -0
- package/templates/astro/src/components/docs-app.tsx +165 -0
- package/templates/astro/src/components/theme-provider.tsx +106 -0
- package/templates/astro/src/env.d.ts +2 -0
- package/templates/astro/src/heyo-docs-icons.tsx +79 -0
- package/templates/astro/src/layouts/docs-layout.astro +70 -0
- package/templates/astro/src/lib/docs.ts +31 -0
- package/templates/astro/src/lib/openapi-request.ts +160 -0
- package/templates/astro/src/lib/seo.ts +221 -0
- package/templates/astro/src/lib/theme.ts +74 -0
- package/templates/astro/src/pages/[...slug].astro +50 -0
- package/templates/astro/src/pages/[...slug].md.ts +28 -0
- package/templates/astro/src/pages/heyo-docs-internal/openapi-request.ts +8 -0
- package/templates/astro/src/pages/llms-full.txt.ts +13 -0
- package/templates/astro/src/pages/llms.txt.ts +13 -0
- package/templates/astro/src/pages/robots.txt.ts +19 -0
- package/templates/astro/src/pages/rss.xml.ts +13 -0
- package/templates/astro/src/pages/sitemap.xml.ts +19 -0
- package/templates/astro/src/styles/app.css +8 -0
- package/templates/astro/src/virtual.d.ts +19 -0
- package/templates/astro/tsconfig.json +12 -0
- package/templates/next/app/[[...slug]]/page.tsx +59 -0
- package/templates/next/app/app.css +8 -0
- package/templates/next/app/components/docs-app.tsx +139 -0
- package/templates/next/app/components/theme-provider.tsx +93 -0
- package/templates/next/app/error.tsx +8 -0
- package/templates/next/app/heyo-docs-icons.tsx +70 -0
- package/templates/next/app/heyo-docs-internal/markdown/[[...slug]]/route.ts +26 -0
- package/templates/next/app/heyo-docs-internal/openapi-request/route.ts +5 -0
- package/templates/next/app/layout.tsx +79 -0
- package/templates/next/app/lib/docs.ts +43 -0
- package/templates/next/app/lib/openapi-request.ts +147 -0
- package/templates/next/app/lib/seo.ts +189 -0
- package/templates/next/app/lib/theme.ts +61 -0
- package/templates/next/app/llms-full.txt/route.ts +10 -0
- package/templates/next/app/llms.txt/route.ts +10 -0
- package/templates/next/app/not-found.tsx +13 -0
- package/templates/next/app/robots.txt/route.ts +16 -0
- package/templates/next/app/rss.xml/route.ts +10 -0
- package/templates/next/app/sitemap.xml/route.ts +14 -0
- package/templates/next/components.json +20 -0
- package/templates/next/content/getting-started.mdx +16 -0
- package/templates/next/content/index.mdx +22 -0
- package/templates/next/content/mdx-components.mdx +41 -0
- package/templates/next/heyo-docs.config.ts +24 -0
- package/templates/next/mdx.d.ts +6 -0
- package/templates/next/next-env.d.ts +7 -0
- package/templates/next/next.config.ts +23 -0
- package/templates/next/package.json +40 -0
- package/templates/next/postcss.config.mjs +5 -0
- package/templates/next/public/apple-touch-icon.png +0 -0
- package/templates/next/public/favicon-96x96.png +0 -0
- package/templates/next/public/favicon.ico +0 -0
- package/templates/next/public/favicon.svg +1 -0
- package/templates/next/public/logo.svg +12 -0
- package/templates/next/public/site.webmanifest +21 -0
- package/templates/next/public/web-app-manifest-192x192.png +0 -0
- package/templates/next/public/web-app-manifest-512x512.png +0 -0
- package/templates/next/scripts/generate-heyo-docs.ts +50 -0
- package/templates/next/tsconfig.json +27 -0
- package/templates/react-router/app/app.css +8 -0
- package/templates/react-router/app/components/theme-provider.tsx +106 -0
- package/templates/react-router/app/heyo-docs-icons.tsx +71 -0
- package/templates/react-router/app/lib/theme.ts +74 -0
- package/templates/react-router/app/root.tsx +140 -0
- package/templates/react-router/app/routes/docs.tsx +179 -0
- package/templates/react-router/app/routes/home.tsx +1 -0
- package/templates/react-router/app/routes/llms-full.ts +13 -0
- package/templates/react-router/app/routes/llms.ts +13 -0
- package/templates/react-router/app/routes/markdown.ts +25 -0
- package/templates/react-router/app/routes/openapi-request.ts +159 -0
- package/templates/react-router/app/routes/page.tsx +1 -0
- package/templates/react-router/app/routes/robots.ts +19 -0
- package/templates/react-router/app/routes/rss.ts +13 -0
- package/templates/react-router/app/routes/sitemap.ts +18 -0
- package/templates/react-router/app/routes.ts +13 -0
- package/templates/react-router/app/virtual.d.ts +19 -0
- package/templates/react-router/components.json +25 -0
- package/templates/react-router/content/getting-started.mdx +16 -0
- package/templates/react-router/content/index.mdx +22 -0
- package/templates/react-router/content/mdx-components.mdx +293 -0
- package/templates/react-router/heyo-docs.config.ts +24 -0
- package/templates/react-router/package.json +36 -0
- package/templates/react-router/platform.vite.ts +2 -0
- package/templates/react-router/public/apple-touch-icon.png +0 -0
- package/templates/react-router/public/favicon-96x96.png +0 -0
- package/templates/react-router/public/favicon.ico +0 -0
- package/templates/react-router/public/favicon.svg +1 -0
- package/templates/react-router/public/logo.svg +12 -0
- package/templates/react-router/public/site.webmanifest +21 -0
- package/templates/react-router/public/web-app-manifest-192x192.png +0 -0
- package/templates/react-router/public/web-app-manifest-512x512.png +0 -0
- package/templates/react-router/react-router.config.ts +20 -0
- package/templates/react-router/tsconfig.json +20 -0
- 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,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,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
|
+
}
|