blume 0.5.0 → 0.5.2

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.
@@ -2041,16 +2041,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
2041
2041
  }>, "many">>;
2042
2042
  /** Explicit sidebar override; when omitted the sidebar is generated. */
2043
2043
  sidebar: z.ZodOptional<z.ZodArray<z.ZodType<SidebarItemConfig, z.ZodTypeDef, SidebarItemConfig>, "many">>;
2044
- sidebarVariants: z.ZodDefault<z.ZodArray<z.ZodObject<{
2045
- items: z.ZodDefault<z.ZodArray<z.ZodType<SidebarItemConfig, z.ZodTypeDef, SidebarItemConfig>, "many">>;
2046
- path: z.ZodString;
2047
- }, "strict", z.ZodTypeAny, {
2048
- path: string;
2049
- items: SidebarItemConfig[];
2050
- }, {
2051
- path: string;
2052
- items?: SidebarItemConfig[] | undefined;
2053
- }>, "many">>;
2054
2044
  tabs: z.ZodOptional<z.ZodArray<z.ZodObject<{
2055
2045
  icon: z.ZodOptional<z.ZodString>;
2056
2046
  items: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -2127,10 +2117,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
2127
2117
  }[];
2128
2118
  kind: "version" | "dropdown" | "language" | "product";
2129
2119
  }[];
2130
- sidebarVariants: {
2131
- path: string;
2132
- items: SidebarItemConfig[];
2133
- }[];
2134
2120
  sidebar?: SidebarItemConfig[] | undefined;
2135
2121
  tabs?: {
2136
2122
  path: string;
@@ -2175,10 +2161,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
2175
2161
  tag?: string | undefined;
2176
2162
  }[] | undefined;
2177
2163
  }[] | undefined;
2178
- sidebarVariants?: {
2179
- path: string;
2180
- items?: SidebarItemConfig[] | undefined;
2181
- }[] | undefined;
2182
2164
  tabs?: {
2183
2165
  path: string;
2184
2166
  label: string;
@@ -2811,10 +2793,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
2811
2793
  }[];
2812
2794
  kind: "version" | "dropdown" | "language" | "product";
2813
2795
  }[];
2814
- sidebarVariants: {
2815
- path: string;
2816
- items: SidebarItemConfig[];
2817
- }[];
2818
2796
  sidebar?: SidebarItemConfig[] | undefined;
2819
2797
  tabs?: {
2820
2798
  path: string;
@@ -3195,10 +3173,6 @@ export declare const blumeConfigSchema: z.ZodObject<{
3195
3173
  tag?: string | undefined;
3196
3174
  }[] | undefined;
3197
3175
  }[] | undefined;
