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.
Files changed (119) hide show
  1. package/dist/cli/index.js +2429 -792
  2. package/dist/cli/index.js.map +63 -44
  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 +313 -778
  7. package/dist/types/core/types.d.ts +2 -2
  8. package/dist/types/migrate/mintlify/assets.d.ts +8 -0
  9. package/docs/01-quickstart.mdx +5 -16
  10. package/docs/02-deployment.mdx +26 -40
  11. package/docs/advanced/api-reference.mdx +10 -37
  12. package/docs/advanced/blog.mdx +9 -25
  13. package/docs/advanced/changelog.mdx +10 -33
  14. package/docs/advanced/custom-pages.mdx +66 -61
  15. package/docs/configuration/ai.mdx +47 -91
  16. package/docs/configuration/analytics.mdx +20 -38
  17. package/docs/configuration/customization.mdx +92 -27
  18. package/docs/configuration/export.mdx +9 -34
  19. package/docs/configuration/index.mdx +78 -85
  20. package/docs/configuration/search.mdx +17 -54
  21. package/docs/configuration/seo.mdx +18 -44
  22. package/docs/configuration/theming.mdx +20 -42
  23. package/docs/content/components.mdx +42 -101
  24. package/docs/content/i18n.mdx +21 -72
  25. package/docs/content/index.mdx +18 -48
  26. package/docs/content/islands.mdx +79 -33
  27. package/docs/content/meta.mdx +23 -50
  28. package/docs/content/navigation.mdx +42 -56
  29. package/docs/content/sources.mdx +20 -83
  30. package/docs/content/syntax.mdx +37 -105
  31. package/docs/index.mdx +13 -51
  32. package/docs/reference/cli.mdx +49 -18
  33. package/docs/reference/frontmatter.mdx +2 -5
  34. package/package.json +3 -1
  35. package/src/ai/ask-context.ts +131 -0
  36. package/src/ai/ask-data.ts +25 -0
  37. package/src/astro/component-slots.ts +165 -0
  38. package/src/astro/generate.ts +132 -13
  39. package/src/astro/integration.ts +85 -3
  40. package/src/astro/islands.ts +6 -2
  41. package/src/astro/markdown-negotiation.ts +17 -3
  42. package/src/astro/pages.ts +11 -13
  43. package/src/astro/static-assets.ts +117 -0
  44. package/src/astro/templates.ts +120 -50
  45. package/src/blume-modules.d.ts +25 -0
  46. package/src/cli/args.ts +23 -0
  47. package/src/cli/commands/build.ts +209 -1
  48. package/src/cli/commands/check.ts +62 -0
  49. package/src/cli/commands/dev.ts +32 -3
  50. package/src/cli/commands/doctor.ts +32 -6
  51. package/src/cli/commands/eject.ts +3 -1
  52. package/src/cli/commands/init.ts +184 -16
  53. package/src/cli/commands/preview.ts +2 -1
  54. package/src/cli/commands/validate.ts +27 -2
  55. package/src/cli/dev-lock.ts +84 -0
  56. package/src/cli/index.ts +15 -0
  57. package/src/cli/internal-error.ts +63 -0
  58. package/src/cli/log.ts +41 -1
  59. package/src/cli/prepare.ts +17 -3
  60. package/src/cli/required-secrets.ts +44 -0
  61. package/src/components/BlumePage.astro +109 -0
  62. package/src/components/content/YouTube.astro +35 -0
  63. package/src/components/content/youtube.ts +46 -0
  64. package/src/components/index.ts +3 -3
  65. package/src/components/islands/ask-ai.tsx +29 -15
  66. package/src/components/islands/hooks.ts +188 -0
  67. package/src/components/layout/Empty.astro +6 -0
  68. package/src/components/layout/Header.astro +24 -39
  69. package/src/components/layout/Logo.astro +50 -0
  70. package/src/components/layout/NavSelector.astro +75 -0
  71. package/src/components/layout/PageLayout.astro +38 -2
  72. package/src/components/layout/RootLayout.astro +70 -4
  73. package/src/components/layout/hydration-hint.ts +30 -0
  74. package/src/components/layout/overrides.ts +6 -4
  75. package/src/components/props.ts +71 -0
  76. package/src/core/assets.ts +31 -0
  77. package/src/core/bridge.ts +10 -0
  78. package/src/core/builtin-tags.ts +40 -0
  79. package/src/core/component-diagnostics.ts +44 -0
  80. package/src/core/component-overrides.ts +478 -0
  81. package/src/core/config.ts +8 -0
  82. package/src/core/data.ts +14 -0
  83. package/src/core/define-components.ts +9 -2
  84. package/src/core/diagnostics.ts +95 -1
  85. package/src/core/gitignore.ts +30 -0
  86. package/src/core/graph.ts +7 -0
  87. package/src/core/links.ts +60 -19
  88. package/src/core/nav-diagnostics.ts +205 -0
  89. package/src/core/project-graph.ts +40 -1
  90. package/src/core/schema.ts +35 -96
  91. package/src/core/sources/mdx-remote.ts +54 -8
  92. package/src/core/sources/normalize.ts +57 -1
  93. package/src/core/sources/notion.ts +49 -5
  94. package/src/core/sources/sanity.ts +5 -1
  95. package/src/core/types.ts +2 -2
  96. package/src/deploy/redirects.ts +43 -0
  97. package/src/deploy/rss.ts +1 -8
  98. package/src/deploy/sitemap.ts +20 -1
  99. package/src/deploy/xml.ts +8 -0
  100. package/src/markdown/directives.ts +15 -7
  101. package/src/markdown/package-commands.ts +26 -4
  102. package/src/migrate/fumadocs/content.ts +14 -1
  103. package/src/migrate/fumadocs/groups.ts +7 -0
  104. package/src/migrate/fumadocs/index.ts +5 -2
  105. package/src/migrate/mintlify/assets.ts +46 -0
  106. package/src/migrate/mintlify/config.ts +1 -176
  107. package/src/migrate/mintlify/index.ts +53 -45
  108. package/src/migrate/shared.ts +12 -27
  109. package/src/migrate/starlight/config.ts +0 -4
  110. package/src/og/card.ts +175 -38
  111. package/src/registry/eject.ts +52 -12
  112. package/src/registry/registry.ts +172 -0
  113. package/src/registry/rewrite-imports.ts +31 -19
  114. package/src/runtime/index.ts +61 -0
  115. package/src/search/documents.ts +23 -5
  116. package/src/search/sync/algolia.ts +5 -1
  117. package/src/search/sync/typesense.ts +24 -16
  118. package/src/theme/palette.ts +26 -7
  119. package/src/vite-env.d.ts +14 -0
