@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,140 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ Links,
4
+ Meta,
5
+ Outlet,
6
+ Scripts,
7
+ ScrollRestoration,
8
+ type LinksFunction,
9
+ type MetaFunction,
10
+ } from "react-router";
11
+ import { pathnameFromMarkdownPath } from "@heyo-sh/heyo-docs";
12
+ import type { Route } from "./+types/root";
13
+
14
+ import "./app.css";
15
+ import { ThemeProvider } from "./components/theme-provider";
16
+ import config from "../heyo-docs.config";
17
+ import { getThemeScript, THEME_STORAGE_KEY } from "./lib/theme";
18
+
19
+ export const meta: MetaFunction = () => {
20
+ const canonical = config.siteUrl ? `${config.siteUrl}/` : undefined;
21
+
22
+ return [
23
+ { title: config.title },
24
+ { name: "description", content: config.description },
25
+ { name: "robots", content: "index, follow" },
26
+ { name: "referrer", content: "strict-origin-when-cross-origin" },
27
+ { property: "og:type", content: "website" },
28
+ { property: "og:site_name", content: config.title },
29
+ { property: "og:title", content: config.title },
30
+ { property: "og:description", content: config.description },
31
+ { name: "twitter:card", content: "summary" },
32
+ { name: "twitter:title", content: config.title },
33
+ { name: "twitter:description", content: config.description },
34
+ ...(canonical
35
+ ? [
36
+ { property: "og:url", content: canonical },
37
+ {
38
+ tagName: "link" as const,
39
+ rel: "canonical",
40
+ href: canonical,
41
+ },
42
+ ]
43
+ : []),
44
+ {
45
+ "script:ld+json": {
46
+ "@context": "https://schema.org",
47
+ "@type": "WebSite",
48
+ name: config.title,
49
+ description: config.description,
50
+ ...(config.siteUrl ? { url: config.siteUrl } : {}),
51
+ },
52
+ },
53
+ ];
54
+ };
55
+
56
+ export const links: LinksFunction = () => [
57
+ { rel: "icon", href: "/favicon.ico", sizes: "any" },
58
+ { rel: "icon", href: "/favicon.svg", type: "image/svg+xml" },
59
+ {
60
+ rel: "icon",
61
+ href: "/favicon-96x96.png",
62
+ sizes: "96x96",
63
+ type: "image/png",
64
+ },
65
+ {
66
+ rel: "apple-touch-icon",
67
+ href: "/apple-touch-icon.png",
68
+ sizes: "180x180",
69
+ },
70
+ { rel: "manifest", href: "/site.webmanifest" },
71
+ ...(config.groups.some((group) => group.type === "changelog")
72
+ ? [
73
+ {
74
+ rel: "alternate",
75
+ href: "/rss.xml",
76
+ type: "application/rss+xml",
77
+ title: `${config.title} updates`,
78
+ },
79
+ ]
80
+ : []),
81
+ ];
82
+
83
+ export function Layout({ children }: { children: ReactNode }) {
84
+ return (
85
+ <html lang="en" suppressHydrationWarning>
86
+ <head>
87
+ <meta charSet="utf-8" />
88
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
89
+ <meta name="color-scheme" content="light dark" />
90
+ <script
91
+ dangerouslySetInnerHTML={{
92
+ __html: getThemeScript(THEME_STORAGE_KEY, config.mode),
93
+ }}
94
+ />
95
+ <Meta />
96
+ <Links />
97
+ </head>
98
+ <body>
99
+ <ThemeProvider
100
+ defaultTheme={config.mode}
101
+ storageKey={THEME_STORAGE_KEY}
102
+ >
103
+ {children}
104
+ <ScrollRestoration />
105
+ <Scripts />
106
+ </ThemeProvider>
107
+ </body>
108
+ </html>
109
+ );
110
+ }
111
+
112
+ export default function App() {
113
+ return <Outlet />;
114
+ }
115
+
116
+ const MARKDOWN_RESOURCE_PREFIX = "/__heyo-docs/markdown";
117
+
118
+ /**
119
+ * Follows the Fumadocs React Router pattern: Markdown needs a resource route,
120
+ * while this middleware keeps the public URL as `/<page>.md`.
121
+ */
122
+ const markdownMiddleware: Route.MiddlewareFunction = async (
123
+ { request },
124
+ next,
125
+ ) => {
126
+ const url = new URL(request.url);
127
+ if (
128
+ !url.pathname.startsWith(`${MARKDOWN_RESOURCE_PREFIX}/`) &&
129
+ pathnameFromMarkdownPath(url.pathname) !== undefined
130
+ ) {
131
+ const target = new URL(
132
+ `${MARKDOWN_RESOURCE_PREFIX}${url.pathname}${url.search}`,
133
+ url,
134
+ );
135
+ return Response.redirect(target, 307);
136
+ }
137
+ return next();
138
+ };
139
+
140
+ export const middleware = [markdownMiddleware];
@@ -0,0 +1,179 @@
1
+ import {
2
+ createDocsModel,
3
+ DocsApp,
4
+ findDocsPage,
5
+ findOpenApiEndpoint,
6
+ openApiEndpointDataPath,
7
+ openApiEndpointDetail,
8
+ type OpenApiEndpoint,
9
+ } from "@heyo-sh/heyo-docs";
10
+ import {
11
+ Link,
12
+ useLoaderData,
13
+ useParams,
14
+ type LoaderFunctionArgs,
15
+ type MetaFunction,
16
+ } from "react-router";
17
+ import { forwardRef, type ComponentPropsWithoutRef } from "react";
18
+
19
+ import config from "../../heyo-docs.config";
20
+ import { iconSet } from "../heyo-docs-icons";
21
+ import { useTheme } from "../components/theme-provider";
22
+ import { pages } from "virtual:heyo-docs-content";
23
+ import { openApiDocuments } from "virtual:heyo-docs-openapi";
24
+ import { openApiEndpoints } from "virtual:heyo-docs-openapi/index";
25
+
26
+ function pathnameFor(params: Record<string, string | undefined>): string {
27
+ const path = params["*"]?.replace(/^\/+|\/+$/g, "");
28
+ return path ? `/${path}` : "/";
29
+ }
30
+
31
+ const RouterLink = forwardRef<HTMLAnchorElement, ComponentPropsWithoutRef<"a">>(
32
+ function RouterLink({ href = "/", ...props }, ref) {
33
+ return <Link {...props} ref={ref} to={href} />;
34
+ },
35
+ );
36
+
37
+ export async function loader({ params }: LoaderFunctionArgs) {
38
+ const pathname = pathnameFor(params);
39
+ const model = createDocsModel(config, pages, [], openApiEndpoints);
40
+ const page = findDocsPage(model.pages, pathname);
41
+ const endpoint = findOpenApiEndpoint(model.endpoints, pathname);
42
+ if (!page && !endpoint) throw new Response("Not Found", { status: 404 });
43
+
44
+ if (!endpoint) return {};
45
+
46
+ // Prerendering serialises this operation-specific data with its static route,
47
+ // so the browser's first paint already contains the complete API reference.
48
+ if (typeof window === "undefined") {
49
+ const serverModel = createDocsModel(config, pages, openApiDocuments);
50
+ const currentOpenApiEndpoint = findOpenApiEndpoint(
51
+ serverModel.endpoints,
52
+ pathname,
53
+ );
54
+ return {
55
+ currentOpenApiEndpoint: currentOpenApiEndpoint
56
+ ? openApiEndpointDetail(currentOpenApiEndpoint)
57
+ : undefined,
58
+ };
59
+ }
60
+
61
+ // Client transitions wait for the static shard before committing the new
62
+ // route. This prevents rendering the compact index and replacing it later.
63
+ const response = await fetch(openApiEndpointDataPath(endpoint.slug));
64
+ if (!response.ok)
65
+ throw new Response("OpenAPI endpoint data could not be loaded", {
66
+ status: response.status,
67
+ });
68
+ return {
69
+ currentOpenApiEndpoint: (await response.json()) as OpenApiEndpoint,
70
+ };
71
+ }
72
+
73
+ export const meta: MetaFunction = ({ params }) => {
74
+ const model = createDocsModel(config, pages, [], openApiEndpoints);
75
+ const pathname = pathnameFor(params);
76
+ const page = findDocsPage(model.pages, pathname);
77
+ const endpoint = findOpenApiEndpoint(model.endpoints, pathname);
78
+ if (!page && !endpoint)
79
+ return [
80
+ { title: `Not found | ${config.title}` },
81
+ { name: "robots", content: "noindex" },
82
+ ];
83
+ const title = page?.seo.title ?? `${endpoint!.title} | ${config.title}`;
84
+ const description =
85
+ page?.seo.description ??
86
+ endpoint?.description ??
87
+ `${endpoint!.method.toUpperCase()} ${endpoint!.path} API endpoint.`;
88
+ const canonical =
89
+ page?.seo.canonical ??
90
+ (config.siteUrl ? `${config.siteUrl}${pathname}` : undefined);
91
+ return [
92
+ { title },
93
+ { name: "description", content: description },
94
+ { name: "robots", content: "index, follow" },
95
+ { property: "og:type", content: "article" },
96
+ { property: "og:site_name", content: config.title },
97
+ { property: "og:title", content: title },
98
+ { property: "og:description", content: description },
99
+ { name: "twitter:card", content: "summary" },
100
+ { name: "twitter:title", content: title },
101
+ { name: "twitter:description", content: description },
102
+ ...(canonical
103
+ ? [
104
+ { property: "og:url", content: canonical },
105
+ {
106
+ tagName: "link" as const,
107
+ rel: "canonical",
108
+ href: canonical,
109
+ },
110
+ ]
111
+ : []),
112
+ {
113
+ "script:ld+json": [
114
+ {
115
+ "@context": "https://schema.org",
116
+ "@type": "WebSite",
117
+ name: config.title,
118
+ description: config.description,
119
+ ...(config.siteUrl ? { url: config.siteUrl } : {}),
120
+ },
121
+ {
122
+ "@context": "https://schema.org",
123
+ "@type": "TechArticle",
124
+ headline: page?.title ?? endpoint!.title,
125
+ description,
126
+ ...(canonical ? { url: canonical } : {}),
127
+ isPartOf: {
128
+ "@type": "WebSite",
129
+ name: config.title,
130
+ ...(config.siteUrl ? { url: config.siteUrl } : {}),
131
+ },
132
+ },
133
+ ],
134
+ },
135
+ ];
136
+ };
137
+
138
+ export default function DocsRoute() {
139
+ const { currentOpenApiEndpoint } = useLoaderData<typeof loader>();
140
+ return (
141
+ <DocsShell
142
+ currentOpenApiEndpoint={currentOpenApiEndpoint}
143
+ pathname={pathnameFor(useParams())}
144
+ />
145
+ );
146
+ }
147
+
148
+ function DocsShell({
149
+ currentOpenApiEndpoint,
150
+ pathname,
151
+ }: {
152
+ currentOpenApiEndpoint?: OpenApiEndpoint;
153
+ pathname: string;
154
+ }) {
155
+ const { mounted, resolvedTheme, setTheme } = useTheme();
156
+
157
+ return (
158
+ <DocsApp
159
+ config={config}
160
+ currentOpenApiEndpoint={currentOpenApiEndpoint}
161
+ iconSet={iconSet}
162
+ link={RouterLink}
163
+ openApiEndpoints={openApiEndpoints}
164
+ openApiRequestUrl="/heyo-docs-internal/openapi-request"
165
+ pages={pages}
166
+ pathname={pathname}
167
+ isDark={mounted ? resolvedTheme === "dark" : undefined}
168
+ onThemeToggle={
169
+ mounted
170
+ ? () => setTheme(resolvedTheme === "dark" ? "light" : "dark")
171
+ : undefined
172
+ }
173
+ />
174
+ );
175
+ }
176
+
177
+ export function ErrorBoundary() {
178
+ return <DocsShell pathname="/__not-found" />;
179
+ }
@@ -0,0 +1 @@
1
+ export { default, loader, meta, ErrorBoundary } from "./docs";
@@ -0,0 +1,13 @@
1
+ import { llmsFull } from "@heyo-sh/heyo-docs";
2
+ import type { LoaderFunctionArgs } from "react-router";
3
+
4
+ import config from "../../heyo-docs.config";
5
+ import { pages } from "virtual:heyo-docs-content/server";
6
+
7
+ /** The complete documentation corpus, ready for an LLM to consume. */
8
+ export function loader({ request }: LoaderFunctionArgs) {
9
+ const siteUrl = config.siteUrl ?? new URL(request.url).origin;
10
+ return new Response(llmsFull(pages, siteUrl), {
11
+ headers: { "content-type": "text/plain; charset=utf-8" },
12
+ });
13
+ }
@@ -0,0 +1,13 @@
1
+ import { llmsIndex } from "@heyo-sh/heyo-docs";
2
+ import type { LoaderFunctionArgs } from "react-router";
3
+
4
+ import config from "../../heyo-docs.config";
5
+ import { pages } from "virtual:heyo-docs-content/server";
6
+
7
+ /** A concise, discoverable index of every documentation page. */
8
+ export function loader({ request }: LoaderFunctionArgs) {
9
+ const siteUrl = config.siteUrl ?? new URL(request.url).origin;
10
+ return new Response(llmsIndex(pages, config, siteUrl), {
11
+ headers: { "content-type": "text/plain; charset=utf-8" },
12
+ });
13
+ }
@@ -0,0 +1,25 @@
1
+ import { markdownForPage, pathnameFromMarkdownPath } from "@heyo-sh/heyo-docs";
2
+ import type { LoaderFunctionArgs } from "react-router";
3
+
4
+ import { pages } from "virtual:heyo-docs-content/server";
5
+
6
+ /**
7
+ * Resource route used by the root middleware to serve `*.md` paths. Keeping
8
+ * it route-only makes React Router send the Response body directly.
9
+ */
10
+ export function loader({ params }: LoaderFunctionArgs) {
11
+ const pagePathname = pathnameFromMarkdownPath(`/${params["*"] ?? ""}`);
12
+ const page = pagePathname
13
+ ? pages.find((candidate) => candidate.slug === pagePathname)
14
+ : undefined;
15
+
16
+ if (!page)
17
+ return new Response("Not Found", {
18
+ status: 404,
19
+ headers: { "content-type": "text/plain; charset=utf-8" },
20
+ });
21
+
22
+ return new Response(markdownForPage(page), {
23
+ headers: { "content-type": "text/markdown; charset=utf-8" },
24
+ });
25
+ }
@@ -0,0 +1,159 @@
1
+ import {
2
+ createDocsModel,
3
+ findOpenApiEndpoint,
4
+ type OpenApiEndpoint,
5
+ } from "@heyo-sh/heyo-docs";
6
+ import type { ActionFunctionArgs } from "react-router";
7
+
8
+ import config from "../../heyo-docs.config";
9
+ import { pages } from "virtual:heyo-docs-content";
10
+ import { openApiDocuments } from "virtual:heyo-docs-openapi";
11
+
12
+ interface OpenApiRequestPayload {
13
+ bearerToken: string;
14
+ body?: string;
15
+ endpointSlug: string;
16
+ parameters: Record<string, string>;
17
+ server: string;
18
+ }
19
+
20
+ /**
21
+ * Sends documented API calls from the server, so browsers do not need the API
22
+ * to expose CORS headers. Targets are limited to servers from the OpenAPI file.
23
+ */
24
+ export async function action({ request }: ActionFunctionArgs) {
25
+ if (request.method !== "POST")
26
+ return new Response("Method Not Allowed", {
27
+ headers: { Allow: "POST" },
28
+ status: 405,
29
+ });
30
+
31
+ const payload = await requestPayload(request);
32
+ if (!payload)
33
+ return new Response("Invalid OpenAPI request.", { status: 400 });
34
+
35
+ const model = createDocsModel(config, pages, openApiDocuments);
36
+ const endpoint = findOpenApiEndpoint(model.endpoints, payload.endpointSlug);
37
+ if (!endpoint) return new Response("Endpoint not found.", { status: 404 });
38
+ if (endpoint.servers.length && !endpoint.servers.includes(payload.server))
39
+ return new Response("API server is not declared in the OpenAPI schema.", {
40
+ status: 400,
41
+ });
42
+ if (!endpoint.servers.length && payload.server)
43
+ return new Response("This endpoint does not declare an API server.", {
44
+ status: 400,
45
+ });
46
+
47
+ const headers = requestHeaders(
48
+ endpoint,
49
+ payload.parameters,
50
+ payload.bearerToken,
51
+ );
52
+ const url = requestUrl(
53
+ endpoint,
54
+ payload.server,
55
+ payload.parameters,
56
+ request.url,
57
+ );
58
+
59
+ try {
60
+ const response = await fetch(url, {
61
+ method: endpoint.method.toUpperCase(),
62
+ headers,
63
+ body: payload.body,
64
+ });
65
+ const responseHeaders = new Headers();
66
+ const contentType = response.headers.get("content-type");
67
+ if (contentType) responseHeaders.set("content-type", contentType);
68
+ const body = [204, 205, 304].includes(response.status)
69
+ ? null
70
+ : await response.arrayBuffer();
71
+ return new Response(body, {
72
+ headers: responseHeaders,
73
+ status: response.status,
74
+ statusText: response.statusText,
75
+ });
76
+ } catch {
77
+ return new Response("The API server could not be reached.", {
78
+ status: 502,
79
+ });
80
+ }
81
+ }
82
+
83
+ async function requestPayload(
84
+ request: Request,
85
+ ): Promise<OpenApiRequestPayload | undefined> {
86
+ try {
87
+ const value: unknown = await request.json();
88
+ if (!isRecord(value)) return undefined;
89
+ const endpointSlug = stringValue(value.endpointSlug);
90
+ const server = stringValue(value.server);
91
+ if (!endpointSlug || server === undefined) return undefined;
92
+ return {
93
+ bearerToken: stringValue(value.bearerToken) ?? "",
94
+ body: stringValue(value.body),
95
+ endpointSlug,
96
+ parameters: stringRecord(value.parameters),
97
+ server,
98
+ };
99
+ } catch {
100
+ return undefined;
101
+ }
102
+ }
103
+
104
+ function requestUrl(
105
+ endpoint: OpenApiEndpoint,
106
+ server: string,
107
+ parameters: Record<string, string>,
108
+ requestUrl: string,
109
+ ) {
110
+ let path = endpoint.path.replace(/\{([^}]+)\}/g, (match, name: string) => {
111
+ const value = parameters[`path:${name}`];
112
+ return value ? encodeURIComponent(value) : match;
113
+ });
114
+ const query = new URLSearchParams();
115
+ for (const parameter of endpoint.parameters) {
116
+ if (parameter.in !== "query") continue;
117
+ const value = parameters[`query:${parameter.name}`];
118
+ if (value) query.set(parameter.name, value);
119
+ }
120
+ const search = query.toString();
121
+ path = `${path}${search ? `?${search}` : ""}`;
122
+ return server
123
+ ? `${server.replace(/\/$/, "")}${path}`
124
+ : new URL(path, requestUrl).toString();
125
+ }
126
+
127
+ function requestHeaders(
128
+ endpoint: OpenApiEndpoint,
129
+ parameters: Record<string, string>,
130
+ bearerToken: string,
131
+ ) {
132
+ const headers = new Headers();
133
+ for (const parameter of endpoint.parameters) {
134
+ if (parameter.in !== "header") continue;
135
+ const value = parameters[`header:${parameter.name}`];
136
+ if (value) headers.set(parameter.name, value);
137
+ }
138
+ if (endpoint.requestBody?.contentType)
139
+ headers.set("Content-Type", endpoint.requestBody.contentType);
140
+ if (bearerToken) headers.set("Authorization", `Bearer ${bearerToken}`);
141
+ return headers;
142
+ }
143
+
144
+ function isRecord(value: unknown): value is Record<string, unknown> {
145
+ return typeof value === "object" && value !== null && !Array.isArray(value);
146
+ }
147
+
148
+ function stringRecord(value: unknown): Record<string, string> {
149
+ if (!isRecord(value)) return {};
150
+ return Object.fromEntries(
151
+ Object.entries(value).filter(
152
+ (entry): entry is [string, string] => typeof entry[1] === "string",
153
+ ),
154
+ );
155
+ }
156
+
157
+ function stringValue(value: unknown): string | undefined {
158
+ return typeof value === "string" ? value : undefined;
159
+ }
@@ -0,0 +1 @@
1
+ export { default, loader, meta, ErrorBoundary } from "./docs";
@@ -0,0 +1,19 @@
1
+ import type { Route } from "./+types/robots";
2
+
3
+ import config from "../../heyo-docs.config";
4
+
5
+ export function loader({ request }: Route.LoaderArgs) {
6
+ const siteUrl = config.siteUrl ?? new URL(request.url).origin;
7
+
8
+ return new Response(
9
+ [
10
+ "User-agent: *",
11
+ "Allow: /",
12
+ "Disallow: /__heyo-docs/",
13
+ "",
14
+ `Sitemap: ${siteUrl}/sitemap.xml`,
15
+ "",
16
+ ].join("\n"),
17
+ { headers: { "content-type": "text/plain; charset=utf-8" } },
18
+ );
19
+ }
@@ -0,0 +1,13 @@
1
+ import { rssXml } from "@heyo-sh/heyo-docs";
2
+ import type { LoaderFunctionArgs } from "react-router";
3
+
4
+ import config from "../../heyo-docs.config";
5
+ import { pages } from "virtual:heyo-docs-content/server";
6
+
7
+ /** RSS 2.0 feed for every `<Update>` in the configured changelog groups. */
8
+ export function loader({ request }: LoaderFunctionArgs) {
9
+ const siteUrl = config.siteUrl ?? new URL(request.url).origin;
10
+ return new Response(rssXml(pages, config, siteUrl), {
11
+ headers: { "content-type": "application/rss+xml; charset=utf-8" },
12
+ });
13
+ }
@@ -0,0 +1,18 @@
1
+ import { createDocsModel, sitemapXml } from "@heyo-sh/heyo-docs";
2
+ import type { Route } from "./+types/sitemap";
3
+
4
+ import config from "../../heyo-docs.config";
5
+ import { pages } from "virtual:heyo-docs-content";
6
+ import { openApiDocuments } from "virtual:heyo-docs-openapi";
7
+
8
+ export function loader({ request }: Route.LoaderArgs) {
9
+ const siteUrl = config.siteUrl ?? new URL(request.url).origin;
10
+ const model = createDocsModel(config, pages, openApiDocuments);
11
+ return new Response(
12
+ sitemapXml(
13
+ siteUrl,
14
+ [...model.pages, ...model.endpoints].map((page) => page.slug),
15
+ ),
16
+ { headers: { "content-type": "application/xml; charset=utf-8" } },
17
+ );
18
+ }
@@ -0,0 +1,13 @@
1
+ import { index, route, type RouteConfig } from "@react-router/dev/routes";
2
+
3
+ export default [
4
+ index("routes/home.tsx"),
5
+ route("robots.txt", "routes/robots.ts"),
6
+ route("sitemap.xml", "routes/sitemap.ts"),
7
+ route("rss.xml", "routes/rss.ts"),
8
+ route("llms.txt", "routes/llms.ts"),
9
+ route("llms-full.txt", "routes/llms-full.ts"),
10
+ route("__heyo-docs/markdown/*", "routes/markdown.ts"),
11
+ route("heyo-docs-internal/openapi-request", "routes/openapi-request.ts"),
12
+ route("*", "routes/page.tsx"),
13
+ ] satisfies RouteConfig;
@@ -0,0 +1,19 @@
1
+ declare module "virtual:heyo-docs-content" {
2
+ import type { DocsPage } from "@heyo-sh/heyo-docs";
3
+ export const pages: DocsPage[];
4
+ }
5
+
6
+ declare module "virtual:heyo-docs-content/server" {
7
+ import type { MarkdownPage } from "@heyo-sh/heyo-docs";
8
+ export const pages: MarkdownPage[];
9
+ }
10
+
11
+ declare module "virtual:heyo-docs-openapi" {
12
+ import type { OpenApiDocumentSource } from "@heyo-sh/heyo-docs";
13
+ export const openApiDocuments: OpenApiDocumentSource[];
14
+ }
15
+
16
+ declare module "virtual:heyo-docs-openapi/index" {
17
+ import type { OpenApiEndpoint } from "@heyo-sh/heyo-docs";
18
+ export const openApiEndpoints: OpenApiEndpoint[];
19
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "base-mira",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/app.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "remixicon",
14
+ "rtl": false,
15
+ "aliases": {
16
+ "components": "~/components",
17
+ "utils": "~/lib/utils",
18
+ "ui": "~/components/ui",
19
+ "lib": "~/lib",
20
+ "hooks": "~/hooks"
21
+ },
22
+ "menuColor": "default",
23
+ "menuAccent": "subtle",
24
+ "registries": {}
25
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: Getting started
3
+ description: Run your new documentation site locally.
4
+ ---
5
+
6
+ # Getting started
7
+
8
+ Install dependencies with your chosen package manager and start the development server.
9
+
10
+ ## Create a page
11
+
12
+ Create `content/guides/deploying.mdx` to get the `/guides/deploying` route. Its frontmatter controls its title and description.
13
+
14
+ ## Configure the documentation
15
+
16
+ Use `heyo-docs.config.ts` to change the title, colors, tabs, sidebar groups, footer links and selected theme without regenerating your project. Add pages or directories to a section's `pages` list to control the sidebar order.