From 063394ddd26f818216b1e34b5acf44aacb71185c Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Thu, 10 Sep 2026 19:29:28 +0200 Subject: [PATCH] docs: rewrite the codebase guide for a Nuxt-only site .claude/CLAUDE.md still described the strangler-fig arrangement: two generators, a migration-status table, `npm start` for the 11ty stack, a src/ directory layout, Nunjucks layouts per content type, and a rule against adding a new .njk page enforced by a test that no longer exists. All of that is gone. What it says now: one Nuxt app, where each kind of content lives, and the two collections synced in from other repositories. The Blueprint Library gets a section of its own, since where it comes from and why it is gitignored here is the least obvious thing in the tree: resolved at build time, with an authenticated clone of the private library as the production path. The CTA section keeps its detail but loses the Nunjucks half and points at nuxt/lib/cta-destinations.ts, which has been the single source of that data since before this migration finished. The attribution rule and the Icons section are kept as they were, with the icon path moved to nuxt/assets/icons. README's blueprint table named the override `BLUEPRINTS_LOCAL`; the variable nuxt/lib/blueprints-sync.mjs reads is `FLOWFUSE_BLUEPRINTS_LOCAL`. --- .claude/CLAUDE.md | 203 ++++++++++++++++++++++------------------------ README.md | 4 +- 2 files changed, 100 insertions(+), 107 deletions(-) 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