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.
- package/dist/cli/index.js +1921 -560
- package/dist/cli/index.js.map +36 -24
- package/dist/types/core/data.d.ts +16 -0
- package/dist/types/core/define-components.d.ts +9 -2
- package/dist/types/core/diagnostics.d.ts +5 -0
- package/dist/types/core/schema.d.ts +26 -502
- package/dist/types/core/types.d.ts +2 -2
- package/docs/02-deployment.mdx +21 -2
- package/docs/advanced/custom-pages.mdx +63 -1
- package/docs/configuration/ai.mdx +20 -3
- package/docs/configuration/customization.mdx +103 -5
- package/docs/configuration/index.mdx +13 -0
- package/docs/configuration/seo.mdx +5 -0
- package/docs/content/islands.mdx +73 -0
- package/docs/content/navigation.mdx +25 -0
- package/docs/index.mdx +3 -12
- package/docs/reference/cli.mdx +42 -0
- package/package.json +3 -1
- package/src/ai/ask-context.ts +131 -0
- package/src/ai/ask-data.ts +25 -0
- package/src/astro/component-slots.ts +165 -0
- package/src/astro/generate.ts +132 -13
- package/src/astro/integration.ts +59 -0
- package/src/astro/pages.ts +5 -12
- package/src/astro/templates.ts +92 -44
- package/src/blume-modules.d.ts +25 -0
- package/src/cli/commands/build.ts +186 -1
- package/src/cli/commands/check.ts +62 -0
- package/src/cli/commands/dev.ts +21 -1
- package/src/cli/commands/doctor.ts +23 -6
- package/src/cli/commands/init.ts +163 -15
- package/src/cli/commands/validate.ts +16 -2
- package/src/cli/index.ts +15 -0
- package/src/cli/internal-error.ts +63 -0
- package/src/cli/log.ts +30 -1
- package/src/cli/prepare.ts +17 -3
- package/src/cli/required-secrets.ts +44 -0
- package/src/components/BlumePage.astro +107 -0
- package/src/components/index.ts +3 -3
- package/src/components/islands/ask-ai.tsx +15 -1
- package/src/components/islands/hooks.ts +188 -0
- package/src/components/layout/Empty.astro +6 -0
- package/src/components/layout/Header.astro +24 -39
- package/src/components/layout/Logo.astro +50 -0
- package/src/components/layout/NavSelector.astro +75 -0
- package/src/components/layout/PageLayout.astro +38 -2
- package/src/components/layout/RootLayout.astro +70 -4
- package/src/components/layout/hydration-hint.ts +30 -0
- package/src/components/layout/overrides.ts +6 -4
- package/src/components/props.ts +68 -0
- package/src/core/builtin-tags.ts +39 -0
- package/src/core/component-diagnostics.ts +44 -0
- package/src/core/component-overrides.ts +478 -0
- package/src/core/config.ts +8 -0
- package/src/core/data.ts +14 -0
- package/src/core/define-components.ts +9 -2
- package/src/core/diagnostics.ts +90 -1
- package/src/core/graph.ts +7 -0
- package/src/core/nav-diagnostics.ts +205 -0
- package/src/core/project-graph.ts +40 -1
- package/src/core/schema.ts +28 -96
- package/src/core/sources/normalize.ts +51 -0
- package/src/core/types.ts +2 -2
- package/src/deploy/redirects.ts +43 -0
- package/src/migrate/mintlify/config.ts +1 -176
- package/src/migrate/starlight/config.ts +0 -4
- package/src/og/card.ts +163 -38
- package/src/registry/eject.ts +39 -9
- package/src/registry/registry.ts +166 -0
- package/src/runtime/index.ts +61 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
129
|
+
const header = container({
|
|
40
130
|
children: [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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:
|
|
77
|
-
color:
|
|
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:
|
|
207
|
+
padding: 72,
|
|
83
208
|
width: WIDTH,
|
|
84
209
|
},
|
|
85
210
|
});
|
package/src/registry/eject.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|