@heyo-sh/create-heyo-docs 0.4.0 → 1.0.1

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.
@@ -0,0 +1,5 @@
1
+ declare namespace Cloudflare {
2
+ interface Env {
3
+ OPENAI_API_KEY: string;
4
+ }
5
+ }
@@ -0,0 +1,30 @@
1
+ import { createAiChatResponse } from "@heyo-sh/heyo-docs/ai";
2
+ import type { APIRoute } from "astro";
3
+ import { env } from "cloudflare:workers";
4
+
5
+ import config from "../../../heyo-docs.config";
6
+ import { pages as markdownPages } from "virtual:heyo-docs-content/server";
7
+
8
+ export const prerender = false;
9
+
10
+ export const POST: APIRoute = ({ request }) => {
11
+ const apiKey = env.OPENAI_API_KEY;
12
+ if (!apiKey)
13
+ return Response.json(
14
+ { error: "The OPENAI_API_KEY Worker secret is not configured." },
15
+ { status: 500 },
16
+ );
17
+
18
+ return createAiChatResponse(request, {
19
+ ai: config.ai,
20
+ auth: { type: "api-key", token: apiKey },
21
+ markdownPages,
22
+ pages: markdownPages.map((page) => ({
23
+ description: page.description,
24
+ searchContent: page.raw,
25
+ slug: page.slug,
26
+ title: page.title,
27
+ })),
28
+ title: config.title,
29
+ });
30
+ };
@@ -0,0 +1,25 @@
1
+ import { createAiChatResponse } from "@heyo-sh/heyo-docs/ai";
2
+ import { getCloudflareContext } from "@opennextjs/cloudflare";
3
+
4
+ import config from "../../../heyo-docs.config";
5
+ import { docsPages, markdownPages } from "../../_heyo-docs/server";
6
+
7
+ export const runtime = "nodejs";
8
+
9
+ export async function POST(request: Request) {
10
+ const { env } = getCloudflareContext();
11
+ const apiKey = env.OPENAI_API_KEY;
12
+ if (!apiKey)
13
+ return Response.json(
14
+ { error: "The OPENAI_API_KEY Worker secret is not configured." },
15
+ { status: 500 },
16
+ );
17
+
18
+ return createAiChatResponse(request, {
19
+ ai: config.ai,
20
+ auth: { type: "api-key", token: apiKey },
21
+ markdownPages,
22
+ pages: docsPages,
23
+ title: config.title,
24
+ });
25
+ }
@@ -0,0 +1,4 @@
1
+ /** Cloudflare Worker secret configured with `wrangler secret put`. */
2
+ interface CloudflareEnv {
3
+ OPENAI_API_KEY: string;
4
+ }
@@ -0,0 +1,4 @@
1
+ /** Cloudflare Worker secret configured with `wrangler secret put`. */
2
+ interface Env {
3
+ OPENAI_API_KEY: string;
4
+ }
@@ -0,0 +1,4 @@
1
+ import { createContext } from "react-router";
2
+
3
+ /** Cloudflare bindings supplied to the Worker for the current request. */
4
+ export const cloudflareContext = createContext<Env>();
@@ -0,0 +1,30 @@
1
+ import type { ActionFunctionArgs } from "react-router";
2
+
3
+ import config from "../../heyo-docs.config";
4
+ import { cloudflareContext } from "../lib/cloudflare-context";
5
+ import { pages } from "virtual:heyo-docs-content";
6
+ import { pages as markdownPages } from "virtual:heyo-docs-content/server";
7
+
8
+ export async function action({ context, request }: ActionFunctionArgs) {
9
+ if (request.method !== "POST")
10
+ return new Response("Method Not Allowed", {
11
+ headers: { Allow: "POST" },
12
+ status: 405,
13
+ });
14
+
15
+ const apiKey = context.get(cloudflareContext).OPENAI_API_KEY;
16
+ if (!apiKey)
17
+ return Response.json(
18
+ { error: "The OPENAI_API_KEY Worker secret is not configured." },
19
+ { status: 500 },
20
+ );
21
+
22
+ const { createAiChatResponse } = await import("@heyo-sh/heyo-docs/ai");
23
+ return createAiChatResponse(request, {
24
+ ai: config.ai,
25
+ auth: { type: "api-key", token: apiKey },
26
+ markdownPages,
27
+ pages,
28
+ title: config.title,
29
+ });
30
+ }
@@ -1,12 +1,16 @@
1
1
  import { createRequestHandler, RouterContextProvider } from "react-router";
2
2
 
3
+ import { cloudflareContext } from "../app/lib/cloudflare-context";
4
+
3
5
  const handleRequest = createRequestHandler(
4
6
  () => import("virtual:react-router/server-build"),
5
7
  import.meta.env.MODE,
6
8
  );
7
9
 
