blume 0.6.7 → 0.8.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 (211) hide show
  1. package/CHANGELOG.md +618 -0
  2. package/LICENSE +21 -0
  3. package/README.md +107 -0
  4. package/dist/cli/index.js +2609 -1041
  5. package/dist/cli/index.js.map +110 -103
  6. package/dist/types/ai/component-markdown.d.ts +34 -0
  7. package/dist/types/components/content/youtube.d.ts +18 -0
  8. package/dist/types/core/base-path.d.ts +47 -0
  9. package/dist/types/core/config-input.d.ts +110 -12
  10. package/dist/types/core/config.d.ts +6 -4
  11. package/dist/types/core/data.d.ts +4 -0
  12. package/dist/types/core/i18n-ui.d.ts +477 -135
  13. package/dist/types/core/schema.d.ts +309 -195
  14. package/dist/types/core/sources/types.d.ts +2 -0
  15. package/dist/types/core/types.d.ts +6 -1
  16. package/dist/types/index.d.ts +1 -0
  17. package/dist/types/openapi/references.d.ts +60 -0
  18. package/docs/01-quickstart.mdx +5 -2
  19. package/docs/02-deployment.mdx +24 -9
  20. package/docs/03-faq.mdx +46 -16
  21. package/docs/advanced/custom-pages.mdx +1 -1
  22. package/docs/advanced/skills.mdx +1 -1
  23. package/docs/configuration/ai.mdx +49 -10
  24. package/docs/configuration/customization.mdx +11 -0
  25. package/docs/configuration/index.mdx +33 -3
  26. package/docs/configuration/seo.mdx +2 -2
  27. package/docs/content/components.mdx +30 -3
  28. package/docs/content/i18n.mdx +1 -1
  29. package/docs/content/islands.mdx +8 -0
  30. package/docs/content/navigation.mdx +3 -3
  31. package/docs/content/sources.mdx +1 -1
  32. package/docs/content/syntax.mdx +17 -2
  33. package/docs/index.mdx +2 -2
  34. package/docs/reference/cli.mdx +8 -6
  35. package/package.json +15 -4
  36. package/skills/blume/SKILL.md +5 -3
  37. package/skills/blume-update-docs/SKILL.md +3 -2
  38. package/src/ai/agent-readability.ts +11 -5
  39. package/src/ai/ask-context.ts +7 -2
  40. package/src/ai/ask-data.ts +3 -0
  41. package/src/ai/ask.ts +12 -7
  42. package/src/ai/component-markdown.ts +461 -0
  43. package/src/ai/llms.ts +143 -23
  44. package/src/ai/markdown.ts +35 -6
  45. package/src/ai/mcp/data.ts +33 -8
  46. package/src/ai/mcp/discovery.ts +10 -3
  47. package/src/ai/mcp/server.ts +24 -7
  48. package/src/ai/visibility.ts +74 -0
  49. package/src/astro/component-slots.ts +16 -4
  50. package/src/astro/examples.ts +12 -7
  51. package/src/astro/generate.ts +393 -189
  52. package/src/astro/index.ts +5 -1
  53. package/src/astro/integration.ts +9 -5
  54. package/src/astro/islands.ts +11 -5
  55. package/src/astro/markdown-negotiation.ts +2 -2
  56. package/src/astro/pages.ts +89 -22
  57. package/src/astro/templates.ts +259 -25
  58. package/src/blume-modules.d.ts +8 -0
  59. package/src/cli/commands/build.ts +131 -38
  60. package/src/cli/commands/check.ts +1 -1
  61. package/src/cli/commands/dev.ts +71 -17
  62. package/src/cli/commands/doctor.ts +2 -2
  63. package/src/cli/commands/eject.ts +47 -19
  64. package/src/cli/commands/init.ts +120 -180
  65. package/src/cli/commands/preview.ts +4 -1
  66. package/src/cli/commands/validate.ts +44 -2
  67. package/src/cli/dev-lock.ts +34 -19
  68. package/src/cli/eject-scripts.ts +72 -0
  69. package/src/cli/env.ts +15 -5
  70. package/src/cli/init/questions.ts +158 -0
  71. package/src/cli/init/scaffold.ts +380 -0
  72. package/src/cli/required-secrets.ts +2 -1
  73. package/src/components/content/AccordionItem.astro +23 -4
  74. package/src/components/content/Badge.astro +3 -1
  75. package/src/components/content/Card.astro +4 -2
  76. package/src/components/content/CodeBlock.astro +3 -0
  77. package/src/components/content/Component.astro +30 -16
  78. package/src/components/content/Diff.astro +3 -1
  79. package/src/components/content/Step.astro +10 -1
  80. package/src/components/content/Tabs.astro +15 -3
  81. package/src/components/content/Tile.astro +2 -1
  82. package/src/components/content/Tooltip.astro +3 -1
  83. package/src/components/content/Update.astro +9 -2
  84. package/src/components/content/auto-type-table.ts +25 -9
  85. package/src/components/content/base-href.ts +33 -0
  86. package/src/components/content/changelog-element.ts +9 -2
  87. package/src/components/content/diff.ts +12 -6
  88. package/src/components/content/mermaid-element.ts +10 -2
  89. package/src/components/index.ts +23 -1
  90. package/src/components/islands/AskAI.astro +5 -2
  91. package/src/components/islands/ask-ai.tsx +68 -12
  92. package/src/components/islands/base-path.ts +28 -0
  93. package/src/components/islands/hooks.ts +44 -9
  94. package/src/components/layout/Banner.astro +12 -3
  95. package/src/components/layout/Breadcrumbs.astro +2 -1
  96. package/src/components/layout/Favicon.astro +3 -2
  97. package/src/components/layout/Header.astro +15 -5
  98. package/src/components/layout/LanguageSwitcher.astro +2 -1
  99. package/src/components/layout/Logo.astro +13 -4
  100. package/src/components/layout/NavSelector.astro +2 -1
  101. package/src/components/layout/NavTree.astro +22 -7
  102. package/src/components/layout/PageActions.astro +25 -10
  103. package/src/components/layout/PageFeedback.astro +4 -1
  104. package/src/components/layout/PageLayout.astro +51 -9
  105. package/src/components/layout/Pagination.astro +3 -2
  106. package/src/components/layout/ReferenceLayout.astro +8 -1
  107. package/src/components/layout/RootLayout.astro +74 -13
  108. package/src/components/layout/Search.astro +107 -27
  109. package/src/components/layout/nav-utils.ts +18 -10
  110. package/src/components/layout/search/algolia.ts +11 -2
  111. package/src/components/layout/search/endpoint.ts +11 -5
  112. package/src/components/layout/search/orama-cloud.ts +8 -2
  113. package/src/components/layout/search/pagefind.ts +3 -0
  114. package/src/components/layout/search/types.ts +5 -1
  115. package/src/components/layout/search/typesense.ts +4 -1
  116. package/src/components/layout/toc-element.ts +8 -2
  117. package/src/components/openapi/ApiTagOperations.astro +2 -1
  118. package/src/components/openapi/Operation.astro +47 -40
  119. package/src/components/openapi/RequestPanel.astro +8 -2
  120. package/src/components/openapi/helpers.ts +71 -3
  121. package/src/components/openapi/panel.ts +1 -1
  122. package/src/components/openapi/snippets.ts +25 -11
  123. package/src/core/base-path.ts +94 -0
  124. package/src/core/builtin-tags.ts +2 -0
  125. package/src/core/component-overrides.ts +103 -74
  126. package/src/core/config-input.ts +118 -17
  127. package/src/core/config.ts +8 -5
  128. package/src/core/content.ts +2 -0
  129. package/src/core/data.ts +4 -0
  130. package/src/core/diagnostics.ts +54 -34
  131. package/src/core/gitignore.ts +4 -1
  132. package/src/core/graph.ts +166 -88
  133. package/src/core/i18n-ui.ts +63 -3
  134. package/src/core/last-modified.ts +15 -6
  135. package/src/core/links.ts +69 -25
  136. package/src/core/manifest.ts +62 -45
  137. package/src/core/nav-diagnostics.ts +1 -1
  138. package/src/core/navigation.ts +144 -58
  139. package/src/core/package-json.ts +17 -2
  140. package/src/core/project-graph.ts +25 -15
  141. package/src/core/schema.ts +605 -620
  142. package/src/core/sources/assets.ts +6 -1
  143. package/src/core/sources/filesystem.ts +4 -0
  144. package/src/core/sources/github-releases.ts +2 -1
  145. package/src/core/sources/mdx-remote.ts +76 -63
  146. package/src/core/sources/normalize.ts +236 -91
  147. package/src/core/sources/notion.ts +27 -18
  148. package/src/core/sources/types.ts +2 -0
  149. package/src/core/tsconfig-aliases.ts +59 -30
  150. package/src/core/types.ts +6 -1
  151. package/src/core/ui-packs/ar.ts +1 -0
  152. package/src/core/ui-packs/bg.ts +1 -0
  153. package/src/core/ui-packs/bn.ts +1 -0
  154. package/src/core/ui-packs/ca.ts +1 -0
  155. package/src/core/ui-packs/cs.ts +1 -0
  156. package/src/core/ui-packs/da.ts +1 -0
  157. package/src/core/ui-packs/de.ts +1 -0
  158. package/src/core/ui-packs/el.ts +1 -0
  159. package/src/core/ui-packs/es.ts +1 -0
  160. package/src/core/ui-packs/fa.ts +1 -0
  161. package/src/core/ui-packs/fi.ts +1 -0
  162. package/src/core/ui-packs/fr.ts +2 -1
  163. package/src/core/ui-packs/he.ts +1 -0
  164. package/src/core/ui-packs/hi.ts +1 -0
  165. package/src/core/ui-packs/hr.ts +1 -0
  166. package/src/core/ui-packs/hu.ts +1 -0
  167. package/src/core/ui-packs/id.ts +1 -0
  168. package/src/core/ui-packs/it.ts +1 -0
  169. package/src/core/ui-packs/ja.ts +1 -0
  170. package/src/core/ui-packs/ko.ts +1 -0
  171. package/src/core/ui-packs/nl.ts +1 -0
  172. package/src/core/ui-packs/no.ts +1 -0
  173. package/src/core/ui-packs/pl.ts +1 -0
  174. package/src/core/ui-packs/pt-br.ts +1 -0
  175. package/src/core/ui-packs/pt.ts +1 -0
  176. package/src/core/ui-packs/ro.ts +1 -0
  177. package/src/core/ui-packs/ru.ts +1 -0
  178. package/src/core/ui-packs/sk.ts +1 -0
  179. package/src/core/ui-packs/sr.ts +1 -0
  180. package/src/core/ui-packs/sv.ts +1 -0
  181. package/src/core/ui-packs/th.ts +1 -0
  182. package/src/core/ui-packs/tr.ts +1 -0
  183. package/src/core/ui-packs/uk.ts +1 -0
  184. package/src/core/ui-packs/vi.ts +1 -0
  185. package/src/core/ui-packs/zh-tw.ts +1 -0
  186. package/src/core/ui-packs/zh.ts +1 -0
  187. package/src/deploy/adapter-output.ts +18 -8
  188. package/src/deploy/redirects.ts +25 -2
  189. package/src/deploy/robots.ts +6 -1
  190. package/src/deploy/rss.ts +10 -3
  191. package/src/deploy/sitemap.ts +59 -13
  192. package/src/index.ts +5 -0
  193. package/src/markdown/base-links.ts +60 -0
  194. package/src/markdown/code-title.ts +11 -14
  195. package/src/markdown/index.ts +46 -9
  196. package/src/markdown/inline-code.ts +14 -4
  197. package/src/markdown/package-commands.ts +10 -4
  198. package/src/markdown/themes.ts +24 -0
  199. package/src/openapi/model.ts +15 -5
  200. package/src/openapi/parse.ts +21 -0
  201. package/src/openapi/references.ts +75 -21
  202. package/src/openapi/render-mdx.ts +11 -6
  203. package/src/openapi/scalar.ts +32 -16
  204. package/src/openapi/source.ts +59 -10
  205. package/src/registry/eject.ts +247 -19
  206. package/src/registry/registry.ts +0 -3
  207. package/src/search/build.ts +3 -0
  208. package/src/search/documents.ts +36 -4
  209. package/src/search/sync/typesense.ts +6 -4
  210. package/src/seo/jsonld.ts +28 -17
  211. package/src/theme/entry.ts +85 -20