3198
- sidebarVariants?: {
3199
- path: string;
3200
- items?: SidebarItemConfig[] | undefined;
3201
- }[] | undefined;
3202
3176
  tabs?: {
3203
3177
  path: string;
3204
3178
  label: string;
@@ -168,11 +168,6 @@ export interface NavSelector {
168
168
  kind: "dropdown" | "language" | "product" | "version";
169
169
  items: NavSelectorItem[];
170
170
  }
171
- /** Sidebar tree used when the current route belongs to a nav partition. */
172
- export interface NavSidebarVariant {
173
- path: string;
174
- sidebar: NavNode[];
175
- }
176
171
  /** Chrome overrides used when the current route belongs to a nav partition. */
177
172
  export interface NavChromeVariant {
178
173
  path: string;
@@ -184,7 +179,6 @@ export interface Navigation {
184
179
  selectors: NavSelector[];
185
180
  chromeVariants: NavChromeVariant[];
186
181
  sidebar: NavNode[];
187
- sidebarVariants: NavSidebarVariant[];
188
182
  /** Repo URL for the header link, or null when hidden (`navigation.repo`). */
189
183
  repoUrl?: string | null;
190
184
  }
@@ -1,2 +1,16 @@
1
1
  import type { BlumeConfig } from "../../core/schema.ts";
2
+ type JsonObject = Record<string, unknown>;
3
+ export interface MintlifyRedirectPartition {
4
+ /** Static redirects Blume can honor, translated to Blume's `from`/`to` shape. */
5
+ kept: NonNullable<BlumeConfig["redirects"]>;
6
+ /** Source paths of dynamic redirects dropped because Blume can't model them. */
7
+ dropped: string[];
8
+ }
9
+ /**
10
+ * Split a spec's redirects into the static ones Blume emits and the dynamic
11
+ * (wildcard/param) ones it drops. Keeping a dynamic redirect crashes the Astro
12
+ * build, so the migrator surfaces the dropped sources as a warning instead.
13
+ */
14
+ export declare const partitionMintlifyRedirects: (spec: JsonObject) => MintlifyRedirectPartition;
2
15
  export declare const loadMintlifyConfig: (root: string, file: string) => Promise<BlumeConfig>;
16
+ export {};
@@ -51,7 +51,9 @@ Go from an empty folder to a running docs site in a few commands. Blume needs **
51
51
  </Step>
52
52
  </Steps>
53
53
 
54
- :::tip Blume works with any package manager and never requires you to set up Astro or Tailwind yourself. :::
54
+ :::tip
55
+ Blume works with any package manager and never requires you to set up Astro or Tailwind yourself.
56
+ :::
55
57
 
56
58
  ## Write your first page
57
59
 
@@ -65,7 +67,9 @@ description: Welcome to my docs.
65
67
 
66
68
  Welcome! Use **Markdown** and built-in components — no imports required:
67
69
 
68
- :::note Blume ships callouts, cards, tabs, steps, and more. :::
70
+ :::note
71
+ Blume ships callouts, cards, tabs, steps, and more.
72
+ :::
69
73
  ```
70
74
 
71
75
  Save it, and the dev server reloads instantly. Navigation, search, and page metadata are inferred from your files as you add them — keep writing, and the site keeps up.
@@ -86,7 +86,9 @@ On **Vercel**, **Netlify**, and **Cloudflare Pages**, Blume picks the matching a
86
86
 
87
87
  A server build includes everything a static build does, plus any Astro endpoints or middleware you add. The `node` adapter produces a standalone server you can run directly.
88
88
 
89
- :::note Server features have their own configuration — for example, Ask AI needs a model API key. See the [AI guide](/docs/configuration/ai) for setup. :::
89
+ :::note
90
+ Server features have their own configuration — for example, Ask AI needs a model API key. See the [AI guide](/docs/configuration/ai) for setup.
91
+ :::
90
92
 
91
93
  ## Redirects
92
94
 
@@ -14,7 +14,9 @@ openapi: {
14
14
 
15
15
  That mounts the reference at `/reference` (an overview page) with each operation at `/reference/<tag>/<operation>`, and adds a header tab. The `spec` is either an `http(s)` URL or a path to a local file in your project. Blume parses it with [Scalar's OpenAPI parser](https://github.com/scalar/scalar) — Swagger 2.0 and OpenAPI 3.0 specs are upgraded to 3.1 automatically.
16
16
 
17
- :::note Operations are indexed for search by their **summary and tag**. The rendered schema tables and code samples aren't full-text indexed; search matches an operation's title and section, then links to its own page. :::
17
+ :::note
18
+ Operations are indexed for search by their **summary and tag**. The rendered schema tables and code samples aren't full-text indexed; search matches an operation's title and section, then links to its own page.
19
+ :::
18
20
 
19
21
  ## A local spec
20
22
 
@@ -94,4 +96,6 @@ asyncapi: {
94
96
  }
95
97
  ```
96
98
 
97
- :::warning Scalar's AsyncAPI support is still a work in progress — it renders channels, operations, messages, and a Models section, but there's **no interactive playground** for events yet. It improves upstream over time. :::
99
+ :::warning
100
+ Scalar's AsyncAPI support is still a work in progress — it renders channels, operations, messages, and a Models section, but there's **no interactive playground** for events yet. It improves upstream over time.
101
+ :::
@@ -26,7 +26,9 @@ Bridge mode is entirely automatic, gated on two conditions:
26
26
 
27
27
  With both true, Blume treats the Mintlify config as its source of truth for that run.
28
28
 
29
- :::note Bridge mode applies to `blume build` too, not just `blume dev` — you can produce a static site from an unconverted Mintlify project. Only `blume dev` prints the detection notice. :::
29
+ :::note
30
+ Bridge mode applies to `blume build` too, not just `blume dev` — you can produce a static site from an unconverted Mintlify project. Only `blume dev` prints the detection notice.
31
+ :::
30
32
 
31
33
  ## What happens under the hood
32
34
 
@@ -260,7 +260,9 @@ import data from "blume:data";
260
260
  </RootLayout>
261
261
  ```
262
262
 
263
- :::note `RootLayout` is part of the generated runtime, so its props can change between releases. When you want a layout that's fully yours, [`blume eject`](/docs/configuration/customization#eject) turns `.blume/` into a standard Astro project you own outright. :::
263
+ :::note
264
+ `RootLayout` is part of the generated runtime, so its props can change between releases. When you want a layout that's fully yours, [`blume eject`](/docs/configuration/customization#eject) turns `.blume/` into a standard Astro project you own outright.
265
+ :::
264
266
 
265
267
  ## 404 page
266
268
 
@@ -18,7 +18,9 @@ The `<tool>` is the framework you're coming from:
18
18
  | [Nextra](#nextra) | `content/` or `pages/` + `_meta` | `blume migrate nextra` |
19
19
  | [Starlight](#starlight) | `src/content/docs` + `astro.config.*` | `blume migrate starlight` |
20
20
 
21
- :::warning A migration **rewrites files in place** — pages, config, navigation, and assets. Commit (or stash) your work first so you can review the diff and roll back cleanly. :::
21
+ :::warning
22
+ A migration **rewrites files in place** — pages, config, navigation, and assets. Commit (or stash) your work first so you can review the diff and roll back cleanly.
23
+ :::
22
24
 
23
25
  ## What a migration does
24
26
 
@@ -62,7 +64,9 @@ Reads `docs.json` (or legacy `mint.json`) and rewrites every page **in place**
62
64
  - Fonts map to [`theme.fonts`](/docs/configuration/theming) when the family is one of Blume's curated Google Fonts (`fonts.family`, or a `heading`/`body` split); a family outside that set is warned about, not guessed. Header links (`navbar.links`/`navbar.primary`) and footer socials (`footer.socials`) have no `blume.config` equivalent yet, so they're reported as warnings rather than dropped silently — re-add them with [`navigation.tabs`](/docs/content/navigation) or a Header/Footer [layout override](/docs/advanced/custom-pages). The contextual page menu and last-updated timestamp are already Blume defaults.
63
65
  - Field components — [`<ParamField>`, `<ResponseField>`, `<RequestField>`](/docs/content/components#api-fields) — render natively via Blume's compat components, so hand-written CLI/SDK/endpoint field docs carry over unchanged. Any remaining component with no Blume equivalent (e.g. `<Update>`) is flagged for manual review.
64
66
 
65
- :::tip Want to preview Blume against a Mintlify codebase **without** rewriting anything? Run `blume dev` with no config and Blume serves `docs.json` as-is in [Bridge mode](/docs/advanced/bridge). Migrate when you're ready to commit. :::
67
+ :::tip
68
+ Want to preview Blume against a Mintlify codebase **without** rewriting anything? Run `blume dev` with no config and Blume serves `docs.json` as-is in [Bridge mode](/docs/advanced/bridge). Migrate when you're ready to commit.
69
+ :::
66
70
 
67
71
  ### Fumadocs
68
72
 
@@ -125,7 +125,9 @@ ai: {
125
125
 
126
126
  Set `apiKeyEnv` (and, for the named providers, `baseUrl`) on any backend to point at a different env var or proxy.
127
127
 
128
- :::note **Inkeep** answers from the content you've indexed in the Inkeep dashboard — it runs its own retrieval — so Blume leaves it ungrounded. Every other backend is [grounded](#grounding) in this site's pages. :::
128
+ :::note
129
+ **Inkeep** answers from the content you've indexed in the Inkeep dashboard — it runs its own retrieval — so Blume leaves it ungrounded. Every other backend is [grounded](#grounding) in this site's pages.
130
+ :::
129
131
 
130
132
  Keys are read with `process.env`, which covers the Node, Vercel, and Netlify adapters. On Cloudflare, expose the key through the platform's [runtime binding](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#environment-variables-and-secrets). Enabling Ask AI also turns on React for the in-page island — see [Customization](/docs/configuration/customization#interactive-islands).
131
133
 
@@ -7,7 +7,9 @@ Blume injects analytics for you from a single `analytics` block in `blume.config
7
7
 
8
8
  Analytics loads in **production builds only**. The scripts are emitted by `blume build`, never by `blume dev`, so local traffic never reaches your dashboards and you don't need a separate "development" project.
9
9
 
10
- :::note Analytics is opt-in. With no `analytics` block, Blume injects nothing. :::
10
+ :::note
11
+ Analytics is opt-in. With no `analytics` block, Blume injects nothing.
12
+ :::
11
13
 
12
14
  ## Vercel Web Analytics
13
15
 
@@ -30,7 +30,9 @@ Any format you omit (or set to `false`) is left out of the menu. With both off
30
30
 
31
31
  Because it prints the live, fully-styled page, the result keeps crisp, selectable text, real fonts, and syntax-highlighted code. It needs no dependencies and works the same in dev, in production, and on static hosts.
32
32
 
33
- :::note PDF export goes through the browser's native print dialog, so the exact "Save as PDF" wording and options depend on the browser. Enable **Background graphics** in the dialog to keep code-block and callout backgrounds. :::
33
+ :::note
34
+ PDF export goes through the browser's native print dialog, so the exact "Save as PDF" wording and options depend on the browser. Enable **Background graphics** in the dialog to keep code-block and callout backgrounds.
35
+ :::
34
36
 
35
37
  ## EPUB
36
38
 
@@ -42,4 +44,6 @@ To keep the file readable on a device with no stylesheet or JavaScript of its ow
42
44
 
43
45
  Both formats export the **single page** a reader is viewing — not the whole site — which matches where the action lives, beneath that page's table of contents.
44
46
 
45
- :::note In an EPUB there's no JavaScript to switch tabs, so tabbed content — like [package-install](/docs/content/syntax) blocks and code groups — is expanded to show every panel at once. Client-rendered embeds such as Mermaid diagrams aren't included. :::
47
+ :::note
48
+ In an EPUB there's no JavaScript to switch tabs, so tabbed content — like [package-install](/docs/content/syntax) blocks and code groups — is expanded to show every panel at once. Client-rendered embeds such as Mermaid diagrams aren't included.
49
+ :::
@@ -94,7 +94,9 @@ seo:
94
94
  ---
95
95
  ```
96
96
 
97
- :::note OG rendering uses hex internally, so an `oklch` custom accent falls back to the default. Use a named accent (`blue`, `teal`, …) or a hex value for custom cards. :::
97
+ :::note
98
+ OG rendering uses hex internally, so an `oklch` custom accent falls back to the default. Use a named accent (`blue`, `teal`, …) or a hex value for custom cards.
99
+ :::
98
100
 
99
101
  `seo.image` is frontmatter, so it only covers Markdown and MDX content. To give a custom [`.astro` page](/docs/advanced/custom-pages) its own social image — a marketing home or landing page, and the way to give the home page alone a bespoke share image — pass the `ogImage` prop to `PageLayout`.
100
102
 
@@ -22,7 +22,9 @@ i18n: {
22
22
 
23
23
  Each locale has a `code` (used in URLs), a `label` (shown in the language switcher), and an optional `dir` for right-to-left scripts (`"ltr"` by default).
24
24
 
25
- :::tip Migrating from Mintlify? [`blume migrate mintlify`](/docs/advanced/migrate) reads `navigation.languages[]` and writes the `i18n` block for you — the `default: true` language becomes `defaultLocale`, and translated folders already match Blume's layout. :::
25
+ :::tip
26
+ Migrating from Mintlify? [`blume migrate mintlify`](/docs/advanced/migrate) reads `navigation.languages[]` and writes the `i18n` block for you — the `default: true` language becomes `defaultLocale`, and translated folders already match Blume's layout.
27
+ :::
26
28
 
27
29
  ## Organize translated content
28
30
 
@@ -98,7 +100,9 @@ i18n: {
98
100
 
99
101
  Fallback pages are excluded from the search index and aren't advertised as real translations in `hreflang`, so untranslated content doesn't compete for ranking. They still appear in that locale's sidebar, so navigation stays complete — a reader can reach every page in any language.
100
102
 
101
- :::tip Start by translating your most important pages — the homepage, quickstart, and top guides — and let the rest fall back. You can fill in translations over time without breaking any links. :::
103
+ :::tip
104
+ Start by translating your most important pages — the homepage, quickstart, and top guides — and let the rest fall back. You can fill in translations over time without breaking any links.
105
+ :::
102
106
 
103
107
  ## The language switcher
104
108
 
@@ -24,7 +24,9 @@ Here's a live counter: <Counter />
24
24
 
25
25
  The filename is the component name, so it **must be a PascalCase identifier** — letters, digits, and underscores only (`Counter.tsx` → `<Counter />`). Lowercase filenames, names with dashes/dots/spaces (like `Time-Picker.tsx`), and two islands that resolve to the same name are skipped with a build warning.
26
26
 
27
- :::note Islands are for **interactive** UI. For a static component you reuse across pages (a styled callout, a pricing table), use an [MDX override](/docs/configuration/customization) instead — it ships no JavaScript. :::
27
+ :::note
28
+ Islands are for **interactive** UI. For a static component you reuse across pages (a styled callout, a pricing table), use an [MDX override](/docs/configuration/customization) instead — it ships no JavaScript.
29
+ :::
28
30
 
29
31
  ## Registering islands in `components.ts`
30
32
 
@@ -98,7 +100,9 @@ const on = ref(false);
98
100
 
99
101
  Props you pass in MDX (`<Counter start={5} />`) are forwarded to the component, and children (`<Counter>label</Counter>`) arrive as the default slot.
100
102
 
101
- :::tip Islands hydrate on the client, so anything you pass as a prop must be serializable — strings, numbers, plain objects, not functions. :::
103
+ :::tip
104
+ Islands hydrate on the client, so anything you pass as a prop must be serializable — strings, numbers, plain objects, not functions.
105
+ :::
102
106
 
103
107
  ## Hooks
104
108
 
@@ -45,7 +45,9 @@ The `pages` array lists children by slug — the folder or file name with its nu
45
45
  - **`group`** — a collapsible `<details>` disclosure. Pair it with `collapsed` to set the initial state; a group containing the current page always starts open.
46
46
  - **`page`** — a single row that, when clicked, slides the sidebar into a sub-panel showing only that group's items, with a back arrow at the top. The panel is route-aware, so landing directly on a page inside the group opens straight to it.
47
47
 
48
- :::tip `page` mode keeps deep sections tidy — reach for it when a group has many children and you'd rather drill into it than scroll past it. :::
48
+ :::tip
49
+ `page` mode keeps deep sections tidy — reach for it when a group has many children and you'd rather drill into it than scroll past it.
50
+ :::
49
51
 
50
52
  `display` works the same on a group in an [explicit sidebar](/docs/content/navigation#explicit-sidebar) config.
51
53
 
@@ -242,7 +242,9 @@ config.title;
242
242
  ```
243
243
  ````
244
244
 
245
- :::note Hide the language icons or wrap long lines instead of scrolling with `markdown: { code: { icons: false, wrap: true } }` in `blume.config.ts`. :::
245
+ :::note
246
+ Hide the language icons or wrap long lines instead of scrolling with `markdown: { code: { icons: false, wrap: true } }` in `blume.config.ts`.
247
+ :::
246
248
 
247
249
  ## Package install
248
250
 
@@ -288,60 +290,82 @@ Callouts pull a reader's attention to context, advice, or risk. Write them as `:
288
290
 
289
291
  Neutral, supporting context the reader should keep in mind.
290
292
 
291
- :::note Blume regenerates `.blume/` on every run — never edit it by hand. :::
293
+ :::note
294
+ Blume regenerates `.blume/` on every run — never edit it by hand.
295
+ :::
292
296
 
293
297
  ```md
294
- :::note Blume regenerates `.blume/` on every run — never edit it by hand. :::
298
+ :::note
299
+ Blume regenerates `.blume/` on every run — never edit it by hand.
300
+ :::
295
301
  ```
296
302
 
297
303
  ### Tip
298
304
 
299
305
  A helpful shortcut or best practice that isn't required but makes life easier.
300
306
 
301
- :::tip Set `deployment.site` so sitemaps and Open Graph images use absolute URLs. :::
307
+ :::tip
308
+ Set `deployment.site` so sitemaps and Open Graph images use absolute URLs.
309
+ :::
302
310
 
303
311
  ```md
304
- :::tip Set `deployment.site` so sitemaps and Open Graph images use absolute URLs. :::
312
+ :::tip
313
+ Set `deployment.site` so sitemaps and Open Graph images use absolute URLs.
314
+ :::
305
315
  ```
306
316
 
307
317
  ### Success
308
318
 
309
319
  Confirm a positive outcome or that a step completed as expected.
310
320
 
311
- :::success Your docs built successfully and are ready to deploy. :::
321
+ :::success
322
+ Your docs built successfully and are ready to deploy.
323
+ :::
312
324
 
313
325
  ```md
314
- :::success Your docs built successfully and are ready to deploy. :::
326
+ :::success
327
+ Your docs built successfully and are ready to deploy.
328
+ :::
315
329
  ```
316
330
 
317
331
  ### Warning
318
332
 
319
333
  Flag something that needs care to avoid a mistake or surprising behavior.
320
334
 
321
- :::warning[Heads up] Switching to `output: "server"` requires an adapter before you can deploy. :::
335
+ :::warning[Heads up] Switching to `output: "server"` requires an adapter before you can deploy.
336
+ :::
322
337
 
323
338
  ```md
324
- :::warning[Heads up] Switching to `output: "server"` requires an adapter before you can deploy. :::
339
+ :::warning[Heads up] Switching to `output: "server"` requires an adapter before you can deploy.
340
+ :::
325
341
  ```
326
342
 
327
343
  ### Danger
328
344
 
329
345
  Call out a destructive or breaking action that can't easily be undone.
330
346
 
331
- :::danger `blume eject` is a one-way step — the generated Astro project becomes yours. :::
347
+ :::danger
348
+ `blume eject` is a one-way step — the generated Astro project becomes yours.
349
+ :::
332
350
 
333
351
  ```md
334
- :::danger `blume eject` is a one-way step — the generated Astro project becomes yours. :::
352
+ :::danger
353
+ `blume eject` is a one-way step — the generated Astro project becomes yours.
354
+ :::
335
355
  ```
336
356
 
337
357
  ### Info
338
358
 
339
359
  An informational aside; an alias-friendly default that reads as neutral.
340
360
 
341
- :::info The core theme ships no client framework JS. :::
361
+ :::info
362
+ The core theme ships no client framework JS.
363
+ :::
342
364
 
343
365
  ```md
344
- :::info The core theme ships no client framework JS. :::
366
+ :::info
367
+ The core theme ships no client framework JS.
368
+ :::
345
369
  ```
346
370
 
347
371
  The names `caution`, `error`, `important`, and `warn` are accepted as aliases for `warning`, `danger`, `note`, and `warning` respectively.
@@ -364,7 +388,9 @@ $$
364
388
  $$
365
389
  ```
366
390
 
367
- :::note Math is opt-in because `$` is common in prose and code. Enable it with `markdown: { math: true }` in `blume.config.ts`. :::
391
+ :::note
392
+ Math is opt-in because `$` is common in prose and code. Enable it with `markdown: { math: true }` in `blume.config.ts`.
393
+ :::
368
394
 
369
395
  ## Smart punctuation
370
396
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blume",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Documentation that's fast, AI-ready, and zero-config.",
5
5
  "keywords": [
6
6
  "astro",
@@ -618,7 +618,6 @@ export const buildRuntimeData = (project: BlumeProject): string => {
618
618
  chromeVariants: [],
619
619
  selectors: [],
620
620
  sidebar: [],
621
- sidebarVariants: [],
622
621
  tabs: [],
623
622
  }
624
623
  ),
@@ -974,6 +973,11 @@ export const generateRuntime = async (
974
973
  // entryId so i18n duplicates of one entry write a single file.
975
974
  const staged = collectStaged(project);
976
975
  const hasStaged = staged.size > 0;
976
+ // Only emit a project-scanning `docs` collection when a filesystem source
977
+ // actually feeds it. Bridge mode has just the staged Mintlify source, so the
978
+ // `docs` glob would otherwise scan (and watch) the whole project root for
979
+ // nothing — see contentConfigTemplate.
980
+ const hasFilesystemSource = project.sources.some((source) => !source.staged);
977
981
 
978
982
  const structural = await Promise.all([
979
983
  write(
@@ -1004,7 +1008,12 @@ export const generateRuntime = async (
1004
1008
  write(join(srcDir, "env.d.ts"), envTemplate()),
1005
1009
  write(
1006
1010
  join(srcDir, "content.config.ts"),
1007
- contentConfigTemplate({ config, context, staged: hasStaged })
1011
+ contentConfigTemplate({
1012
+ config,
1013
+ context,
1014
+ filesystem: hasFilesystemSource,
1015
+ staged: hasStaged,
1016
+ })
1008
1017
  ),
1009
1018
  write(
1010
1019
  join(srcDir, "pages", "[...slug].astro"),
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
 
3
- import { dirname, join } from "pathe";
3
+ import { dirname, isAbsolute, join, relative } from "pathe";
4
4
 
5
5
  import { askBackendRuntimeDep } from "../ai/ask.ts";
6
6
  import type { AskBackend } from "../ai/ask.ts";
@@ -392,18 +392,44 @@ export const contentConfigTemplate = (options: {
392
392
  staged?: boolean;
393
393
  /** Base dir for the staged collection; defaults to `<outDir>/content`. */
394
394
  stagedBase?: string;
395
+ /**
396
+ * Whether any filesystem (non-staged) source feeds the `docs` collection.
397
+ * When false (e.g. Mintlify bridge mode, where every page is staged), the
398
+ * collection globs nothing — see below.
399
+ */
400
+ filesystem?: boolean;
395
401
  }): string => {
396
402
  const { context, config } = options;
397
403
  const stagedBase = options.stagedBase ?? stagedContentDir(context.outDir);
398
404
 
399
405
  // Fold the content excludes into the glob as negative patterns so the `docs`
400
- // collection never walks into ignored trees. This matters when `content.root`
401
- // is the project root (Mintlify bridge mode, or a migrated `.`-rooted project):
402
- // without it the collection would scan `node_modules`, `snippets`, etc.
403
- const docsPattern = [
404
- ...config.content.include,
405
- ...(config.content.exclude ?? []).map((pattern) => `!${pattern}`),
406
- ];
406
+ // collection doesn't ingest ignored trees (`node_modules`, `snippets`, the
407
+ // staged bodies under `.blume/content`, …) as entries. This matters when
408
+ // `content.root` is the project root (a migrated `.`-rooted project).
409
+ const outDirRel = relative(context.contentRoot, context.outDir);
410
+ const outDirIgnore =
411
+ outDirRel && !outDirRel.startsWith("..") && !isAbsolute(outDirRel)
412
+ ? [`!${outDirRel}/**`]
413
+ : [];
414
+
415
+ // With no filesystem source, no route renders through `docs`, so glob nothing.
416
+ // Beyond skipping wasted work, this is the only thing that keeps Astro's
417
+ // content-layer *watcher* out of `.blume/`: bridge mode roots the collection
418
+ // at the project dir (which contains `.blume/.astro/fonts`, rewritten on every
419
+ // request), and the watcher's match test is `picomatch.isMatch(path, pattern)`
420
+ // — with array-OR semantics, any `!ignored/**` negation *matches* unrelated
421
+ // files, so negative patterns can't exclude a subtree there. An empty pattern
422
+ // matches nothing, so the watcher stays silent. The collection is still
423
+ // declared below so `getCollection("docs")` / `getEntry` resolve (to empty).
424
+ const filesystem = options.filesystem ?? true;
425
+ const docsPattern = filesystem
426
+ ? [
427
+ ...config.content.include,
428
+ ...(config.content.exclude ?? []).map((pattern) => `!${pattern}`),
429
+ "!**/node_modules/**",
430
+ ...outDirIgnore,
431
+ ]
432
+ : [];
407
433
 
408
434
  // Non-filesystem sources render through a parallel `staged` collection backed
409
435
  // by materialized MDX, so the filesystem `docs` collection stays untouched.
@@ -5,35 +5,10 @@ import { defineCommand } from "citty";
5
5
  import { basename, dirname, isAbsolute, join, relative } from "pathe";
6
6
 
7
7
  import { ensureGitignore } from "../../core/gitignore.ts";
8
- import { getBlumeVersion } from "../../core/version.ts";
8
+ import { blumePackageJson, toPackageName } from "../../core/package-json.ts";
9
9
  import { eject } from "../../registry/eject.ts";
10
10
  import { logger } from "../log.ts";
11
11
 
12
- /**
13
- * Derive a valid npm package name from a directory name, falling back to
14
- * `docs` when nothing usable remains.
15
- */
16
- const toPackageName = (raw: string): string =>
17
- raw
18
- .toLowerCase()
19
- .replaceAll(/[^a-z0-9._-]+/gu, "-")
20
- .replaceAll(/^[-_.]+|[-_.]+$/gu, "") || "docs";
21
-
22
- const packageTemplate = (name: string, version: string): string => `{
23
- "name": ${JSON.stringify(name)},
24
- "private": true,
25
- "type": "module",
26
- "scripts": {
27
- "dev": "blume dev",
28
- "build": "blume build",
29
- "doctor": "blume doctor"
30
- },
31
- "dependencies": {
32
- "blume": "^${version}"
33
- }
34
- }
35
- `;
36
-
37
12
  const TEMPLATES = ["docs", "api", "sdk", "changelog"] as const;
38
13
  type Template = (typeof TEMPLATES)[number];
39
14
 
@@ -221,7 +196,7 @@ export const initCommand = defineCommand({
221
196
  const starter = STARTERS[template];
222
197
  const createdPackage = await writeFileSafe(
223
198
  join(root, "package.json"),
224
- packageTemplate(toPackageName(basename(root)), getBlumeVersion())
199
+ blumePackageJson(toPackageName(basename(root)))
225
200
  );
226
201
  await writeFileSafe(join(root, "blume.config.ts"), starter.config);
227
202
  await Promise.all(
package/src/core/graph.ts CHANGED
@@ -96,7 +96,6 @@ export const buildContentGraph = (
96
96
  selectors: options.navigation.selectors,
97
97
  sharedFolderMeta: options.sharedFolderMeta,
98
98
  sidebar: options.navigation.sidebar,
99
- sidebarVariants: options.navigation.sidebarVariants,
100
99
  tabs,
101
100
  });
102
101
  }
@@ -104,7 +103,6 @@ export const buildContentGraph = (
104
103
  chromeVariants: [],
105
104
  selectors: [],
106
105
  sidebar: [],
107
- sidebarVariants: [],
108
106
  tabs: [],
109
107
  };
110
108
  } else {
@@ -114,7 +112,6 @@ export const buildContentGraph = (
114
112
  selectors: options.navigation.selectors,
115
113
  sharedFolderMeta: options.sharedFolderMeta,
116
114
  sidebar: options.navigation.sidebar,
117
- sidebarVariants: options.navigation.sidebarVariants,
118
115
  tabs: options.navigation.tabs,
119
116
  });
120
117
  }
@@ -42,10 +42,7 @@ const collectIcons = (
42
42
  push(item.icon, `selector "${item.label}"`);
43
43
  }
44
44
  }
