blume 1.0.3 → 1.0.4
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/CHANGELOG.md +29 -0
- package/dist/cli/index.js +441 -259
- package/dist/cli/index.js.map +12 -11
- package/dist/types/core/config-input.d.ts +27 -8
- package/dist/types/core/data.d.ts +12 -0
- package/dist/types/core/i18n-ui.d.ts +136 -136
- package/dist/types/core/schema.d.ts +420 -350
- package/dist/types/core/types.d.ts +10 -0
- package/dist/types/openapi/references.d.ts +12 -7
- package/docs/advanced/api-reference.mdx +11 -3
- package/docs/configuration/seo.mdx +20 -1
- package/docs/content/components.mdx +1 -2
- package/docs/content/navigation.mdx +10 -0
- package/docs/content/syntax.mdx +116 -4
- package/package.json +1 -1
- package/skills/blume-migrate/SKILL.md +170 -0
- package/skills/blume-migrate/assets/oxfmt@0.55.0.patch +20 -0
- package/skills/blume-migrate/references/docusaurus.md +95 -0
- package/skills/blume-migrate/references/fumadocs.md +95 -0
- package/skills/blume-migrate/references/mintlify.md +155 -0
- package/skills/blume-migrate/references/monorepo.md +224 -0
- package/skills/blume-migrate/references/nextra.md +76 -0
- package/skills/blume-migrate/references/starlight.md +116 -0
- package/skills/blume-migrate/scripts/mintlify-codemod.mjs +466 -0
- package/src/astro/component-slots.ts +3 -2
- package/src/astro/generate.ts +82 -23
- package/src/astro/templates.ts +93 -34
- package/src/components/content/Callout.astro +8 -2
- package/src/components/content/Prompt.astro +25 -13
- package/src/components/layout/Header.astro +4 -8
- package/src/components/layout/Logo.astro +13 -1
- package/src/components/layout/PageFeedback.astro +1 -1
- package/src/components/layout/PageLayout.astro +4 -8
- package/src/components/layout/Pagination.astro +6 -6
- package/src/components/layout/RootLayout.astro +4 -8
- package/src/components/layout/Search.astro +1 -1
- package/src/components/layout/nav-utils.ts +9 -7
- package/src/core/config-input.ts +29 -8
- package/src/core/data.ts +9 -1
- package/src/core/navigation.ts +55 -13
- package/src/core/schema.ts +17 -1
- package/src/core/sources/watch.ts +5 -0
- package/src/core/types.ts +10 -0
- package/src/markdown/index.ts +2 -0
- package/src/markdown/language-icon.ts +2 -1
- package/src/markdown/table-wrap.ts +43 -0
- package/src/og/card.ts +39 -12
- package/src/og/index.ts +1 -1
- package/src/og/logo.ts +21 -0
- package/src/openapi/references.ts +19 -16
- package/src/theme/entry.ts +50 -5
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Fumadocs → Blume
|
|
2
|
+
|
|
3
|
+
Fumadocs is code-first: navigation comes from the folder tree + per-folder `meta.json`. This maps cleanly to Blume — most content passes through, and `meta.json` becomes `meta.ts`. Fumadocs v16 runs on Next.js, React Router, TanStack Start, Astro, or Waku — detect by the Fumadocs files, not by Next.js files, and adjust the teardown list to whichever host framework is present.
|
|
4
|
+
|
|
5
|
+
## Detect
|
|
6
|
+
|
|
7
|
+
- Content under **`content/docs/`** (the Fumadocs convention).
|
|
8
|
+
- Per-folder **`meta.json`** files.
|
|
9
|
+
- `fumadocs-ui` / `fumadocs-core` / `fumadocs-mdx` deps and a `source.config.ts`.
|
|
10
|
+
- A `loader({ baseUrl: "/docs" })` call in `lib/source.ts` (or `app/source.ts`, `src/lib/source.ts`, `source.ts`).
|
|
11
|
+
|
|
12
|
+
## Config
|
|
13
|
+
|
|
14
|
+
Fumadocs declares almost nothing Blume needs — only two things map:
|
|
15
|
+
|
|
16
|
+
- **Title** — read `package.json` `name`, prettified (drop scope, split on `-_`, Title-Case). For a generic monorepo name (`web`, `app`, `docs`…), use the repo-root directory name instead.
|
|
17
|
+
- **Route prefix** — the `baseUrl` in the source loader (required in Fumadocs, so it's always declared). If it's a real prefix (e.g. `/docs`), set top-level **`basePath: "/docs"`** — it prefixes every route while staying invisible to the sidebar, which matches Fumadocs' behavior. (A `content.sources` `prefix` also works but adds a wrapping nav group — only use it when you _want_ the group.) If `baseUrl` is `"/"`, serve from the site root (nothing to set).
|
|
18
|
+
|
|
19
|
+
Everything else is `defineConfig({ title })`.
|
|
20
|
+
|
|
21
|
+
**Also read `source.config.ts` `mdxOptions`** — remark/rehype plugins live there:
|
|
22
|
+
|
|
23
|
+
- `remark-math`/`rehype-katex` → nothing to configure: block math `$$…$$` renders in `.mdx` out of the box (there is **no** `markdown.math` field). Inline `$…$` is **not** supported — convert inline math to `$$…$$` or drop it (report).
|
|
24
|
+
- A Twoslash transformer → Blume supports the `twoslash` fence meta natively; drop the plugin.
|
|
25
|
+
- Custom Shiki transformers/themes or other plugins → report.
|
|
26
|
+
|
|
27
|
+
## Navigation: `meta.json` → `meta.ts`
|
|
28
|
+
|
|
29
|
+
**Every `meta.json` becomes a `meta.ts` — this is the primary navigation carry-over for Fumadocs, always required, never optional.** `meta.json` is Fumadocs' canonical nav source; no filesystem-only shortcut reproduces its ordering, icons, and collapse state, so don't skip it in favor of filename inference. Move content from `content/docs/` to your chosen `content.root` (e.g. `docs/`), then convert each `meta.json` to a `meta.ts` (`defineMeta`):
|
|
30
|
+
|
|
31
|
+
| Fumadocs `meta.json` | Blume `meta.ts` |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `title` | `title` |
|
|
34
|
+
| `icon` | `icon` — **convert the casing** (see Icons below) |
|
|
35
|
+
| `defaultOpen: false` | `collapsed: true` |
|
|
36
|
+
| `defaultOpen: true` | `collapsed: false` |
|
|
37
|
+
| `root: true` | a **`navigation.tabs` entry** (see below) — **not** a `meta.ts` field |
|
|
38
|
+
| `pages: [...]` slugs | `pages: [...]` (ordering) |
|
|
39
|
+
| `description` | **drop** (folders have no description) |
|
|
40
|
+
| `collapsible: false` | **drop** (report — no per-folder equivalent) |
|
|
41
|
+
|
|
42
|
+
`meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages` — there is **no `display` field** (writing one is a build error). Collapse rendering is global: when any folder needs collapsing, set `navigation.sidebar.display: "group"` once in `blume.config.ts`.
|
|
43
|
+
|
|
44
|
+
**`root: true` folders are Fumadocs' tab mechanism** — Fumadocs UI renders them as layout tabs and scopes the sidebar to the active one. That is exactly Blume's `navigation.tabs`: add `{ label, path, icon? }` per root folder (label/icon from its `meta.json`), pointing at the folder's route. Blume then scopes the sidebar by URL prefix the same way. Don't try to model root folders inside `meta.ts`.
|
|
45
|
+
|
|
46
|
+
Handle the `pages` array items:
|
|
47
|
+
|
|
48
|
+
- **`"..."`** (rest marker) / `""` → drop; Blume appends unlisted pages automatically. **`"z...a"`** (reversed rest) → no equivalent; list the pages explicitly in the intended order.
|
|
49
|
+
- **`"!page"`** (exclusion) → Blume _appends_ unlisted pages, so a dropped `!page` **resurfaces in the sidebar**. Set that page's frontmatter `sidebar.hidden: true` instead.
|
|
50
|
+
- **`"---Section---"`** (separator; also the `---[Icon]Label---` variant) → Blume has no flat separator. Turn each section into a **`(Section)/` group folder** (route-transparent — the `(…)` segment is stripped from URLs), and move the section's pages into it. If a section wraps a single existing folder, leave it in place and set that folder's `meta.ts` `title` instead.
|
|
51
|
+
- **`"...folder"`** (extract) → Blume can't flatten a folder inline; keep it as a normal group at that ordering position and report it.
|
|
52
|
+
- **`"[Text](url)"`** (link; also `[Icon][Text](url)` and `external:` variants) → a top-level/utility link belongs in **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route). A link buried deep in one folder has no folder-meta home — drop and report (or model that one folder via an explicit `navigation.sidebar`).
|
|
53
|
+
- **`pagesIndex`** → make that page the folder's `index` (rename the file); report the route change and add a redirect.
|
|
54
|
+
|
|
55
|
+
## Frontmatter
|
|
56
|
+
|
|
57
|
+
Fumadocs' core frontmatter (`title`, `description`, `icon`) matches Blume — but **convert `icon` casing** (see Icons). Drops: **`full`** (Fumadocs' full-width/no-TOC layout — no equivalent; report) and **`_openapi`** (generated API stubs — delete the whole page, see OpenAPI). Any other non-schema key is a build error, so drop and report.
|
|
58
|
+
|
|
59
|
+
## Icons: lucide-react names → kebab-case
|
|
60
|
+
|
|
61
|
+
Fumadocs icons are strings resolved by the repo's own `icon` handler in `loader()` — in practice **PascalCase lucide-react export names** (`"BookOpen"`, `"HomeIcon"`). Blume resolves **kebab-case** Lucide names and silently renders nothing on a miss, so pass-through loses every icon. Read the loader's icon handler to confirm the scheme, then convert each name: strip any `Icon` suffix, kebab-case it (`BookOpen` → `book-open`, `HomeIcon` → `home`), and verify it exists at [lucide.dev/icons](https://lucide.dev/icons).
|
|
62
|
+
|
|
63
|
+
## Components
|
|
64
|
+
|
|
65
|
+
- **Callouts:** `<Callout type="x">` → `:::` directive. Fumadocs types are `info` (the default), `warn`, `warning`, `error`, `success`, `idea`: `warn`/`warning`→`:::warning`, `error`→`:::danger`, `success`→`:::success`, `idea`→`:::tip`, `info` and **bare `<Callout>`** → `:::info`. `title` → `:::type[Title]`; drop `icon`.
|
|
66
|
+
- **Cards:** `<Cards>` → `<CardGroup>`. `<Card>` needs **prop surgery**, not pass-through: Fumadocs `icon` is a JSX element (`icon={<Cpu />}` imported from `lucide-react`) → Blume takes a string name (`icon="cpu"`); Fumadocs `description="…"` → Blume has no `description` prop — move the text into the card body.
|
|
67
|
+
- **Accordions (container/item inversion):** `<Accordions>` → `<Accordion>` (container); `<Accordion>` → `<AccordionItem>` (item).
|
|
68
|
+
- **File trees:** `<Files>` → **`<Tree>`** (the JSX container for `Tree.*` children — _not_ `<FileTree>`, which wraps a Markdown list); `<Folder>` → `<Tree.Folder>` (`defaultOpen` carries over); `<File>` → `<Tree.File>`. Or convert the whole block to a list-driven `<FileTree>`.
|
|
69
|
+
- **Tabs:** Fumadocs declares labels on the parent (`<Tabs items={['npm','pnpm']}>`) and selects with `<Tab value="npm">`. Blume's `<Tab>` carries its own `title`. Strip `items={[…]}` from `<Tabs>` and give each child `<Tab>` a `title` (from its `value`, or the positional `items` entry).
|
|
70
|
+
- **Pass through unchanged:** `<Steps>`/`<Step>`, `<TypeTable>` (Blume's was modeled on Fumadocs' — identical `type` record shape), `<GithubInfo>`.
|
|
71
|
+
- **`<Banner>`** (layout-mounted, not per-page) → the `banner` config field (`{ content, link, dismissible, id }`).
|
|
72
|
+
- **`<include>./partial.mdx</include>`** — Blume has no runtime include. **Inline** the partial's body (strip its frontmatter) at migration time; resolve nested includes recursively.
|
|
73
|
+
- **No equivalent — report:** `<DynamicCodeBlock>`, `<ImageZoom>` (Blume zooms content images by default), `<InlineTOC>`.
|
|
74
|
+
- **Strip or convert every import** — not just `fumadocs-*`: `lucide-react` imports (icon JSX → string names), `next/image`/`next/link` (→ Markdown image/link), and local components. **Inventory `mdx-components.tsx` before deleting it** — components registered there are used import-free in MDX bodies; port or inline each usage first.
|
|
75
|
+
|
|
76
|
+
## Code fences
|
|
77
|
+
|
|
78
|
+
- ` ```npm ` fences (Fumadocs' remark-npm accepts both) → ` ```package-install `.
|
|
79
|
+
- Adjacent fences sharing `tab="…"` meta render as grouped tabs in Fumadocs → wrap them in a Blume `<CodeGroup>` and drop the `tab=` attrs.
|
|
80
|
+
|
|
81
|
+
## OpenAPI
|
|
82
|
+
|
|
83
|
+
`fumadocs-openapi` writes **generated MDX stubs into the content tree** (`generateFiles()` output: pages containing `<APIPage>`/`<OpenAPIPage>` with `_openapi` frontmatter), plus `lib/openapi.ts` (`createOpenAPI`) and a generate script. Treat these exactly like Mintlify endpoint stubs: **delete the generated pages**, point `openapi: { enabled: true, sources: [{ spec }] }` at the spec (vendor it locally), add a `navigation.tabs` entry for the reference route, and remove `fumadocs-openapi`, `lib/openapi.ts`, and the generate script. Keep hand-written conceptual pages (intro/auth) under the reference route.
|
|
84
|
+
|
|
85
|
+
## i18n
|
|
86
|
+
|
|
87
|
+
A `loader({ i18n })` setup (locale-suffixed files or locale dirs) → Blume `i18n: { defaultLocale, locales: [{ code, label }] }`. Locale **directories** match Blume's `dir` parser as-is; locale **file suffixes** (`page.cn.mdx`) need restructuring into locale folders. Report whichever transform you apply.
|
|
88
|
+
|
|
89
|
+
## Package.json & teardown
|
|
90
|
+
|
|
91
|
+
Repoint scripts (`dev`→`blume dev`, `build`→`blume build`, `start`→`blume preview`), remove the `fumadocs-*` deps and the host framework's deps (`next`, `react-router`, `@tanstack/*`…), add `blume`. Safe to delete after harvesting (see above for what to read first): `source.config.*`, `mdx-components.tsx`, the app/route dir, and host-framework config (`next.config.*`, `next-env.d.ts`, the `next` tsconfig plugin — or the React Router/TanStack equivalents).
|
|
92
|
+
|
|
93
|
+
## Dropped — report these
|
|
94
|
+
|
|
95
|
+
Folder `description` and `collapsible: false`; reversed rest (`z...a`) ordering; the extract (`...folder`) flatten semantics; deep-folder sidebar links; frontmatter `full`; `<DynamicCodeBlock>`/`<InlineTOC>`; custom Shiki transformers; any icon with no Lucide equivalent after casing conversion.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Mintlify → Blume
|
|
2
|
+
|
|
3
|
+
The deepest migration. Mintlify declares its **entire navigation in `docs.json`**; Blume derives navigation from the filesystem. The bulk of the work is reshaping content into folders + tabs and translating config, callouts, and icons.
|
|
4
|
+
|
|
5
|
+
## Detect
|
|
6
|
+
|
|
7
|
+
- `docs.json` (current) or `mint.json` (legacy) at the repo root — the config file.
|
|
8
|
+
- A `mintlify` dependency / `mintlify dev` script.
|
|
9
|
+
- Content is `.mdx` under the repo root (Mintlify has no `content.root` — pages live at the top level), with a `/snippets` folder and image dirs like `/images`.
|
|
10
|
+
|
|
11
|
+
Read `docs.json` first; it drives everything.
|
|
12
|
+
|
|
13
|
+
## Config: `docs.json`/`mint.json` → `blume.config.ts`
|
|
14
|
+
|
|
15
|
+
Resolve `$ref` includes first (Mintlify splits config across files). Map only what's set:
|
|
16
|
+
|
|
17
|
+
| Mintlify | Blume | Notes |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| `name` / `title` | `title` | |
|
|
20
|
+
| `description` | `description` | |
|
|
21
|
+
| `logo` (string or `{ light, dark, href }`) | `logo` (string, or `{ image: { light, dark, alt }, text, href }`) | Mintlify's `light`/`dark` nest under Blume's `image`; ensure files land in `public/`. **If the SVG is local and monochrome** (a solid black or white mark), rewrite its `fill`/`stroke` to `currentColor` and collapse `{ light, dark }` to the string shorthand `logo: "/logo.svg"` — one file that inherits the theme text color and flips with light/dark automatically. If the logo is a **wordmark** (brand name baked in), set `text: ""` so it doesn't render twice beside `title` |
|
|
22
|
+
| `favicon` (string **or `{ light, dark }`**) | **drop the field** — copy **one** file into `public/` under the conventional name (`favicon.svg`/`icon.png`) | Blume auto-detects by filename; there is **no** favicon config field, and **no** light/dark favicon — a `{ light, dark }` source collapses to one; report the loss |
|
|
23
|
+
| `colors.primary` + `colors.light` | `theme.accent` (`{ light, dark }`) | accent is now **per-mode**: `accent: { light: <colors.primary>, dark: <colors.light> }`. If Mintlify sets only `primary`, collapse to the string shorthand `accent: "<colors.primary>"` (applies to both modes) |
|
|
24
|
+
| `colors.dark` | `theme.action` | |
|
|
25
|
+
| `appearance.default` | `theme.mode` (`light`/`dark`/`system`) | |
|
|
26
|
+
| `appearance.strict` | **drop** | `theme.strict` was removed — writing it is a config error |
|
|
27
|
+
| `background.color.{light,dark}` | `theme.background` (`{ light, dark }`) | one field now: `background: { light: …, dark: … }` (a bare string applies to both) |
|
|
28
|
+
| `background.image` | `theme.backgroundImage` (`{ light, dark }`) | same per-mode shape as `background` |
|
|
29
|
+
| `background.decoration` | **drop** | no Blume equivalent |
|
|
30
|
+
| `fonts.family` / `fonts.{heading,body}.family` | `theme.fonts.{display,body}` | only if the family is a curated Google-font slug (kebab-case, e.g. `space-grotesk`); otherwise drop and tell the user to add `@font-face` in `theme.css` |
|
|
31
|
+
| `banner` | `banner` (`{ content, dismissible, id, link }`) | **only** those keys — drop `banner.color`/`banner.type` |
|
|
32
|
+
| `styling.latex: true` | **drop the field** — block math `$$…$$` renders in `.mdx` with no config | there is **no** `markdown.math` field; inline `$…$` is **not** supported — convert inline math to `$$…$$` or drop it (report) |
|
|
33
|
+
| `styling.codeblocks.theme` | `markdown.codeBlocks.theme` (`{ light, dark }`) | |
|
|
34
|
+
| `search.prompt` | **drop** | no equivalent |
|
|
35
|
+
| `seo.metatags` | **drop** | no equivalent; use per-page `seo` frontmatter |
|
|
36
|
+
| `seo.indexing: "all"` | `search.indexing.includeHiddenPages: true` | |
|
|
37
|
+
| `variables` (`{{name}}`) | **inline into content** | Blume has no runtime `{{var}}` substitution — replace each `{{name}}` with its value in the pages |
|
|
38
|
+
| `integrations.posthog` (`{ apiKey, apiHost }`) | `analytics.posthog` (`{ key, host }`) | preserve the host verbatim (e.g. `us.posthog.com` — Blume's default is `us.i.posthog.com`) |
|
|
39
|
+
| `integrations` (GA, Plausible, Fathom, …) | `analytics.scripts` / `analytics.vercel` | one `scripts[]` entry per provider (`{ src, strategy, attributes }`); no first-class mapping beyond PostHog/Vercel |
|
|
40
|
+
| `contextual` (`["copy","chatgpt","claude",…]`) | **mostly free** | Copy-as-Markdown and Open-in-chat are default page actions; `mcp` needs `mcp.enabled` + server output (report as a follow-up) |
|
|
41
|
+
| `redirects` | `redirects: [{ from, to }]` | static only — see below |
|
|
42
|
+
| `navigation.languages` | `i18n` | see i18n below |
|
|
43
|
+
|
|
44
|
+
A minimal result is often just `defineConfig({ title, logo, theme: { accent } })`.
|
|
45
|
+
|
|
46
|
+
## Icons: FontAwesome → Lucide (required)
|
|
47
|
+
|
|
48
|
+
Mintlify defaults to **FontAwesome**; Blume is **Lucide-only**. Convert every icon reference — frontmatter `icon`, nav-group `icon`, `<Icon>`, `<Card icon>` — to the closest Lucide name. Discard Mintlify's `iconType` (solid/regular/brands) entirely.
|
|
49
|
+
|
|
50
|
+
**Automate the frontmatter pass — don't hand-edit it.** `scripts/mintlify-codemod.mjs` (in this skill, zero-dependency) remaps every **frontmatter** `icon:` using the table below, drops brand/no-equivalent icons, and — in the same pass — drops/renames unsupported frontmatter keys (see [Frontmatter](#frontmatter)). It's deterministic and idempotent, and reports what it changed per file plus what it couldn't (unknown icons, OpenAPI-stub flags):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# dry run first (report only), then apply:
|
|
54
|
+
node <this-skill>/scripts/mintlify-codemod.mjs <content-dir>
|
|
55
|
+
node <this-skill>/scripts/mintlify-codemod.mjs --write <content-dir>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The codemod touches **only frontmatter**. Icons in MDX **body** (`<Icon icon="…">`, `<Card icon="…">`, nav-group icons in `docs.json`) it does not see — convert those by hand using the same table. Common mappings:
|
|
59
|
+
|
|
60
|
+
| FontAwesome | Lucide | | FontAwesome | Lucide |
|
|
61
|
+
| --- | --- | --- | --- | --- |
|
|
62
|
+
| `bolt` | `zap` | | `gear`/`cog` | `settings` |
|
|
63
|
+
| `circle-info`/`info` | `info` | | `wand-magic-sparkles`/`magic` | `sparkles` |
|
|
64
|
+
| `house` | `house` | | `magnifying-glass` | `search` |
|
|
65
|
+
| `gauge`/`gauge-high` | `gauge` | | `puzzle-piece` | `puzzle` |
|
|
66
|
+
| `envelope` | `mail` | | `file-lines` | `file-text` |
|
|
67
|
+
| `pen-to-square` | `square-pen` | | `trash-can` | `trash-2` |
|
|
68
|
+
| `xmark`/`times` | `x` | | `circle-check` | `circle-check` |
|
|
69
|
+
| `triangle-exclamation` | `triangle-alert` | | `circle-exclamation` | `circle-alert` |
|
|
70
|
+
| `arrow-right-from-bracket` | `log-out` | | `right-to-bracket` | `log-in` |
|
|
71
|
+
| `location-dot`/`map-marker` | `map-pin` | | `comments` | `messages-square` |
|
|
72
|
+
| `cube` | `box` | | `cubes`/`boxes` | `boxes` |
|
|
73
|
+
| `layer-group` | `layers` | | `diagram-project`/`sitemap` | `workflow`/`network` |
|
|
74
|
+
| `chart-line` | `chart-line` | | `chart-simple`/`chart-column` | `chart-column` |
|
|
75
|
+
| `flask` | `flask-conical` | | `robot` | `bot` |
|
|
76
|
+
| `screwdriver-wrench`/`toolbox` | `wrench` | | `circle-question`/`question` | `circle-help` |
|
|
77
|
+
| `life-ring` | `life-buoy` | | `shield-halved` | `shield` |
|
|
78
|
+
| `rocket`/`book`/`book-open`/`code`/`terminal`/`key`/`lock`/`user`/`users`/`database`/`server`/`cloud`/`bell`/`calendar`/`star`/`heart`/`tag`/`folder`/`globe`/`link`/`download`/`upload`/`check`/`copy`/`play`/`filter` | _(same name — verify)_ |
|
|
79
|
+
|
|
80
|
+
**Rules:** verify each Lucide name exists at [lucide.dev/icons](https://lucide.dev/icons) before writing it. **Brand icons** (`fa6-brands:*` — github, discord, x, slack, linkedin…) mostly have **no** Lucide equivalent: for GitHub use the `github` config (renders the header repo link); for other socials, drop the icon and report it (or add via a Footer override after `blume eject`). Where no Lucide counterpart exists, **drop the icon and report it** — an unknown icon name is a build error.
|
|
81
|
+
|
|
82
|
+
## Navigation: `docs.json` `navigation` → filesystem + tabs
|
|
83
|
+
|
|
84
|
+
Mintlify's `navigation` object (`tabs`/`anchors`/`dropdowns`/`products`/`versions`/`languages`/`groups`/`pages`) is fully config-declared. **Prefer restructuring content into folders**, not porting the config verbatim:
|
|
85
|
+
|
|
86
|
+
- **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — is **not** a `meta.ts` field; it's the global `navigation.sidebar.display` in `blume.config.ts`.)
|
|
87
|
+
- **Config-only nested groups don't exist on disk — you must materialize them, or they flatten silently.** A nested `{ group, pages }` almost never has a matching subfolder: its pages sit **flat in the parent directory** (e.g. `platform/analytics/getting-started.mdx`, `…/quick-reference.mdx`) and the grouping lives **only** in the `docs.json` `pages` array. If you leave the files where they are, filesystem-derived nav sees one flat folder and the inner group vanishes — Mintlify's `Analytics → Reference → {…}` becomes a flat `Analytics → {…}`. To preserve it you must **either** move those pages into a real subfolder (`platform/analytics/reference/`, with a `meta.ts` for the label/`collapsed`), **or** declare the shape in an explicit `navigation.sidebar`. Walk **every** `pages` array recursively and treat any nested `group` object as a folder-move to plan, not files already in place. When inventorying the nav (workflow step 2), record the config nesting depth separately from the on-disk depth — they diverge exactly here.
|
|
88
|
+
- **This is the canonical case for `navigation.sidebar` over folders.** Materializing a purely-presentational nested group as a subfolder changes URLs (`/platform/analytics/getting-started` → `/platform/analytics/reference/getting-started`) for no reason other than a visual grouping, forcing a `redirects` entry per page. When you want to keep the nesting **and** the URLs, an explicit `navigation.sidebar` group (`{ label, items }`) is the better trade — it nests the existing routes without moving any file. Pick per group; don't reflexively flatten.
|
|
89
|
+
- **`pages`** entries are page refs (paths without extension) → files at the corresponding path. An entry that's `"GET /path"` is an OpenAPI endpoint stub → **delete it** (Blume generates these; see OpenAPI).
|
|
90
|
+
- **`tabs`** (top-level `navigation.tabs`, e.g. `App` / `API` / `API reference`) → Blume's **header tabs** `navigation.tabs` (`{ label, path, icon? }`). **Keep them as tabs — do not flatten them into `navigation.sidebar` groups or sections.** Flattening is the easy translation error: it collapses the site's top-level structure into one global sidebar and loses the header tab bar entirely. A Mintlify top-level tab is a header tab; map it to one. Put each tab's pages in **one folder** and point the tab's `path` at it — the tab then scopes the sidebar automatically (under the tab's `path`, the sidebar shows only that folder). **Blume resolves the active tab by URL prefix** (the tab whose `path` is the longest prefix of the current route), so every page in a tab must live under that tab's single `path`. Mintlify tabs freely mix pages from any folder / arbitrary routes; Blume tabs scope by prefix, so a tab that pulls in routes from all over is **not portable as-is**. You have two choices, per tab:
|
|
91
|
+
- **Move the pages under one prefix** so they share the tab's `path` (a route change → add a `redirects` entry for each moved page). This preserves the exact tab grouping.
|
|
92
|
+
- **Accept the closest shape** — assign each shared/cross-cutting page to one tab's folder and link to it from the others — when moving routes isn't worth the churn.
|
|
93
|
+
|
|
94
|
+
Call this trade out explicitly in the migration report; don't silently pick one. **The scoping is bidirectional:** on the root/landing route Blume _hides_ every tab folder and lists only untabbed top-level pages — so a Mintlify home whose single sidebar showed everything becomes a lean root list plus one sidebar per tab. That's automatic; don't try to exclude tab folders from the root by hand.
|
|
95
|
+
|
|
96
|
+
- **`dropdowns`/`products`/`versions`** → `navigation.selectors` (`{ kind, label, items: [{ label, path, icon?, description?, tag? }] }`). Use `kind` `dropdown`/`product`/`version` accordingly.
|
|
97
|
+
- **`anchors` / `navigation.global.anchors` / `navbar.links`** (persistent header links — Blog, Changelog, Community, Contact/Support) → **`navigation.featured`** (`{ label, href, icon? }`). These pin to the **top of the sidebar, above every section, on every route** (not tab-scoped) — the right home for Mintlify's always-visible utility links. An external `href` opens in a new tab; an internal one (`/contact`) is build-time validated against your pages. Convert the FontAwesome `icon` to Lucide as usual. (This replaces the old "drop and report" for anchors.) The `navbar.primary` **CTA button** has no featured equivalent — see Dropped.
|
|
98
|
+
- **`languages`** → `i18n`, not a selector (see below).
|
|
99
|
+
- Only fall back to an explicit `navigation.sidebar` for a shape the filesystem genuinely can't express.
|
|
100
|
+
|
|
101
|
+
This reshaping **changes URLs** — a page moved from `getting-started/quickstart` into the API tab's folder becomes `/api/…`, an `index` promotion drops a segment, etc. Record each old→new path and add a `redirects` entry for it (see below); otherwise every existing link and bookmark 404s.
|
|
102
|
+
|
|
103
|
+
## Content & component transforms
|
|
104
|
+
|
|
105
|
+
Rewrite each page's MDX:
|
|
106
|
+
|
|
107
|
+
- **Callouts → directives** (directives are **MDX-only** — Mintlify content is already `.mdx`, so this just works; never rename a page to `.md`): `<Note>`→`:::note`, `<Tip>`→`:::tip`, `<Warning>`→`:::warning`, `<Info>`→`:::info`, `<Check>`→`:::success`, `<Danger>`/`<Error>`→`:::danger`. `<Callout type="x">` maps by type (`caution`→warning, `check`→success). A `title` attr → `:::type[Title]`. Drop `icon`/color props.
|
|
108
|
+
- **Accordions — container/item inversion!** Mintlify nests `<Accordion title="…">` inside `<AccordionGroup>`. Blume inverts: `<AccordionGroup>`→`<Accordion>` (container), and each Mintlify `<Accordion title="…">`→`<AccordionItem title="…">` (item).
|
|
109
|
+
- **`<RequestExample>`/`<ResponseExample>`** → `<CodeGroup>` (titled-fence tabs).
|
|
110
|
+
- **These pass through — Blume ships them natively:** `<Columns>`/`<Column>`, `<Expandable>`, `<Tooltip>`, `<Frame>`, `<Panel>`, `<Card>`/`<CardGroup>`, `<Tabs>`/`<Tab>`, `<Steps>`/`<Step>`. Keep them as-is.
|
|
111
|
+
- **API fields → `TypeTable`.** Blume does **not** ship `<ParamField>`/`<ResponseField>`/`<RequestField>`. Convert a cluster of fields into one `<TypeTable>` (rows keyed by field name, each `{ type, required?, default?, description }`). For a fully spec'd API, prefer deleting the hand-written fields and using the [OpenAPI reference](#openapi) instead.
|
|
112
|
+
- **`<Update>`** (a Mintlify changelog entry) has no component form → convert to a `type: changelog` page, or use the `github-releases` source.
|
|
113
|
+
- **Snippets are inlined, not imported.** Blume has no `/snippets` import mechanism. For each `import X from "/snippets/x.mdx"` + `<X prop="v" />`, inline the snippet's body (substituting `{prop}` placeholders), then delete the import and the `/snippets` file. Named string imports (`import { foo } from "/snippets/vars.mdx"`) → inline the value at each `{foo}`.
|
|
114
|
+
|
|
115
|
+
## Frontmatter
|
|
116
|
+
|
|
117
|
+
Mintlify page frontmatter → Blume's strict schema. **`scripts/mintlify-codemod.mjs --write` does the mechanical rows automatically** (rename, drop, icon-remap) and flags the judgment rows (`openapi`/`asyncapi`/`api`) for you — see [Icons](#icons-fontawesome--lucide-required) for the invocation. The table is both the mapping reference and a description of exactly what the codemod does:
|
|
118
|
+
|
|
119
|
+
| Mintlify | Blume | Codemod |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `title` / `description` | pass through | — |
|
|
122
|
+
| `sidebarTitle` | `sidebar.label` | renames |
|
|
123
|
+
| `icon` | remapped to a Lucide name in place (top-level `icon` is valid Blume frontmatter; keep it there) | remaps |
|
|
124
|
+
| `tag` | `sidebar.badge` | renames |
|
|
125
|
+
| `canonical` | `seo.canonical` | renames |
|
|
126
|
+
| `og:image` | `seo.image` | renames |
|
|
127
|
+
| `hidden: true` | valid top-level in Blume — **kept as-is**; add `noindex: true` yourself if the page must also leave the search index | left (do by hand) |
|
|
128
|
+
| `openapi`/`asyncapi`/`api` | usually an endpoint stub → **delete the page** (Blume generates operation pages); else `type: api` | **flags** for review — never auto-deletes a page |
|
|
129
|
+
| `mode`, `public`, `rss`, `groups`, `keywords`, `hideApiMarker`, `hideFooterPagination`, `iconType` | **drop** (report) | drops |
|
|
130
|
+
|
|
131
|
+
The codemod leaves the source key in place and reports a conflict rather than clobbering data when a rename target already exists (e.g. a page already has `sidebar.label`) or the value is too structured to move safely — resolve those by hand. Remove any duplicate H1 in the body — `title` renders the H1. (The codemod only edits frontmatter; it never touches the body.)
|
|
132
|
+
|
|
133
|
+
## OpenAPI
|
|
134
|
+
|
|
135
|
+
Top-level `openapi`, `api.openapi`, or a per-group/per-tab `openapi` → `openapi: { enabled: true, sources: [{ spec, label?, route? }] }`. A Mintlify `{ source, directory }` object: `directory` → the source's `route`. **Delete every per-endpoint stub page** (frontmatter `openapi: "GET /path"` or a `"GET /path"` nav entry) — Blume's native renderer generates one real page per operation. **Add a `navigation.tabs` entry pointing at the reference `route` yourself** (Mintlify's API tab maps to it); the reference does not create a header tab automatically.
|
|
136
|
+
|
|
137
|
+
- **Vendor the spec.** Mintlify usually points at a spec **URL**. Copying that straight into `spec:` makes every build fetch it at build time — a single point of failure in CI/offline/behind a proxy, and a failed fetch silently drops the reference (leaving the tab pointing at a route that 404s). Prefer downloading it into the repo (`curl … -o openapi/<name>.json`) and pointing `spec` at that local path. If you keep the URL, report the dependency and consider a `prebuild` refresh-with-fallback.
|
|
138
|
+
- **Fix endpoint links.** Blume operation routes are `<route>/<slugified-tag>/<slugified-operationId>` (tag `Models` + id `listModels` → `/api-reference/models/listmodels`) — this differs from Mintlify's endpoint URLs, so **rewrite every inbound link to an operation**. `blume validate` resolves operation pages like any other route and flags the ones you miss.
|
|
139
|
+
- **Keep the "Introduction" page.** Mintlify commonly has a written intro/auth page in an "Introduction" group beside the "Endpoints" (openapi) group in the same tab. Keep it: a normal content page placed under the openapi `route` (e.g. `<root>/api-reference/introduction.mdx`) merges into the reference tab's sidebar alongside the generated operations. Delete only the per-endpoint stubs, not the conceptual pages.
|
|
140
|
+
|
|
141
|
+
## Assets
|
|
142
|
+
|
|
143
|
+
Mintlify serves every top-level dir (e.g. `/images`) at the site root. Blume serves `public/` at the site root. **Move root asset dirs into `public/`** (`mv images public/images`) — every `/images/...` reference still resolves, unchanged. Move loose root files (`logo.png`, `favicon.png`) under `public/` too.
|
|
144
|
+
|
|
145
|
+
## i18n
|
|
146
|
+
|
|
147
|
+
`navigation.languages` (≥2) → `i18n: { defaultLocale, locales: [{ code, label }] }`. The `default: true` language → `defaultLocale`. Translated content already lives in ISO-code directories, which match Blume's `dir` parser — no file moves. Remove any language selector; language switching is handled by i18n.
|
|
148
|
+
|
|
149
|
+
## Dropped — report these
|
|
150
|
+
|
|
151
|
+
- **`navbar.primary`** (the prominent header **CTA button**, e.g. "Get Started"/"Sign Up") → no button equivalent; re-add via `navigation.tabs`, a `navigation.featured` link, or a Header override. (Plain `navbar.links` and `anchors` map to `navigation.featured` — see Navigation above, not here.)
|
|
152
|
+
- **`footer.socials`** → suggest the `github` config, or a Footer override.
|
|
153
|
+
- **Per-language banners** (`navigation.languages[].banner`) → no equivalent.
|
|
154
|
+
- **Dynamic redirects** (`:slug*`/`:id` params) → can't be static path-to-path; move to host rules (`_redirects`, `vercel.json`).
|
|
155
|
+
- **`<Update>`** changelog components, `iconType`, `background.decoration`, `search.prompt`, `seo.metatags`, non-curated fonts.
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# Migrating into a real repo (monorepos, pnpm, Vercel)
|
|
2
|
+
|
|
3
|
+
The framework references (`mintlify.md`, etc.) cover translating _content and config_. This file covers the **host repo** the docs live in — the concerns that surface when the target isn't a bare docs folder but a pnpm/Turbo workspace deployed on Vercel. All six were real failures in a production migration; none are Blume bugs, they're integration steps.
|
|
4
|
+
|
|
5
|
+
**Golden rule for this whole file: detect, then edit surgically, then say what you changed and why.** Every edit here touches a file the repo owns (`pnpm-workspace.yaml`, `vercel.json`, `package.json`, the lockfile). Never guess a path depth or a workspace name — read it out of the repo first. Where a step can't be fully automated (the Astro patch, the Vercel Root Directory setting), leave a copy-pasteable manual instruction rather than a half-applied change.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Non-standard content layout → scope `content.root` + `include`
|
|
10
|
+
|
|
11
|
+
`content.root` defaults to `"docs"`, and `blume` runs from the directory that holds `blume.config.ts` (in a monorepo, that's the docs _package_, e.g. `apps/docs/`). Many repos **don't** have a `docs/` subfolder — the content sits directly under the app dir in topic folders (`apps/docs/api/`, `apps/docs/getting-started/`). With the default root, Blume looks for `apps/docs/docs/` and finds nothing.
|
|
12
|
+
|
|
13
|
+
**Detect by finding where `.md`/`.mdx` actually live** — never assume `docs/`. From the docs package dir:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# top two path segments of every content file, deduped — your content folders
|
|
17
|
+
find . -path ./node_modules -prune -o \( -name '*.md' -o -name '*.mdx' \) -print \
|
|
18
|
+
| sed 's|^\./||' | cut -d/ -f1 | sort -u
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If that lists a real `docs` folder, keep the default. If it lists content folders directly (`api`, `getting-started`, `guides`), the content root is the package dir itself.
|
|
22
|
+
|
|
23
|
+
**Set `content.root` to the package dir and scope with explicit `include` globs — do _not_ use `content.root: "."` alone.** A bare `.` root scans the whole package: `src/`, `public/`, `README.md`, config files, and (in a monorepo) anything a loose glob reaches. That's slow and pulls non-content Markdown into the site. Instead pin `root` and list the real content folders in `content.include`:
|
|
24
|
+
|
|
25
|
+
```ts blume.config.ts
|
|
26
|
+
import { defineConfig } from "blume";
|
|
27
|
+
|
|
28
|
+
export default defineConfig({
|
|
29
|
+
content: {
|
|
30
|
+
root: ".", // content sits directly under this package (apps/docs/), no docs/ subfolder
|
|
31
|
+
// include/exclude globs are relative to `content.root`. List the actual
|
|
32
|
+
// content folders so the scan never wanders into src/, public/, etc.
|
|
33
|
+
include: [
|
|
34
|
+
"api/**/*.{md,mdx}",
|
|
35
|
+
"getting-started/**/*.{md,mdx}",
|
|
36
|
+
"guides/**/*.{md,mdx}",
|
|
37
|
+
],
|
|
38
|
+
exclude: ["**/node_modules/**", "**/_*", "**/.*"],
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Schema field names, exactly: **`content.root`** (string, relative to the project/cwd), **`content.include`** (array of globs, relative to `content.root`, default `["**/*.{md,mdx}"]`), **`content.exclude`** (array of globs, relative to `content.root`, default `["**/_*", "**/.*"]`). Singular `include`/`exclude`, both arrays.
|
|
44
|
+
|
|
45
|
+
Report to the user: the detected content root, the folders you scoped `include` to, and anything Markdown you deliberately left out (a top-level `README.md`, a `CHANGELOG.md`) so they can confirm it isn't content.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. pnpm `minimumReleaseAge` blocks installing fresh Blume
|
|
50
|
+
|
|
51
|
+
pnpm's supply-chain guard (`minimumReleaseAge`, often set to `1440` = 24h) refuses to install any package version published more recently than the window. Right after Blume publishes, that's **every** Blume release — so `pnpm add blume` / the workspace install fails or silently pins an older version.
|
|
52
|
+
|
|
53
|
+
**Do not disable the policy globally** — that removes the guard for the whole dependency tree. Add **only `blume`** to the exclude list.
|
|
54
|
+
|
|
55
|
+
**Detect** in `pnpm-workspace.yaml` (pnpm 10+, the primary location) and, for older setups, `.npmrc`:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
grep -rn "minimumReleaseAge\|minimum-release-age" pnpm-workspace.yaml .npmrc 2>/dev/null
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Fix — `pnpm-workspace.yaml`** (surgical add; create the key if the policy is set but no exclude exists):
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
minimumReleaseAge: 1440
|
|
65
|
+
minimumReleaseAgeExclude:
|
|
66
|
+
- blume
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Fix — `.npmrc`** form (if the policy lives there instead):
|
|
70
|
+
|
|
71
|
+
```ini
|
|
72
|
+
minimum-release-age=1440
|
|
73
|
+
minimum-release-age-exclude[]=blume
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Report: "Added `blume` to `minimumReleaseAgeExclude` so the just-published version installs; the release-age guard still applies to everything else."
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 3. Frozen lockfile — regenerate and commit `pnpm-lock.yaml` in the same change
|
|
81
|
+
|
|
82
|
+
CI and Vercel install with `--frozen-lockfile` (pnpm's default in CI): if `package.json` and `pnpm-lock.yaml` disagree by even one dependency, the install **fails immediately** — before any build step runs. A migration always edits dependencies (add `blume`, drop the old framework), so the lockfile is guaranteed stale.
|
|
83
|
+
|
|
84
|
+
**After every dependency edit, regenerate the lockfile and stage it with the manifest:**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pnpm install # NOT --frozen-lockfile — this rewrites pnpm-lock.yaml
|
|
88
|
+
git add package.json apps/docs/package.json pnpm-lock.yaml
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Rules:
|
|
92
|
+
|
|
93
|
+
- Run `pnpm install` (plain) from the **workspace root**, not the docs package, so the single root lockfile updates.
|
|
94
|
+
- The lockfile change belongs in the **same commit** as the `package.json` change. A commit that edits deps without the matching lockfile is a broken build for anyone who pulls it.
|
|
95
|
+
- Verify locally the way CI will: `pnpm install --frozen-lockfile` should succeed with no diff. If it errors "lockfile not up to date," you forgot the plain install.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 4. Vercel monorepo recipe (root-aware install + build)
|
|
100
|
+
|
|
101
|
+
A plain `blume build` works locally, but Vercel in a workspace needs three things aligned: the install must run at the **workspace root** (so pnpm resolves the whole graph under a frozen lockfile), the build must run in the **docs package**, and Vercel must be pointed at the built `dist/`. `blume build` emits a static site to `dist/` **relative to where it runs** — so building in `apps/docs/` produces `apps/docs/dist/`.
|
|
102
|
+
|
|
103
|
+
This is a **copyable template**, not prose. Three pieces:
|
|
104
|
+
|
|
105
|
+
**a) Docs package `apps/docs/package.json`** — the standard Blume scripts:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"scripts": {
|
|
110
|
+
"dev": "blume dev",
|
|
111
|
+
"build": "blume build",
|
|
112
|
+
"preview": "blume preview"
|
|
113
|
+
},
|
|
114
|
+
"dependencies": {
|
|
115
|
+
"blume": "^1"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**b) `apps/docs/vercel.json`** — root-aware install, local build, `dist` output:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"$schema": "https://openapi.vercel.sh/vercel.json",
|
|
125
|
+
"framework": null,
|
|
126
|
+
"installCommand": "cd ../.. && pnpm install --frozen-lockfile",
|
|
127
|
+
"buildCommand": "pnpm run build",
|
|
128
|
+
"outputDirectory": "dist"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**c) The one setting `vercel.json` can't hold — set it in the Vercel project (dashboard → Settings → General, or `vercel` CLI):**
|
|
133
|
+
|
|
134
|
+
- **Root Directory** = `apps/docs`. This makes install/build/output paths resolve from the docs package, which is what the `vercel.json` above assumes.
|
|
135
|
+
- **Node version** = 22 or newer (Blume requires it). Or pin it in the docs package: `"engines": { "node": ">=22" }`.
|
|
136
|
+
|
|
137
|
+
How the pieces fit, with Root Directory = `apps/docs`:
|
|
138
|
+
|
|
139
|
+
- `installCommand` does `cd ../..` to reach the **workspace root** and runs a frozen install of the whole graph. **Adjust `../..` to the app's actual depth** — an app at `packages/web/docs/` needs `cd ../../..`. Read the path; don't assume two levels.
|
|
140
|
+
- `buildCommand` runs in `apps/docs/` and invokes `blume build` (via the package's `build` script) → emits `apps/docs/dist/`.
|
|
141
|
+
- `outputDirectory: "dist"` is relative to the Root Directory (`apps/docs`), so it points at `apps/docs/dist/`.
|
|
142
|
+
- `framework: null` stops Vercel from auto-detecting Astro and overriding your build/output with its Astro preset (which expects a different output path). Blume's static output is plain files — Vercel just serves the folder.
|
|
143
|
+
|
|
144
|
+
If the repo uses **Turborepo**, you can instead build through the root pipeline — `"buildCommand": "cd ../.. && pnpm turbo build --filter=docs"` — for remote-cache reuse. Keep `outputDirectory: "dist"` and Root Directory `apps/docs`.
|
|
145
|
+
|
|
146
|
+
Report every piece you wrote and the two dashboard settings the user must set by hand (Root Directory, Node version) — those can't be committed.
|
|
147
|
+
|
|
148
|
+
**Redirect caveat on this deploy path:** a static build emits `vercel.json` redirect rules into `dist/`, but Vercel's **git-integration** builds read `vercel.json` only from the project's Root Directory — the copy inside `dist/` is honored only when the dist folder is deployed directly via the Vercel CLI. On the §4 setup, `blume.config.ts` redirects still work (Blume also emits per-route meta-refresh pages), but they're soft redirects, not real 3xx. If real 3xx responses matter (SEO for a large moved site), copy the generated redirect rules from `dist/vercel.json` after a build into the committed `apps/docs/vercel.json`'s `redirects` array, and note they must be re-synced when `redirects` change. (Most migrations are fine with the soft fallback — say which you chose.)
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 5. Astro/Vite version mismatch → pnpm patch (manual)
|
|
153
|
+
|
|
154
|
+
Blume pins **`astro ^7`**. If the workspace forces a specific **Vite** version — a root `pnpm.overrides` / `resolutions` entry, or a `vite` in the workspace catalog — that pinned Vite can diverge from the version Astro 7 expects, and Astro's internal build behavior mismatches. The symptom is a **build-time crash inside Astro/Vite** (a Rollup/Vite internal error, a missing-export or hook-signature error deep in `astro`/`vite` during `blume build`) that doesn't reproduce in a clean, override-free project.
|
|
155
|
+
|
|
156
|
+
**Detect the risk factor** before it bites:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
grep -rn "\"vite\"\|vite@\| vite:" pnpm-workspace.yaml package.json 2>/dev/null | grep -i "override\|resolution\|catalog\|vite"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Blume is adding early version validation separately, so a future release will flag this at startup. Until then, **recognize the symptom and apply a pinned patch** — pnpm's `patch` workflow writes a committed diff that pnpm reapplies on every install (survives CI and frozen installs):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# 1. Open the offending version in an editable temp dir (use the exact version
|
|
166
|
+
# pnpm resolved — check `pnpm why astro`):
|
|
167
|
+
pnpm patch astro@7.0.6
|
|
168
|
+
|
|
169
|
+
# 2. Edit files in the printed temp path to reconcile the Astro/Vite interface
|
|
170
|
+
# (the exact edit depends on the mismatch; the crash's stack trace names the
|
|
171
|
+
# file and call site). Then commit the patch:
|
|
172
|
+
pnpm patch-commit '<the temp path pnpm printed>'
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`patch-commit` writes `patches/astro@7.0.6.patch` and registers it under `patchedDependencies` in `pnpm-workspace.yaml` (pnpm 10+) or root `package.json` (`pnpm.patchedDependencies` on pnpm 9):
|
|
176
|
+
|
|
177
|
+
```yaml
|
|
178
|
+
# pnpm-workspace.yaml
|
|
179
|
+
patchedDependencies:
|
|
180
|
+
astro@7.0.6: patches/astro@7.0.6.patch
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Commit **both** `patches/astro@7.0.6.patch` and the `patchedDependencies` entry, then re-run `pnpm install` and `blume build`.
|
|
184
|
+
|
|
185
|
+
This is a **manual step — do not fabricate the patch contents.** Leave the user this exact recipe plus the crash's stack trace, and tell them: the cleaner long-term fix is to relax the workspace's Vite pin so Astro 7 resolves its own compatible Vite, and drop the patch once Blume ships version validation. Don't fail silently — if a build crashes inside Astro/Vite and a Vite override exists, name this as the likely cause.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## 6. Ultracite / oxfmt formatting → oxfmt patch (ship the bundled patch)
|
|
190
|
+
|
|
191
|
+
If the repo uses (or the user wants to adopt) **[Ultracite](https://www.ultracite.ai)** for formatting — an `ultracite check` / `ultracite fix` script, an `ultracite` dev dep, or oxlint/oxfmt in the toolchain — it will format the migrated `.md`/`.mdx` **and break Blume's `:::` directives.** Ultracite's formatter is **oxfmt**, and under `proseWrap` oxfmt joins the opening/closing `:::` fence line into the surrounding prose, which invalidates the directive (`:::note` … `:::` callouts, tabs, steps — the exact syntax you convert callouts _into_ in step 5). Blume's own repo hits this and pins a patched oxfmt; a migrated repo needs the same patch or every directive silently degrades to literal text on the next `ultracite fix`.
|
|
192
|
+
|
|
193
|
+
The fix is a committed **pnpm patch** (`patches/oxfmt@0.55.0.patch`), shipped with this skill at `assets/oxfmt@0.55.0.patch`. Unlike the Astro/Vite patch, this one is a **known, deterministic diff** — copy it in, don't regenerate it:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# 1. Copy the shipped patch into the target repo's patches/ dir (keep the exact filename):
|
|
197
|
+
mkdir -p patches
|
|
198
|
+
cp "<skill>/assets/oxfmt@0.55.0.patch" patches/oxfmt@0.55.0.patch
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Then register it under `patchedDependencies` — `pnpm-workspace.yaml` on pnpm 10+, or root `package.json` (`pnpm.patchedDependencies`) on pnpm 9:
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
# pnpm-workspace.yaml
|
|
205
|
+
patchedDependencies:
|
|
206
|
+
oxfmt@0.55.0: patches/oxfmt@0.55.0.patch
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Commit **both** the patch file and the `patchedDependencies` entry, then re-run `pnpm install`.
|
|
210
|
+
|
|
211
|
+
- **The patch is pinned to `oxfmt@0.55.0`** (the version Ultracite 7.8.x resolves). pnpm requires an exact version match — if `pnpm why oxfmt` reports a different version, the patch won't apply. Bump the key to the resolved version (the hunk is a one-line prose-wrap guard and usually still applies cleanly); if it doesn't, tell the user and fall back to keeping directive-heavy files out of the formatter's globs.
|
|
212
|
+
- **Not on pnpm?** The patch mechanism is pnpm-specific. For npm/yarn, either pin oxfmt and apply the diff with `patch-package`, or exclude `.md`/`.mdx` from Ultracite formatting so it never touches the directives — report whichever you chose.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Checklist for a workspace migration
|
|
217
|
+
|
|
218
|
+
- [ ] Located real content folders (not assumed `docs/`); set `content.root` + scoped `content.include`.
|
|
219
|
+
- [ ] `minimumReleaseAge` present? Added `blume` to `minimumReleaseAgeExclude` only.
|
|
220
|
+
- [ ] Ran plain `pnpm install`; committed `pnpm-lock.yaml` with the `package.json` change; verified `pnpm install --frozen-lockfile` is clean.
|
|
221
|
+
- [ ] Wrote `apps/docs/vercel.json` + package scripts; told the user to set Root Directory + Node 22 in the Vercel project.
|
|
222
|
+
- [ ] Checked for a workspace Vite override; if the build crashes inside Astro/Vite, gave the pnpm-patch recipe.
|
|
223
|
+
- [ ] Uses Ultracite/oxfmt? Shipped `patches/oxfmt@0.55.0.patch` + registered it under `patchedDependencies` so formatting doesn't mangle `:::` directives.
|
|
224
|
+
- [ ] Reported every repo-specific edit and every manual step left to the user.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Nextra → Blume
|
|
2
|
+
|
|
3
|
+
Nextra (on Next.js) declares navigation and per-page labels in `_meta` files — and crucially, **a folder's title and its pages' sidebar labels live in the _parent_ `_meta`**. Reconciling that cross-file inheritance is the main task. Nextra 4 was a near-rewrite, so **fingerprint the generation first** — it decides where config lives, where content lives, and what the components are called.
|
|
4
|
+
|
|
5
|
+
## Detect & fingerprint the generation
|
|
6
|
+
|
|
7
|
+
- **`_meta.{js,mjs,cjs,ts,jsx,tsx,json}`** files — the strongest fingerprint (v4 also has a single-file `_meta.global.*` variant).
|
|
8
|
+
- **v2/v3 (Pages Router):** content in `pages/`, `_meta.json` (v2) or `_meta.{js,ts}` (v3), a `theme.config.{js,jsx,ts,tsx}`, components from `nextra-theme-docs` (v2) or `nextra/components` (v3).
|
|
9
|
+
- **v4 (App Router):** **no `theme.config`** — config lives as component props in `app/layout.{jsx,tsx}` (`<Layout>`, `<Navbar>`, `<Banner>`, `<Footer>`, `<Search>`). Content is either a **`content/`** directory (with a catch-all `app/[[...mdxPath]]/page.jsx` gateway) **or** Next page files: **`app/**/page.mdx`**.
|
|
10
|
+
- `nextra` + `next` deps; `next.config.*` wrapping `nextra({…})`.
|
|
11
|
+
|
|
12
|
+
**v4 app-directory repos need a restructure, not a `content.root`.** Pointing `content.root` at `app/` gives every route a trailing `/page` and scans `layout.jsx`/gateway files. Move each `app/foo/page.mdx` → `foo.mdx` (or `foo/index.mdx` when it has children), and exclude the non-MDX app files. The catch-all `page.jsx`, `mdx-components.*`, and `app/layout.*` are teardown artifacts — but **harvest `app/layout.*` first** (see Config).
|
|
13
|
+
|
|
14
|
+
## Config
|
|
15
|
+
|
|
16
|
+
None of it maps automatically — read the config surface for the generation by hand and reconstruct in `blume.config.ts`:
|
|
17
|
+
|
|
18
|
+
- **v2/v3 — `theme.config.*`:** `logo` (JSX — extract text/image) → `logo`; `project.link` → `github` (renders the header repo link); `docsRepositoryBase` → `github` (owner/repo/branch — and any trailing sub-path → `github.dir`); `banner.text`/`banner.key` → `banner.content`/`banner.id` (`dismissible` maps); `primaryHue`/`primarySaturation` → pick an equivalent `theme.accent` color; `footer` → drop (report); `faviconGlyph` → drop (Blume's favicon is a file convention); `useNextSeoProps`/head → per-page `seo` frontmatter or drop.
|
|
19
|
+
- **v4 — `app/layout.*` props:** the same facts moved: `<Navbar logo projectLink>`, `<Layout docsRepositoryBase editLink sidebar={{…}} toc={{…}}>`, `<Banner>`, `<Footer>`. Map them the same way; sidebar/TOC tuning mostly drops (Blume's `toc` config covers min/max heading levels).
|
|
20
|
+
- **`next.config.*`:** harvest **`redirects()`** — static entries become Blume `redirects: [{ from, to }]`; wildcard/dynamic ones move to host config (report). A `latex: true` flag means math is in play (see Math below). Then delete the file.
|
|
21
|
+
- Root `_meta` entries with **`type: "page"`** → `navigation.tabs` (`{ label, path }`, where `path` is `/` for `index`, else `/<slug>`). `type: "page"` only maps at the **root**.
|
|
22
|
+
|
|
23
|
+
## Navigation: `_meta` → `meta.ts` + frontmatter
|
|
24
|
+
|
|
25
|
+
**Every `_meta` file must be carried over — always convert it to `meta.ts` + frontmatter, never drop it in favor of filename inference.** `_meta` is Nextra's canonical nav source (ordering, labels, folder titles); the filesystem alone can't reproduce it.
|
|
26
|
+
|
|
27
|
+
For each `_meta` entry (`key` = slug, value = string title or `{ title, type, display, href, theme }`):
|
|
28
|
+
|
|
29
|
+
| Nextra `_meta` entry | Blume |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| ordinary page/folder (string or `{ title }`) | slug → parent `meta.ts` `pages` (ordering). A **page** title → that page's frontmatter `sidebar.label`. A **folder** title → that child folder's `meta.ts` `title` (title lives in the parent!). A JSX title → extract its text. |
|
|
32
|
+
| `display: "hidden"` | frontmatter `sidebar.hidden: true` |
|
|
33
|
+
| `type: "separator"` | drop → recreate as a `(Group)/` folder / `meta.ts` boundary if needed |
|
|
34
|
+
| `type: "menu"` (navbar dropdown) | drop → recreate via `navigation.selectors` if wanted |
|
|
35
|
+
| `href` (external link) | **`navigation.featured`** (`{ label, href, icon? }` — pinned above the sidebar on every route); only drop deep-nested ones (report) |
|
|
36
|
+
| `type: "page"` (subfolder, not root) | drop (only root → tabs) |
|
|
37
|
+
| `theme: { collapsed }` on a folder | `meta.ts` `collapsed` |
|
|
38
|
+
| `theme: { layout: "full" \| sidebar: false \| … }` | drop (report — no per-page layout switches) |
|
|
39
|
+
| `"*"` wildcard entry | apply its value as the default for unlisted siblings, then drop |
|
|
40
|
+
|
|
41
|
+
Write each folder's `meta.ts` with its `pages` order (from its own `_meta`) and its `title` (inherited from the parent's `_meta`). A v4 **`_meta.global.*`** file holds the whole tree in one place (folder entries carry `items`) — split it into per-folder `meta.ts` files with the same rules.
|
|
42
|
+
|
|
43
|
+
## Frontmatter & titles — synthesize, don't just pass through
|
|
44
|
+
|
|
45
|
+
Most Nextra pages have **no frontmatter**; the title falls back `_meta` title → `sidebarTitle` → `title` → **first body H1** → filename. Blume renders frontmatter `title` as the page H1, so for **every page**: set frontmatter `title` (from the `_meta` title, else the body H1, else a humanized filename), **remove the body H1**, and map `sidebarTitle` → `sidebar.label`. `asIndexPage: true` (v4 folder-index marker) → make the file the folder's `index.mdx` and drop the key. Drop any other non-schema key and report it.
|
|
46
|
+
|
|
47
|
+
## Components
|
|
48
|
+
|
|
49
|
+
- **Callouts:** `<Callout type="x">` → directive. Types: `default` (the prop default) → `:::note`, `info`→`:::info`, `warning`→`:::warning`, `error`→`:::danger`, `important` (v4) → `:::note`. Bare `<Callout>` → `:::note`. Drop `emoji` (there is no `title` prop to carry — Nextra callouts have none).
|
|
50
|
+
- **v2 flat names:** v2 exported `Tab` and `Card` (flat) from **`nextra-theme-docs`**; v3/v4 use `Tabs.Tab`/`Cards.Card` from `nextra/components`. Handle both: `<Cards>`/`<Cards.Card>`/`<Card>` → `<CardGroup>`/`<Card>`; `<Tabs items={[…]}>` + `<Tabs.Tab>`/`<Tab>` → `<Tabs>`/`<Tab title="…">` (move labels from the parent `items` onto each `<Tab>`). `Cards` `num` → `CardGroup` `cols`.
|
|
51
|
+
- **`<Steps>`:** Nextra's wraps **Markdown headings (h2–h6)**, one step per heading. Convert each heading to a `<Step title="…">` child (Blume `<Steps>`/`<Step>`) and delete the heading — this also keeps step titles out of the TOC, matching Nextra's behavior.
|
|
52
|
+
- **`<FileTree>`/`<FileTree.Folder>`/`<FileTree.File>`** → `<Tree>`/`<Tree.Folder>`/`<Tree.File>` (or a list-driven `<FileTree>`).
|
|
53
|
+
- **`<Bleed>`** (full-bleed) → no equivalent; drop the wrapper and report. **`<Table>`** → a plain Markdown table. **`<Banner>`** (v4, in layout) → the `banner` config.
|
|
54
|
+
- **GitHub alert blockquotes** (v4 renders them): `> [!NOTE]`/`[!TIP]`/`[!WARNING]`/`[!IMPORTANT]`/`[!CAUTION]` → `:::note`/`:::tip`/`:::warning`/`:::note`/`:::warning` directives — Blume renders them as plain blockquotes otherwise.
|
|
55
|
+
- **Strip or convert every import:** `nextra`, `nextra/*`, `nextra-theme-docs`, `nextra-theme-blog`, plus `next/image` (→ Markdown image or `<Frame>`), `next/link` (→ plain link), and local components (port or inline; report).
|
|
56
|
+
|
|
57
|
+
## Code fences
|
|
58
|
+
|
|
59
|
+
Nextra's fence meta differs from Blume's — rewrite it: `filename="app.js"` → a space-separated title (` ```js app.js `); `showLineNumbers` → `lineNumbers`; line highlighting `{1,4-5}` carries over unchanged; **drop** word-highlight `/word/`, `copy`/`copy=false`, and inline-code `{:lang}` suffixes. ` ```sh npm2yarn ` fences → ` ```package-install `.
|
|
60
|
+
|
|
61
|
+
## Math
|
|
62
|
+
|
|
63
|
+
Nextra enables math via `nextra({ latex: true })` (KaTeX or MathJax). In Blume, block math `$$…$$` renders in `.mdx` with **no config** (there is no `markdown.math` field). Convert Nextra's ` ```math ` fences → `$$…$$` blocks (they render as plain code blocks otherwise). Inline `$…$` is **not** supported — convert to display math or drop (report). MathJax-specific macros → report.
|
|
64
|
+
|
|
65
|
+
## i18n
|
|
66
|
+
|
|
67
|
+
- **v2/v3:** locale **file suffixes** (`index.en.mdx`, `index.zh.mdx`) + `i18n` in `next.config` → restructure into locale **folders** (default locale at the content root, others under `<code>/`), then `i18n: { defaultLocale, locales: [{ code, label }] }`.
|
|
68
|
+
- **v4:** `content/<lang>/` dirs already match Blume's `dir` parser — map the locale list, no file moves.
|
|
69
|
+
|
|
70
|
+
## Package.json & teardown
|
|
71
|
+
|
|
72
|
+
Repoint `dev`/`build`/`start` scripts to the Blume CLI; remove `next`/`nextra`/`nextra-theme-*` deps and add `blume`. Delete after harvesting: `next.config.*` (redirects first!), `theme.config.*` (v2/v3), `app/layout.*` + the catch-all gateway + `mdx-components.*` (v4).
|
|
73
|
+
|
|
74
|
+
## Dropped — report these
|
|
75
|
+
|
|
76
|
+
Footer content; `primaryHue`-style theming beyond an accent color; `faviconGlyph`; `_meta` separators, menus, `newWindow`; per-page `theme` layout switches; `<Bleed>`; MathJax macros; word-highlight/copy fence meta; any icon you can't reconstruct as a Lucide name.
|