@@ -47,17 +47,35 @@ const MARKDOWN_PUNCT = /[*_~>]+/gu;
47
47
  const WHITESPACE = /\s+/gu;
48
48
 
49
49
  /** Reduce Markdown/MDX to plain, searchable text. */
50
- const toPlainText = (markdown: string): string =>
51
- markdown
50
+ const toPlainText = (markdown: string): string => {
51
+ const withoutBlocks = markdown
52
52
  .replaceAll(CODE_FENCE, " ")
53
53
  .replaceAll(IMAGE, " ")
54
- .replaceAll(LINK, "$<text>")
55
- .replaceAll(HTML_OR_JSX, " ")
56
- .replaceAll(INLINE_CODE, "$<code>")
54
+ .replaceAll(LINK, "$<text>");
55
+
56
+ // Strip HTML/JSX from the prose, but keep the contents of inline code — an
57
+ // angle-bracket span like `<T>` inside `Array<T>` is a type parameter, not a
58
+ // tag, and stripping it would drop those tokens from the search index. Split
59
+ // on inline-code spans and only run the HTML strip on the text between them.
60
+ const pieces: string[] = [];
61
+ let cursor = 0;
62
+ for (const match of withoutBlocks.matchAll(INLINE_CODE)) {
63
+ const start = match.index ?? 0;
64
+ pieces.push(
65
+ withoutBlocks.slice(cursor, start).replaceAll(HTML_OR_JSX, " ")
66
+ );
67
+ pieces.push(match.groups?.code ?? "");
68
+ cursor = start + match[0].length;
69
+ }
70
+ pieces.push(withoutBlocks.slice(cursor).replaceAll(HTML_OR_JSX, " "));
71
+
72
+ return pieces
73
+ .join("")
57
74
  .replaceAll(HEADING_MARK, "")
58
75
  .replaceAll(MARKDOWN_PUNCT, " ")
59
76
  .replaceAll(WHITESPACE, " ")
60
77
  .trim();
78
+ };
61
79
 