@@ -1,4 +1,8 @@
1
- export { generateRuntime, prerenderDepsPlugin } from "./generate.ts";
1
+ export {
2
+ generateRuntime,
3
+ prerenderDepsPlugin,
4
+ serverAppResolvePlugin,
5
+ } from "./generate.ts";
2
6
  export type { GenerateResult } from "./generate.ts";
3
7
  export { blumeIntegration } from "./integration.ts";
4
8
  export type { BlumeIntegrationOptions, BlumePageRoute } from "./integration.ts";
@@ -30,17 +30,21 @@ const overlayChannel = (): OverlayChannel | undefined =>
30
30
  * overlay clears itself on the next successful HMR update.
31
31
  */
32
32
  export const showBlumeErrorOverlay = (diagnostics: Diagnostic[]): void => {
33
- const errors = diagnostics
34
- .filter((diagnostic) => diagnostic.severity === "error")
35
- .map(enrichDiagnostic);
33
+ const errors: Diagnostic[] = [];
34
+ for (const diagnostic of diagnostics) {
35
+ if (diagnostic.severity === "error") {
36
+ errors.push(enrichDiagnostic(diagnostic));
37
+ }
38
+ }
36
39
  const channel = overlayChannel();
37
40
  if (errors.length === 0 || !channel) {
38
41
  return;
39
42
  }
40
43
  const body = errors
41
44
  .map((diagnostic) => {
45
+ const lineSuffix = diagnostic.line ? `:${diagnostic.line}` : "";
42
46
  const where = diagnostic.file
43
- ? `\n at ${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}` : ""}`
47
+ ? `\n at ${diagnostic.file}${lineSuffix}`
44
48
  : "";
45
49
  const fix = diagnostic.suggestion
46
50
  ? `\n fix: ${diagnostic.suggestion}`
@@ -104,7 +108,7 @@ const negotiateMarkdown =
104
108
  * Blume's Astro integration. Mounts user-authored pages from `pages/` into the
105
109
  * generated runtime via `injectRoute`, keeping each file in its original
106
110
  * location so relative imports and `getStaticPaths` keep working, and teaches
107
- * the dev server to honour `Accept: text/markdown`.
111
+ * the dev server to honor `Accept: text/markdown`.
108
112
  */
109
113
  export const blumeIntegration = (
110
114
  options: BlumeIntegrationOptions
@@ -97,12 +97,14 @@ export const discoverIslands = async (
97
97
  const warnings: string[] = [];
98
98
  const seen = new Map<string, string>();
99
99
 
100
- for (const [index, file] of files.entries()) {
100
+ // Extracted so the skip paths become early `return`s (one `continue` budget
101
+ // per loop under the lint rule) instead of `continue` statements.
102
+ const collectIsland = (file: string, source: string): void => {
101
103
  const base = basename(file);
102
104
  const ext = base.match(ISLAND_FILE)?.groups?.ext;
103
105
  const framework = ext ? FRAMEWORK_BY_EXT[ext] : undefined;
104
106
  if (!framework) {
105
- continue;
107
+ return;
106
108
  }
107
109
  const name = base.replace(ISLAND_FILE, "");
108
110
  // The name is used verbatim as both an MDX tag and an unquoted object key
@@ -113,22 +115,26 @@ export const discoverIslands = async (
113
115
  warnings.push(
114
116
  `Island "${file}" must have a PascalCase identifier filename to be used in MDX (letters, digits, and underscores only, e.g. Counter.tsx → <Counter />); skipping it.`
115
117
  );
116
- continue;
118
+ return;
117
119
  }
118
120
  const existing = seen.get(name);
119
121
  if (existing) {
120
122
  warnings.push(
121
123
  `Two islands both resolve to <${name}> ("${existing}" and "${file}"); ignoring the second. Give them distinct filenames.`
122
124
  );
123
- continue;
125
+ return;
124
126
  }
125
127
  seen.set(name, file);
126
128
  islands.push({
127
- client: readClientMode(sources[index] ?? "", file, warnings),
129
+ client: readClientMode(source, file, warnings),
128
130
  file,
129
131
  framework,
130
132
  name,
131
133
  });
134
+ };
135
+
136
+ for (const [index, file] of files.entries()) {
137
+ collectIsland(file, sources[index] ?? "");
132
138
  }
133
139
 
134
140
  return { islands, warnings };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * HTTP content negotiation for the raw-Markdown variants. The `<route>.md`
3
3
  * endpoints already serve a page's source verbatim; these helpers let the dev
4
- * server honour `Accept: text/markdown` by transparently rewriting a page
4
+ * server honor `Accept: text/markdown` by transparently rewriting a page
5
5
  * request to its `.md` variant.
6
6
  */
7
7
 
@@ -18,7 +18,7 @@ const parseAccept = (accept: string): AcceptEntry[] =>
18
18
  .slice(1)
19
19
  .map((segment) => segment.trim())
20
20
  .find((segment) => segment.startsWith("q="));
21
- const q = qSegment ? Number.parseFloat(qSegment.slice(2)) : 1;
21
+ const q = qSegment ? Number(qSegment.slice(2)) : 1;
22
22
  return { q: Number.isNaN(q) ? 1 : q, type };
23
23
  });
24
24
 
@@ -1,23 +1,14 @@
1
1
  import { extname, relative } from "pathe";
2
- import { glob } from "tinyglobby";
2
+ import { glob, globSync } from "tinyglobby";
3
3
 
4
+ import type { BlumeProject } from "../core/project-graph.ts";
4
5
  import type { BlumePageRoute } from "./integration.ts";
5
6
 
6
- /**
7
- * Discover user `.astro` pages and map them to route patterns. Files keep their
8
- * original location; only the route pattern is derived (index -> parent,
9
- * dynamic `[param]` segments preserved).
10
- */
11
- export const discoverPages = async (
12
- pagesRoot: string
13
- ): Promise<BlumePageRoute[]> => {
14
- const files = await glob(["**/*.astro"], {
15
- absolute: true,
16
- cwd: pagesRoot,
17
- onlyFiles: true,
18
- });
19
- files.sort();
7
+ const PAGE_GLOB = ["**/*.astro"];
20
8
 
9
+ /** Map discovered page files to routes; shared by the async/sync discoverers. */
10
+ const toPageRoutes = (pagesRoot: string, files: string[]): BlumePageRoute[] => {
11
+ files.sort();
21
12
  return files.map((file) => {
22
13
  const rel = relative(pagesRoot, file);
23
14
  const withoutExt = rel.slice(0, rel.length - extname(rel).length);
@@ -32,6 +23,26 @@ export const discoverPages = async (
32
23
  });
33
24
  };
34
25
 
26
+ /**
27
+ * Discover user `.astro` pages and map them to route patterns. Files keep their
28
+ * original location; only the route pattern is derived (index -> parent,
29
+ * dynamic `[param]` segments preserved).
30
+ */
31
+ export const discoverPages = async (
32
+ pagesRoot: string
33
+ ): Promise<BlumePageRoute[]> =>
34
+ toPageRoutes(
35
+ pagesRoot,
36
+ await glob(PAGE_GLOB, { absolute: true, cwd: pagesRoot, onlyFiles: true })
37
+ );
38
+
39
+ /** {@link discoverPages} for synchronous callers (e.g. the sitemap builder). */
40
+ export const discoverPagesSync = (pagesRoot: string): BlumePageRoute[] =>
41
+ toPageRoutes(
42
+ pagesRoot,
43
+ globSync(PAGE_GLOB, { absolute: true, cwd: pagesRoot, onlyFiles: true })
44
+ );
45
+
35
46
  /**
36
47
  * Whether the project already owns `route` — through a custom `.astro` page
37
48
  * (injected, so matched on `pattern`) or a content page (matched on `route`).
@@ -57,6 +68,59 @@ export interface OgCustomRoute {
57
68
  /** Skip private (`_partial`, `.well-known`) and Astro dynamic (`[param]`) parts. */
58
69
  const PRIVATE_SEGMENT = /^[._]/u;
59
70
 
71
+ /** Segments of a static, shareable page pattern; null for dynamic/private ones. */
72
+ const staticSegments = (pattern: string): string[] | null => {
73
+ const segments = pattern.split("/").filter(Boolean);
74
+ return segments.some(
75
+ (part) => PRIVATE_SEGMENT.test(part) || part.includes("[")
76
+ )
77
+ ? null
78
+ : segments;
79
+ };
80
+
81
+ /**
82
+ * The static routes served by custom `.astro` pages — the same filtering as
83
+ * {@link customOgRoutes}, but yielding the routes themselves. Feeds the route
84
+ * sets that must know every servable page beyond the content graph (the link
85
+ * checker, the sitemap); dynamic (`[param]`) and private segments are skipped
86
+ * because their concrete URLs can't be enumerated statically.
87
+ */
88
+ export const customStaticRoutes = (pages: { pattern: string }[]): string[] => {
89
+ const routes = new Set<string>();
90
+ for (const { pattern } of pages) {
91
+ const segments = staticSegments(pattern);
92
+ if (segments !== null) {
93
+ routes.add(segments.length === 0 ? "/" : `/${segments.join("/")}`);
94
+ }
95
+ }
96
+ return [...routes];
97
+ };
98
+
99
+ /**
100
+ * Whether the generated `/changelog` index route exists for this project —
101
+ * `generate.ts` (which writes the page) and the sitemap/link validator all
102
+ * share this check: there are visible `type: changelog` entries — or a
103
+ * release-backed changelog source, whose route must resolve even when a fetch
104
+ * fails — and no user content or custom page already owns `/changelog`.
105
+ */
106
+ export const hasGeneratedChangelog = (
107
+ project: BlumeProject,
108
+ userPages: { pattern: string }[]
109
+ ): boolean => {
110
+ const hasChangelog = project.graph.pages.some(
111
+ (page) =>
112
+ page.contentType === "changelog" &&
113
+ !(page.meta.draft || page.meta.sidebar.hidden)
114
+ );
115
+ const hasChangelogSource = (project.config.content.sources ?? []).some(
116
+ (source) => source.type === "github-releases"
117
+ );
118
+ return (
119
+ (hasChangelog || hasChangelogSource) &&
120
+ !routeIsTaken(userPages, project.graph.pages, "/changelog")
121
+ );
122
+ };
123
+
60
124
  const humanizeSegment = (segment: string): string =>
61
125
  segment
62
126
  .split(/[-_]/u)
@@ -80,20 +144,23 @@ export const customOgRoutes = (
80
144
  ): OgCustomRoute[] => {
81
145
  const seen = new Set<string>();
82
146
  const routes: OgCustomRoute[] = [];
83
- for (const { pattern } of pages) {
84
- const segments = pattern.split("/").filter(Boolean);
85
- if (
86
- segments.some((part) => PRIVATE_SEGMENT.test(part) || part.includes("["))
87
- ) {
88
- continue;
147
+ // Extracted so the skip paths become early `return`s (one `continue` budget
148
+ // per loop under the lint rule) instead of `continue` statements.
149
+ const collectRoute = (pattern: string): void => {
150
+ const segments = staticSegments(pattern);
151
+ if (segments === null) {
152
+ return;
89
153
  }
90
154
  const slug = segments.length === 0 ? "index" : segments.join("/");
91
155
  if (seen.has(slug)) {
92
- continue;
156
+ return;
93
157
  }
94
158
  seen.add(slug);
95
159
  const last = segments.at(-1);
96
160
  routes.push({ slug, title: last ? humanizeSegment(last) : siteTitle });
161
+ };
162
+ for (const { pattern } of pages) {
163
+ collectRoute(pattern);
97
164
  }
98
165
  return routes;
99
166
  };