@heyo-sh/create-heyo-docs 1.0.0 → 2.0.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 (122) hide show
  1. package/deployments/cloudflare/astro/package.json +2 -1
  2. package/deployments/cloudflare/astro/src/env.d.ts +8 -0
  3. package/deployments/cloudflare/astro/src/pages/heyo-docs-internal/ai-chat.ts +32 -0
  4. package/deployments/cloudflare/next/app/heyo-docs-internal/ai-chat/route.ts +27 -0
  5. package/deployments/cloudflare/next/cloudflare-env.d.ts +7 -0
  6. package/deployments/cloudflare/react-router/app/cloudflare-env.d.ts +7 -0
  7. package/deployments/cloudflare/react-router/app/lib/cloudflare-context.ts +4 -0
  8. package/deployments/cloudflare/react-router/app/routes/ai-chat.ts +31 -0
  9. package/deployments/cloudflare/react-router/package.json +2 -1
  10. package/deployments/cloudflare/react-router/workers/app.ts +6 -2
  11. package/deployments/vercel/astro/package.json +1 -0
  12. package/dist/index.js +37 -16
  13. package/dist/project-name.d.ts +1 -1
  14. package/dist/project-name.d.ts.map +1 -1
  15. package/dist/scaffold.d.ts.map +1 -1
  16. package/dist/utils.d.ts.map +1 -1
  17. package/package.json +2 -2
  18. package/templates/astro/content/index.mdx +8 -0
  19. package/templates/astro/heyo-docs.config.ts +4 -48
  20. package/templates/astro/src/components/docs-app.tsx +30 -130
  21. package/templates/astro/src/heyo-docs-icons.tsx +3 -1
  22. package/templates/astro/src/layouts/docs-layout.astro +20 -504
  23. package/templates/astro/src/lib/docs.ts +3 -2
  24. package/templates/astro/src/lib/openapi-request.ts +6 -146
  25. package/templates/astro/src/pages/[...slug].astro +7 -15
  26. package/templates/astro/src/pages/[...slug].md.ts +4 -1
  27. package/templates/astro/src/pages/heyo-docs-internal/ai-chat.ts +10 -3
  28. package/templates/astro/src/pages/heyo-docs-internal/openapi-request.ts +6 -0
  29. package/templates/astro/src/pages/index.astro +18 -9
  30. package/templates/astro/src/pages/llms-full.txt.ts +1 -1
  31. package/templates/astro/src/pages/llms.txt.ts +1 -1
  32. package/templates/astro/src/pages/rss.xml.ts +1 -1
  33. package/templates/astro/src/pages/sitemap.xml.ts +2 -2
  34. package/templates/astro/src/virtual.d.ts +5 -5
  35. package/templates/next/app/[[...slug]]/page.tsx +10 -9
  36. package/templates/next/app/components/docs-app.tsx +28 -120
  37. package/templates/next/app/heyo-docs-icons.tsx +1 -1
  38. package/templates/next/app/heyo-docs-internal/ai-chat/route.ts +9 -2
  39. package/templates/next/app/heyo-docs-internal/markdown/[[...slug]]/route.ts +1 -1
  40. package/templates/next/app/layout.tsx +25 -444
  41. package/templates/next/app/lib/docs.ts +3 -3
  42. package/templates/next/app/lib/openapi-request.ts +4 -136
  43. package/templates/next/app/llms-full.txt/route.ts +1 -1
  44. package/templates/next/app/llms.txt/route.ts +1 -1
  45. package/templates/next/app/rss.xml/route.ts +1 -1
  46. package/templates/next/app/sitemap.xml/route.ts +1 -1
  47. package/templates/next/content/index.mdx +8 -0
  48. package/templates/next/heyo-docs.config.ts +3 -47
  49. package/templates/next/next.config.ts +7 -1
  50. package/templates/react-router/app/heyo-docs-icons.tsx +3 -1
  51. package/templates/react-router/app/root.tsx +28 -465
  52. package/templates/react-router/app/routes/ai-chat.ts +6 -2
  53. package/templates/react-router/app/routes/docs.tsx +22 -63
  54. package/templates/react-router/app/routes/home.tsx +6 -1
  55. package/templates/react-router/app/routes/llms-full.ts +1 -1
  56. package/templates/react-router/app/routes/llms.ts +1 -1
  57. package/templates/react-router/app/routes/markdown.ts +4 -1
  58. package/templates/react-router/app/routes/openapi-request.ts +11 -144
  59. package/templates/react-router/app/routes/rss.ts +1 -1
  60. package/templates/react-router/app/routes/sitemap.ts +5 -8
  61. package/templates/react-router/app/virtual.d.ts +5 -5
  62. package/templates/react-router/content/index.mdx +8 -0
  63. package/templates/react-router/heyo-docs.config.ts +4 -48
  64. package/templates/astro/content/api-overview.mdx +0 -20
  65. package/templates/astro/content/assets/local-architecture.svg +0 -22
  66. package/templates/astro/content/assets/quick-reference.txt +0 -5
  67. package/templates/astro/content/changelog.mdx +0 -38
  68. package/templates/astro/content/configuration.mdx +0 -117
  69. package/templates/astro/content/deploying.mdx +0 -57
  70. package/templates/astro/content/essentials/callouts.mdx +0 -34
  71. package/templates/astro/content/essentials/code-blocks.mdx +0 -43
  72. package/templates/astro/content/essentials/lists.mdx +0 -36
  73. package/templates/astro/content/essentials/tables.mdx +0 -30
  74. package/templates/astro/content/essentials/tabs.mdx +0 -40
  75. package/templates/astro/content/essentials/text-styling.mdx +0 -43
  76. package/templates/astro/content/getting-started.mdx +0 -16
  77. package/templates/astro/content/mdx-components.mdx +0 -44
  78. package/templates/astro/content/openapi-petstore.json +0 -160
  79. package/templates/astro/content/openapi.json +0 -278
  80. package/templates/astro/content/quickstart.mdx +0 -60
  81. package/templates/astro/src/components/theme-provider.tsx +0 -106
  82. package/templates/astro/src/lib/seo.ts +0 -221
  83. package/templates/astro/src/lib/theme.ts +0 -74
  84. package/templates/next/app/components/theme-provider.tsx +0 -93
  85. package/templates/next/app/lib/seo.ts +0 -189
  86. package/templates/next/app/lib/theme.ts +0 -61
  87. package/templates/next/content/api-overview.mdx +0 -20
  88. package/templates/next/content/assets/local-architecture.svg +0 -22
  89. package/templates/next/content/assets/quick-reference.txt +0 -5
  90. package/templates/next/content/changelog.mdx +0 -38
  91. package/templates/next/content/configuration.mdx +0 -117
  92. package/templates/next/content/deploying.mdx +0 -57
  93. package/templates/next/content/essentials/callouts.mdx +0 -34
  94. package/templates/next/content/essentials/code-blocks.mdx +0 -43
  95. package/templates/next/content/essentials/lists.mdx +0 -36
  96. package/templates/next/content/essentials/tables.mdx +0 -30
  97. package/templates/next/content/essentials/tabs.mdx +0 -40
  98. package/templates/next/content/essentials/text-styling.mdx +0 -43
  99. package/templates/next/content/getting-started.mdx +0 -16
  100. package/templates/next/content/mdx-components.mdx +0 -41
  101. package/templates/next/content/openapi-petstore.json +0 -160
  102. package/templates/next/content/openapi.json +0 -278
  103. package/templates/next/content/quickstart.mdx +0 -60
  104. package/templates/react-router/app/components/theme-provider.tsx +0 -106
  105. package/templates/react-router/app/lib/theme.ts +0 -74
  106. package/templates/react-router/content/api-overview.mdx +0 -20
  107. package/templates/react-router/content/assets/local-architecture.svg +0 -22
  108. package/templates/react-router/content/assets/quick-reference.txt +0 -5
  109. package/templates/react-router/content/changelog.mdx +0 -38
  110. package/templates/react-router/content/configuration.mdx +0 -117
  111. package/templates/react-router/content/deploying.mdx +0 -57
  112. package/templates/react-router/content/essentials/callouts.mdx +0 -34
  113. package/templates/react-router/content/essentials/code-blocks.mdx +0 -43
  114. package/templates/react-router/content/essentials/lists.mdx +0 -36
  115. package/templates/react-router/content/essentials/tables.mdx +0 -30
  116. package/templates/react-router/content/essentials/tabs.mdx +0 -40
  117. package/templates/react-router/content/essentials/text-styling.mdx +0 -43
  118. package/templates/react-router/content/getting-started.mdx +0 -16
  119. package/templates/react-router/content/mdx-components.mdx +0 -293
  120. package/templates/react-router/content/openapi-petstore.json +0 -160
  121. package/templates/react-router/content/openapi.json +0 -278
  122. package/templates/react-router/content/quickstart.mdx +0 -60
