# Migrating into a real repo (monorepos, pnpm, Vercel) 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. **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. --- ## 1. Non-standard content layout → scope `content.root` + `include` `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. **Detect by finding where `.md`/`.mdx` actually live** — never assume `docs/`. From the docs package dir: ```bash # top two path segments of every content file, deduped — your content folders find . -path ./node_modules -prune -o \( -name '*.md' -o -name '*.mdx' \) -print \ | sed 's|^\./||' | cut -d/ -f1 | sort -u ``` 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. **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`: ```ts blume.config.ts import { defineConfig } from "blume"; export default defineConfig({ content: { root: ".", // content sits directly under this package (apps/docs/), no docs/ subfolder // include/exclude globs are relative to `content.root`. List the actual // content folders so the scan never wanders into src/, public/, etc. include: [ "api/**/*.{md,mdx}", "getting-started/**/*.{md,mdx}", "guides/**/*.{md,mdx}", ], exclude: ["**/node_modules/**", "**/_*", "**/.*"], }, }); ``` 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. 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. --- ## 2. pnpm `minimumReleaseAge` blocks installing fresh Blume 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. **Do not disable the policy globally** — that removes the guard for the whole dependency tree. Add **only `blume`** to the exclude list. **Detect** in `pnpm-workspace.yaml` (pnpm 10+, the primary location) and, for older setups, `.npmrc`: ```bash grep -rn "minimumReleaseAge\|minimum-release-age" pnpm-workspace.yaml .npmrc 2>/dev/null ``` **Fix — `pnpm-workspace.yaml`** (surgical add; create the key if the policy is set but no exclude exists): ```yaml minimumReleaseAge: 1440 minimumReleaseAgeExclude: - blume ``` **Fix — `.npmrc`** form (if the policy lives there instead): ```ini minimum-release-age=1440 minimum-release-age-exclude[]=blume ``` Report: "Added `blume` to `minimumReleaseAgeExclude` so the just-published version installs; the release-age guard still applies to everything else." --- ## 3. Frozen lockfile — regenerate and commit `pnpm-lock.yaml` in the same change 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. **After every dependency edit, regenerate the lockfile and stage it with the manifest:** ```bash pnpm install # NOT --frozen-lockfile — this rewrites pnpm-lock.yaml git add package.json apps/docs/package.json pnpm-lock.yaml ``` Rules: - Run `pnpm install` (plain) from the **workspace root**, not the docs package, so the single root lockfile updates. - 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. - 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. --- ## 4. Vercel monorepo recipe (root-aware install + build) 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/`. This is a **copyable template**, not prose. Three pieces: **a) Docs package `apps/docs/package.json`** — the standard Blume scripts: ```json { "scripts": { "dev": "blume dev", "build": "blume build", "preview": "blume preview" }, "dependencies": { "blume": "^1" } } ``` **b) `apps/docs/vercel.json`** — root-aware install, local build, `dist` output: ```json { "$schema": "https://openapi.vercel.sh/vercel.json", "framework": null, "installCommand": "cd ../.. && pnpm install --frozen-lockfile", "buildCommand": "pnpm run build", "outputDirectory": "dist" } ``` **c) The one setting `vercel.json` can't hold — set it in the Vercel project (dashboard → Settings → General, or `vercel` CLI):** - **Root Directory** = `apps/docs`. This makes install/build/output paths resolve from the docs package, which is what the `vercel.json` above assumes. - **Node version** = 22 or newer (Blume requires it). Or pin it in the docs package: `"engines": { "node": ">=22" }`. How the pieces fit, with Root Directory = `apps/docs`: - `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. - `buildCommand` runs in `apps/docs/` and invokes `blume build` (via the package's `build` script) → emits `apps/docs/dist/`. - `outputDirectory: "dist"` is relative to the Root Directory (`apps/docs`), so it points at `apps/docs/dist/`. - `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. 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`. 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. **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.) --- ## 5. Astro/Vite version mismatch → pnpm patch (manual) 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. **Detect the risk factor** before it bites: ```bash grep -rn "\"vite\"\|vite@\| vite:" pnpm-workspace.yaml package.json 2>/dev/null | grep -i "override\|resolution\|catalog\|vite" ``` 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): ```bash # 1. Open the offending version in an editable temp dir (use the exact version # pnpm resolved — check `pnpm why astro`): pnpm patch astro@7.0.6 # 2. Edit files in the printed temp path to reconcile the Astro/Vite interface # (the exact edit depends on the mismatch; the crash's stack trace names the # file and call site). Then commit the patch: pnpm patch-commit '' ``` `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): ```yaml # pnpm-workspace.yaml patchedDependencies: astro@7.0.6: patches/astro@7.0.6.patch ``` Commit **both** `patches/astro@7.0.6.patch` and the `patchedDependencies` entry, then re-run `pnpm install` and `blume build`. 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. --- ## 6. Ultracite / oxfmt formatting → oxfmt patch (ship the bundled patch) 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`. 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: ```bash # 1. Copy the shipped patch into the target repo's patches/ dir (keep the exact filename; # = this skill's directory, the one containing SKILL.md): mkdir -p patches cp "/assets/oxfmt@0.55.0.patch" patches/oxfmt@0.55.0.patch ``` Then register it under `patchedDependencies` — `pnpm-workspace.yaml` on pnpm 10+, or root `package.json` (`pnpm.patchedDependencies`) on pnpm 9: ```yaml # pnpm-workspace.yaml patchedDependencies: oxfmt@0.55.0: patches/oxfmt@0.55.0.patch ``` Commit **both** the patch file and the `patchedDependencies` entry, then re-run `pnpm install`. - **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. - **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. --- ## Checklist for a workspace migration - [ ] Located real content folders (not assumed `docs/`); set `content.root` + scoped `content.include`. - [ ] `minimumReleaseAge` present? Added `blume` to `minimumReleaseAgeExclude` only. - [ ] Ran plain `pnpm install`; committed `pnpm-lock.yaml` with the `package.json` change; verified `pnpm install --frozen-lockfile` is clean. - [ ] Wrote `apps/docs/vercel.json` + package scripts; told the user to set Root Directory + Node 22 in the Vercel project. - [ ] Checked for a workspace Vite override; if the build crashes inside Astro/Vite, gave the pnpm-patch recipe. - [ ] Uses Ultracite/oxfmt? Shipped `patches/oxfmt@0.55.0.patch` + registered it under `patchedDependencies` so formatting doesn't mangle `:::` directives. - [ ] Reported every repo-specific edit and every manual step left to the user.