blume 0.2.0 → 0.4.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 +2429 -792
- package/dist/cli/index.js.map +63 -44
- 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 +313 -778
- package/dist/types/core/types.d.ts +2 -2
- package/dist/types/migrate/mintlify/assets.d.ts +8 -0
- package/docs/01-quickstart.mdx +5 -16
- package/docs/02-deployment.mdx +26 -40
- package/docs/advanced/api-reference.mdx +10 -37
- package/docs/advanced/blog.mdx +9 -25
- package/docs/advanced/changelog.mdx +10 -33
- package/docs/advanced/custom-pages.mdx +66 -61
- package/docs/configuration/ai.mdx +47 -91
- package/docs/configuration/analytics.mdx +20 -38
- package/docs/configuration/customization.mdx +92 -27
- package/docs/configuration/export.mdx +9 -34
- package/docs/configuration/index.mdx +78 -85
- package/docs/configuration/search.mdx +17 -54
- package/docs/configuration/seo.mdx +18 -44
- package/docs/configuration/theming.mdx +20 -42
- package/docs/content/components.mdx +42 -101
- package/docs/content/i18n.mdx +21 -72
- package/docs/content/index.mdx +18 -48
- package/docs/content/islands.mdx +79 -33
- package/docs/content/meta.mdx +23 -50
- package/docs/content/navigation.mdx +42 -56
- package/docs/content/sources.mdx +20 -83
- package/docs/content/syntax.mdx +37 -105
- package/docs/index.mdx +13 -51
- package/docs/reference/cli.mdx +49 -18
- package/docs/reference/frontmatter.mdx +2 -5
- 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 +85 -3
- package/src/astro/islands.ts +6 -2
- package/src/astro/markdown-negotiation.ts +17 -3
- package/src/astro/pages.ts +11 -13
- package/src/astro/static-assets.ts +117 -0
- package/src/astro/templates.ts +120 -50
- package/src/blume-modules.d.ts +25 -0
- package/src/cli/args.ts +23 -0
- package/src/cli/commands/build.ts +209 -1
- package/src/cli/commands/check.ts +62 -0
- package/src/cli/commands/dev.ts +32 -3
- package/src/cli/commands/doctor.ts +32 -6
- package/src/cli/commands/eject.ts +3 -1
- package/src/cli/commands/init.ts +184 -16
- package/src/cli/commands/preview.ts +2 -1
- package/src/cli/commands/validate.ts +27 -2
- package/src/cli/dev-lock.ts +84 -0
- package/src/cli/index.ts +15 -0
- package/src/cli/internal-error.ts +63 -0
- package/src/cli/log.ts +41 -1
- package/src/cli/prepare.ts +17 -3
- package/src/cli/required-secrets.ts +44 -0
- package/src/components/BlumePage.astro +109 -0
- package/src/components/content/YouTube.astro +35 -0
- package/src/components/content/youtube.ts +46 -0
- package/src/components/index.ts +3 -3
- package/src/components/islands/ask-ai.tsx +29 -15
- 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 +71 -0
- package/src/core/assets.ts +31 -0
- package/src/core/bridge.ts +10 -0
- package/src/core/builtin-tags.ts +40 -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 +95 -1
- package/src/core/gitignore.ts +30 -0
- package/src/core/graph.ts +7 -0
- package/src/core/links.ts +60 -19
- package/src/core/nav-diagnostics.ts +205 -0
- package/src/core/project-graph.ts +40 -1
- package/src/core/schema.ts +35 -96
- package/src/core/sources/mdx-remote.ts +54 -8
- package/src/core/sources/normalize.ts +57 -1
- package/src/core/sources/notion.ts +49 -5
- package/src/core/sources/sanity.ts +5 -1
- package/src/core/types.ts +2 -2
- package/src/deploy/redirects.ts +43 -0
- package/src/deploy/rss.ts +1 -8
- package/src/deploy/sitemap.ts +20 -1
- package/src/deploy/xml.ts +8 -0
- package/src/markdown/directives.ts +15 -7
- package/src/markdown/package-commands.ts +26 -4
- package/src/migrate/fumadocs/content.ts +14 -1
- package/src/migrate/fumadocs/groups.ts +7 -0
- package/src/migrate/fumadocs/index.ts +5 -2
- package/src/migrate/mintlify/assets.ts +46 -0
- package/src/migrate/mintlify/config.ts +1 -176
- package/src/migrate/mintlify/index.ts +53 -45
- package/src/migrate/shared.ts +12 -27
- package/src/migrate/starlight/config.ts +0 -4
- package/src/og/card.ts +175 -38
- package/src/registry/eject.ts +52 -12
- package/src/registry/registry.ts +172 -0
- package/src/registry/rewrite-imports.ts +31 -19
- package/src/runtime/index.ts +61 -0
- package/src/search/documents.ts +23 -5
- package/src/search/sync/algolia.ts +5 -1
- package/src/search/sync/typesense.ts +24 -16
- package/src/theme/palette.ts +26 -7
- package/src/vite-env.d.ts +14 -0
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,198 @@ 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
|
+
/**
|
|
60
|
+
* Truncate to `max` code points with an ellipsis. Slices by code points, not
|
|
61
|
+
* UTF-16 units, so cutting mid-emoji doesn't leave a lone surrogate (a broken
|
|
62
|
+
* glyph) before the ellipsis.
|
|
63
|
+
*/
|
|
64
|
+
export const truncate = (value: string, max: number): string => {
|
|
65
|
+
const chars = [...value];
|
|
66
|
+
return chars.length > max
|
|
67
|
+
? `${chars
|
|
68
|
+
.slice(0, max - 1)
|
|
69
|
+
.join("")
|
|
70
|
+
.trimEnd()}…`
|
|
71
|
+
: value;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Brand mark sizing: target this height, but scale down so a wide wordmark logo
|
|
75
|
+
// stays within the lockup.
|
|
76
|
+
const MARK_HEIGHT = 32;
|
|
77
|
+
const MARK_MAX_WIDTH = 100;
|
|
78
|
+
const VIEW_BOX = /viewBox="0 0 (?<w>[\d.]+) (?<h>[\d.]+)"/u;
|
|
79
|
+
|
|
80
|
+
// Render the configured logo as the brand mark. A `currentColor` logo carries
|
|
81
|
+
// no intrinsic color, so it is painted in the foreground to read on the light
|
|
82
|
+
// card, then handed to Takumi as a data URI sized from the SVG's aspect ratio.
|
|
83
|
+
const logoMark = (svg: string): Node => {
|
|
84
|
+
const painted = svg.replaceAll("currentColor", FOREGROUND);
|
|
85
|
+
const box = painted.match(VIEW_BOX);
|
|
86
|
+
const w = Number(box?.groups?.w);
|
|
87
|
+
const h = Number(box?.groups?.h);
|
|
88
|
+
let height = MARK_HEIGHT;
|
|
89
|
+
let width = w && h ? (MARK_HEIGHT * w) / h : MARK_HEIGHT;
|
|
90
|
+
if (width > MARK_MAX_WIDTH) {
|
|
91
|
+
height = w && h ? (MARK_MAX_WIDTH * h) / w : MARK_HEIGHT;
|
|
92
|
+
width = MARK_MAX_WIDTH;
|
|
93
|
+
}
|
|
94
|
+
return image({
|
|
95
|
+
height: Math.round(height),
|
|
96
|
+
src: `data:image/svg+xml;base64,${Buffer.from(painted).toString("base64")}`,
|
|
97
|
+
width: Math.round(width),
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Fallback mark when no SVG logo is configured: an accent tile with the brand's
|
|
102
|
+
// initial, matching the docs favicon aesthetic.
|
|
103
|
+
const initialMark = (accent: string, initial: string): Node =>
|
|
104
|
+
container({
|
|
105
|
+
children: initial
|
|
106
|
+
? [text(initial, { color: "#ffffff", fontSize: 32, fontWeight: 600 })]
|
|
107
|
+
: [],
|
|
108
|
+
style: {
|
|
109
|
+
alignItems: "center",
|
|
110
|
+
backgroundColor: accent,
|
|
111
|
+
borderRadius: 14,
|
|
112
|
+
display: "flex",
|
|
113
|
+
height: 60,
|
|
114
|
+
justifyContent: "center",
|
|
115
|
+
width: 60,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// The headline shrinks as the title grows so it never spills past a couple of
|
|
120
|
+
// lines within the card's content width.
|
|
121
|
+
const titleSize = (title: string): number => {
|
|
122
|
+
if (title.length > 60) {
|
|
123
|
+
return 52;
|
|
124
|
+
}
|
|
125
|
+
if (title.length > 40) {
|
|
126
|
+
return 64;
|
|
127
|
+
}
|
|
128
|
+
return 76;
|
|
129
|
+
};
|
|
130
|
+
|
|
35
131
|
/** Render a 1200x630 Open Graph card to a PNG buffer. */
|
|
36
132
|
export const renderOgImage = (options: OgCardOptions): Promise<Buffer> => {
|
|
37
133
|
const accent = resolveAccent(options.accent ?? "blue");
|
|
134
|
+
const brand = options.brand?.trim();
|
|
135
|
+
const logo = options.logo?.trim();
|
|
136
|
+
const initial = brand ? brand.charAt(0).toUpperCase() : "";
|
|
137
|
+
const description = options.description?.trim();
|
|
138
|
+
const repo = options.repo?.trim();
|
|
139
|
+
const site = options.site?.trim();
|
|
38
140
|
|
|
39
|
-
const
|
|
141
|
+
const header = container({
|
|
40
142
|
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
|
-
},
|
|
143
|
+
logo ? logoMark(logo) : initialMark(accent, initial),
|
|
144
|
+
brand
|
|
145
|
+
? text(brand, {
|
|
146
|
+
color: FOREGROUND,
|
|
147
|
+
fontSize: 30,
|
|
148
|
+
fontWeight: 500,
|
|
149
|
+
letterSpacing: "-0.01em",
|
|
150
|
+
})
|
|
151
|
+
: container({}),
|
|
152
|
+
],
|
|
153
|
+
style: { alignItems: "center", display: "flex", gap: 18 },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const body = container({
|
|
157
|
+
children: [
|
|
158
|
+
text(truncate(options.title, 64), {
|
|
159
|
+
color: FOREGROUND,
|
|
160
|
+
fontSize: titleSize(options.title),
|
|
161
|
+
fontWeight: 600,
|
|
162
|
+
letterSpacing: "-0.03em",
|
|
163
|
+
lineHeight: 1.05,
|
|
164
|
+
maxWidth: 1010,
|
|
165
|
+
textWrap: "balance",
|
|
73
166
|
}),
|
|
167
|
+
description
|
|
168
|
+
? text(truncate(description, 140), {
|
|
169
|
+
color: MUTED,
|
|
170
|
+
fontSize: 30,
|
|
171
|
+
lineHeight: 1.4,
|
|
172
|
+
marginTop: 28,
|
|
173
|
+
maxWidth: 900,
|
|
174
|
+
textWrap: "balance",
|
|
175
|
+
})
|
|
176
|
+
: container({}),
|
|
74
177
|
],
|
|
178
|
+
style: { display: "flex", flexDirection: "column" },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const footer =
|
|
182
|
+
repo || site
|
|
183
|
+
? container({
|
|
184
|
+
children: [
|
|
185
|
+
container({
|
|
186
|
+
style: { backgroundColor: BORDER, height: 1, width: "100%" },
|
|
187
|
+
}),
|
|
188
|
+
container({
|
|
189
|
+
children: [
|
|
190
|
+
repo
|
|
191
|
+
? text(repo, { color: MUTED, fontSize: 22 })
|
|
192
|
+
: container({}),
|
|
193
|
+
site
|
|
194
|
+
? text(site, { color: FAINT, fontSize: 22 })
|
|
195
|
+
: container({}),
|
|
196
|
+
],
|
|
197
|
+
style: {
|
|
198
|
+
alignItems: "center",
|
|
199
|
+
display: "flex",
|
|
200
|
+
justifyContent: "space-between",
|
|
201
|
+
marginTop: 28,
|
|
202
|
+
width: "100%",
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
],
|
|
206
|
+
style: { display: "flex", flexDirection: "column", width: "100%" },
|
|
207
|
+
})
|
|
208
|
+
: container({});
|
|
209
|
+
|
|
210
|
+
const node = container({
|
|
211
|
+
children: [header, body, footer],
|
|
75
212
|
style: {
|
|
76
|
-
backgroundColor:
|
|
77
|
-
color:
|
|
213
|
+
backgroundColor: BG,
|
|
214
|
+
color: FOREGROUND,
|
|
78
215
|
display: "flex",
|
|
79
216
|
flexDirection: "column",
|
|
80
217
|
height: HEIGHT,
|
|
81
218
|
justifyContent: "space-between",
|
|
82
|
-
padding:
|
|
219
|
+
padding: 72,
|
|
83
220
|
width: WIDTH,
|
|
84
221
|
},
|
|
85
222
|
});
|
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
|
|
@@ -109,7 +140,12 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
109
140
|
const hasStaged = staged.size > 0;
|
|
110
141
|
const stagedDir = "blume-staged";
|
|
111
142
|
|
|
112
|
-
const files: {
|
|
143
|
+
const files: {
|
|
144
|
+
path: string;
|
|
145
|
+
content: string;
|
|
146
|
+
/** Don't overwrite a file the user already owns (e.g. a tuned tsconfig). */
|
|
147
|
+
skipIfExists?: boolean;
|
|
148
|
+
}[] = [
|
|
113
149
|
{
|
|
114
150
|
content: astroConfigTemplate({
|
|
115
151
|
config,
|
|
@@ -129,6 +165,8 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
129
165
|
{
|
|
130
166
|
content: runtimeTsconfigTemplate(),
|
|
131
167
|
path: join(root, "tsconfig.json"),
|
|
168
|
+
// Never clobber a hand-tuned tsconfig; only write ours if none exists.
|
|
169
|
+
skipIfExists: true,
|
|
132
170
|
},
|
|
133
171
|
{ content: envTemplate(), path: join(srcDir, "env.d.ts") },
|
|
134
172
|
{
|
|
@@ -146,11 +184,15 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
146
184
|
exportEpub,
|
|
147
185
|
exportPdf,
|
|
148
186
|
mathEnabled: config.markdown.math,
|
|
187
|
+
needsReact,
|
|
149
188
|
}),
|
|
150
189
|
path: join(srcDir, "pages", "[...slug].astro"),
|
|
151
190
|
},
|
|
152
191
|
{
|
|
153
|
-
|
|
192
|
+
// Eject keeps the portable re-export form (relative import to the user's
|
|
193
|
+
// components file); hydration/island wrappers would need machine-specific
|
|
194
|
+
// absolute paths, so the ejected app owns and wires those itself.
|
|
195
|
+
content: planComponentSlots(componentsImport, null).module,
|
|
154
196
|
path: join(genDir, "components.ts"),
|
|
155
197
|
},
|
|
156
198
|
// Island/example maps the catch-all imports; written even when empty so the
|
|
@@ -192,17 +234,12 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
192
234
|
];
|
|
193
235
|
|
|
194
236
|
if (askEnabled) {
|
|
195
|
-
files.push(
|
|
196
|
-
content: askEndpointTemplate(resolveAskBackend(config.ai.ask)),
|
|
197
|
-
path: join(srcDir, "pages", "api", "ask.ts"),
|
|
198
|
-
});
|
|
237
|
+
files.push(...(await askFiles(project, srcDir, genDir)));
|
|
199
238
|
}
|
|
200
239
|
|
|
201
240
|
if (config.seo.og.enabled) {
|
|
202
241
|
files.push({
|
|
203
|
-
content: ogEndpointTemplate(
|
|
204
|
-
customOgRoutes(pages, config.title, config.description)
|
|
205
|
-
),
|
|
242
|
+
content: ogEndpointTemplate(customOgRoutes(pages, config.title)),
|
|
206
243
|
path: join(srcDir, "pages", "og", "[...slug].png.ts"),
|
|
207
244
|
});
|
|
208
245
|
}
|
|
@@ -297,8 +334,11 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
297
334
|
files.push({ content, path: join(root, stagedDir, entryId) });
|
|
298
335
|
}
|
|
299
336
|
|
|
337
|
+
const written = files.filter(
|
|
338
|
+
(file) => !(file.skipIfExists && existsSync(file.path))
|
|
339
|
+
);
|
|
300
340
|
await Promise.all(
|
|
301
|
-
|
|
341
|
+
written.map(async (file) => {
|
|
302
342
|
await mkdir(join(file.path, ".."), { recursive: true });
|
|
303
343
|
await writeFile(file.path, file.content, "utf-8");
|
|
304
344
|
})
|
|
@@ -317,5 +357,5 @@ export const eject = async (root: string): Promise<string[]> => {
|
|
|
317
357
|
// The hidden runtime is no longer the source of truth.
|
|
318
358
|
await rm(context.outDir, { force: true, recursive: true });
|
|
319
359
|
|
|
320
|
-
return
|
|
360
|
+
return written.map((file) => file.path);
|
|
321
361
|
};
|
package/src/registry/registry.ts
CHANGED
|
@@ -62,6 +62,171 @@ const layoutComponent = (config: {
|
|
|
62
62
|
};
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* A built-in MDX content component offered as editable source. Like
|
|
67
|
+
* {@link layoutComponent}, but registered under the `mdx` map (the tag you write
|
|
68
|
+
* in `.mdx`) rather than a `layout` slot.
|
|
69
|
+
*/
|
|
70
|
+
const contentComponent = (config: {
|
|
71
|
+
name: string;
|
|
72
|
+
description: string;
|
|
73
|
+
/** Source basename under `src/components/content`. */
|
|
74
|
+
file: string;
|
|
75
|
+
/** MDX tag / component name, also the import name in the post-install hint. */
|
|
76
|
+
tag: string;
|
|
77
|
+
}): RegistryItem => {
|
|
78
|
+
const target = `components/blume/${config.file}`;
|
|
79
|
+
return {
|
|
80
|
+
description: config.description,
|
|
81
|
+
files: [
|
|
82
|
+
{
|
|
83
|
+
rewrite: true,
|
|
84
|
+
source: `components/content/${config.file}`,
|
|
85
|
+
target,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
name: config.name,
|
|
89
|
+
postInstall: [
|
|
90
|
+
"Register it in components.ts:",
|
|
91
|
+
' import { defineComponents } from "blume";',
|
|
92
|
+
` import ${config.tag} from "./${target}";`,
|
|
93
|
+
"",
|
|
94
|
+
` export default defineComponents({ mdx: { ${config.tag} } });`,
|
|
95
|
+
"",
|
|
96
|
+
"It imports the rest from `blume/*`, so it matches the built-in until you edit it.",
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/** Every user-facing content component, by `blume add` name → source basename. */
|
|
102
|
+
const CONTENT_COMPONENTS: {
|
|
103
|
+
name: string;
|
|
104
|
+
description: string;
|
|
105
|
+
file: string;
|
|
106
|
+
tag: string;
|
|
107
|
+
}[] = [
|
|
108
|
+
{
|
|
109
|
+
description: "Aside for notes, tips, and warnings.",
|
|
110
|
+
file: "Callout.astro",
|
|
111
|
+
name: "callout",
|
|
112
|
+
tag: "Callout",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
description: "A linkable card with icon, title, and body.",
|
|
116
|
+
file: "Card.astro",
|
|
117
|
+
name: "card",
|
|
118
|
+
tag: "Card",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
description: "A responsive grid of cards.",
|
|
122
|
+
file: "CardGroup.astro",
|
|
123
|
+
name: "card-group",
|
|
124
|
+
tag: "CardGroup",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
description: "Tabbed code blocks for multiple languages.",
|
|
128
|
+
file: "CodeGroup.astro",
|
|
129
|
+
name: "code-group",
|
|
130
|
+
tag: "CodeGroup",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
description: "A small status/label badge.",
|
|
134
|
+
file: "Badge.astro",
|
|
135
|
+
name: "badge",
|
|
136
|
+
tag: "Badge",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
description: "A numbered list of steps.",
|
|
140
|
+
file: "Steps.astro",
|
|
141
|
+
name: "steps",
|
|
142
|
+
tag: "Steps",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
description: "A single step within Steps.",
|
|
146
|
+
file: "Step.astro",
|
|
147
|
+
name: "step",
|
|
148
|
+
tag: "Step",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
description: "A tabbed content panel.",
|
|
152
|
+
file: "Tabs.astro",
|
|
153
|
+
name: "tabs",
|
|
154
|
+
tag: "Tabs",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
description: "A single tab within Tabs.",
|
|
158
|
+
file: "Tab.astro",
|
|
159
|
+
name: "tab",
|
|
160
|
+
tag: "Tab",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
description: "A collapsible accordion group.",
|
|
164
|
+
file: "Accordion.astro",
|
|
165
|
+
name: "accordion",
|
|
166
|
+
tag: "Accordion",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
description: "A single item within an Accordion.",
|
|
170
|
+
file: "AccordionItem.astro",
|
|
171
|
+
name: "accordion-item",
|
|
172
|
+
tag: "AccordionItem",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
description: "A multi-column layout.",
|
|
176
|
+
file: "Columns.astro",
|
|
177
|
+
name: "columns",
|
|
178
|
+
tag: "Columns",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
description: "A single column within Columns.",
|
|
182
|
+
file: "Column.astro",
|
|
183
|
+
name: "column",
|
|
184
|
+
tag: "Column",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
description: "A bordered frame around an image or embed.",
|
|
188
|
+
file: "Frame.astro",
|
|
189
|
+
name: "frame",
|
|
190
|
+
tag: "Frame",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
description: "An inline expand/collapse disclosure.",
|
|
194
|
+
file: "Expandable.astro",
|
|
195
|
+
name: "expandable",
|
|
196
|
+
tag: "Expandable",
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
description: "A titled content panel.",
|
|
200
|
+
file: "Panel.astro",
|
|
201
|
+
name: "panel",
|
|
202
|
+
tag: "Panel",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
description: "A hover tooltip.",
|
|
206
|
+
file: "Tooltip.astro",
|
|
207
|
+
name: "tooltip",
|
|
208
|
+
tag: "Tooltip",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
description: "A compact linkable tile.",
|
|
212
|
+
file: "Tile.astro",
|
|
213
|
+
name: "tile",
|
|
214
|
+
tag: "Tile",
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
description: "A styled prompt / terminal block.",
|
|
218
|
+
file: "Prompt.astro",
|
|
219
|
+
name: "prompt",
|
|
220
|
+
tag: "Prompt",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
description: "A responsive, privacy-friendly YouTube embed.",
|
|
224
|
+
file: "YouTube.astro",
|
|
225
|
+
name: "youtube",
|
|
226
|
+
tag: "YouTube",
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
|
|
65
230
|
/** The built-in, Blume-owned source registry. */
|
|
66
231
|
export const registry: RegistryItem[] = [
|
|
67
232
|
layoutComponent({
|
|
@@ -94,6 +259,13 @@ export const registry: RegistryItem[] = [
|
|
|
94
259
|
name: "pagination",
|
|
95
260
|
slot: "Pagination",
|
|
96
261
|
}),
|
|
262
|
+
layoutComponent({
|
|
263
|
+
description: 'The "Was this page helpful?" feedback rating.',
|
|
264
|
+
file: "PageFeedback.astro",
|
|
265
|
+
name: "feedback",
|
|
266
|
+
slot: "Feedback",
|
|
267
|
+
}),
|
|
268
|
+
...CONTENT_COMPONENTS.map(contentComponent),
|
|
97
269
|
];
|
|
98
270
|
|
|
99
271
|
export const findItem = (name: string): RegistryItem | undefined =>
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { dirname, relative, resolve } from "pathe";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
// A relative specifier (starting with `.`) in an `import … from "…"` or
|
|
4
|
+
// `export … from "…"` statement. Anchored to the start of a line (`m` flag) and
|
|
5
|
+
// bounded by `[^;]` so it only matches a real statement — not a `from "./…"`
|
|
6
|
+
// that happens to appear inside a string or JSX text — while still allowing a
|
|
7
|
+
// multiline import body between the keyword and `from`.
|
|
8
|
+
const FROM_IMPORT =
|
|
9
|
+
/(?<prefix>^[ \t]*(?:import|export)\b[^;]*?\bfrom[ \t]*)(?<quote>["'])(?<spec>\.[^"']*)\k<quote>/gmu;
|
|
10
|
+
|
|
11
|
+
// A side-effect `import "./…"` at the start of a line.
|
|
12
|
+
const SIDE_EFFECT_IMPORT =
|
|
13
|
+
/(?<prefix>^[ \t]*import[ \t]+)(?<quote>["'])(?<spec>\.[^"']*)\k<quote>/gmu;
|
|
8
14
|
|
|
9
15
|
/**
|
|
10
16
|
* Rewrite a built-in component's relative imports to `blume/*` package
|
|
@@ -22,18 +28,24 @@ export const rewriteImports = (
|
|
|
22
28
|
content: string,
|
|
23
29
|
sourceFile: string,
|
|
24
30
|
srcRoot: string
|
|
25
|
-
): string =>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
): string => {
|
|
32
|
+
const rewrite = (
|
|
33
|
+
match: string,
|
|
34
|
+
prefix: string,
|
|
35
|
+
quote: string,
|
|
36
|
+
spec: string
|
|
37
|
+
): string => {
|
|
38
|
+
const resolved = resolve(dirname(sourceFile), spec);
|
|
39
|
+
if (resolved === sourceFile) {
|
|
40
|
+
return match;
|
|
41
|
+
}
|
|
42
|
+
const rel = relative(srcRoot, resolved);
|
|
43
|
+
if (rel.startsWith("..")) {
|
|
44
|
+
return match;
|
|
38
45
|
}
|
|
39
|
-
|
|
46
|
+
return `${prefix}${quote}blume/${rel}${quote}`;
|
|
47
|
+
};
|
|
48
|
+
return content
|
|
49
|
+
.replaceAll(FROM_IMPORT, rewrite)
|
|
50
|
+
.replaceAll(SIDE_EFFECT_IMPORT, rewrite);
|
|
51
|
+
};
|
package/src/runtime/index.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* (config, navigation, page collections) without reaching into generated
|
|
6
6
|
* runtime internals. The surface grows with the customization milestone.
|
|
7
7
|
*/
|
|
8
|
+
import type { BlumeData, BlumeRoute } from "../core/data.ts";
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
BlumeData,
|
|
12
|
+
BlumeDataConfig,
|
|
13
|
+
BlumeFeed,
|
|
14
|
+
BlumeRoute,
|
|
15
|
+
} from "../core/data.ts";
|
|
8
16
|
export type {
|
|
9
17
|
Heading,
|
|
10
18
|
NavNode,
|
|
@@ -12,3 +20,56 @@ export type {
|
|
|
12
20
|
NavTab,
|
|
13
21
|
PageRecord,
|
|
14
22
|
} from "../core/types.ts";
|
|
23
|
+
|
|
24
|
+
/** Query for {@link getBlumeCollection}. */
|
|
25
|
+
export interface BlumeCollectionQuery {
|
|
26
|
+
/** Astro collection to read from. Defaults to `"docs"`. */
|
|
27
|
+
collection?: string;
|
|
28
|
+
/** Include drafts, hidden pages, and translation fallbacks. Default `false`. */
|
|
29
|
+
includeHidden?: boolean;
|
|
30
|
+
/** Restrict to a locale code (matches `route.locale`). */
|
|
31
|
+
locale?: string;
|
|
32
|
+
/** Restrict to routes whose path starts with this prefix, e.g. `"/blog"`. */
|
|
33
|
+
prefix?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Select content routes from the `blume:data` snapshot — for building custom
|
|
38
|
+
* index pages, listings, or feeds without touching generated internals. Pass the
|
|
39
|
+
* imported `data` and an optional query; results are sorted by path.
|
|
40
|
+
*
|
|
41
|
+
* ```astro
|
|
42
|
+
* ---
|
|
43
|
+
* import data from "blume:data";
|
|
44
|
+
* import { getBlumeCollection } from "blume/runtime";
|
|
45
|
+
* const posts = getBlumeCollection(data, { prefix: "/blog" });
|
|
46
|
+
* ---
|
|
47
|
+
* <ul>{posts.map((p) => <li><a href={p.path}>{p.title}</a></li>)}</ul>
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export const getBlumeCollection = (
|
|
51
|
+
data: BlumeData,
|
|
52
|
+
query: BlumeCollectionQuery = {}
|
|
53
|
+
): BlumeRoute[] => {
|
|
54
|
+
const collection = query.collection ?? "docs";
|
|
55
|
+
return data.routes
|
|
56
|
+
.filter((route) => {
|
|
57
|
+
if (route.collection !== collection) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (query.locale && route.locale !== query.locale) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (query.prefix && !route.path.startsWith(query.prefix)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
!query.includeHidden &&
|
|
68
|
+
(route.draft || route.hidden || route.fallback)
|
|
69
|
+
) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
})
|
|
74
|
+
.toSorted((a, b) => a.path.localeCompare(b.path));
|
|
75
|
+
};
|