blume 1.2.0 → 1.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 (77) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cli/index.js +1715 -539
  3. package/dist/cli/index.js.map +40 -31
  4. package/dist/types/core/config-input.d.ts +131 -11
  5. package/dist/types/core/config.d.ts +9 -1
  6. package/dist/types/core/data.d.ts +24 -5
  7. package/dist/types/core/i18n-ui.d.ts +58 -799
  8. package/dist/types/core/schema.d.ts +534 -3305
  9. package/dist/types/theme/fonts.d.ts +55 -11
  10. package/docs/02-deployment.mdx +2 -0
  11. package/docs/07-faq.mdx +14 -14
  12. package/docs/advanced/skills.mdx +2 -2
  13. package/docs/configuration/ai.mdx +126 -2
  14. package/docs/configuration/index.mdx +19 -1
  15. package/docs/configuration/search.mdx +17 -0
  16. package/docs/configuration/seo.mdx +26 -3
  17. package/docs/configuration/theming.mdx +44 -2
  18. package/docs/content/syntax.mdx +18 -2
  19. package/docs/reference/cli.mdx +3 -3
  20. package/package.json +9 -8
  21. package/skills/blume/SKILL.md +6 -4
  22. package/skills/blume-migrate/SKILL.md +5 -3
  23. package/skills/blume-migrate/references/mintlify.md +5 -5
  24. package/skills/blume-migrate/references/monorepo.md +2 -1
  25. package/src/ai/agent-readability.ts +31 -1
  26. package/src/ai/api-catalog.ts +81 -0
  27. package/src/ai/ask-context.ts +7 -1
  28. package/src/ai/ask-data.ts +1 -0
  29. package/src/ai/link-headers.ts +52 -0
  30. package/src/ai/llms.ts +12 -1
  31. package/src/ai/markdown.ts +15 -2
  32. package/src/ai/mcp/data.ts +7 -0
  33. package/src/ai/mcp/discovery.ts +70 -15
  34. package/src/ai/mcp/server.ts +14 -8
  35. package/src/ai/mcp/stdio.ts +4 -1
  36. package/src/ai/skills.ts +193 -0
  37. package/src/ai/tar.ts +104 -0
  38. package/src/ai/web-bot-auth.ts +30 -0
  39. package/src/astro/generate.ts +116 -6
  40. package/src/astro/integration.ts +52 -14
  41. package/src/astro/templates.ts +191 -37
  42. package/src/audit/catalog.ts +20 -0
  43. package/src/audit/checks/dns-aid.ts +190 -0
  44. package/src/audit/report.ts +5 -0
  45. package/src/audit/run.ts +2 -0
  46. package/src/cli/commands/build.ts +178 -9
  47. package/src/cli/init/scaffold.ts +1 -1
  48. package/src/components/islands/ask-ai.tsx +4 -1
  49. package/src/components/islands/webmcp.ts +203 -0
  50. package/src/components/layout/NavTree.astro +4 -4
  51. package/src/components/layout/PageLayout.astro +2 -0
  52. package/src/components/layout/ReferenceLayout.astro +2 -0
  53. package/src/components/layout/RootLayout.astro +63 -11
  54. package/src/components/layout/Search.astro +2 -2
  55. package/src/components/layout/WebMcp.astro +49 -0
  56. package/src/components/layout/search/orama.ts +5 -2
  57. package/src/core/config-input.ts +143 -11
  58. package/src/core/config.ts +17 -1
  59. package/src/core/content-assets.ts +199 -0
  60. package/src/core/data.ts +21 -5
  61. package/src/core/diagnostics.ts +6 -5
  62. package/src/core/i18n-ui.ts +19 -28
  63. package/src/core/project-graph.ts +6 -0
  64. package/src/core/schema.ts +224 -71
  65. package/src/core/sources/normalize.ts +5 -5
  66. package/src/deploy/headers.ts +45 -3
  67. package/src/deploy/vercel-negotiation.ts +233 -0
  68. package/src/markdown/mermaid.ts +7 -1
  69. package/src/markdown/table-wrap.ts +33 -1
  70. package/src/og/card.ts +91 -22
  71. package/src/og/derive.ts +200 -0
  72. package/src/og/index.ts +6 -1
  73. package/src/search/orama-index.ts +151 -7
  74. package/src/theme/entry.ts +34 -13
  75. package/src/theme/fonts.ts +183 -30
  76. package/dist/types/og/card.d.ts +0 -63
  77. package/dist/types/og/dimensions.d.ts +0 -12
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Curated Google Fonts exposed through `theme.fonts`.
2
+ * Fonts exposed through `theme.fonts`.
3
3
  *
