blume 0.2.0 → 0.3.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 (71) hide show
  1. package/dist/cli/index.js +1921 -560
  2. package/dist/cli/index.js.map +36 -24
  3. package/dist/types/core/data.d.ts +16 -0
  4. package/dist/types/core/define-components.d.ts +9 -2
  5. package/dist/types/core/diagnostics.d.ts +5 -0
  6. package/dist/types/core/schema.d.ts +26 -502
  7. package/dist/types/core/types.d.ts +2 -2
  8. package/docs/02-deployment.mdx +21 -2
  9. package/docs/advanced/custom-pages.mdx +63 -1
  10. package/docs/configuration/ai.mdx +20 -3
  11. package/docs/configuration/customization.mdx +103 -5
  12. package/docs/configuration/index.mdx +13 -0
  13. package/docs/configuration/seo.mdx +5 -0
  14. package/docs/content/islands.mdx +73 -0
  15. package/docs/content/navigation.mdx +25 -0
  16. package/docs/index.mdx +3 -12
  17. package/docs/reference/cli.mdx +42 -0
  18. package/package.json +3 -1
  19. package/src/ai/ask-context.ts +131 -0
  20. package/src/ai/ask-data.ts +25 -0
  21. package/src/astro/component-slots.ts +165 -0
  22. package/src/astro/generate.ts +132 -13
  23. package/src/astro/integration.ts +59 -0
  24. package/src/astro/pages.ts +5 -12
  25. package/src/astro/templates.ts +92 -44
  26. package/src/blume-modules.d.ts +25 -0
  27. package/src/cli/commands/build.ts +186 -1
  28. package/src/cli/commands/check.ts +62 -0
  29. package/src/cli/commands/dev.ts +21 -1
  30. package/src/cli/commands/doctor.ts +23 -6
  31. package/src/cli/commands/init.ts +163 -15
  32. package/src/cli/commands/validate.ts +16 -2
  33. package/src/cli/index.ts +15 -0
  34. package/src/cli/internal-error.ts +63 -0
  35. package/src/cli/log.ts +30 -1
  36. package/src/cli/prepare.ts +17 -3
  37. package/src/cli/required-secrets.ts +44 -0
  38. package/src/components/BlumePage.astro +107 -0
  39. package/src/components/index.ts +3 -3
  40. package/src/components/islands/ask-ai.tsx +15 -1
  41. package/src/components/islands/hooks.ts +188 -0
  42. package/src/components/layout/Empty.astro +6 -0
  43. package/src/components/layout/Header.astro +24 -39
  44. package/src/components/layout/Logo.astro +50 -0
  45. package/src/components/layout/NavSelector.astro +75 -0
  46. package/src/components/layout/PageLayout.astro +38 -2
  47. package/src/components/layout/RootLayout.astro +70 -4
  48. package/src/components/layout/hydration-hint.ts +30 -0
  49. package/src/components/layout/overrides.ts +6 -4
  50. package/src/components/props.ts +68 -0
  51. package/src/core/builtin-tags.ts +39 -0
  52. package/src/core/component-diagnostics.ts +44 -0
  53. package/src/core/component-overrides.ts +478 -0
  54. package/src/core/config.ts +8 -0
  55. package/src/core/data.ts +14 -0
  56. package/src/core/define-components.ts +9 -2
  57. package/src/core/diagnostics.ts +90 -1
  58. package/src/core/graph.ts +7 -0
  59. package/src/core/nav-diagnostics.ts +205 -0
  60. package/src/core/project-graph.ts +40 -1
  61. package/src/core/schema.ts +28 -96
  62. package/src/core/sources/normalize.ts +51 -0
  63. package/src/core/types.ts +2 -2
  64. package/src/deploy/redirects.ts +43 -0
  65. package/src/migrate/mintlify/config.ts +1 -176
  66. package/src/migrate/starlight/config.ts +0 -4
  67. package/src/og/card.ts +163 -38
  68. package/src/registry/eject.ts +39 -9
  69. package/src/registry/registry.ts +166 -0
  70. package/src/runtime/index.ts +61 -0
  71. package/src/vite-env.d.ts +14 -0
@@ -579,72 +579,6 @@ const mintlifySelectors = (spec: JsonObject): NavigationSelectors => {
579
579
  ].filter((selector) => selector.items.length > 0);
580
580
  };
581
581
 
