diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 0c880342c7..a38b07713a 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -10,91 +10,78 @@ for the change: they are accountable for reviewing, testing, and standing behind they are the only committer/author of record. An AI byline would blur that — it reads as a second party sharing responsibility for a change only one person actually owns. -## Architecture (Nuxt-first, 11ty being phased out) +## Architecture -The site is migrating from Eleventy (11ty) to Nuxt 3. Nuxt is the primary framework going forward; 11ty is being phased out section by section using a strangler-fig pattern. +One Nuxt 3 application serves the whole site. It was generated by Eleventy (11ty) until +September 2026, and page groups were moved across one at a time using a strangler-fig +pattern; that migration is finished and Eleventy is gone. A `.njk` filename or a `src/` +path in a comment names what a page used to be, not a file that still exists. -- **Primary framework**: Nuxt 3 (`nuxt/`) with `@nuxt/content` v3 for content-driven pages -- **Legacy SSG**: Eleventy (11ty) v3, source in `src/`, output to `_site/` — being phased out -- **Strategy**: Nuxt is the front door. In dev, Nuxt proxies un-migrated routes to 11ty (port 8080). In production, `nuxt generate` produces the final output. -- **CSS**: Tailwind v3 via PostCSS → `_site/css/style.css` (shared between both) -- **Templates (legacy)**: Nunjucks (`.njk`) + Markdown (11ty only) -- **Search**: Algolia (`scripts/index-algolia.js`) -- **Hosting**: Netlify; final output from `nuxt generate` - -### Migration status - -| Section | Status | -|---------|--------| -| `/handbook/**` | **Migrated** — served by Nuxt (`nuxt/content/handbook/`) | -| `/docs/**` | **Migrated** — served by Nuxt; source resolved from `flowfuse/flowfuse` at build time | -| All other routes | Still on 11ty, proxied through Nuxt in dev | - -### New pages belong in Nuxt - -Never create a new `.njk` page under `src/` — even a landing/marketing page that looks like the fastest way to match an existing 11ty page's pattern. Build it in Nuxt (`nuxt/pages/`, `.vue`/`.md`) instead; only edits to already-existing 11ty pages belong in `src/`. `nuxt/lib/legacy-pages.test.mjs` enforces this with no allowlist to maintain: it fetches main and fails `npm test` (and therefore the required `test_website / Build and check` PR check) if any `.njk` file under `src/` didn't already exist there — i.e. a brand-new `.njk` file, anywhere in `src/`, on any route. Editing an existing `.njk` file is unaffected. - -This is scoped to `.njk` on purpose and does not extend to `.md`: new content in `src/blog/`, `src/changelog/`, `src/customer-stories/`, `src/webinars/`, and `src/blueprints/` (new posts, entries, blueprints) is expected routine work and must keep landing there unchanged, no matter how this section reads out of context. The gap this leaves — a brand-new page built as a bare `.md` file against an *existing* layout, with no new `.njk` involved — is real and not caught by the test; avoiding that is a matter of following this rule, not something enforced automatically. See `/handbook/marketing/website#new-pages-must-be-built-in-nuxt` for the non-engineer-facing explanation. +- **Framework**: Nuxt 3 (`nuxt/`) with `@nuxt/content` v3 for content-driven pages +- **Content**: markdown and YAML in `nuxt/content/`; two collections are synced in from + other repositories at build time (product docs, the Blueprint Library) +- **Files served as-is**: `nuxt/public/`, a tracked asset tree rather than build output +- **CSS**: Tailwind v4 via PostCSS, `nuxt/assets/css/style.css` → `nuxt/public/css/style.css` +- **Search**: Algolia (`scripts/index-algolia.js`), indexed from the built output +- **Hosting**: Netlify, from `nuxt/dist` ### Production build order ``` -clean:nuxt → build:js:nuxt → blueprints → prod:postcss-nuxt → prod:eleventy-nuxt → prod:nuxt +clean:nuxt → build:js:nuxt → prod:postcss-nuxt → prod:nuxt ``` -The `docs-source` Nuxt module runs automatically during `prod:nuxt` and calls `nuxt/lib/docs-sync.mjs` to resolve `docs/` from `flowfuse/flowfuse` (see **Local docs development** below). 11ty outputs to `nuxt/public/` so Nuxt can serve 11ty-generated assets. `nuxt/public/` is gitignored (fully build-generated). - -The `blueprints` step resolves `src/blueprints/` from `FlowFuse/blueprint-library` (private) via `nuxt/lib/blueprints-sync.mjs` — same local → sibling → clone precedence as docs, but the clone step authenticates with a minted GitHub App installation token (`GH_BOT_APP_ID`/`GH_BOT_APP_KEY`) since that repo isn't public. Netlify deploys straight from `main`; there is no `live` branch anymore. - -`GH_BOT_APP_KEY` is stored in Netlify as the PEM private key **Base64-encoded** (Netlify's env var UI rejects multiline values) — `nuxt/lib/github-app-token.mjs` decodes it before use. Rotating the key means re-encoding the new PEM to Base64 before saving it, not pasting the PEM in directly. +Two Nuxt modules run during `prod:nuxt` and resolve content that does not live here: +`docs-source` calls `nuxt/lib/docs-sync.mjs` for the product docs from `flowfuse/flowfuse` +(see **Local docs development** below), and `blueprints-source` calls +`nuxt/lib/blueprints-sync.mjs` for the Blueprint Library from `FlowFuse/blueprint-library`. +Both write into `nuxt/content/
` and `nuxt/public/
`, and both of those +pairs are gitignored: never edit them, they are wiped every build. ## Dev commands ```bash -npm start # all watchers in parallel (11ty + nuxt + postcss + blueprints) -npm run dev # eleventy + postcss + nuxt only -npm run dev:eleventy # 11ty only, port 8080 (legacy; most work doesn't need this) -npm run dev:nuxt # Nuxt only, port 3000 — use this for handbook, docs, and migrated pages +npm run dev # everything: Nuxt on port 3000, plus the postcss, docs and blueprint watchers +npm run dev:nuxt # Nuxt only, without the watchers npm run docs # resolve product docs into nuxt/content/docs, no build -npm run build # production build +npm run blueprints # resolve the Blueprint Library, no build +npm run build:nuxt # production build ``` -> When working on the handbook, docs, or other migrated sections, `npm run dev:nuxt` is sufficient. `npm start` is only needed when also touching 11ty-served pages. For product docs, add `npm run dev:docs` beside it: that watcher re-syncs each edited page, and without it a docs edit only appears after a restart. `npm run dev` and `npm start` already include it. +> `npm run dev` is the normal entry point. For product docs its `dev:docs` watcher re-syncs +> each edited page; `npm run dev:nuxt` on its own does not include it, and without it a docs +> edit only appears after a restart. > > **Local docs development:** a checkout of `flowfuse/flowfuse` sitting next to this repo (`../flowfuse`) is picked up automatically, with no configuration. Full resolution order, which every build logs: `FLOWFUSE_DOCS_LOCAL` (explicit path, and a path that does not exist is an error), then a sibling checkout, then a clone of `FLOWFUSE_DOCS_REF` (default `main`) — this is what Netlify production deploys use. CI relies on the sibling rule: `FlowFuse/flowfuse`'s `Publish Documentation` workflow checks itself out next to the website so a docs PR is validated against its own changes. ## Directory layout ``` -src/ -├── _data/ # Global data files (authors, tags, site config, etc.) -├── _includes/ -│ ├── layouts/ # Nunjucks layout templates -│ └── components/ # Reusable partials -├── blog/ # Blog posts → /blog/YYYY/MM/slug/ -├── changelog/ # Changelog entries → /changelog/YYYY/MM/slug/ -├── customer-stories/ # Case studies → /customer-stories/slug/ -├── css/ # Tailwind + custom CSS -├── images/ # Static images -└── public/ # Pass-through static files nuxt/ -├── content/ +├── content/ # @nuxt/content sources (see content.config.ts for the collections) +│ ├── blog/ # Blog posts → /blog/YYYY/MM/slug/ +│ ├── changelog/ # Changelog entries → /changelog/YYYY/MM/slug/ +│ ├── customer-stories/ +│ ├── webinars/ │ ├── handbook/ # Handbook pages (edit here) -│ └── docs/ # Product docs (build-generated, gitignored — do not edit) -├── modules/ -│ └── docs-source.ts # Wires docs into Nuxt; resolution lives in nuxt/lib/docs-sync.mjs -├── composables/ -│ ├── useHandbookNav.ts -│ └── useDocsNav.ts +│ ├── docs/ # Product docs (synced, gitignored, do not edit) +│ └── blueprints/ # Blueprint Library (synced, gitignored, do not edit) +├── content-guides/ # This repo's own /docs pages, overlaid onto the synced docs tree +├── data/ # JSON/YAML the app imports directly (site, nav, team, guests) +├── assets/ +│ ├── css/ # Tailwind entry point and the stylesheets it imports +│ ├── icons/ # FlowFuse's own SVG art: the nav icons and the `ff:` icon collection +│ └── js/ # cookieconsent-config.js, bundled by build:js:nuxt +├── public/ # Served as-is: images, downloads, favicons (tracked) +├── modules/ # docs-source.ts, blueprints-source.ts +├── lib/ # Plain-JS helpers with node --test unit tests +├── server/ # Nitro: api/, routes/, plugins/, lib/, assets/ ├── components/ -│ ├── HandbookLeftNav.vue -│ └── DocsLeftNav.vue +├── composables/ +├── utils/ └── pages/ - ├── handbook/[...slug].vue - └── docs/[...slug].vue -scripts/ # Build-time scripts (copy_blueprints.js, etc.) -lib/ # Shared helpers used by .eleventy.js and scripts -.eleventy.js # Main Eleventy config +scripts/ # sync_docs.mjs, sync_blueprints.mjs, their watchers, index-algolia.js +lib/ # search-index.js, shared with scripts/index-algolia.js ``` --- @@ -103,9 +90,9 @@ lib/ # Shared helpers used by .eleventy.js and scripts ### Blog posts -**Source:** `src/blog/YYYY/MM/{slug}.md` +**Source:** `nuxt/content/blog/YYYY/MM/{slug}.md` **URL:** `/blog/YYYY/MM/{slug}/` -**Layout:** `layouts/post.njk` +**Rendered by:** `nuxt/pages/blog/[...slug].vue` ```yaml --- @@ -113,7 +100,7 @@ title: "Post title" subtitle: "Optional subtitle" description: "SEO meta description" date: 2026-04-09 -authors: ["username"] # must match an entry in src/_data/team/ or guests/ +authors: ["username"] # must match an entry in nuxt/data/team/ or guests/ image: /blog/YYYY/MM/images/hero.png video: "youtube_id" # optional tags: @@ -131,7 +118,7 @@ cta: # optional call-to-action block --- ``` -Tag options are defined in `src/_data/blogTags.json`. Future-dated posts are excluded from collections until their date arrives. +Tag options are defined in `nuxt/data/blogTags.json`. Future-dated posts are excluded from collections until their date arrives. #### Inline Image CTAs (`CtaImage`) @@ -146,9 +133,9 @@ Tag options are defined in `src/_data/blogTags.json`. Future-dated posts are exc ### Changelog entries -**Source:** `src/changelog/YYYY/MM/{slug}.md` +**Source:** `nuxt/content/changelog/YYYY/MM/{slug}.md` **URL:** `/changelog/YYYY/MM/{slug}/` -**Layout:** `layouts/post-changelog.njk` +**Rendered by:** `nuxt/pages/changelog/[...slug].vue` ```yaml --- @@ -163,7 +150,7 @@ issues: # optional GitHub issue links --- ``` -Each year has a `src/changelog/YYYY/YYYY.json` that tags the collection. +Collection config: `nuxt/content.config.ts` (defines the `changelog` collection). --- @@ -248,9 +235,9 @@ layout: redirect ### Customer stories -**Source:** `src/customer-stories/{slug}.md` +**Source:** `nuxt/content/customer-stories/{slug}.md` **URL:** `/customer-stories/{slug}/` -**Layout:** `layouts/story.njk` +**Rendered by:** `nuxt/pages/customer-stories/[slug].vue` ```yaml --- @@ -276,45 +263,58 @@ story: --- ``` -Collection config: `src/customer-stories/customer-stories.json` +Collection config: `nuxt/content.config.ts` (defines the `stories` collection) + +--- + +### Blueprints + +**Source:** `FlowFuse/blueprint-library`, one `//README.md` per blueprint. +Not editable here: a blueprint change is a PR to that repository. +**URL:** `/blueprints/{category}/{slug}/` +**Rendered by:** `nuxt/pages/blueprints/[category]/[slug].vue`; listed, 12 per page, by +`nuxt/pages/blueprints/index.vue` and `[page].vue` through `BlueprintListing.vue` + +`nuxt/modules/blueprints-source.ts` resolves the library and `nuxt/lib/blueprints-sync.mjs` +copies it in: the markdown into `nuxt/content/blueprints//.md` (image paths +rewritten, the git date recorded as `updated`), the screenshots and `flow.json` into +`nuxt/public/blueprints///`. Both are gitignored here. + +Resolution order, the same precedence as the docs: `FLOWFUSE_BLUEPRINTS_LOCAL`, then a +sibling `../blueprint-library` checkout, then a clone. The library is private, so the clone +authenticates with a minted GitHub App installation token (`GH_BOT_APP_ID`/`GH_BOT_APP_KEY`, +see `nuxt/lib/github-app-token.mjs`), and a build without those credentials skips it and +keeps whatever is already in `nuxt/content/blueprints`. The clone is what Netlify production +deploys use: Netlify deploys straight from `main`, and there is no `live` branch anymore. + +`GH_BOT_APP_KEY` is stored in Netlify as the PEM private key **Base64-encoded** (Netlify's env var UI rejects multiline values) — `nuxt/lib/github-app-token.mjs` decodes it before use. Rotating the key means re-encoding the new PEM to Base64 before saving it, not pasting the PEM in directly. --- -## Global data (`src/_data/`) +## Global data (`nuxt/data/`) + +Imported directly by components and by `nuxt.config.ts`, rather than queried as content. | File | Purpose | |------|---------| -| `site.json` | Global site config (URL, name, etc.) | +| `site.json` | Global site config (URL, app URL, messaging) | +| `chrome.json` | The mega nav and the footer link lists | +| `navHighlights.json` | The promoted cards in the nav dropdowns | | `blogTags.json` | Valid blog tag values | | `team/` | Staff author profiles | | `guests/` | Guest author profiles | -| `companies/` | Customer company records | | `testimonials.json` | Pull-quote testimonials | -| `events.yaml` | Event calendar | -| `features.json` | Product feature catalog | -| `integrations.js` | Integration listings | -| `eleventyComputed.js` | Dynamic computed properties | - -## Layouts - -| Layout | Used by | -|--------|---------| -| `layouts/base.njk` | HTML shell | -| `layouts/post.njk` | Blog posts | -| `layouts/post-changelog.njk` | Changelog entries | -| `layouts/documentation.njk` | Node-RED learning resources (with sidebar nav) | -| `layouts/story.njk` | Customer stories | -| `layouts/nohero.njk` | General pages without hero | +| `events.yaml` | Event banner calendar | ## Redirects -**Add new redirects to `nuxt/redirects.ts`, not `netlify.toml`.** Nuxt is the front door for every route now, so retired-route redirects are handled as native Nitro route rules (`{ redirect: { to, statusCode: 301 } }`) keyed by the old path. `netlify.toml`'s `[[redirects]]` blocks are legacy and reserved for edge-level concerns that predate Nuxt (domain aliasing: `flowforge.com`/`flowfuze.com`/`flowfuse.io` → `flowfuse.com`) — don't add page-to-page redirects there. +**Add new redirects to `nuxt/redirects.ts`, not `netlify.toml`.** Nuxt is the front door for every route now, so retired-route redirects are handled as native Nitro route rules (`{ redirect: { to, statusCode: 301 } }`) keyed by the old path. `netlify.toml`'s `[[redirects]]` blocks are reserved for the cases Nitro's route rules cannot express, and each one there says why: domain aliasing (`flowforge.com`/`flowfuze.com`/`flowfuse.io` → `flowfuse.com`), old URLs that differ from the canonical one only in case (a Nitro rule matches case-insensitively and would redirect the canonical URL to itself), and one URL that differs only by a percent-encoded space (Nitro matches the decoded path). Don't add ordinary page-to-page redirects there. This also covers old `/docs/**` paths left behind by a rename in `flowfuse/flowfuse`'s `docs/` tree (e.g. a doc file renamed to fix a `nuxt-link-checker` `no-underscores` warning) — add the old path here so bookmarks/indexed links 301 instead of 404ing once the rename ships. ## Call-to-Action components -Both frameworks have their own implementation of the same system — Vue components on Nuxt, Nunjucks macros on 11ty (see **11ty equivalent** below). The rendering/styling code itself is kept in sync by hand (Vue and Nunjucks share nothing at that level), but the *data* — event name, href, and fixed label per destination — lives in one file, `src/_data/ctaDestinations.json`, imported by both the Vue components and the Nunjucks macros (via a Nunjucks global, see below) so the two can't drift apart on copy or event names without both sides being touched. `site.json`'s `appURL` is combined with `ctaDestinations`' `hrefSuffix` for `signUp`/`signIn`, since the base URL differs per environment and doesn't belong duplicated a third time in `ctaDestinations.json`. +the *data* (event name, href, and fixed label per destination) lives in one file, `nuxt/lib/cta-destinations.ts`, read by every `Cta*.vue` component and by `CtaCustom.vue`, so no call site carries its own copy of the copy or the event name. `site.json`'s `appURL` supplies the base URL for `signUp`/`signIn`, since it differs per environment. There are exactly five CTA destinations, each with its own component with **fixed copy and href** (a PostHog audit found dozens of different button texts pointing at the same handful of URLs, which made it impossible to tell which copy converted best — see `/handbook/marketing/website#call-to-action-buttons` for the non-engineer-facing explanation and a live gallery of every variant): @@ -345,7 +345,7 @@ This exists because a raw `event` prop would let two different `CtaCustom` insta There's no `size` prop — every real-button variant's padding/font-size is hardcoded to match `.ff-btn` exactly (see the Computed-tab note in the gotchas below), so a size knob would only ever have affected icon dimensions. It was removed once confirmed nothing used a non-default value. -Click tracking: `capture(event, { position, variant, plan? })` via `nuxt/composables/useCapture.ts`, which wraps the global `window.capture()` from `src/_includes/analytics/body.html` (shared with 11ty, no-ops without analytics consent). Event names: `cta-sign-up`, `cta-sign-in`, `cta-contact-us`, `cta-book-demo`, `cta-pricing`. +Click tracking: `capture(event, { position, variant, plan? })` via `nuxt/composables/useCapture.ts`, which wraps the global `window.capture()` from `nuxt/server/assets/analytics/body.html` (a Nitro server asset injected by `nuxt/server/plugins/analytics.ts`; no-ops without analytics consent). Event names: `cta-sign-up`, `cta-sign-in`, `cta-contact-us`, `cta-book-demo`, `cta-pricing`. ### Gotchas already solved here (don't re-discover them) @@ -355,17 +355,7 @@ Click tracking: `capture(event, { position, variant, plan? })` via `nuxt/composa - **`.handbook-content a` must exclude Nuxt UI components, or use `:where()`.** A plain `.handbook-content a { color: ... }` rule (even layered) beats a `UButton`'s own utility classes if it has higher specificity, flattening any Cta* button rendered inside handbook markdown to a plain link color. Fixed by moving the rule into `@layer base` and using `:where(a)` to zero out its added specificity, so any component's own classes win normally (same file as above). - **`not-prose` on a wrapper also strips Tailwind Typography's code-block styling** (the dark background on `
`) for anything nested inside it, not just its own prose text styling. `nuxt/components/content/CtaExample.vue` (the handbook's live example gallery) doesn't use `not-prose` for this reason, even though it also renders non-prose button/grid markup.
 - **The `ui` prop override doesn't reach compoundVariants-driven classes.** The gotcha above (full-string replacement) only applies to the app.config-level base extension; UButton's own `variant`/`color`-driven classes (e.g. `ghost`'s `hover:bg-{color}/10`) are computed separately and still get merged in via `tv()` regardless of what `ui.base` says. `CtaButton.vue`'s ghost color classes explicitly add `hover:bg-transparent` to cancel that default hover background, since a ghost CTA should have none at all.
-- **UButton's `to` prop treats any same-origin-looking path as a Nuxt route, even if Nuxt doesn't serve it.** `CtaButton.vue` takes a fixed (non-caller-configurable) `external` prop per destination, set by each `Cta*` wrapper — `true` when the href still points at an 11ty-served route (would otherwise 404 via client-side Vue Router instead of reaching the 11ty proxy), `false` once that route is served by Nuxt. `CtaContactUs`/`CtaBookDemo` now set `false` — `/contact-us` and `/book-demo` are Nuxt routes (`nuxt/pages/contact-us/index.vue`, `nuxt/pages/book-demo/index.vue`). `CtaSignUp`/`CtaSignIn` set `false` too, moot since those hrefs are already cross-origin.
-
-### 11ty equivalent
-
-`src/_includes/components/cta/` holds five Nunjucks macros (`ctaSignUp`, `ctaSignIn`, `ctaContactUs`, `ctaBookDemo`, `ctaPricing`) mirroring the Vue components above — same fixed copy/href/event per destination, same `{ position, variant }` capture payload (no `preview` or `external` prop: 11ty has no handbook gallery to guard against, and every `` is a real page load already, no Vue Router to fight). All five delegate to a shared `ctaButton` macro in the same folder (`cta-button.njk`) for the actual class-building/tracking, same relationship as `CtaButton.vue` to its wrappers.
-
-No `plan` param on the 11ty side — that's only used on the pricing table, which is Nuxt-only (there's no 11ty pricing page to call it from). If 11ty ever grows a caller that needs it, add it back rather than passing it unused today.
-
-`ctaSignUp`/`ctaSignIn` take `site` as their first argument (`site.appURL` for the href) instead of reading it from the calling template's context — Nunjucks macros don't inherit the caller's context unless explicitly imported `with context`, and passing `site` explicitly avoids relying on that import mode everywhere the macro is used.
-
-Every hand-written `` pointing at one of the five destinations has been migrated to these macros — there should be no new ones. A link to a URL outside the five fixed destinations (e.g. `/ai/`) is not part of this system and stays hand-written.
+- **UButton's `to` prop treats any same-origin-looking path as a Nuxt route, even if Nuxt doesn't serve it.** `CtaButton.vue` takes a fixed (non-caller-configurable) `external` prop per destination, set by each `Cta*` wrapper. Every one of the five is `false` now that Nuxt serves every route; the prop exists because `CtaCustom` links off-site, and because a path Nuxt does not serve would 404 through client-side Vue Router rather than reaching the server.
 
 ## Icons
 
@@ -373,7 +363,7 @@ Every hand-written `` pointing at one of the five destinat
 
 `` renders as a masked ``, not an `` — no intrinsic aspect ratio, so give it an explicit size (`w-6 h-6`) rather than `w-full h-full`.
 
-**FlowFuse's own custom icon art** lives as raw SVGs in `src/_includes/components/icons/*.svg`:
+**FlowFuse's own custom icon art** lives as raw SVGs in `nuxt/assets/icons/*.svg`:
 - **Header/nav:** `nuxt/utils/navIcons.ts` + `NavIcon.vue`, which renders a real inline `` — needed because the header's CSS targets `` elements directly, which a `` span doesn't have.
 - **Everywhere else:** a one-off SFC under `nuxt/components/icons/` (e.g. `GithubIcon.vue`) with the SVG pasted into the template:
   ```vue
@@ -388,5 +378,8 @@ Every hand-written `` pointing at one of the five destinat
 
 - All slugs: **kebab-case**
 - Blog/changelog: folder path mirrors publish date (`YYYY/MM/`)
-- Images for a post live alongside it: `src/blog/YYYY/MM/images/`
-- Author usernames must match a file in `src/_data/team/` or `src/_data/guests/`
+- Images for a post live at the matching path under `nuxt/public/`: a post at
+  `nuxt/content/blog/YYYY/MM/{slug}.md` references `./images/x.png`, which @nuxt/content
+  resolves against the post's own directory, so the file goes in
+  `nuxt/public/blog/YYYY/MM/images/x.png`
+- Author usernames must match a file in `nuxt/data/team/` or `nuxt/data/guests/`
diff --git a/README.md b/README.md
index 24aeaf4f75..b37fad11fe 100644
--- a/README.md
+++ b/README.md
@@ -122,7 +122,7 @@ Every build resolves blueprints in this order, and logs which one it used:
 
 | Order | Source | Used when |
 |-------|--------|-----------|
-| 1 | `BLUEPRINTS_LOCAL=/path/to/blueprint-library` | The env var is set. A path that does not exist is an error, not a fallback. |
+| 1 | `FLOWFUSE_BLUEPRINTS_LOCAL=/path/to/blueprint-library` | The env var is set. A path that does not exist is an error, not a fallback. |
 | 2 | A sibling checkout: `../blueprint-library` | It exists. This is what local development relies on. |
 | 3 | A clone, authenticated with a minted GitHub App installation token (`GH_BOT_APP_ID`/`GH_BOT_APP_KEY`) | Nothing above applied, and those env vars are set. This is what Netlify production deploys use — `blueprint-library` is private, so this can't clone anonymously the way docs does. |
 | 4 | Skipped | Nothing above applied. Matches the previous behaviour for contributors without access to the private repo. |
@@ -133,7 +133,7 @@ Every build resolves blueprints in this order, and logs which one it used:
 
 `/llms.txt` (and `/llms-full.txt`) are generated by the [`nuxt-llms`](https://github.com/nuxtlabs/nuxt-llms) module, configured in `nuxt/nuxt.config.ts` under the `llms` key. Sections are built from `@nuxt/content` collections (`docs`, `blog`, `changelog`, `ebooks`, `whitepapers`) plus a small hardcoded list of standalone Nuxt routes (pricing, integrations, etc.) that aren't backed by a collection.
 
-The `handbook` collection is deliberately excluded — it's internal company documentation, not product content, so it has no `contentCollection` section here.
+The `handbook` collection is deliberately excluded: it's internal company documentation, not product content, so it has no `contentCollection` section here.
 
 ## How to add blog posts