4
- * Each slug maps to the official Google family name (Astro's font provider needs
5
- * the exact name), a docs-appropriate set of weights, and a fallback category.
6
- * Fonts are self-hosted and optimized by Astro's built-in Fonts API; this module
7
- * only resolves config slugs into the data that drives it.
4
+ * Each role accepts a curated Google Font slug, a remote-provider family (any
5
+ * Google/Fontsource/Bunny/Fontshare family by name), or local font files. All
6
+ * forms resolve to entries for Astro's built-in Fonts API, which self-hosts
7
+ * and optimizes them; this module only resolves config values into the data
8
+ * that drives it.
8
9
  */
9
10
 
10
11
  export type FontCategory = "sans" | "serif" | "mono";
@@ -12,22 +13,74 @@ export type FontCategory = "sans" | "serif" | "mono";
12
13
  /** The three configurable roles in `theme.fonts`. */
13
14
  export type FontSlot = "display" | "body" | "mono";
14
15
 
16
+ /** Zero-config Astro font providers usable from `theme.fonts`. */
17
+ export type RemoteFontProvider =
18
+ | "google"
19
+ | "fontsource"
20
+ | "bunny"
21
+ | "fontshare";
22
+
23
+ /** A remote-provider family: any family name the provider knows. */
24
+ export interface RemoteFontConfig {
25
+ /** Family name as the provider lists it, e.g. `"Noto Sans JP"`. */
26
+ name: string;
27
+ /** Which provider serves the family. Defaults to `"google"`. */
28
+ provider?: RemoteFontProvider;
29
+ /** Weights (or variable ranges like `"100..900"`) to load. Defaults to `[400, 500, 600, 700]`. */
30
+ weights?: (number | string)[];
31
+ /** Fallback stack category. Defaults to `"mono"` for the mono role, `"sans"` otherwise. */
32
+ fallback?: FontCategory;
33
+ }
34
+
35
+ /** One `@font-face` declaration for a local font. */
36
+ export interface LocalFontVariant {
37
+ /** Font file path, relative to the project root. */
38
+ src: string;
39
+ /** Face weight; inferred from the file when omitted. */
40
+ weight?: number | string;
41
+ /** Face style; inferred from the file when omitted. */
42
+ style?: "normal" | "italic" | "oblique";
43
+ }
44
+
45
+ /** A self-hosted family loaded from files in the project. */
46
+ export interface LocalFontConfig {
47
+ /** Family name used in CSS and the OG card. */
48
+ name: string;
49
+ /** The faces to declare (at least one). */
50
+ variants: LocalFontVariant[];
51
+ /** Fallback stack category. Defaults to `"mono"` for the mono role, `"sans"` otherwise. */
52
+ fallback?: FontCategory;
53
+ }
54
+
55
+ /** A role's font: curated slug, remote family, or local files. */
56
+ export type FontValue = string | RemoteFontConfig | LocalFontConfig;
57
+
15
58
  interface FontDef {
16
59
  category: FontCategory;
17
60
  family: string;
18
61
  weights: number[];
19
62
  }
20
63
 
21
- /** Resolved theme fonts (a validated slug per role, all optional). */
22
- export type FontsConfig = Partial<Record<FontSlot, string>> | undefined;
64
+ /** Resolved theme fonts (a validated value per role, all optional). */
65
+ export type FontsConfig = Partial<Record<FontSlot, FontValue>> | undefined;
23
66
 
24
- /** A single Astro `fonts:` entry (sans the literal `fontProviders.google()`). */
25
- export interface FontEntry {
26
- cssVariable: string;
27
- fallbacks: string[];
28
- name: string;
29
- weights: number[];
30
- }
67
+ /** A single Astro `fonts:` entry (sans the literal `fontProviders.*()` call). */
68
+ export type FontEntry =
69
+ | {
70
+ kind: "remote";
71
+ provider: RemoteFontProvider;
72
+ cssVariable: string;
73
+ fallbacks: string[];
74
+ name: string;
75
+ weights: (number | string)[];
76
+ }
77
+ | {
78
+ kind: "local";
79
+ cssVariable: string;
80
+ fallbacks: string[];
81
+ name: string;
82
+ variants: LocalFontVariant[];
83
+ };
31
84
 