45
- const sidebars = [
46
- navigation.sidebar,
47
- ...navigation.sidebarVariants.map((variant) => variant.sidebar),
48
- ];
45
+ const sidebars = [navigation.sidebar];
49
46
  for (const sidebar of sidebars) {
50
47
  for (const node of flattenNodes(sidebar)) {
51
48
  push(node.icon, `"${node.label}"`);
@@ -140,10 +137,6 @@ const duplicateLabelDiagnostics = (navigation: Navigation): Diagnostic[] => {
140
137
  };
141
138
  const sidebars: { nodes: NavNode[]; where: string }[] = [
142
139
  { nodes: navigation.sidebar, where: "at the top level" },
143
- ...navigation.sidebarVariants.map((variant) => ({
144
- nodes: variant.sidebar,
145
- where: `in the "${variant.path}" section`,
146
- })),
147
140
  ];
148
141
  for (const { nodes, where } of sidebars) {
149
142
  checkLevel(nodes, where);
@@ -162,10 +155,7 @@ const hiddenInSidebarDiagnostics = (
162
155
  if (hidden.size === 0) {
163
156
  return [];
164
157
  }
165
- const sidebars = [
166
- navigation.sidebar,
167
- ...navigation.sidebarVariants.map((variant) => variant.sidebar),
168
- ];
158
+ const sidebars = [navigation.sidebar];
169
159
  const diagnostics: Diagnostic[] = [];
170
160
  const seen = new Set<string>();
171
161
  for (const sidebar of sidebars) {