@@ -1,160 +1,20 @@
1
- import {
2
- createDocsModel,
3
- findOpenApiEndpoint,
4
- type OpenApiEndpoint,
5
- } from "@heyo-sh/heyo-docs";
1
+ import { createDocsModel } from "@heyo-sh/heyo-docs/model";
2
+ import { handleOpenApiRequest as forwardOpenApiRequest } from "@heyo-sh/heyo-docs/openapi/request";
6
3
 
7
4
  import config from "../../heyo-docs.config";
8
5
  import { openApiDocuments } from "virtual:heyo-docs-openapi";
9
6
  import { pages } from "virtual:heyo-docs-content";
10
7
 
11
- interface OpenApiRequestPayload {
12
- bearerToken: string;
13
- body?: string;
14
- endpointSlug: string;
15
- parameters: Record<string, string>;
16
- server: string;
17
- }
18
-
19
- /**
20
- * Sends documented API calls from the server, so browsers do not need the API
21
- * to expose CORS headers. Targets are limited to servers from the OpenAPI file.
22
- */
23
8
  export async function handleOpenApiRequest(
24
9
  request: Request,
25
10
  ): Promise<Response> {
26
11
  if (request.method !== "POST")
27
12
  return new Response("Method Not Allowed", {
28
- headers: { Allow: "POST" },
29
13
  status: 405,
14
+ headers: { Allow: "POST" },
30
15
  });
31
-
32
- const payload = await requestPayload(request);
33
- if (!payload)
34
- return new Response("Invalid OpenAPI request.", { status: 400 });
35
-
36
- const model = createDocsModel(config, pages, openApiDocuments);
37
- const endpoint = findOpenApiEndpoint(model.endpoints, payload.endpointSlug);
38
- if (!endpoint) return new Response("Endpoint not found.", { status: 404 });
39
- if (endpoint.servers.length && !endpoint.servers.includes(payload.server))
40
- return new Response("API server is not declared in the OpenAPI schema.", {
41
- status: 400,
42
- });
43
- if (!endpoint.servers.length && payload.server)
44
- return new Response("This endpoint does not declare an API server.", {
45
- status: 400,
46
- });
47
-
48
- const headers = requestHeaders(
49
- endpoint,
50
- payload.parameters,
51
- payload.bearerToken,
52
- );
53
- const url = requestUrl(
54
- endpoint,
55
- payload.server,
56
- payload.parameters,
57
- request.url,
58
- );
59
-
60
- try {
61
- const response = await fetch(url, {
62
- method: endpoint.method.toUpperCase(),
63
- headers,
64
- body: payload.body,
65
- });
66
- const responseHeaders = new Headers();
67
- const contentType = response.headers.get("content-type");
68
- if (contentType) responseHeaders.set("content-type", contentType);
69
- const body = [204, 205, 304].includes(response.status)
70
- ? null
71
- : await response.arrayBuffer();
72
- return new Response(body, {
73
- headers: responseHeaders,
74
- status: response.status,
75
- statusText: response.statusText,
76
- });
77
- } catch {
78
- return new Response("The API server could not be reached.", {
79
- status: 502,
80
- });
81
- }
82
- }
83
-
84
- async function requestPayload(
85
- request: Request,
86
- ): Promise<OpenApiRequestPayload | undefined> {
87
- try {
88
- const value: unknown = await request.json();
89
- if (!isRecord(value)) return undefined;
90
- const endpointSlug = stringValue(value.endpointSlug);
91
- const server = stringValue(value.server);
92
- if (!endpointSlug || server === undefined) return undefined;
93
- return {
94
- bearerToken: stringValue(value.bearerToken) ?? "",
95
- body: stringValue(value.body),
96
- endpointSlug,
97
- parameters: stringRecord(value.parameters),
98
- server,
99
- };
100
- } catch {
101
- return undefined;
102
- }
103
- }
104
-
105
- function requestUrl(
106
- endpoint: OpenApiEndpoint,
107
- server: string,
108
- parameters: Record<string, string>,
109
- requestUrl: string,
110
- ) {
111
- let path = endpoint.path.replace(/\{([^}]+)\}/g, (match, name: string) => {
112
- const value = parameters[`path:${name}`];
113
- return value ? encodeURIComponent(value) : match;
114
- });
115
- const query = new URLSearchParams();
116
- for (const parameter of endpoint.parameters) {
117
- if (parameter.in !== "query") continue;
118
- const value = parameters[`query:${parameter.name}`];
119
- if (value) query.set(parameter.name, value);
120
- }
121
- const search = query.toString();
122
- path = `${path}${search ? `?${search}` : ""}`;
123
- return server
124
- ? `${server.replace(/\/$/, "")}${path}`
125
- : new URL(path, requestUrl).toString();
126
- }
127
-
128
- function requestHeaders(
129
- endpoint: OpenApiEndpoint,
130
- parameters: Record<string, string>,
131
- bearerToken: string,
132
- ) {
133
- const headers = new Headers();
134
- for (const parameter of endpoint.parameters) {
135
- if (parameter.in !== "header") continue;
136
- const value = parameters[`header:${parameter.name}`];
137
- if (value) headers.set(parameter.name, value);
138
- }
139
- if (endpoint.requestBody?.contentType)
140
- headers.set("Content-Type", endpoint.requestBody.contentType);
141
- if (bearerToken) headers.set("Authorization", `Bearer ${bearerToken}`);
142
- return headers;
143
- }
144
-
145
- function isRecord(value: unknown): value is Record<string, unknown> {
146
- return typeof value === "object" && value !== null && !Array.isArray(value);
147
- }
148
-
149
- function stringRecord(value: unknown): Record<string, string> {
150
- if (!isRecord(value)) return {};
151
- return Object.fromEntries(
152
- Object.entries(value).filter(
153
- (entry): entry is [string, string] => typeof entry[1] === "string",
154
- ),
16
+ return forwardOpenApiRequest(
17
+ request,
18
+ createDocsModel(config, pages, openApiDocuments).endpoints,
155
19
  );
156
20
  }
157
-
158
- function stringValue(value: unknown): string | undefined {
159
- return typeof value === "string" ? value : undefined;
160
- }
@@ -2,15 +2,15 @@
2
2
  import { AstroDocsApp } from "../components/docs-app";
3
3
  import DocsLayout from "../layouts/docs-layout.astro";
4
4
  import { docsContext, pathnameForSlug } from "../lib/docs";
5
- import { docsSeo } from "../lib/seo";
6
- import { openApiEndpointDetail } from "@heyo-sh/heyo-docs";
5
+ import { docsSeo, siteSeo } from "@heyo-sh/heyo-docs/seo";
6
+ import { openApiEndpointDetail } from "@heyo-sh/heyo-docs/openapi";
7
7
  import { pages } from "virtual:heyo-docs-content";
8
8
  import { openApiEndpoints } from "virtual:heyo-docs-openapi/index";
9
9
 
10
10
  export async function getStaticPaths() {
11
- return [...pages, ...openApiEndpoints].map(({ slug }) => ({
12
- params: { slug: slug === "/" ? undefined : slug.slice(1) },
13
- }));
11
+ return [...pages, ...openApiEndpoints]
12
+ .filter(({ slug }) => slug !== "/")
13
+ .map(({ slug }) => ({ params: { slug: slug.slice(1) } }));
14
14
  }
15
15
 
16
16
  const pathname = pathnameForSlug(Astro.params.slug);
@@ -25,16 +25,8 @@ const seo = exists
25
25
  ? docsSeo({ ...context, pathname })
26
26
  : {
27
27
  title: `Not found | ${context.config.title}`,
28
- description: context.config.description,
29
- structuredData: [
30
- {
31
- "@context": "https://schema.org",
32
- "@type": "WebSite",
33
- name: context.config.title,
34
- description: context.config.description,
35
- ...(context.config.siteUrl ? { url: context.config.siteUrl } : {}),
36
- },
37
- ],
28
+ description: siteSeo(context.config).description,
29
+ structuredData: siteSeo(context.config).structuredData,
38
30
  };
39
31
  ---
40
32
 
@@ -1,5 +1,8 @@
1
1
  import type { APIRoute } from "astro";
2
- import { markdownForPage, pathnameFromMarkdownPath } from "@heyo-sh/heyo-docs";
2
+ import {
3
+ markdownForPage,
4
+ pathnameFromMarkdownPath,
5
+ } from "@heyo-sh/heyo-docs/llm";
3
6
 
4
7
  import { pages } from "virtual:heyo-docs-content/server";
5
8
 
@@ -1,4 +1,3 @@
1
- import { createAiChatResponse } from "@heyo-sh/heyo-docs/ai";
2
1
  import type { APIRoute } from "astro";
3
2
 
4
3
  import config from "../../../heyo-docs.config";
@@ -6,8 +5,15 @@ import { pages as markdownPages } from "virtual:heyo-docs-content/server";
6
5
 
7
6
  export const prerender = false;
8
7
 
9
- export const POST: APIRoute = ({ request }) =>
10
- createAiChatResponse(request, {
8
+ export const POST: APIRoute = async ({ request }) => {
9
+ if (!config.ai?.chat)
10
+ return Response.json(
11
+ { error: "AI chat is not configured." },
12
+ { status: 404 },
13
+ );
14
+
15
+ const { createAiChatResponse } = await import("@heyo-sh/heyo-docs/ai");
16
+ return createAiChatResponse(request, {
11
17
  ai: config.ai,
12
18
  markdownPages,
13
19
  pages: markdownPages.map((page) => ({
@@ -18,3 +24,4 @@ export const POST: APIRoute = ({ request }) =>
18
24
  })),
19
25
  title: config.title,
20
26
  });
27
+ };
@@ -5,4 +5,10 @@ import { handleOpenApiRequest } from "../../lib/openapi-request";
5
5
  /** The only dynamic OpenAPI surface: a same-origin proxy for Try it requests. */
6
6
  export const prerender = false;
7
7
 
8
+ export const GET: APIRoute = () =>
9
+ new Response("Method Not Allowed", {
10
+ headers: { Allow: "POST" },
11
+ status: 405,
12
+ });
13
+
8
14
  export const POST: APIRoute = ({ request }) => handleOpenApiRequest(request);
@@ -1,13 +1,22 @@
1
1
  ---
2
- import { navigationPages } from "@heyo-sh/heyo-docs";
3
-
2
+ import { AstroDocsApp } from "../components/docs-app";
3
+ import DocsLayout from "../layouts/docs-layout.astro";
4
4
  import { docsContext } from "../lib/docs";
5
+ import { docsSeo, siteSeo } from "@heyo-sh/heyo-docs/seo";
5
6
 
6
- const firstPage = docsContext("/")
7
- .model.navigation.flatMap((group) => navigationPages(group.sections))
8
- .at(0);
9
-
10
- if (!firstPage) throw new Error("No local documentation page is configured.");
11
-
12
- return Astro.redirect(firstPage.slug);
7
+ const pathname = "/";
8
+ const context = docsContext(pathname);
9
+ const exists = Boolean(context.page || context.endpoint);
10
+ if (!exists) Astro.response.status = 404;
11
+ const seo = exists
12
+ ? docsSeo({ ...context, pathname })
13
+ : {
14
+ title: `Not found | ${context.config.title}`,
15
+ description: siteSeo(context.config).description,
16
+ structuredData: siteSeo(context.config).structuredData,
17
+ };
13
18
  ---
19
+
20
+ <DocsLayout {...seo} robots={exists ? "index, follow" : "noindex"}>
21
+ <AstroDocsApp client:load pathname={pathname} />
22
+ </DocsLayout>
@@ -1,5 +1,5 @@
1
1
  import type { APIRoute } from "astro";
2
- import { llmsFull } from "@heyo-sh/heyo-docs";
2
+ import { llmsFull } from "@heyo-sh/heyo-docs/llm";
3
3
 
4
4
  import config from "../../heyo-docs.config";
5
5
  import { pages } from "virtual:heyo-docs-content/server";
@@ -1,5 +1,5 @@
1
1
  import type { APIRoute } from "astro";
2
- import { llmsIndex } from "@heyo-sh/heyo-docs";
2
+ import { llmsIndex } from "@heyo-sh/heyo-docs/llm";
3
3
 
4
4
  import config from "../../heyo-docs.config";
5
5
  import { pages } from "virtual:heyo-docs-content/server";
@@ -1,5 +1,5 @@
1
1
  import type { APIRoute } from "astro";
2
- import { rssXml } from "@heyo-sh/heyo-docs";
2
+ import { rssXml } from "@heyo-sh/heyo-docs/rss";
3
3
 
4
4
  import config from "../../heyo-docs.config";
5
5
  import { pages } from "virtual:heyo-docs-content/server";
@@ -1,8 +1,8 @@
1
1
  import type { APIRoute } from "astro";
2
- import { sitemapXml } from "@heyo-sh/heyo-docs";
2
+ import { sitemapXml } from "@heyo-sh/heyo-docs/seo";
3
3
 
4
4
  import config from "../../heyo-docs.config";
5
- import { createDocsModel } from "@heyo-sh/heyo-docs";
5
+ import { createDocsModel } from "@heyo-sh/heyo-docs/model";
6
6
  import { openApiDocuments } from "virtual:heyo-docs-openapi";
7
7
  import { pages } from "virtual:heyo-docs-content";
8
8
 
@@ -1,26 +1,26 @@
1
1
  declare module "virtual:heyo-docs-content" {
2
- import type { DocsPage } from "@heyo-sh/heyo-docs";
2
+ import type { DocsPage } from "@heyo-sh/heyo-docs/types";
3
3
  export const pages: DocsPage[];
4
4
  }
5
5
 
6
6
  declare module "virtual:heyo-docs-config" {
7
- import type { HeyoDocsConfig } from "@heyo-sh/heyo-docs";
7
+ import type { HeyoDocsConfig } from "@heyo-sh/heyo-docs/types";
8
8
  export const config: HeyoDocsConfig;
9
9
  }
10
10
 
11
11
  declare module "virtual:heyo-docs-theme.css";
12
12
 
13
13
  declare module "virtual:heyo-docs-content/server" {
14
- import type { MarkdownPage } from "@heyo-sh/heyo-docs";
14
+ import type { MarkdownPage } from "@heyo-sh/heyo-docs/types";
15
15
  export const pages: MarkdownPage[];
16
16
  }
17
17
 
18
18
  declare module "virtual:heyo-docs-openapi" {
19
- import type { OpenApiDocumentSource } from "@heyo-sh/heyo-docs";
19
+ import type { OpenApiDocumentSource } from "@heyo-sh/heyo-docs/types";
20
20
  export const openApiDocuments: OpenApiDocumentSource[];
21
21
  }
22
22
 
23
23
  declare module "virtual:heyo-docs-openapi/index" {
24
- import type { OpenApiEndpoint } from "@heyo-sh/heyo-docs";
24
+ import type { OpenApiEndpoint } from "@heyo-sh/heyo-docs/types";
25
25
  export const openApiEndpoints: OpenApiEndpoint[];
26
26
  }
@@ -1,13 +1,12 @@
1
1
  import type { Metadata } from "next";
2
2
  import { notFound, redirect } from "next/navigation";
3
- import {
4
- navigationPages,
5
- openApiEndpointDetail,
6
- } from "@heyo-sh/heyo-docs/node";
3
+ import { nextDocsSeo } from "@heyo-sh/heyo-docs/seo/next";
4
+ import { navigationPages } from "@heyo-sh/heyo-docs/navigation";
5
+ import { openApiEndpointDetail } from "@heyo-sh/heyo-docs/openapi";
6
+ import { serializeJsonLd } from "@heyo-sh/heyo-docs/seo";
7
7
 
8
8
  import { NextDocsApp } from "../components/docs-app";
9
9
  import { docsContext, docsModel, pathnameForSegments } from "../lib/docs";
10
- import { docsSeo } from "../lib/seo";
11
10
 
12
11
  interface PageProps {
13
12
  params: Promise<{ slug?: string[] }>;
@@ -41,7 +40,7 @@ export async function generateMetadata({
41
40
  title: `Not found | ${context.config.title}`,
42
41
  robots: { index: false, follow: false },
43
42
  };
44
- return docsSeo({ ...context, pathname }).metadata;
43
+ return nextDocsSeo({ ...context, pathname }).metadata;
45
44
  }
46
45
 
47
46
  export default async function DocsPage({ params }: PageProps) {
@@ -55,18 +54,20 @@ export default async function DocsPage({ params }: PageProps) {
55
54
  if (!firstPage)
56
55
  throw new Error("No local documentation page is configured.");
57
56
 
58
- redirect(firstPage.slug);
57
+ // A minimal project owns `/` through content/index.mdx. Do not redirect
58
+ // the root route to itself.
59
+ if (firstPage.slug !== "/") redirect(firstPage.slug);
59
60
  }
60
61
  const context = docsContext(pathname);
61
62
  if (!context.page && !context.endpoint) notFound();
62
- const { structuredData } = docsSeo({ ...context, pathname });
63
+ const { structuredData } = nextDocsSeo({ ...context, pathname });
63
64
 
64
65
  return (
65
66
  <>
66
67
  <script
67
68
  type="application/ld+json"
68
69
  dangerouslySetInnerHTML={{
69
- __html: JSON.stringify(structuredData).replace(/</g, "\\u003c"),
70
+ __html: serializeJsonLd(structuredData),
70
71
  }}
71
72
  />
72
73
  <NextDocsApp
@@ -1,58 +1,23 @@
1
1
  "use client";
2
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";
3
+ import Link from "next/link";
4
+ import { DocsApp } from "@heyo-sh/heyo-docs";
5
+ import type { DocsLinkProps } from "@heyo-sh/heyo-docs/link";
6
+ import type { OpenApiEndpoint } from "@heyo-sh/heyo-docs/types";
7
+ import { useDocsTheme } from "@heyo-sh/heyo-docs/theme/provider";
8
+ import { forwardRef } from "react";
9
+ import { __HEYO_THEME_IDENTIFIER__ } from "@heyo-sh/heyo-docs/theme/__HEYO_THEME__";
19
10
 
20
11
  import { docsConfig, openApiEndpoints, pages } from "../_heyo-docs/content";
21
12
  import { iconSet } from "../heyo-docs-icons";
22
- import { useTheme } from "./theme-provider";
23
13
 
24
- type Navigate = (href: string) => void;
25
-
26
- const NextNavigationContext = createContext<Navigate | undefined>(undefined);
27
-
28
- const NextDocsLink = forwardRef<
29
- HTMLAnchorElement,
30
- ComponentPropsWithoutRef<"a">
31
- >(function NextDocsLink({ href, onClick, target, ...props }, ref) {
32
- const navigate = useContext(NextNavigationContext);
33
-
34
- const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
35
- onClick?.(event);
36
- if (
37
- !navigate ||
38
- event.defaultPrevented ||
39
- event.button !== 0 ||
40
- event.metaKey ||
41
- event.altKey ||
42
- event.ctrlKey ||
43
- event.shiftKey ||
44
- target
45
- )
46
- return;
47
-
48
- event.preventDefault();
49
- navigate(href ?? "/");
50
- };
51
-
52
- return (
53
- <a {...props} href={href} onClick={handleClick} ref={ref} target={target} />
54
- );
55
- });
14
+ const NextDocsLink = forwardRef<HTMLAnchorElement, DocsLinkProps>(
15
+ function NextDocsLink({ href = "/", ...props }, ref) {
16
+ // Next owns history, RSC payloads, metadata and scroll restoration. Docs
17
+ // links must not emulate routing with pushState in a client component.
18
+ return <Link {...props} href={href} ref={ref} />;
19
+ },
20
+ );
56
21
 
57
22
  export function NextDocsApp({
58
23
  currentOpenApiEndpoint,
@@ -61,78 +26,21 @@ export function NextDocsApp({
61
26
  currentOpenApiEndpoint?: OpenApiEndpoint;
62
27
  pathname: string;
63
28
  }) {
64
- const { mounted, resolvedTheme, setTheme } = useTheme();
65
- const [activePathname, setActivePathname] = useState(pathname);
66
- const [activeOpenApiEndpoint, setActiveOpenApiEndpoint] = useState(
67
- currentOpenApiEndpoint,
68
- );
69
- const navigationId = useRef(0);
70
-
71
- const navigate = useCallback(async (href: string, updateHistory = true) => {
72
- const destination = new URL(href, window.location.href);
73
- if (destination.origin !== window.location.origin) {
74
- window.location.assign(href);
75
- return;
76
- }
77
-
78
- const nextPathname = destination.pathname;
79
- const indexedEndpoint = openApiEndpoints.find(
80
- (endpoint) => endpoint.slug === nextPathname,
81
- );
82
- const page = pages.find((candidate) => candidate.slug === nextPathname);
83
- if (!indexedEndpoint && !page) {
84
- window.location.assign(href);
85
- return;
86
- }
87
-
88
- const currentNavigation = ++navigationId.current;
89
- let endpoint: OpenApiEndpoint | undefined;
90
- if (indexedEndpoint) {
91
- const response = await fetch(
92
- openApiEndpointDataPath(indexedEndpoint.slug),
93
- );
94
- if (!response.ok) {
95
- window.location.assign(href);
96
- return;
97
- }
98
- endpoint = (await response.json()) as OpenApiEndpoint;
99
- }
100
- if (currentNavigation !== navigationId.current) return;
101
-
102
- if (updateHistory) window.history.pushState({}, "", destination);
103
- document.title =
104
- page?.seo.title ?? `${indexedEndpoint!.title} | ${docsConfig.title}`;
105
- setActiveOpenApiEndpoint(endpoint);
106
- setActivePathname(nextPathname);
107
- }, []);
108
-
109
- useEffect(() => {
110
- const onPopState = () => {
111
- void navigate(window.location.href, false);
112
- };
113
-
114
- window.addEventListener("popstate", onPopState);
115
- return () => window.removeEventListener("popstate", onPopState);
116
- }, [navigate]);
29
+ const { mounted, resolvedTheme, toggleTheme } = useDocsTheme();
117
30
 
118
31
  return (
119
- <NextNavigationContext.Provider value={navigate}>
120
- <DocsApp
121
- config={docsConfig}
122
- iconSet={iconSet}
123
- isDark={mounted ? resolvedTheme === "dark" : undefined}
124
- link={NextDocsLink}
125
- onThemeToggle={
126
- mounted
127
- ? () => setTheme(resolvedTheme === "dark" ? "light" : "dark")
128
- : undefined
129
- }
130
- currentOpenApiEndpoint={activeOpenApiEndpoint}
131
- openApiEndpoints={openApiEndpoints}
132
- openApiRequestUrl="/heyo-docs-internal/openapi-request"
133
- pages={pages}
134
- pathname={activePathname}
135
- />
136
- </NextNavigationContext.Provider>
32
+ <DocsApp
33
+ config={docsConfig}
34
+ currentOpenApiEndpoint={currentOpenApiEndpoint}
35
+ iconSet={iconSet}
36
+ isDark={mounted ? resolvedTheme === "dark" : undefined}
37
+ link={NextDocsLink}
38
+ onThemeToggle={mounted ? toggleTheme : undefined}
39
+ openApiEndpoints={openApiEndpoints}
40
+ openApiRequestUrl="/heyo-docs-internal/openapi-request"
41
+ pages={pages}
42
+ pathname={pathname}
43
+ theme={__HEYO_THEME_IDENTIFIER__}
44
+ />
137
45
  );
138
46
  }
@@ -31,7 +31,7 @@ import {
31
31
  RiStarLine,
32
32
  RiSunLine,
33
33
  } from "@remixicon/react";
34
- import type { IconSet } from "@heyo-sh/heyo-docs";
34
+ import type { IconSet } from "@heyo-sh/heyo-docs/types";
35
35
 
36
36
  /** Statically selected icons keep unused packs out of the client bundle. */
37
37
  export const iconSet = {
@@ -1,11 +1,18 @@
1
- import { createAiChatResponse } from "@heyo-sh/heyo-docs/ai";
2
-
3
1
  import config from "../../../heyo-docs.config";
4
2
  import { docsPages, markdownPages } from "../../_heyo-docs/server";
5
3
 
6
4
  export const runtime = "nodejs";
7
5
 
8
6
  export async function POST(request: Request) {
7
+ if (!config.ai?.chat)
8
+ return Response.json(
9
+ { error: "AI chat is not configured." },
10
+ { status: 404 },
11
+ );
12
+
13
+ // Pi's provider catalogue is a server-only optional feature. Importing it
14
+ // only for an enabled request keeps static docs builds free of AI runtime.
15
+ const { createAiChatResponse } = await import("@heyo-sh/heyo-docs/ai");
9
16
  return createAiChatResponse(request, {
10
17
  ai: config.ai,
11
18
  markdownPages,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  markdownForPage,
3
3
  pathnameFromMarkdownPath,
4
- } from "@heyo-sh/heyo-docs/node";
4
+ } from "@heyo-sh/heyo-docs/llm";
5
5
 
6
6
  import { markdownPages } from "../../../lib/docs";
7
7