32
85
  const FALLBACKS: Record<FontCategory, string[]> = {
33
86
  mono: ["ui-monospace", "SF Mono", "Menlo", "monospace"],
@@ -149,28 +202,126 @@ export const FONT_SLUGS = Object.keys(GOOGLE_FONTS);
149
202
  export const isFontSlug = (value: string): value is FontSlug =>
150
203
  Object.hasOwn(GOOGLE_FONTS, value);
151
204
 
205
+ /** Weights loaded for a remote family when the config does not pin them. */
206
+ const DEFAULT_REMOTE_WEIGHTS: (number | string)[] = [400, 500, 600, 700];
207
+
208
+ /** Kebab-case a family name into a slug (`"Noto Sans JP"` -> `"noto-sans-jp"`). */
209
+ export const slugifyFontName = (name: string): string =>
210
+ name
211
+ .toLowerCase()
212
+ .replaceAll(/[^a-z0-9]+/gu, "-")
213
+ .replaceAll(/^-+|-+$/gu, "");
214
+
152
215
  /** The CSS variable Astro populates for a given font (shared across roles). */
153
216
  const fontVar = (slug: string): string => `--blume-ff-${slug}`;
154
217
 
155
218
  const SLOTS: FontSlot[] = ["display", "body", "mono"];
156
219
 
157
- /** The unique Astro `fonts:` entries for the configured roles (deduped). */
158
- export const buildFontEntries = (fonts: FontsConfig): FontEntry[] => {
159
- if (!fonts) {
160
- return [];
161
- }
162
- const slugs = SLOTS.map((slot) => fonts[slot]).filter(
163
- (slug): slug is FontSlug => typeof slug === "string" && isFontSlug(slug)
164
- );
165
- return [...new Set(slugs)].map((slug) => {
166
- const def = GOOGLE_FONTS[slug];
220
+ /** The fallback category a role uses when a custom font does not pick one. */
221
+ const slotCategory = (slot: FontSlot): FontCategory =>
222
+ slot === "mono" ? "mono" : "sans";
223
+
224
+ /** A slot value normalized into an entry, or null for an unknown slug string. */
225
+ const resolveFontValue = (
226
+ slot: FontSlot,
227
+ value: FontValue
228
+ ): FontEntry | null => {
229
+ if (typeof value === "string") {
230
+ if (!isFontSlug(value)) {
231
+ return null;
232
+ }
233
+ const def = GOOGLE_FONTS[value];
167
234
  return {
168
- cssVariable: fontVar(slug),
235
+ cssVariable: fontVar(value),
169
236
  fallbacks: FALLBACKS[def.category],
237
+ kind: "remote",
170
238
  name: def.family,
239
+ provider: "google",
171
240
  weights: def.weights,
172
241
  };
173
- });
242
+ }
243
+ const slug = slugifyFontName(value.name);
244
+ const fallbacks = FALLBACKS[value.fallback ?? slotCategory(slot)];
245
+ if ("variants" in value) {
246
+ return {
247
+ cssVariable: fontVar(slug),
248
+ fallbacks,
249
+ kind: "local",
250
+ name: value.name,
251
+ variants: value.variants,
252
+ };
253
+ }
254
+ return {
255
+ cssVariable: fontVar(slug),
256
+ fallbacks,
257
+ kind: "remote",
258
+ name: value.name,
259
+ provider: value.provider ?? "google",
260
+ weights: value.weights ?? DEFAULT_REMOTE_WEIGHTS,
261
+ };
262
+ };
263
+
264
+ /**
265
+ * Merge two entries that resolved to the same CSS variable. The same remote
266
+ * family configured twice unions its weights (so a custom `{ name: "Inter" }`
267
+ * coexists with the curated `inter` default in another role); identical local
268
+ * definitions collapse. Anything else is a config conflict worth failing on.
269
+ */
270
+ const mergeFontEntries = (current: FontEntry, next: FontEntry): FontEntry => {
271
+ if (
272
+ current.kind === "remote" &&
273
+ next.kind === "remote" &&
274
+ current.provider === next.provider &&
275
+ current.name === next.name
276
+ ) {
277
+ return {
278
+ ...current,
279
+ weights: [...new Set([...current.weights, ...next.weights])],
280
+ };
281
+ }
282
+ if (
283
+ current.kind === "local" &&
284
+ next.kind === "local" &&
285
+ current.name === next.name &&
286
+ JSON.stringify(current.variants) === JSON.stringify(next.variants)
287
+ ) {
288
+ return current;
289
+ }
290
+ throw new Error(
291
+ `theme.fonts: "${next.name}" conflicts with "${current.name}" — both resolve to the CSS variable "${current.cssVariable}" with different definitions. Rename one family or align their definitions.`
292
+ );
293
+ };
294
+
295
+ /** The unique Astro `fonts:` entries for the configured roles (deduped). */
296
+ export const buildFontEntries = (fonts: FontsConfig): FontEntry[] => {
297
+ if (!fonts) {
298
+ return [];
299
+ }
300
+ const entries = new Map<string, FontEntry>();
301
+ for (const slot of SLOTS) {
302
+ const value = fonts[slot];
303
+ if (value === undefined) {
304
+ continue;
305
+ }
306
+ const entry = resolveFontValue(slot, value);
307
+ if (!entry) {
308
+ continue;
309
+ }
310
+ const current = entries.get(entry.cssVariable);
311
+ entries.set(
312
+ entry.cssVariable,
313
+ current ? mergeFontEntries(current, entry) : entry
314
+ );
315
+ }
316
+ return [...entries.values()];
317
+ };
318
+
319
+ /** The slug backing a slot's CSS variable, or null for an unknown slug string. */
320
+ const slotSlug = (value: FontValue): string | null => {
321
+ if (typeof value === "string") {
322
+ return isFontSlug(value) ? value : null;
323
+ }
324
+ return slugifyFontName(value.name);
174
325
  };
175
326
 
176
327
  /**
@@ -183,10 +334,12 @@ export const buildFontsCss = (fonts: FontsConfig): string => {
183
334
  return "";
184
335
  }
185
336
  const lines = SLOTS.flatMap((slot) => {
186
- const slug = fonts[slot];
187
- return slug && isFontSlug(slug)
188
- ? [` --blume-font-${slot}-src: var(${fontVar(slug)});`]
189
- : [];
337
+ const value = fonts[slot];
338
+ if (value === undefined) {
339
+ return [];
340
+ }
341
+ const slug = slotSlug(value);
342
+ return slug ? [` --blume-font-${slot}-src: var(${fontVar(slug)});`] : [];
190
343
  });
191
344
  return lines.length > 0
192
345
  ? `/* Generated by Blume from theme.fonts. */\n:root {\n${lines.join("\n")}\n}\n`
@@ -1,63 +0,0 @@
1
- import type { RenderOptions } from "takumi-js";
2
- /**
3
- * A Google Font family to load into the OG card renderer. A bare string is the
4
- * family name (weight 400, normal style); the object form pins weight and style.
5
- * Handed straight to Takumi's `googleFonts` helper, which fetches the family
6
- * from Google Fonts at build and returns per-glyph coverage subsets.
7
- */
8
- export type OgFont = string | {
9
- /** Google Fonts family name, e.g. `"Noto Sans JP"`. */
10
- name: string;
11
- /** `400`, `[400, 700]`, or a variable range like `"100..900"`. */
12
- weight?: number | number[] | string;
13
- /** `"normal"`, `"italic"`, or both. */
14
- style?: "normal" | "italic" | ("normal" | "italic")[];
15
- };
16
- export interface OgCardPalette {
17
- accent?: string;
18
- background?: string;
19
- border?: string;
20
- foreground?: string;
21
- muted?: string;
22
- }
23
- export interface OgCardOptions {
24
- /** Large headline — the page title. */
25
- title: string;
26
- /** Accent color (named preset or any CSS color) for the fallback brand mark. */
27
- accent?: string;
28
- /** Brand/site name shown in the top-left lockup. */
29
- brand?: string;
30
- /** Muted subtitle under the headline (usually the site description). */
31
- description?: string;
32
- /**
33
- * Inlined SVG markup of the configured logo, painted into
34
- * the brand lockup. Falls back to an accent mark when absent.
35
- */
36
- logo?: string;
37
- /** Optional colors for the generated card. */
38
- palette?: OgCardPalette;
39
- /** Footer-left repository slug, e.g. `owner/repo`. */
40
- repo?: string;
41
- /** Footer-right site host, e.g. `docs.acme.com`. */
42
- site?: string;
43
- /**
44
- * Pre-fetched image entries, or a group controlling how remote images (and
45
- * emoji glyphs) are fetched. Blume merges in a shared glyph cache; see
46
- * {@link resolveImages}.
47
- */
48
- images?: RenderOptions["images"];
49
- /**
50
- * Google Font families for non-Latin titles. Takumi's built-in font covers
51
- * only Latin, so a CJK (etc.) title renders as tofu without a family that
52
- * covers its script — see {@link loadFonts}.
53
- */
54
- fonts?: OgFont[];
55
- }
56
- /**
57
- * Truncate to `max` code points with an ellipsis. Slices by code points, not
58
- * UTF-16 units, so cutting mid-emoji doesn't leave a lone surrogate (a broken
59
- * glyph) before the ellipsis.
60
- */
61
- export declare const truncate: (value: string, max: number) => string;
62
- /** Render a 1200x630 Open Graph card to a PNG buffer. */
63
- export declare const renderOgImage: (options: OgCardOptions) => Promise<Uint8Array>;
@@ -1,12 +0,0 @@
1
- /**
2
- * Dimensions of a generated OG card, shared by the renderer (`card.ts`) and the
3
- * layouts that declare them as `og:image:width`/`og:image:height` so a crawler
4
- * can lay out the card without fetching the PNG first.
5
- *
6
- * This lives apart from `card.ts` because that module imports the Takumi native
7
- * binding at load; a layout importing it would drag the renderer into every
8
- * page render (and into the prerender/SSR bundles that externalize it).
9
- */
10
- export declare const OG_IMAGE_WIDTH = 1200;
11
- export declare const OG_IMAGE_HEIGHT = 630;
12
- export declare const OG_IMAGE_TYPE = "image/png";