62
80
  interface Crumbs {
63
81
  breadcrumb: string[];
@@ -9,6 +9,10 @@ export interface AlgoliaSyncConfig {
9
9
  * Upload the search records to Algolia. Uses the admin key from
10
10
  * `ALGOLIA_ADMIN_API_KEY` (never the config, which holds only the public,
11
11
  * search-only key). Throws on a missing key/config so the caller can warn.
12
+ *
13
+ * Uses `replaceAllObjects`, which atomically replaces the index contents, so
14
+ * pages deleted or renamed since the last sync don't linger as stale search
15
+ * hits that 404 when clicked.
12
16
  */
13
17
  export const syncAlgolia = async (
14
18
  records: SearchRecord[],
@@ -23,7 +27,7 @@ export const syncAlgolia = async (
23
27
  }
24
28
  const { algoliasearch } = await import("algoliasearch");
25
29
  const client = algoliasearch(config.appId, adminKey);
26
- await client.saveObjects({
30
+ await client.replaceAllObjects({
27
31
  indexName: config.indexName,
28
32
  objects: records.map((record) => ({ ...record, objectID: record._id })),
29
33
  });
@@ -8,9 +8,13 @@ export interface TypesenseSyncConfig {
8
8
  }
9
9
 
10
10
  /**
11
- * Import the search records into a Typesense collection, creating the
12
- * collection on first run. Uses the admin key from `TYPESENSE_ADMIN_API_KEY`.
13
- * Throws on a missing key/config so the caller can warn.
11
+ * Import the search records into a Typesense collection. Uses the admin key
12
+ * from `TYPESENSE_ADMIN_API_KEY`. Throws on a missing key/config so the caller
13
+ * can warn.
14
+ *
15
+ * The collection is dropped and recreated on each sync so that pages deleted or
16
+ * renamed since the last sync don't linger as stale search hits that 404 when
17
+ * clicked (an upsert alone never removes them).
14
18
  */
15
19
  export const syncTypesense = async (
16
20
  records: SearchRecord[],
@@ -35,20 +39,24 @@ export const syncTypesense = async (
35
39
  ],
36
40
  });
37
41
 
38
- try {
39
- await client.collections(config.collection).retrieve();
40
- } catch {
41
- await client.collections().create({
42
- fields: [
43
- { name: "title", type: "string" },
44
- { name: "description", optional: true, type: "string" },
45
- { name: "content", type: "string" },
46
- { name: "url", type: "string" },
47
- { facet: true, name: "tag", optional: true, type: "string" },
48
- ],
49
- name: config.collection,
50
- });
42
+ const collection = client.collections(config.collection);
43
+ const exists = await collection
44
+ .retrieve()
45
+ .then(() => true)
46
+ .catch(() => false);
47
+ if (exists) {
48
+ await collection.delete();
51
49
  }
50
+ await client.collections().create({
51
+ fields: [
52
+ { name: "title", type: "string" },
53
+ { name: "description", optional: true, type: "string" },
54
+ { name: "content", type: "string" },
55
+ { name: "url", type: "string" },
56
+ { facet: true, name: "tag", optional: true, type: "string" },
57
+ ],
58
+ name: config.collection,
59
+ });
52
60
 
53
61
  const documents = records.map((record) => ({
54
62
  content: record.content,
@@ -1,8 +1,10 @@
1
1
  import type { ResolvedConfig } from "../core/schema.ts";
2
2
 
3
+ const FALLBACK_ACCENT = "oklch(0.62 0.16 250)";
4
+
3
5
  /** Named accent presets mapped to OKLCH values. */
4
6
  const ACCENTS: Record<string, string> = {
5
- blue: "oklch(0.62 0.16 250)",
7
+ blue: FALLBACK_ACCENT,
6
8
  green: "oklch(0.6 0.16 150)",
7
9
  orange: "oklch(0.68 0.17 50)",
8
10
  pink: "oklch(0.65 0.2 350)",
@@ -11,6 +13,19 @@ const ACCENTS: Record<string, string> = {
11
13
  teal: "oklch(0.6 0.12 195)",
12
14
  };
13
15
 
16
+ // Characters valid in a CSS color value (hex, rgb/hsl/oklch functions, named
17
+ // colors). Anything else — notably `;`, `{`, `}` — could break out of the
18
+ // declaration and inject rules, so such a value is rejected.
19
+ const CSS_COLOR = /^[\w\s#%.,()/+-]+$/u;
20
+
21
+ /** Pass a raw color through only if it can't break out of a CSS declaration. */
22
+ const safeColor = (value: string, fallback: string): string =>
23
+ CSS_COLOR.test(value.trim()) ? value.trim() : fallback;
24
+
25
+ /** Like {@link safeColor} but drops an unsafe/absent value to `null`. */
26
+ const safeColorOrNull = (value: string | undefined): string | null =>
27
+ value && CSS_COLOR.test(value.trim()) ? value.trim() : null;
28
+
14
29
  const RADII: Record<ResolvedConfig["theme"]["radius"], string> = {
15
30
  lg: "0.75rem",
16
31
  md: "0.5rem",
@@ -66,7 +81,7 @@ const themeRootCss = (
66
81
  "--blume-action-foreground",
67
82
  options.action ? "oklch(1 0 0)" : null
68
83
  ),
69
- ...cssToken("--blume-background", theme.background),
84
+ ...cssToken("--blume-background", safeColorOrNull(theme.background)),
70
85
  ...cssToken(
71
86
  "--blume-background-image",
72
87
  theme.backgroundImage ? backgroundImageCss(theme.backgroundImage) : null
@@ -83,7 +98,7 @@ const themeDarkCss = (
83
98
  ): string => {
84
99
  const tokens = [
85
100
  ...cssToken("--blume-accent", accentDark),
86
- ...cssToken("--blume-background", theme.backgroundDark),
101
+ ...cssToken("--blume-background", safeColorOrNull(theme.backgroundDark)),
87
102
  ...cssToken(
88
103
  "--blume-background-image",
89
104
  theme.backgroundImageDark
@@ -106,7 +121,7 @@ ${tokens.join("\n")}
106
121
  * arbitrary colors without a config change.
107
122
  */
108
123
  export const resolveAccent = (theme: ResolvedConfig["theme"]): string =>
109
- ACCENTS[theme.accent] ?? theme.accent;
124
+ ACCENTS[theme.accent] ?? safeColor(theme.accent, FALLBACK_ACCENT);
110
125
 
111
126
  /** Resolve the configured radius preset to a CSS length. */
112
127
  export const resolveRadius = (theme: ResolvedConfig["theme"]): string =>
@@ -118,11 +133,15 @@ export const resolveRadius = (theme: ResolvedConfig["theme"]): string =>
118
133
  * arbitrary colors without a config change.
119
134
  */
120
135
  export const buildThemeCss = (theme: ResolvedConfig["theme"]): string => {
121
- const accent = ACCENTS[theme.accent] ?? theme.accent;
136
+ const accent =
137
+ ACCENTS[theme.accent] ?? safeColor(theme.accent, FALLBACK_ACCENT);
122
138
  const accentDark = theme.accentDark
123
- ? (ACCENTS[theme.accentDark] ?? theme.accentDark)
139
+ ? (ACCENTS[theme.accentDark] ??
140
+ safeColor(theme.accentDark, FALLBACK_ACCENT))
141
+ : null;
142
+ const action = theme.action
143
+ ? (ACCENTS[theme.action] ?? safeColor(theme.action, FALLBACK_ACCENT))
124
144
  : null;
125
- const action = theme.action ? (ACCENTS[theme.action] ?? theme.action) : null;
126
145
  const backgroundDecoration = backgroundDecorationCss(
127
146
  theme.backgroundDecoration
128
147
  );
@@ -0,0 +1,14 @@
1
+ // Ambient types for the Vite `import.meta.env` fields Blume's client islands read
2
+ // (`.astro` files aren't typechecked, so only real `.ts`/`.tsx` sources need this).
3
+ interface ImportMetaEnv {
4
+ /** Deployment base path, always with a trailing slash (e.g. `/` or `/docs/`). */
5
+ readonly BASE_URL: string;
6
+ readonly DEV: boolean;
7
+ readonly MODE: string;
8
+ readonly PROD: boolean;
9
+ readonly SSR: boolean;
10
+ }
11
+
12
+ interface ImportMeta {
13
+ readonly env: ImportMetaEnv;
14
+ }