8
10
  export default {
9
- fetch(request) {
10
- return handleRequest(request, new RouterContextProvider());
11
+ fetch(request, env) {
12
+ const context = new RouterContextProvider();
13
+ context.set(cloudflareContext, env);
14
+ return handleRequest(request, context);
11
15
  },
12
16
  } satisfies ExportedHandler<Env>;
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ function packageManagerRunCommand(manager, script) {
47
47
  // src/project-name.ts
48
48
  var PROJECT_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/i;
49
49
  function projectNameError(value) {
50
- if (!value.trim())
50
+ if (!value?.trim())
51
51
  return "Enter a project name.";
52
52
  if (!PROJECT_NAME_PATTERN.test(value))
53
53
  return "Use letters, numbers and hyphens only.";
@@ -289,14 +289,12 @@ function parseArguments(argv) {
289
289
  }
290
290
  async function askQuestions(defaults) {
291
291
  p.intro("Create Heyo Docs");
292
- const projectName = await p.text({
292
+ const projectName = promptValue(await p.text({
293
293
  message: "Project name",
294
294
  placeholder: "my-docs",
295
295
  defaultValue: "my-docs",
296
296
  validate: projectNameError
297
- });
298
- if (p.isCancel(projectName))
299
- cancelCreation();
297
+ }));
300
298
  const template = await selectAvailable("Template", TEMPLATE_OPTIONS, [
301
299
  "react-router",
302
300
  "next",
@@ -309,12 +307,10 @@ async function askQuestions(defaults) {
309
307
  "moss"
310
308
  ]);
311
309
  const packageManager = await selectAvailable("Package manager", PACKAGE_MANAGER_OPTIONS, ["bun", "pnpm", "npm", "yarn"], defaults.packageManager);
312
- const install = await p.confirm({
310
+ const install = promptValue(await p.confirm({
313
311
  message: "Install dependencies?",
314
312
  initialValue: true
315
- });
316
- if (p.isCancel(install))
317
- cancelCreation();
313
+ }));
318
314
  return {
319
315
  projectName,
320
316
  template,
@@ -334,14 +330,19 @@ async function selectDeployment(template, initialValue) {
334
330
  }
335
331
  async function selectAvailable(message, options, available, initialValue) {
336
332
  for (;; ) {
337
- const answer = await p.select({ message, options, initialValue });
338
- if (p.isCancel(answer))
339
- cancelCreation();
333
+ const answer = promptValue(await p.select({ message, options, initialValue }));
340
334
  if (isOneOf(answer, available))
341
335
  return answer;
342
336
  p.log.warn("That option is planned but not available in this MVP.");
343
337
  }
344
338
  }
339
+ function promptValue(value) {
340
+ if (typeof value !== "symbol")
341
+ return value;
342
+ if (p.isCancel(value))
343
+ cancelCreation();
344
+ throw new Error("Prompt did not return a value.");
345
+ }
345
346
  function isOneOf(value, values) {
346
347
  return values.some((candidate) => candidate === value);
347
348
  }
@@ -1,3 +1,3 @@
1
- export declare function projectNameError(value: string): string | undefined;
1
+ export declare function projectNameError(value: string | undefined): string | undefined;
2
2
  export declare function assertProjectName(value: string): string;
3
3
  //# sourceMappingURL=project-name.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-name.d.ts","sourceRoot":"","sources":["../src/project-name.ts"],"names":[],"mappings":"AAEA,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAIlE;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIvD"}
1
+ {"version":3,"file":"project-name.d.ts","sourceRoot":"","sources":["../src/project-name.ts"],"names":[],"mappings":"AAEA,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,MAAM,GAAG,SAAS,CAIpB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIvD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heyo-sh/create-heyo-docs",
3
- "version": "0.4.0",
3
+ "version": "1.0.1",
4
4
  "description": "Create a standalone Heyo Docs documentation project.",
5
5
  "homepage": "https://heyo.sh",
6
6
  "repository": {
@@ -27,7 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@clack/prompts": "0.10.0"
30
+ "@clack/prompts": "1.8.0"
31
31
  },
32
32
  "scripts": {
33
33
  "dev": "bun run build --watch",
@@ -66,15 +66,15 @@ sidebar entry should point to a page outside the local documentation.
66
66
 
67
67
  ## AI chat
68
68
 
69
- Add `ai.chat` to enable a documentation-aware chat. `openai`, `claude`, and `grok`
70
- use the OpenAI, Anthropic, and xAI adapters respectively. Keep the key in a
71
- server environment variable; Heyo Docs sends it only from the chat endpoint.
69
+ Add `ai.chat` to enable a documentation-aware chat through [Pi](https://github.com/badlogic/pi-mono).
70
+ Use any provider identifier bundled with the installed Pi version, such as
71
+ `openai`, `anthropic`, `google`, `openrouter`, or `xai`. Authentication stays
72
+ server-only and is supplied by the route that handles the chat request.
72
73
 
73
74
  <CodeBlock language="ts" title="heyo-docs.config.ts">
74
75
  {`ai: {
75
76
  chat: {
76
77
  provider: "openai",
77
- key: process.env.OPENAI_API_KEY!,
78
78
  model: "gpt-5-mini",
79
79
  variant: "right", // "center" shows the compact prompt first
80
80
  icon: "chat",
@@ -85,8 +85,15 @@ server environment variable; Heyo Docs sends it only from the chat endpoint.
85
85
  },`}
86
86
  </CodeBlock>
87
87
 
88
- `provider`, `key`, and `model` are required. The model must be an actual model
89
- identifier, not the provider name. `variant` defaults to `right`. `name`
88
+ `provider` and `model` are required. Keep the credential out of this
89
+ configuration and pass it as `auth` to `createAiChatResponse` in the server
90
+ route. Cloudflare routes obtain the key from the Worker `env` binding; Node.js
91
+ routes can read their server environment there. GitHub Copilot and OpenAI Codex
92
+ use `auth: { type: "oauth", getAccessToken }`; Amazon Bedrock uses either
93
+ `{ type: "aws", region?, profile? }` and the AWS credential chain, or
94
+ `{ type: "bedrock-bearer", token, region? }`. The model must be an actual model
95
+ identifier, not the provider name. Amazon Bedrock requires a Node.js chat
96
+ endpoint. `variant` defaults to `right`. `name`
90
97
  defaults to `AI`; it is used for the drawer title and the assistant's message
91
98
  label. `placeholder` defaults to
92
99
  `Ask AI about the docs` and is shared by the compact prompt and drawer input.
@@ -30,6 +30,19 @@ the sitemap, RSS feed, robots file, and AI-facing documentation endpoints.
30
30
  Use the equivalent `npm run build`, `pnpm build`, or `yarn build` command
31
31
  when the project uses a different package manager.
32
32
 
33
+ ## Cloudflare secrets
34
+
35
+ For AI chat, save the provider key as a Worker secret:
36
+
37
+ <CodeBlock language="bash" title="Terminal">
38
+ {"bunx wrangler secret put OPENAI_API_KEY"}
39
+ </CodeBlock>
40
+
41
+ Keep the credential out of `heyo-docs.config.ts`. The Cloudflare overlay reads
42
+ the native Worker `env.OPENAI_API_KEY` binding in the AI route, after the
43
+ request reaches the Worker. It is never part of the documentation build or the
44
+ browser bundle.
45
+
33
46
  <Callout type="tip" title="Set the canonical URL">
34
47
  Add the final public origin as <code>siteUrl</code> in
35
48
  <code>heyo-docs.config.ts</code> before the first production release. It keeps
@@ -188,20 +188,16 @@ const zammadBootstrap = zammad ? zammadBootstrapScript() : undefined;
188
188
  <!doctype html>
189
189
  <html lang="en">
190
190
  <head>
191
- {
192
- transcendGoogleConsentDefaults && (
193
- <script is:inline set:html={transcendGoogleConsentDefaults} />
194
- )
195
- }
196
- {
197
- transcend && (
198
- <script
199
- is:inline
200
- data-cfasync={transcend.dataCfasync}
201
- src={transcend.src}
202
- />
203
- )
204
- }
191
+ {transcendGoogleConsentDefaults && (
192
+ <script is:inline set:html={transcendGoogleConsentDefaults}></script>
193
+ )}
194
+ {transcend && (
195
+ <script
196
+ is:inline
197
+ data-cfasync={transcend.dataCfasync}
198
+ src={transcend.src}
199
+ ></script>
200
+ )}
205
201
  <meta charset="utf-8" />
206
202
  <meta name="viewport" content="width=device-width, initial-scale=1" />
207
203
  <meta name="color-scheme" content="light dark" />
@@ -223,334 +219,280 @@ const zammadBootstrap = zammad ? zammadBootstrapScript() : undefined;
223
219
  <link rel="icon" href="/favicon-96x96.png" sizes="96x96" type="image/png" />
224
220
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
225
221
  <link rel="manifest" href="/site.webmanifest" />
226
- {
227
- hasChangelog && (
228
- <link
229
- rel="alternate"
230
- href="/rss.xml"
231
- type="application/rss+xml"
232
- title={`${config.title} updates`}
233
- />
234
- )
235
- }
236
- {osano && <script is:inline src={osano.src} />}
237
- {
238
- googleTagManager && (
239
- <script
240
- is:inline
241
- data-google-tag-manager-container-id={
242
- config.integrations.analytics.gtm?.containerId
243
- }
244
- set:html={googleTagManager}
245
- />
246
- )
247
- }
222
+ {hasChangelog && (
223
+ <link
224
+ rel="alternate"
225
+ href="/rss.xml"
226
+ type="application/rss+xml"
227
+ title={`${config.title} updates`}
228
+ />
229
+ )}
230
+ {osano && <script is:inline src={osano.src}></script>}
231
+ {googleTagManager && (
232
+ <script
233
+ is:inline
234
+ data-google-tag-manager-container-id={
235
+ config.integrations.analytics.gtm?.containerId
236
+ }
237
+ set:html={googleTagManager}
238
+ ></script>
239
+ )}
248
240
  <script
249
241
  is:inline
250
242
  set:html={getThemeScript(THEME_STORAGE_KEY, config.mode)}
251
- />
252
- {adobe && <script is:inline async src={adobe.src} />}
253
- {amplitude && <script is:inline src={amplitude.src} />}
254
- {
255
- clarity && (
243
+ ></script>
244
+ {adobe && <script is:inline async src={adobe.src}></script>}
245
+ {amplitude && <script is:inline src={amplitude.src}></script>}
246
+ {clarity && (
247
+ <script
248
+ is:inline
249
+ data-clarity-project-id={
250
+ config.integrations.analytics.clarity?.projectId
251
+ }
252
+ set:html={clarity}
253
+ ></script>
254
+ )}
255
+ {clearbit && <script is:inline src={clearbit.src}></script>}
256
+ {fathom && (
257
+ <script
258
+ is:inline
259
+ data-site={fathom.siteId}
260
+ data-spa={fathom.spa}
261
+ defer={fathom.defer}
262
+ src={fathom.src}
263
+ ></script>
264
+ )}
265
+ {googleAnalytics && googleAnalyticsBootstrap && (
266
+ <>
256
267
  <script
257
268
  is:inline
258
- data-clarity-project-id={
259
- config.integrations.analytics.clarity?.projectId
269
+ async={googleAnalytics.async}
270
+ src={googleAnalytics.src}
271
+ ></script>
272
+ <script
273
+ is:inline
274
+ data-google-analytics-measurement-id={
275
+ config.integrations.analytics.ga4?.measurementId
260
276
  }
261
- set:html={clarity}
262
- />
263
- )
264
- }
265
- {clearbit && <script is:inline src={clearbit.src} />}
266
- {
267
- fathom && (
277
+ set:html={googleAnalyticsBootstrap}
278
+ ></script>
279
+ </>
280
+ )}
281
+ {heap && (
282
+ <script
283
+ is:inline
284
+ data-heap-environment-id={
285
+ config.integrations.analytics.heap?.environmentId
286
+ }
287
+ set:html={heap}
288
+ ></script>
289
+ )}
290
+ {hotjar && (
291
+ <script
292
+ is:inline
293
+ data-hotjar-site-id={config.integrations.analytics.hotjar?.siteId}
294
+ data-hotjar-snippet-version={
295
+ config.integrations.analytics.hotjar?.snippetVersion
296
+ }
297
+ set:html={hotjar}
298
+ ></script>
299
+ )}
300
+ {logRocket && logRocketBootstrap && (
301
+ <>
268
302
  <script
269
303
  is:inline
270
- data-site={fathom.siteId}
271
- data-spa={fathom.spa}
272
- defer={fathom.defer}
273
- src={fathom.src}
274
- />
275
- )
276
- }
277
- {
278
- googleAnalytics && googleAnalyticsBootstrap && (
279
- <>
280
- <script
281
- is:inline
282
- async={googleAnalytics.async}
283
- src={googleAnalytics.src}
284
- />
285
- <script
286
- is:inline
287
- data-google-analytics-measurement-id={
288
- config.integrations.analytics.ga4?.measurementId
289
- }
290
- set:html={googleAnalyticsBootstrap}
291
- />
292
- </>
293
- )
294
- }
295
- {
296
- heap && (
304
+ crossorigin={logRocket.crossOrigin}
305
+ src={logRocket.src}
306
+ ></script>
297
307
  <script
298
308
  is:inline
299
- data-heap-environment-id={
300
- config.integrations.analytics.heap?.environmentId
301
- }
302
- set:html={heap}
303
- />
304
- )
305
- }
306
- {
307
- hotjar && (
309
+ data-logrocket-app-id={config.integrations.analytics.logrocket?.appId}
310
+ set:html={logRocketBootstrap}
311
+ ></script>
312
+ </>
313
+ )}
314
+ {mixpanel && mixpanelBootstrap && (
315
+ <>
316
+ <script is:inline src={mixpanel.src}></script>
308
317
  <script
309
318
  is:inline
310
- data-hotjar-site-id={config.integrations.analytics.hotjar?.siteId}
311
- data-hotjar-snippet-version={
312
- config.integrations.analytics.hotjar?.snippetVersion
319
+ data-mixpanel-project-token={
320
+ config.integrations.analytics.mixpanel?.projectToken
313
321
  }
314
- set:html={hotjar}
315
- />
316
- )
317
- }
318
- {
319
- logRocket && logRocketBootstrap && (
320
- <>
321
- <script
322
- is:inline
323
- crossorigin={logRocket.crossOrigin}
324
- src={logRocket.src}
325
- />
326
- <script
327
- is:inline
328
- data-logrocket-app-id={
329
- config.integrations.analytics.logrocket?.appId
330
- }
331
- set:html={logRocketBootstrap}
332
- />
333
- </>
334
- )
335
- }
336
- {
337
- mixpanel && mixpanelBootstrap && (
338
- <>
339
- <script is:inline src={mixpanel.src} />
340
- <script
341
- is:inline
342
- data-mixpanel-project-token={
343
- config.integrations.analytics.mixpanel?.projectToken
344
- }
345
- set:html={mixpanelBootstrap}
346
- />
347
- </>
348
- )
349
- }
350
- {
351
- openpanel && openpanelBootstrap && (
352
- <>
353
- <script
354
- is:inline
355
- data-openpanel-api-url={
356
- config.integrations.analytics.openpanel?.apiUrl
357
- }
358
- data-openpanel-client-id={
359
- config.integrations.analytics.openpanel?.clientId
360
- }
361
- set:html={openpanelBootstrap}
362
- />
363
- <script
364
- is:inline
365
- async={openpanel.async}
366
- defer={openpanel.defer}
367
- src={openpanel.src}
368
- />
369
- </>
370
- )
371
- }
372
- {
373
- openReplay && (
322
+ set:html={mixpanelBootstrap}
323
+ ></script>
324
+ </>
325
+ )}
326
+ {openpanel && openpanelBootstrap && (
327
+ <>
374
328
  <script
375
329
  is:inline
376
- data-openreplay-ingest-point={
377
- config.integrations.analytics.openreplay?.ingestPoint
330
+ data-openpanel-api-url={
331
+ config.integrations.analytics.openpanel?.apiUrl
378
332
  }
379
- data-openreplay-project-key={
380
- config.integrations.analytics.openreplay?.projectKey
333
+ data-openpanel-client-id={
334
+ config.integrations.analytics.openpanel?.clientId
381
335
  }
382
- set:html={openReplay}
383
- />
384
- )
385
- }
386
- {
387
- pirsch && (
388
- <script
389
- is:inline
390
- data-code={pirsch.code}
391
- defer={pirsch.defer}
392
- src={pirsch.src}
393
- />
394
- )
395
- }
396
- {
397
- plausible && (
398
- <script
399
- is:inline
400
- data-domain={plausible.domain}
401
- defer={plausible.defer}
402
- src={plausible.src}
403
- />
404
- )
405
- }
406
- {
407
- posthog && posthogBootstrap && posthogHost && (
408
- <>
409
- <script is:inline src={posthog.src} />
410
- <script
411
- is:inline
412
- data-posthog-api-host={posthogHost}
413
- data-posthog-project-api-key={
414
- config.integrations.analytics.posthog?.projectApiKey
415
- }
416
- set:html={posthogBootstrap}
417
- />
418
- </>
419
- )
420
- }
421
- {
422
- rybbit && (
423
- <script
424
- is:inline
425
- data-site-id={rybbit.siteId}
426
- defer={rybbit.defer}
427
- src={rybbit.src}
428
- />
429
- )
430
- }
431
- {
432
- umami && (
336
+ set:html={openpanelBootstrap}
337
+ ></script>
433
338
  <script
434
339
  is:inline
435
- data-website-id={umami.websiteId}
436
- defer={umami.defer}
437
- src={umami.src}
438
- />
439
- )
440
- }
441
- {
442
- chaskiq && (
340
+ async={openpanel.async}
341
+ defer={openpanel.defer}
342
+ src={openpanel.src}
343
+ ></script>
344
+ </>
345
+ )}
346
+ {openReplay && (
347
+ <script
348
+ is:inline
349
+ data-openreplay-ingest-point={
350
+ config.integrations.analytics.openreplay?.ingestPoint
351
+ }
352
+ data-openreplay-project-key={
353
+ config.integrations.analytics.openreplay?.projectKey
354
+ }
355
+ set:html={openReplay}
356
+ ></script>
357
+ )}
358
+ {pirsch && (
359
+ <script
360
+ is:inline
361
+ data-code={pirsch.code}
362
+ defer={pirsch.defer}
363
+ src={pirsch.src}
364
+ ></script>
365
+ )}
366
+ {plausible && (
367
+ <script
368
+ is:inline
369
+ data-domain={plausible.domain}
370
+ defer={plausible.defer}
371
+ src={plausible.src}
372
+ ></script>
373
+ )}
374
+ {posthog && posthogBootstrap && posthogHost && (
375
+ <>
376
+ <script is:inline src={posthog.src}></script>
443
377
  <script
444
378
  is:inline
445
- data-chaskiq-app-id={config.integrations.support.chaskiq?.appId}
446
- data-chaskiq-base-url={config.integrations.support.chaskiq?.baseUrl}
447
- data-chaskiq-ws-url={config.integrations.support.chaskiq?.wsUrl}
448
- set:html={chaskiq}
449
- />
450
- )
451
- }
452
- {
453
- papercups && (
454
- <script
455
- is:inline
456
- data-papercups-base-url={
457
- config.integrations.support.papercups?.baseUrl
379
+ data-posthog-api-host={posthogHost}
380
+ data-posthog-project-api-key={
381
+ config.integrations.analytics.posthog?.projectApiKey
458
382
  }
459
- data-papercups-inbox={config.integrations.support.papercups?.inbox}
460
- data-papercups-token={config.integrations.support.papercups?.token}
461
- set:html={papercups}
462
- />
463
- )
464
- }
465
- {
466
- typebot && (
467
- <script
468
- is:inline
469
- data-heyo-typebot
470
- data-typebot-api-host={config.integrations.support.typebot?.apiHost}
471
- data-typebot-id={config.integrations.support.typebot?.typebot}
472
- set:html={typebot}
473
- type="module"
474
- />
475
- )
476
- }
477
- <script is:inline type="application/ld+json" set:html={jsonLd} />
383
+ set:html={posthogBootstrap}
384
+ ></script>
385
+ </>
386
+ )}
387
+ {rybbit && (
388
+ <script
389
+ is:inline
390
+ data-site-id={rybbit.siteId}
391
+ defer={rybbit.defer}
392
+ src={rybbit.src}
393
+ ></script>
394
+ )}
395
+ {umami && (
396
+ <script
397
+ is:inline
398
+ data-website-id={umami.websiteId}
399
+ defer={umami.defer}
400
+ src={umami.src}
401
+ ></script>
402
+ )}
403
+ {chaskiq && (
404
+ <script
405
+ is:inline
406
+ data-chaskiq-app-id={config.integrations.support.chaskiq?.appId}
407
+ data-chaskiq-base-url={config.integrations.support.chaskiq?.baseUrl}
408
+ data-chaskiq-ws-url={config.integrations.support.chaskiq?.wsUrl}
409
+ set:html={chaskiq}
410
+ ></script>
411
+ )}
412
+ {papercups && (
413
+ <script
414
+ is:inline
415
+ data-papercups-base-url={config.integrations.support.papercups?.baseUrl}
416
+ data-papercups-inbox={config.integrations.support.papercups?.inbox}
417
+ data-papercups-token={config.integrations.support.papercups?.token}
418
+ set:html={papercups}
419
+ ></script>
420
+ )}
421
+ {typebot && (
422
+ <script
423
+ is:inline
424
+ data-heyo-typebot
425
+ data-typebot-api-host={config.integrations.support.typebot?.apiHost}
426
+ data-typebot-id={config.integrations.support.typebot?.typebot}
427
+ set:html={typebot}
428
+ type="module"
429
+ ></script>
430
+ )}
431
+ <script is:inline type="application/ld+json" set:html={jsonLd}></script>
478
432
  </head>
479
433
  <body>
480
- {
481
- googleTagManagerNoScript && (
482
- <noscript>
483
- <iframe
484
- height="0"
485
- src={googleTagManagerNoScript}
486
- style="display:none;visibility:hidden"
487
- title="Google Tag Manager"
488
- width="0"
489
- />
490
- </noscript>
491
- )
492
- }
434
+ {googleTagManagerNoScript && (
435
+ <noscript>
436
+ <iframe
437
+ height="0"
438
+ src={googleTagManagerNoScript}
439
+ style="display:none;visibility:hidden"
440
+ title="Google Tag Manager"
441
+ width="0"
442
+ ></iframe>
443
+ </noscript>
444
+ )}
493
445
  <slot />
494
- {
495
- swetrix && swetrixBootstrap && (
496
- <>
497
- <script is:inline defer={swetrix.defer} src={swetrix.src} />
498
- <script
499
- is:inline
500
- data-swetrix-api-url={config.integrations.analytics.swetrix?.apiUrl}
501
- data-swetrix-project-id={
502
- config.integrations.analytics.swetrix?.projectId
503
- }
504
- set:html={swetrixBootstrap}
505
- />
506
- </>
507
- )
508
- }
509
- {
510
- intercom && (
446
+ {swetrix && swetrixBootstrap && (
447
+ <>
448
+ <script is:inline defer={swetrix.defer} src={swetrix.src}></script>
511
449
  <script
512
450
  is:inline
513
- data-intercom-app-id={config.integrations.support.intercom?.appId}
514
- data-intercom-api-base={config.integrations.support.intercom?.apiBase}
515
- set:html={intercom}
516
- />
517
- )
518
- }
519
- {
520
- front && frontBootstrap && (
521
- <>
522
- <script is:inline src={front.src} />
523
- <script
524
- is:inline
525
- data-front-chat-id={config.integrations.support.front?.chatId}
526
- set:html={frontBootstrap}
527
- />
528
- </>
529
- )
530
- }
531
- {
532
- chatwoot && (
451
+ data-swetrix-api-url={config.integrations.analytics.swetrix?.apiUrl}
452
+ data-swetrix-project-id={
453
+ config.integrations.analytics.swetrix?.projectId
454
+ }
455
+ set:html={swetrixBootstrap}
456
+ ></script>
457
+ </>
458
+ )}
459
+ {intercom && (
460
+ <script
461
+ is:inline
462
+ data-intercom-app-id={config.integrations.support.intercom?.appId}
463
+ data-intercom-api-base={config.integrations.support.intercom?.apiBase}
464
+ set:html={intercom}
465
+ ></script>
466
+ )}
467
+ {front && frontBootstrap && (
468
+ <>
469
+ <script is:inline src={front.src}></script>
533
470
  <script
534
471
  is:inline
535
- data-chatwoot-base-url={config.integrations.support.chatwoot?.baseUrl}
536
- data-chatwoot-website-token={
537
- config.integrations.support.chatwoot?.websiteToken
538
- }
539
- set:html={chatwoot}
540
- />
541
- )
542
- }
543
- {
544
- zammad && zammadBootstrap && (
545
- <>
546
- <script is:inline src={zammad.src} />
547
- <script
548
- is:inline
549
- data-zammad-chat-id={config.integrations.support.zammad?.chatId}
550
- set:html={zammadBootstrap}
551
- />
552
- </>
553
- )
554
- }
472
+ data-front-chat-id={config.integrations.support.front?.chatId}
473
+ set:html={frontBootstrap}
474
+ ></script>
475
+ </>
476
+ )}
477
+ {chatwoot && (
478
+ <script
479
+ is:inline
480
+ data-chatwoot-base-url={config.integrations.support.chatwoot?.baseUrl}
481
+ data-chatwoot-website-token={
482
+ config.integrations.support.chatwoot?.websiteToken
483
+ }
484
+ set:html={chatwoot}
485
+ ></script>
486
+ )}
487
+ {zammad && zammadBootstrap && (
488
+ <>
489
+ <script is:inline src={zammad.src}></script>
490
+ <script
491
+ is:inline
492
+ data-zammad-chat-id={config.integrations.support.zammad?.chatId}
493
+ set:html={zammadBootstrap}
494
+ ></script>
495
+ </>
496
+ )}
555
497
  </body>
556
498
  </html>
@@ -6,9 +6,17 @@ import { pages as markdownPages } from "virtual:heyo-docs-content/server";
6
6
 
7
7
  export const prerender = false;
8
8
 
9
- export const POST: APIRoute = ({ request }) =>
10
- createAiChatResponse(request, {
9
+ export const POST: APIRoute = ({ request }) => {
10
+ const apiKey = process.env.OPENAI_API_KEY;
11
+ if (!apiKey)
12
+ return Response.json(
13
+ { error: "OPENAI_API_KEY is not configured." },
14
+ { status: 500 },
15
+ );
16
+
17
+ return createAiChatResponse(request, {
11
18
  ai: config.ai,
19
+ auth: { type: "api-key", token: apiKey },
12
20
  markdownPages,
13
21
  pages: markdownPages.map((page) => ({
14
22
  description: page.description,
@@ -18,3 +26,4 @@ export const POST: APIRoute = ({ request }) =>
18
26
  })),
19
27
  title: config.title,
20
28
  });
29
+ };
@@ -6,8 +6,16 @@ import { docsPages, markdownPages } from "../../_heyo-docs/server";
6
6
  export const runtime = "nodejs";
7
7
 
8
8
  export async function POST(request: Request) {
9
+ const apiKey = process.env.OPENAI_API_KEY;
10
+ if (!apiKey)
11
+ return Response.json(
12
+ { error: "OPENAI_API_KEY is not configured." },
13
+ { status: 500 },
14
+ );
15
+
9
16
  return createAiChatResponse(request, {
10
17
  ai: config.ai,
18
+ auth: { type: "api-key", token: apiKey },
11
19
  markdownPages,
12
20
  pages: docsPages,
13
21
  title: config.title,
@@ -66,15 +66,15 @@ sidebar entry should point to a page outside the local documentation.
66
66
 
67
67
  ## AI chat
68
68
 
69
- Add `ai.chat` to enable a documentation-aware chat. `openai`, `claude`, and `grok`
70
- use the OpenAI, Anthropic, and xAI adapters respectively. Keep the key in a
71
- server environment variable; Heyo Docs sends it only from the chat endpoint.
69
+ Add `ai.chat` to enable a documentation-aware chat through [Pi](https://github.com/badlogic/pi-mono).
70
+ Use any provider identifier bundled with the installed Pi version, such as
71
+ `openai`, `anthropic`, `google`, `openrouter`, or `xai`. Authentication stays
72
+ server-only and is supplied by the route that handles the chat request.
72
73
 
73
74
  <CodeBlock language="ts" title="heyo-docs.config.ts">
74
75
  {`ai: {
75
76
  chat: {
76
77
  provider: "openai",
77
- key: process.env.OPENAI_API_KEY!,
78
78
  model: "gpt-5-mini",
79
79
  variant: "right", // "center" shows the compact prompt first
80
80
  icon: "chat",
@@ -85,8 +85,15 @@ server environment variable; Heyo Docs sends it only from the chat endpoint.
85
85
  },`}
86
86
  </CodeBlock>
87
87
 
88
- `provider`, `key`, and `model` are required. The model must be an actual model
89
- identifier, not the provider name. `variant` defaults to `right`. `name`
88
+ `provider` and `model` are required. Keep the credential out of this
89
+ configuration and pass it as `auth` to `createAiChatResponse` in the server
90
+ route. Cloudflare routes obtain the key from the Worker `env` binding; Node.js
91
+ routes can read their server environment there. GitHub Copilot and OpenAI Codex
92
+ use `auth: { type: "oauth", getAccessToken }`; Amazon Bedrock uses either
93
+ `{ type: "aws", region?, profile? }` and the AWS credential chain, or
94
+ `{ type: "bedrock-bearer", token, region? }`. The model must be an actual model
95
+ identifier, not the provider name. Amazon Bedrock requires a Node.js chat
96
+ endpoint. `variant` defaults to `right`. `name`
90
97
  defaults to `AI`; it is used for the drawer title and the assistant's message
91
98
  label. `placeholder` defaults to
92
99
  `Ask AI about the docs` and is shared by the compact prompt and drawer input.
@@ -30,6 +30,19 @@ the sitemap, RSS feed, robots file, and AI-facing documentation endpoints.
30
30
  Use the equivalent `npm run build`, `pnpm build`, or `yarn build` command
31
31
  when the project uses a different package manager.
32
32
 
33
+ ## Cloudflare secrets
34
+
35
+ For AI chat, save the provider key as a Worker secret:
36
+
37
+ <CodeBlock language="bash" title="Terminal">
38
+ {"bunx wrangler secret put OPENAI_API_KEY"}
39
+ </CodeBlock>
40
+
41
+ Keep the credential out of `heyo-docs.config.ts`. The Cloudflare overlay reads
42
+ the native Worker `env.OPENAI_API_KEY` binding in the AI route, after the
43
+ request reaches the Worker. It is never part of the documentation build or the
44
+ browser bundle.
45
+
33
46
  <Callout type="tip" title="Set the canonical URL">
34
47
  Add the final public origin as <code>siteUrl</code> in
35
48
  <code>heyo-docs.config.ts</code> before the first production release. It keeps
@@ -1,4 +1,3 @@
1
- import { createAiChatResponse } from "@heyo-sh/heyo-docs/ai";
2
1
  import type { ActionFunctionArgs } from "react-router";
3
2
 
4
3
  import config from "../../heyo-docs.config";
@@ -12,8 +11,17 @@ export async function action({ request }: ActionFunctionArgs) {
12
11
  status: 405,
13
12
  });
14
13
 
14
+ const apiKey = process.env.OPENAI_API_KEY;
15
+ if (!apiKey)
16
+ return Response.json(
17
+ { error: "OPENAI_API_KEY is not configured." },
18
+ { status: 500 },
19
+ );
20
+
21
+ const { createAiChatResponse } = await import("@heyo-sh/heyo-docs/ai");
15
22
  return createAiChatResponse(request, {
16
23
  ai: config.ai,
24
+ auth: { type: "api-key", token: apiKey },
17
25
  markdownPages,
18
26
  pages,
19
27
  title: config.title,
@@ -66,15 +66,15 @@ sidebar entry should point to a page outside the local documentation.
66
66
 
67
67
  ## AI chat
68
68
 
69
- Add `ai.chat` to enable a documentation-aware chat. `openai`, `claude`, and `grok`
70
- use the OpenAI, Anthropic, and xAI adapters respectively. Keep the key in a
71
- server environment variable; Heyo Docs sends it only from the chat endpoint.
69
+ Add `ai.chat` to enable a documentation-aware chat through [Pi](https://github.com/badlogic/pi-mono).
70
+ Use any provider identifier bundled with the installed Pi version, such as
71
+ `openai`, `anthropic`, `google`, `openrouter`, or `xai`. Authentication stays
72
+ server-only and is supplied by the route that handles the chat request.
72
73
 
73
74
  <CodeBlock language="ts" title="heyo-docs.config.ts">
74
75
  {`ai: {
75
76
  chat: {
76
77
  provider: "openai",
77
- key: process.env.OPENAI_API_KEY!,
78
78
  model: "gpt-5-mini",
79
79
  variant: "right", // "center" shows the compact prompt first
80
80
  icon: "chat",
@@ -85,8 +85,15 @@ server environment variable; Heyo Docs sends it only from the chat endpoint.
85
85
  },`}
86
86
  </CodeBlock>
87
87
 
88
- `provider`, `key`, and `model` are required. The model must be an actual model
89
- identifier, not the provider name. `variant` defaults to `right`. `name`
88
+ `provider` and `model` are required. Keep the credential out of this
89
+ configuration and pass it as `auth` to `createAiChatResponse` in the server
90
+ route. Cloudflare routes obtain the key from the Worker `env` binding; Node.js
91
+ routes can read their server environment there. GitHub Copilot and OpenAI Codex
92
+ use `auth: { type: "oauth", getAccessToken }`; Amazon Bedrock uses either
93
+ `{ type: "aws", region?, profile? }` and the AWS credential chain, or
94
+ `{ type: "bedrock-bearer", token, region? }`. The model must be an actual model
95
+ identifier, not the provider name. Amazon Bedrock requires a Node.js chat
96
+ endpoint. `variant` defaults to `right`. `name`
90
97
  defaults to `AI`; it is used for the drawer title and the assistant's message
91
98
  label. `placeholder` defaults to
92
99
  `Ask AI about the docs` and is shared by the compact prompt and drawer input.
@@ -30,6 +30,19 @@ the sitemap, RSS feed, robots file, and AI-facing documentation endpoints.
30
30
  Use the equivalent `npm run build`, `pnpm build`, or `yarn build` command
31
31
  when the project uses a different package manager.
32
32
 
33
+ ## Cloudflare secrets
34
+
35
+ For AI chat, save the provider key as a Worker secret:
36
+
37
+ <CodeBlock language="bash" title="Terminal">
38
+ {"bunx wrangler secret put OPENAI_API_KEY"}
39
+ </CodeBlock>
40
+
41
+ Keep the credential out of `heyo-docs.config.ts`. The Cloudflare overlay reads
42
+ the native Worker `env.OPENAI_API_KEY` binding in the AI route, after the
43
+ request reaches the Worker. It is never part of the documentation build or the
44
+ browser bundle.
45
+
33
46
  <Callout type="tip" title="Set the canonical URL">
34
47
  Add the final public origin as <code>siteUrl</code> in
35
48
  <code>heyo-docs.config.ts</code> before the first production release. It keeps