582
- const navbarTypeLabel = (type: string | undefined): string | undefined => {
583
- if (type === "github") {
584
- return "GitHub";
585
- }
586
- if (type === "discord") {
587
- return "Discord";
588
- }
589
- return undefined;
590
- };
591
-
592
- const navbarLinkType = (
593
- type: string | undefined
594
- ): "github" | "discord" | undefined =>
595
- type === "github" || type === "discord" ? type : undefined;
596
-
597
- const navbarPrimaryType = (
598
- type: string | undefined
599
- ): "button" | "github" | "discord" =>
600
- type === "github" || type === "discord" ? type : "button";
601
-
602
- const mintlifyNavbar = (value: unknown): NonNullable<BlumeConfig["navbar"]> => {
603
- const object = asObject(value);
604
- if (!object) {
605
- return { links: [] };
606
- }
607
-
608
- const links = asArray(object.links).flatMap((item) => {
609
- const itemObject = asObject(item);
610
- const href = itemObject ? asString(itemObject.href) : undefined;
611
- const type = itemObject ? asString(itemObject.type) : undefined;
612
- const label = itemObject
613
- ? (asString(itemObject.label) ?? navbarTypeLabel(type))
614
- : undefined;
615
- if (!itemObject || !href || !label) {
616
- return [];
617
- }
618
- return [
619
- withoutUndefined({
620
- href,
621
- icon: asString(itemObject.icon),
622
- label,
623
- type: navbarLinkType(type),
624
- }),
625
- ];
626
- });
627
-
628
- const primaryObject = asObject(object.primary);
629
- const primaryHref = primaryObject ? asString(primaryObject.href) : undefined;
630
- const primaryType = primaryObject
631
- ? (asString(primaryObject.type) ?? "button")
632
- : undefined;
633
- const primaryLabel = primaryObject
634
- ? (asString(primaryObject.label) ?? navbarTypeLabel(primaryType))
635
- : undefined;
636
- const primary =
637
- primaryObject && primaryHref && primaryLabel
638
- ? withoutUndefined({
639
- href: primaryHref,
640
- label: primaryLabel,
641
- type: navbarPrimaryType(primaryType),
642
- })
643
- : undefined;
644
-
645
- return withoutUndefined({ links, primary });
646
- };
647
-
648
582
  const mintignorePatterns = async (root: string): Promise<string[]> => {
649
583
  try {
650
584
  const raw = await readFile(resolve(root, ".mintignore"), "utf-8");
@@ -678,78 +612,6 @@ const mintlifyRedirects = (
678
612
  return [{ from, to }];
679
613
  });
680
614
 
681
- const mintlifyContextual = (
682
- value: unknown
683
- ): NonNullable<BlumeConfig["contextual"]> => {
684
- const object = asObject(value);
685
- if (!object) {
686
- return { options: [] };
687
- }
688
-
689
- const display = object.display === "toc" ? "toc" : "header";
690
- const options: NonNullable<BlumeConfig["contextual"]>["options"] = [];
691
- for (const option of asArray(object.options)) {
692
- if (typeof option === "string") {
693
- options.push(option);
694
- continue;
695
- }
696
-
697
- const optionObject = asObject(option);
698
- const title = optionObject ? asString(optionObject.title) : undefined;
699
- if (!optionObject || !title) {
700
- continue;
701
- }
702
-
703
- options.push(
704
- withoutUndefined({
705
- description: asString(optionObject.description),
706
- href: asString(optionObject.href),
707
- icon: asString(optionObject.icon),
708
- title,
709
- })
710
- );
711
- }
712
-
713
- return { display, options };
714
- };
715
-
716
- const mintlifyFooter = (value: unknown): NonNullable<BlumeConfig["footer"]> => {
717
- const object = asObject(value);
718
- const socials = asObject(object?.socials);
719
- const links = asArray(object?.links)
720
- .flatMap((group) => {
721
- const groupObject = asObject(group);
722
- const items = asArray(groupObject?.items).flatMap((item) => {
723
- const itemObject = asObject(item);
724
- const label = itemObject ? asString(itemObject.label) : undefined;
725
- const href = itemObject ? asString(itemObject.href) : undefined;
726
- return label && href ? [{ href, label }] : [];
727
- });
728
- if (!groupObject || items.length === 0) {
729
- return [];
730
- }
731
- return [
732
- withoutUndefined({
733
- header: asString(groupObject.header),
734
- items,
735
- }),
736
- ];
737
- })
738
- .slice(0, 4);
739
-
740
- return {
741
- links,
742
- socials: socials
743
- ? Object.fromEntries(
744
- Object.entries(socials).flatMap(([label, href]) => {
745
- const hrefValue = asString(href);
746
- return hrefValue ? [[label, hrefValue]] : [];
747
- })
748
- )
749
- : {},
750
- };
751
- };
752
-
753
615
  const mintlifyLogo = (value: unknown): BlumeConfig["logo"] => {
754
616
  if (typeof value === "string") {
755
617
  return value;
@@ -826,21 +688,13 @@ const mintlifyChromeVariants = (spec: JsonObject): NavigationChromeVariants => {
826
688
  const banner = hasOwn(object, "banner")
827
689
  ? mintlifyBanner(object.banner)
828
690
  : undefined;
829
- const footer = hasOwn(object, "footer")
830
- ? mintlifyFooter(object.footer)
831
- : undefined;
832
- const navbar = hasOwn(object, "navbar")
833
- ? mintlifyNavbar(object.navbar)
834
- : undefined;
835
- if (!banner && !footer && !navbar) {
691
+ if (!banner) {
836
692
  return [];
837
693
  }
838
694
 
839
695
  return [
840
696
  withoutUndefined({
841
697
  banner,
842
- footer,
843
- navbar,
844
698
  path,
845
699
  }),
846
700
  ];
@@ -914,19 +768,6 @@ const mintlifyMarkdown = (
914
768
  });
915
769
  };
916
770
 
917
- const mintlifyStyling = (
918
- value: unknown
919
- ): NonNullable<BlumeConfig["styling"]> => {
920
- const object = asObject(value);
921
- const eyebrows = asString(object?.eyebrows);
922
- return withoutUndefined({
923
- eyebrows:
924
- eyebrows === "breadcrumbs" || eyebrows === "section"
925
- ? eyebrows
926
- : undefined,
927
- });
928
- };
929
-
930
771
  const mintlifySeo = (value: unknown): NonNullable<BlumeConfig["seo"]> => {
931
772
  const object = asObject(value);
932
773
  const metatags = asObject(object?.metatags);
@@ -942,17 +783,6 @@ const mintlifySeo = (value: unknown): NonNullable<BlumeConfig["seo"]> => {
942
783
  };
943
784
  };
944
785
 
945
- const mintlifyIcons = (value: unknown): NonNullable<BlumeConfig["icons"]> => {
946
- const object = asObject(value);
947
- const library = object?.library;
948
- return withoutUndefined({
949
- library:
950
- library === "fontawesome" || library === "lucide" || library === "tabler"
951
- ? library
952
- : undefined,
953
- });
954
- };
955
-
956
786
  export const loadMintlifyConfig = async (
957
787
  root: string,
958
788
  file: string
@@ -993,14 +823,10 @@ export const loadMintlifyConfig = async (
993
823
  ],
994
824
  root: ".",
995
825
  },
996
- contextual: mintlifyContextual(spec.contextual),
997
826
  description: asString(spec.description),
998
827
  favicon: mintlifyFavicon(spec.favicon),
999
- footer: mintlifyFooter(spec.footer),
1000
- icons: mintlifyIcons(spec.icons),
1001
828
  logo: mintlifyLogo(spec.logo),
1002
829
  markdown: mintlifyMarkdown(spec.markdown, styling),
1003
- navbar: mintlifyNavbar(spec.navbar),
1004
830
  navigation: {
1005
831
  chromeVariants: mintlifyChromeVariants(spec),
1006
832
  selectors: mintlifySelectors(spec),
@@ -1016,7 +842,6 @@ export const loadMintlifyConfig = async (
1016
842
  prompt: asString(search.prompt),
1017
843
  },
1018
844
  seo: mintlifySeo(seo),
1019
- styling: mintlifyStyling(styling),
1020
845
  theme: {
1021
846
  accent: asString(colors.primary) ?? "blue",
1022
847
  accentDark: asString(colors.light),
@@ -425,10 +425,6 @@ export const mapStarlightConfig = (
425
425
  }
426
426
 
427
427
  const social = mapSocial(options.social);
428
- if (Object.keys(social.socials).length > 0) {
429
- config.footer = { socials: social.socials };
430
- }
431
-
432
428
  const github = mapEditLink(options.editLink) ?? social.github;
433
429
  if (github) {
434
430
  config.github = github;
package/src/og/card.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Renderer } from "@takumi-rs/core";
2
- import { container, text } from "@takumi-rs/helpers";
2
+ import { container, image, text } from "@takumi-rs/helpers";
3
+ import type { Node } from "@takumi-rs/helpers";
3
4
 
4
5
  // Reuse one renderer (and its loaded default fonts) across all images.
5
6
  let renderer: Renderer | null = null;
@@ -24,62 +25,186 @@ const resolveAccent = (accent: string): string =>
24
25
  ACCENT_HEX[accent] ?? (accent.startsWith("#") ? accent : "#3b82f6");
25
26
 
26
27
  export interface OgCardOptions {
28
+ /** Large headline — the page title. */
27
29
  title: string;
28
- eyebrow?: string;
30
+ /** Accent color (named preset or hex) for the fallback brand mark. */
29
31
  accent?: string;
32
+ /** Brand/site name shown in the top-left lockup. */
33
+ brand?: string;
34
+ /** Muted subtitle under the headline (usually the site description). */
35
+ description?: string;
36
+ /**
37
+ * Inlined SVG markup of the configured logo (`config.logo.svg`), painted into
38
+ * the brand lockup. Falls back to an accent mark when absent.
39
+ */
40
+ logo?: string;
41
+ /** Footer-left repository slug, e.g. `owner/repo`. */
42
+ repo?: string;
43
+ /** Footer-right site host, e.g. `docs.acme.com`. */
44
+ site?: string;
30
45
  }
31
46
 
32
47
  const WIDTH = 1200;
33
48
  const HEIGHT = 630;
34
49
 
50
+ // Light neutral scale mirrored from the docs homepage theme tokens:
51
+ // FOREGROUND = --foreground, MUTED = --muted-foreground, FAINT = that lighter,
52
+ // BORDER = --border.
53
+ const BG = "#fafafa";
54
+ const FOREGROUND = "#0a0a0a";
55
+ const MUTED = "#737373";
56
+ const FAINT = "#a3a3a3";
57
+ const BORDER = "#e5e5e5";
58
+
59
+ const truncate = (value: string, max: number): string =>
60
+ value.length > max ? `${value.slice(0, max - 1).trimEnd()}…` : value;
61
+
62
+ // Brand mark sizing: target this height, but scale down so a wide wordmark logo
63
+ // stays within the lockup.
64
+ const MARK_HEIGHT = 32;
65
+ const MARK_MAX_WIDTH = 100;
66
+ const VIEW_BOX = /viewBox="0 0 (?<w>[\d.]+) (?<h>[\d.]+)"/u;
67
+
68
+ // Render the configured logo as the brand mark. A `currentColor` logo carries
69
+ // no intrinsic color, so it is painted in the foreground to read on the light
70
+ // card, then handed to Takumi as a data URI sized from the SVG's aspect ratio.
71
+ const logoMark = (svg: string): Node => {
72
+ const painted = svg.replaceAll("currentColor", FOREGROUND);
73
+ const box = painted.match(VIEW_BOX);
74
+ const w = Number(box?.groups?.w);
75
+ const h = Number(box?.groups?.h);
76
+ let height = MARK_HEIGHT;
77
+ let width = w && h ? (MARK_HEIGHT * w) / h : MARK_HEIGHT;
78
+ if (width > MARK_MAX_WIDTH) {
79
+ height = w && h ? (MARK_MAX_WIDTH * h) / w : MARK_HEIGHT;
80
+ width = MARK_MAX_WIDTH;
81
+ }
82
+ return image({
83
+ height: Math.round(height),
84
+ src: `data:image/svg+xml;base64,${Buffer.from(painted).toString("base64")}`,
85
+ width: Math.round(width),
86
+ });
87
+ };
88
+
89
+ // Fallback mark when no SVG logo is configured: an accent tile with the brand's
90
+ // initial, matching the docs favicon aesthetic.
91
+ const initialMark = (accent: string, initial: string): Node =>
92
+ container({
93
+ children: initial
94
+ ? [text(initial, { color: "#ffffff", fontSize: 32, fontWeight: 600 })]
95
+ : [],
96
+ style: {
97
+ alignItems: "center",
98
+ backgroundColor: accent,
99
+ borderRadius: 14,
100
+ display: "flex",
101
+ height: 60,
102
+ justifyContent: "center",
103
+ width: 60,
104
+ },
105
+ });
106
+
107
+ // The headline shrinks as the title grows so it never spills past a couple of
108
+ // lines within the card's content width.
109
+ const titleSize = (title: string): number => {
110
+ if (title.length > 60) {
111
+ return 52;
112
+ }
113
+ if (title.length > 40) {
114
+ return 64;
115
+ }
116
+ return 76;
117
+ };
118
+
35
119
  /** Render a 1200x630 Open Graph card to a PNG buffer. */
36
120
  export const renderOgImage = (options: OgCardOptions): Promise<Buffer> => {
37
121
  const accent = resolveAccent(options.accent ?? "blue");
122
+ const brand = options.brand?.trim();
123
+ const logo = options.logo?.trim();
124
+ const initial = brand ? brand.charAt(0).toUpperCase() : "";
125
+ const description = options.description?.trim();
126
+ const repo = options.repo?.trim();
127
+ const site = options.site?.trim();
38
128
 
39
- const node = container({
129
+ const header = container({
40
130
  children: [
41
- container({
42
- children: [
43
- container({
44
- style: {
45
- backgroundColor: accent,
46
- borderRadius: 6,
47
- height: 32,
48
- width: 32,
49
- },
50
- }),
51
- options.eyebrow
52
- ? text(options.eyebrow, {
53
- color: "#94a3b8",
54
- fontSize: 30,
55
- })
56
- : container({}),
57
- ],
58
- style: { alignItems: "center", display: "flex", gap: 16 },
59
- }),
60
- text(options.title, {
61
- color: "#f8fafc",
62
- fontSize: 76,
63
- fontWeight: 700,
64
- lineHeight: 1.1,
65
- }),
66
- container({
67
- style: {
68
- backgroundColor: accent,
69
- borderRadius: 4,
70
- height: 8,
71
- width: 120,
72
- },
131
+ logo ? logoMark(logo) : initialMark(accent, initial),
132
+ brand
133
+ ? text(brand, {
134
+ color: FOREGROUND,
135
+ fontSize: 30,
136
+ fontWeight: 500,
137
+ letterSpacing: "-0.01em",
138
+ })
139
+ : container({}),
140
+ ],
141
+ style: { alignItems: "center", display: "flex", gap: 18 },
142
+ });
143
+
144
+ const body = container({
145
+ children: [
146
+ text(truncate(options.title, 64), {
147
+ color: FOREGROUND,
148
+ fontSize: titleSize(options.title),
149
+ fontWeight: 600,
150
+ letterSpacing: "-0.03em",
151
+ lineHeight: 1.05,
152
+ maxWidth: 1010,
153
+ textWrap: "balance",
73
154
  }),
155
+ description
156
+ ? text(truncate(description, 140), {
157
+ color: MUTED,
158
+ fontSize: 30,
159
+ lineHeight: 1.4,
160
+ marginTop: 28,
161
+ maxWidth: 900,
162
+ textWrap: "balance",
163
+ })
164
+ : container({}),
74
165
  ],
166
+ style: { display: "flex", flexDirection: "column" },
167
+ });
168
+
169
+ const footer =
170
+ repo || site
171
+ ? container({
172
+ children: [
173
+ container({
174
+ style: { backgroundColor: BORDER, height: 1, width: "100%" },
175
+ }),
176
+ container({
177
+ children: [
178
+ repo
179
+ ? text(repo, { color: MUTED, fontSize: 22 })
180
+ : container({}),
181
+ site
182
+ ? text(site, { color: FAINT, fontSize: 22 })
183
+ : container({}),
184
+ ],
185
+ style: {
186
+ alignItems: "center",
187
+ display: "flex",
188
+ justifyContent: "space-between",
189
+ marginTop: 28,
190
+ width: "100%",
191
+ },
192
+ }),
193
+ ],
194
+ style: { display: "flex", flexDirection: "column", width: "100%" },
195
+ })
196
+ : container({});
197
+
198
+ const node = container({
199
+ children: [header, body, footer],
75
200
  style: {
76
- backgroundColor: "#0b1020",
77
- color: "#ffffff",
201
+ backgroundColor: BG,
202
+ color: FOREGROUND,
78
203
  display: "flex",
79
204
  flexDirection: "column",
80
205
  height: HEIGHT,
81
206
  justifyContent: "space-between",
82
- padding: 80,
207
+ padding: 72,
83
208
  width: WIDTH,
84
209
  },
85
210
  });
@@ -3,8 +3,10 @@ import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
 
4
4
  import { join, relative } from "pathe";
5
5
 
6
+ import { buildAskData } from "../ai/ask-data.ts";
6
7
  import { resolveAskBackend } from "../ai/ask.ts";
7
8
  import { buildRawMarkdown } from "../ai/markdown.ts";
9
+ import { planComponentSlots } from "../astro/component-slots.ts";
8
10
  import { discoverExamples } from "../astro/examples.ts";
9
11
  import {
10
12
  buildRuntimeData,
@@ -32,9 +34,9 @@ import {
32
34
  runtimeTsconfigTemplate,
33
35
  searchClientTemplate,
34
36
  searchEndpointTemplate,
35
- userComponentsTemplate,
36
37
  } from "../astro/templates.ts";
37
38
  import { scanProject } from "../core/project-graph.ts";
39
+ import type { BlumeProject } from "../core/project-graph.ts";
38
40
  import type { ProjectContext } from "../core/types.ts";
39
41
  import { buildRssFeeds, renderRssFeed } from "../deploy/rss.ts";
40
42
  import { buildReferenceFiles, hasReferences } from "../openapi/scalar.ts";
@@ -46,6 +48,35 @@ import { twoslashCss } from "../theme/twoslash.ts";
46
48
 
47
49
  const POSIX = (path: string): string => path.split("\\").join("/");
48
50
 
51
+ /**
52
+ * The Ask AI endpoint plus, unless the backend runs its own retrieval (Inkeep),
53
+ * its grounding snapshot. Empty when Ask AI is disabled.
54
+ */
55
+ const askFiles = async (
56
+ project: BlumeProject,
57
+ srcDir: string,
58
+ genDir: string
59
+ ): Promise<{ content: string; path: string }[]> => {
60
+ const { ask } = project.config.ai;
61
+ if (!ask?.enabled) {
62
+ return [];
63
+ }
64
+ const grounded = ask.provider !== "inkeep";
65
+ const files = [
66
+ {
67
+ content: askEndpointTemplate(resolveAskBackend(ask), grounded),
68
+ path: join(srcDir, "pages", "api", "ask.ts"),
69
+ },
70
+ ];
71
+ if (grounded) {
72
+ files.push({
73
+ content: `${JSON.stringify(await buildAskData(project))}\n`,
74
+ path: join(genDir, "ask-data.json"),
75
+ });
76
+ }
77
+ return files;
78
+ };
79
+
49
80
  /**
50
81
  * Promote the generated runtime into the project as an owned Astro app. After
51
82
  * eject the project has a normal `astro.config.mjs` and `src/`, the `blume` CLI
@@ -146,11 +177,15 @@ export const eject = async (root: string): Promise<string[]> => {
146
177
  exportEpub,
147
178
  exportPdf,
148
179
  mathEnabled: config.markdown.math,
180
+ needsReact,
149
181
  }),
150
182
  path: join(srcDir, "pages", "[...slug].astro"),
151
183
  },
152
184
  {
153
- content: userComponentsTemplate(componentsImport),
185
+ // Eject keeps the portable re-export form (relative import to the user's
186
+ // components file); hydration/island wrappers would need machine-specific
187
+ // absolute paths, so the ejected app owns and wires those itself.
188
+ content: planComponentSlots(componentsImport, null).module,
154
189
  path: join(genDir, "components.ts"),
155
190
  },
156
191
  // Island/example maps the catch-all imports; written even when empty so the
@@ -192,17 +227,12 @@ export const eject = async (root: string): Promise<string[]> => {
192
227
  ];
193
228
 
194
229
  if (askEnabled) {
195
- files.push({
196
- content: askEndpointTemplate(resolveAskBackend(config.ai.ask)),
197
- path: join(srcDir, "pages", "api", "ask.ts"),
198
- });
230
+ files.push(...(await askFiles(project, srcDir, genDir)));
199
231
  }
200
232
 
201
233
  if (config.seo.og.enabled) {
202
234
  files.push({
203
- content: ogEndpointTemplate(
204
- customOgRoutes(pages, config.title, config.description)
205
- ),
235
+ content: ogEndpointTemplate(customOgRoutes(pages, config.title)),
206
236
  path: join(srcDir, "pages", "og", "[...slug].png.ts"),
207
237
  });
208
238
  }