Make the Node-RED library part of the documentation, and convert it to Nuxt - #5750
Conversation
✅ Deploy Preview for flowforge-website ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
b451b5a to
63d57a2
Compare
Code reviewFound 4 issues:
website/nuxt/lib/library-markdown.mjs Lines 74 to 79 in 04bc888
website/nuxt/lib/core-nodes-sync.mjs Lines 250 to 254 in 04bc888
website/nuxt/lib/library-markdown.mjs Lines 105 to 108 in 04bc888
Lines 51 to 53 in 04bc888 One thing that is not a bug but is decision-relevant: this extends the sync-script pattern that was objected to on #5495 and has not been revisited since ("Why a hacky script? I would've expected this to use the Nuxt collection primitive?", #5495 (comment)). Worth settling before adding two more generators to it. |
|
All four resolved in 02d2826.
Each of the three code defects has a regression test naming the failure, because all three were silent: none produced a build error and CI was green on every one of them. |
|
Verified every redirect against the deploy preview, not just the source: 121 of 121 old URLs 301 to the correct new URL, no failures, and every one lands on a 200.
Two notes:
|
|
Follow-up opened: #5752, which answers the sync-script objection from #5495 (#5495 (comment)) for the part where it actually applies. It gives the The other two generators stay, for reasons rather than convenience:
Draft, and stacked on this PR. Merge order is #5745, then this, then #5752. |
|
FYI: this PR stackes on #5745 which when merged, will reduce file count in this PR. |
|
Hey @dimitrieh, this looks good. There are two things I noticed, though:
Also, one question: apart from Also, I find this PR really big. Would it be possible to split it into smaller chunks? If not, that's fine. |
…e docs
The library pages were authored as Nunjucks-in-markdown against src/node-red/'s
own shortcodes, so they need more repair than the guides in content-guides/
(authored as MDC) and different repair from the FlowFuse/flowfuse docs.
Reuses joinHtmlBlocks and convertCallouts from docs-markdown rather than
reimplementing them: the library uses the same note/warning shortcodes and the
same column-0 raw-HTML style. Adds what is specific to it.
renderFlow becomes the existing render-flow MDC component, reading its JSON from
a fenced block instead of an inline module script. This has to run before
convertCallouts, which finishes by stripping every remaining Nunjucks tag and
would otherwise delete the flow diagrams and their JSON without a trace. A test
pins that ordering.
Node-RED's own mustaches are the other trap. The pages document {{ msg.payload }}
and guarded it with Nunjucks raw blocks; MDC would read the same braces as a Vue
binding. Each mustache moves into an inline code span, which is how the rest of
the docs already write msg.payload. Fenced blocks are skipped wholesale, so flow
JSON carrying format:"{{msg.payload}}" keeps its exact bytes.
Links to /node-red/ itself are deliberately not rewritten. That page stays on
Eleventy as a marketing page; only the documentation below it moves.
…n index /node-red/learn/ was a table of contents wearing a title that had stopped being true. The count only held if you included the pages that are FlowFuse's own product reference, and the year was written in December 2025 pointing forward and never revisited, so the page has advertised '(2026)' with no content change behind it for most of a year. It becomes the index of a new 'Node-RED' documentation group instead, ranked last (navGroupOrder 7, after Contributing) because this is material a reader reaches for when they want more depth, not a step on the way in. The page now says that plainly, points at nodered.org as the primary source for the project's own manual, and names what this section covers that nodered.org does not: per-node reference on the web, and connecting Node-RED to specific databases, protocols and hardware. Both callouts route a reader who is actually setting FlowFuse up to /docs/user/ rather than deeper into Node-RED background.
The library was the last tenant of the old Eleventy documentation portal. Commit 951aee0 (2024-10-01, "Rename handbook layout to documentation") put /docs, /handbook and /node-red on one layouts/documentation.njk with one Algolia index, told apart only by a `nav` key. The handbook left for Nuxt in June and the docs in July, and the library was left behind on a layout still carrying `class="handbook ff-prose"`, handbookBreadcrumbs, and a `nav == 'docs'` branch nothing reached any more. This finishes that migration rather than starting one. Reference belongs in documentation, and this is reference. /node-red/ itself does not move: it is a marketing page (layout: page, "What is Node-RED?", FAQ structured data) and it stays on Eleventy. Everything below it becomes /docs. The new Node-RED group is ranked last, after Contributing. This is depth a reader reaches for once they have a question, not a step on the way in, and the nine header slots that pointed at our own node reference (mirrored verbatim in the footer, rendered on every page of the site) come out. Those pages are product reference and move into FlowFuse User Manuals as /docs/flowfuse-nodes/ instead. The core-node pages were never files. Eleventy paginated them out of coreNodes.json and each one fetched its help from raw.githubusercontent.com during the build. They are now generated from that catalogue plus a committed snapshot of the upstream help, so a deploy never depends on a third-party URL being reachable, and scripts/refresh_core_node_help.mjs updates the snapshot deliberately. Generation writes into nuxt/content/docs, which is gitignored and wiped per sync, so a build never dirties the tree. Generating instead of scraping surfaced four pages that have been shipping an empty "Node Documentation" section: coreNodes.json asked for `mqtt-in` and `mqtt-out` where upstream now says `mqtt in` and `mqtt out`, and both UDP nodes asked for `upd`, a typo. The old xpath found nothing and rendered the page anyway. The catalogue is corrected, the lookup tolerates the separator, and a miss now throws. Redirects are written one path at a time rather than as a /node-red/** splat, because a splat would swallow the marketing page too, and would turn every stale deep link into a 301 towards a 404. A dev-only middleware rule lets those requests reach Nitro instead of being proxied to an Eleventy tree that no longer has the pages. Retires documentation.njk, left-nav.njk, learning-resources-nav.njk, hardware.njk, docs-banner.njk, core-node-docs.njk and lib/core-node-docs.js.
…issing Replaces the committed help snapshot. A copy cannot be 1:1 with upstream and somebody has to remember to refresh it, so the pages fetch Node-RED's help on every build as they always did. What changes is the failure mode. lib/core-node-docs.js selected the help with an xpath, got an empty node-set when upstream renamed a block, joined it to an empty string and rendered the page anyway. That is how four pages came to ship an empty "Node Documentation" section. Now a help name the catalogue asks for and upstream does not have stops the build and names every miss at once, because when a locale file is reorganised several nodes move together and one error per run takes several runs to work through. Transport failures are told apart from mismatches. A 5xx or a dropped connection is retried with backoff, since one bad minute at GitHub should not fail a deploy. A 404, or a file that parses but lacks the requested help name, is a real mismatch no retry will fix and throws immediately. Responses are cached per locale file rather than per node, so the 38 pages cost 21 requests, about three seconds. Also fixes the prerender break from the previous commit: the generator writes straight into nuxt/content/docs rather than through guides-sync, so it has to emit index.md itself. destinationFor is what renames README.md on the way in, and writing README.md here produced routes like /docs/node-red/core-nodes/README/ that prerendered as 404s.
The link checker caught what the migration missed. rewriteLibraryPaths only ran
over the pages being moved, so every link INTO the library from elsewhere still
pointed at /node-red/**: blog posts, customer stories, the integrations pages,
the AI page, the handbook. Those paths 301 now, but the redirects are Nitro route
rules rather than files, so hyperlink checks the built output, finds nothing at
the old path, and fails the build. Anchored links failed twice over.
The substitution has to be anchored on a delimiter that cannot appear inside a
URL. Two earlier attempts got this wrong in different ways:
- Without a (?<!/docs) guard it re-prefixes what it has already fixed, since
/docs/node-red/ contains /node-red/, giving /docs/docs/node-red/.
- With only that guard it still rewrites third-party URLs, because the
character before /node-red/ in github.com/node-red/node-red is `m`. That
mangled links across the blog and the changelog, and this module's own
upstream constant, which is what made the last build fail.
Requiring a preceding quote, paren, bracket, backtick, equals or whitespace
covers every real link form and excludes both.
/node-red/ on its own is untouched, including both chrome.json slots, because
that page did not move. redirects-node-red.ts and library-markdown's tests are
excluded too: the old paths are the point in both.
Eleventy's markdown-it-anchor and @nuxt/content do not agree on heading ids, so every in-page link written against the old slugs broke on the move. "## 5. Configure a Connection" was #5.-configure-a-connection and is now #_5-configure-a-connection: the dot is dropped and a leading digit gains an underscore, since an HTML id may not start with a digit. Confirmed against the deploy preview's rendered ids rather than inferred. 38 anchors across 6 files. Also drops a stale %3F from an anchor targeting "## What is Modbus?" - the question mark is not part of the slug - and repoints the last link to /node-red/learn/, which no longer exists: its content is the /docs/node-red/ index now. Found the rest of the class locally instead of one CI run at a time, by slugging every heading in the migrated tree and checking each internal anchor against it. All 56 resolve.
1. protectMustaches did the opposite of its job on the common case. It checked
only the character immediately either side of a mustache, so one in the MIDDLE
of a longer code span had no backtick beside it, got re-wrapped, and split the
span in two: `<p>Hello {{payload.name}}!</p>` became
`<p>Hello `{{payload.name}}`!</p>`, leaving the mustache bare for MDC to bind and
turning the prose around it into bogus inline code. It now splits the text on
fences and on code spans and only rewrites what falls between them. Two of the
committed pages carried the damage and are regenerated.
2. The Function node's page and the Function category index resolved to the same
URL. slugFor('Function') is `function`, which is also its category directory, so
core-nodes/function.md and core-nodes/function/index.md both became
/docs/node-red/core-nodes/function/ and @nuxt/content took whichever it indexed
last. The legacy redirect for that path is the node, so the per-category pages
are gone and their grouping is headings on the section index instead. The
redirect map loses the five bare category paths with it: those were never pages
under Eleventy either, since the templates were pagination-only.
3. {% caution %} lost its callout. docs-markdown's convertCallouts knows note,
warning and critical and then strips every remaining Nunjucks tag, so a caution
block silently became an ordinary paragraph. The OPC UA guide flags "server
hosting is not supported on FlowFuse Cloud" with it. Converted locally, in the
same markup shape, before the stripping pass.
4. Two redirects still pointed at moved paths and so double-hopped:
/node-red/core-nodes/mqtt/ and /education/ now go straight to /docs/.
Each of the three code defects gets a regression test naming the failure, since
all three were silent: none produced a build error and CI was green on all of them.
| description: Classify images using ONNX models directly in Node-RED. Supports pre-trained and custom models for tasks like labeling, content moderation, and object recognition. | ||
| --- | ||
|
|
||
| # {{ meta.title }} |
There was a problem hiding this comment.
I think using the template expression way was better, so when needed, we could just change the title variable, and it would be updated automatically.
There was a problem hiding this comment.
Kept as a literal, because {{ }} cannot come along. In MDC it is a Vue expression, and this section is full of literal Node-RED mustaches ({{ msg.payload }}) that the migration has to code-span specifically so MDC does not bind them. Nothing else in nuxt/content/ or nuxt/content-guides/ interpolates frontmatter either, so it would be a new convention for one page.
The H1s are reproduced exactly as Eleventy rendered them, including the three pages where the H1 was deliberately shorter than the SEO title (notification/, peripheral/, getting-started/library/). Those would have been flattened by a {{ meta.title }} heading.
…o preserve Two defects from the review of #5750, both invisible to the build. The nav labels: every nested directory index took the eleventyNavigation key of its parent instead of its own, so getting-started/{editor,library,programming}/README.md all came out as "Getting started", nested underneath the "Getting started" that was already there. Their own keys were Editor, Node-RED Library and Programming, with orders 1, 3 and 7, and those are restored. Programming takes 8 rather than 7, which Eleventy had it tie with Date & Time on. The browser titles: the Eleventy layout picked a title as metaTitle || navTitle || meta.title, the migration moved meta.title to the new top-level title: and dropped metaTitle, and the docs page titles a page navTitle || title. navTitle is the sidebar label, so every moved page's <title> collapsed to it: "Using MySQL with Node-RED (2026 Updated)" became "MySQL", "Node-RED - Inject Node" became "Inject". These 121 URLs 301 onto the new ones and carry their ranking with them, so the title has to survive the move. Each page now carries its Eleventy title in metaTitle, which the docs page prefers, and core-nodes-sync emits the same field for the 39 generated pages. All 82 fit the 60-character guidance the site documents for the field. No page from FlowFuse/flowfuse sets metaTitle, so product docs titles are untouched, and metaTitle is declared in the docs collection schema because @nuxt/content strips an undeclared key, which would have made the whole change a silent no-op. The section index keeps the "Node-RED documentation" keyword from /node-red/learn/, the one page this PR replaces rather than moves. Both defects get a check over the real guides tree, because neither broke a build, a link or a test: nav labels have to be distinguishable from their siblings and from the parent they are indented under, and a page whose title is longer than its nav label has to say which of the two Google gets.
820d71c to
d1a2528
Compare
02d2826 to
35d1a80
Compare
|
@dimitrieh, regarding the Node-RED Core Nodes section, it was previously categorized into sections such as Function, Network, Common, Parser, etc. Now, those categories have been removed. |
|
@dimitrieh, the MQTT In and MQTT Out nodes have duplicated H1 headings. I’ve attached a screenshot from the MQTT Out node as an example. |
The review asked for the Common, Function, Network, Sequence, Parsers and Storage grouping back. It went missing resolving a URL collision in the previous round: the Function node and the Function category both wanted /docs/node-red/core-nodes/function/, @nuxt/content kept whichever it indexed last with no build error, and dropping the category pages was the cheap way out. It also dropped the level of the sidebar that made the node pages navigable. The categories become a directory each instead, which resolves the collision the other way round: the category owns core-nodes/function/ and the node is core-nodes/function/function/, so the two cannot contend for one path. The sidebar nests the way the editor's palette does, because docs-nav builds the tree from paths and has no nav-only grouping below the top-level navGroup. navOrder restarts per category, since a node's siblings are now its own category's nodes. The old flat URLs still reach a node in one hop, now to the nested path. The bare category paths, which were pagination-only templates under Eleventy and were left to 404, have a real page to land on and are mapped too, except /node-red/core-nodes/function/, which was the Function node's own URL and keeps pointing at the node. Every cross-reference to a node page moves with it, and the section index links each category heading as well as each node.
|
All four fixed, in 35d1a80 and 88b3b98.
The nav and title defects each have a check over the guides tree now, because neither broke a build, a link or a test. |
The MQTT In and MQTT Out pages rendered the node name as an H1 twice in a row. Their
use-case includes open with `# {{ meta.title }}`, and the page is assembled here with
its own `# <node name>`, so resolving that interpolation put two identical headings
next to each other.
Eleventy had the same duplicate heading, but empty: `meta.title` was undefined on a
paginated core-node page, so it rendered as a bare `<h1 tabindex="-1">` and nobody saw
it. Making the title resolve is what made it visible.
The include's headings now fold under the one the generator emits. A heading that just
repeats the node name is dropped, since it says nothing the H1 above it does not, and
any other H1 becomes an H2, which is what it should have been inside an include.
Fenced blocks are untouched, so a `#` comment in an example stays a comment. Three
other includes carried a second H1 of their own and are folded the same way.
The check runs over the committed includes rather than a fixture, since the defect was
in the content and only two files had it.
|
Fixed in 11cde53. Both includes open with Eleventy had the same duplicate, but empty ( |
…nvalidated The link checker caught two classes the nesting broke, both of which depended on how deep a core-node page sits. Relative asset references. The use-case includes live in src/_includes/core-nodes/ but their images live with the library pages, so they point at `./images/x.png`. That resolves against the built page's own directory, which only worked while the pages sat directly under core-nodes/, beside images/. Adding the category level sent every one of them to core-nodes/<category>/images/. The asset path is written out now, so a page's depth stops mattering, which is what the includes already did for their <video> sources. Category-less node links. The first rewrite only matched links with a trailing slash, so links written without one stayed flat and became 404s. Both are checked over the committed content rather than a fixture, since both were in the content. The link checker in CI would catch them again, but only after a full build. Also fixes the comments the restructure invalidated. renderCategorySections still carried the whole argument for NOT having category pages, sitting directly above the code that writes them, contradicting the rationale on renderCoreNodeCategoryPage. That one is deleted rather than rewritten, since the correct version already exists. The counts elsewhere in these modules are dropped rather than corrected: they were measuring a corpus the code no longer processes, and a count in a comment goes stale without anything noticing.
…index Keeps the page's own name alongside the keyword. 55 characters rendered, so it still fits inside a search result.
|
@dimitrieh cta looks weird
|
Every one of these built green. The core-node pages lost most of their help. Several catalogue entries name a node family rather than one node: `link` covers link in, link out and link call, and `websocket` covers four. Eleventy selected every help block whose name started with the requested one and joined them; extractHelp took the first and dropped the rest, while still captioning it as the node's complete built-in help. An exact name still wins on its own, so asking for `file` does not also drag in `file in`. The Read File page showed the Watch node's help. The catalogue pointed it at watch/23-watch. This predates the move, and the page has been wrong in production for as long as it has existed, but the whole point of the strict guard added here is that the catalogue is now load-bearing. The palette category and the node sharing its name were labelled identically. The category page for `function` and the Function node inside it both read "Function" in the sidebar, one directly above the other: the exact shape the nav check added earlier in this branch exists to forbid. It was invisible because that check only ever ran over the authored guides, never over the generated tree, so it now runs over both. An indented fence was not recognised as a fence. Both fence splitters anchored at column 0, so a sample indented under a list item was treated as prose: its mustaches were code-spanned and its Nunjucks guards stripped in a way that left the indentation behind and pushed the closing fence out past the point where it still closed anything. The Template node's page teaches Mustache syntax in exactly such a block. Guards now take their whole line, and fences are matched at any indent. Two guides linked the Function node at the category's URL. rewriteLibraryPaths only prefixes, and that path became the category index when the nodes moved a level deeper. The Firebase CTA rendered as raw markdown. Its hand-written wrapper div relied on the blank lines around its contents for them to parse as markdown, and joinHtmlBlocks removed them, making the whole thing one HTML block. Also drops a stray backtick from an importable flow in the Template use case, which would have rendered inside the message the flow prints.
|
Good catch, fixed in e583b3d. That CTA is a hand-written wrapper While in there, a review pass turned up five more of the same shape, all of which built green:
|
…by hand Six correctness fixes landed in this branch after being found by reading, not by any test failing. These are the guards that would have caught them. The redirect map gets derived and compared. Its core-node half is a function of coreNodes.json, so moving a node between palette categories silently strands a 301 on a path the generator no longer writes. The map's targets are also checked to be pages that will exist, and checked not to be redirect sources themselves. Reclassifying one node in the catalogue now fails two assertions instead of shipping a 301 into a 404. The browser-title order moves out of the page and into a module that asserts it. It is one expression, and it decides the search-result title of every page under /docs, and reordering it is invisible: the pages still build and simply have the wrong titles. The nav link checker reads both redirect maps. It only ever parsed nuxt/redirects.ts, whose rules are Nitro objects, so every /node-red/** entry in the new plain-record map was invisible to it. That is the set most likely to be left behind in the nav, and this branch deletes nine nav slots pointing into it. Twenty stale /node-red/** links in the site's data files now point at the moved pages. Most were only 301ing, but feature-catalog.yml's docsLink is matched by equality, so the MCP page was silently losing its plan badges: no match, no badges, and an unbadged page looks exactly like a page nobody catalogued. Three smaller ones. An empty `updated` is omitted rather than written as a valueless key, which YAML reads as null and the schema rejects. The empty-help guard trims, so it agrees with the renderer about what counts as empty rather than letting whitespace through into an empty "Node help" section. And collecting zero docs routes now fails the build, like the identical check for the integrations, instead of logging a zero nobody reads. The guides' collision check runs after the core-node pages are written, so a guide that would take a generated page's URL hits the check's own error rather than @nuxt/content's primary-key failure. .claude/CLAUDE.md said the product docs were the only source of /docs and must not be edited here, which is now wrong for two of the three sources.
|
Heads-up before you look again: the deploy preview on this PR is stale and will stay stale. This PR is now stacked (it targets Merge order is #5745, then this, then #5752. The preview comes back on its own once #5745 merges, because GitHub retargets this PR to Until then, the honest status of the fixes is: green CI, and asserted by unit tests over the real content tree, but not visually confirmed on a rendered page. If you would rather have the preview back than the smaller diff, say so and I will point this PR at |
|
Since this PR's deploy preview is not being built while it is stacked, here are the three fixes rendered from the branch itself, in a dev container running the same Nuxt app. The duplicated "Getting Started" is gone. Editor, Node-RED Library and Programming now carry their own nav labels, which is what their Eleventy nav entries had before the migration flattened them into their parent's. The MQTT Out page has one H1. The use-case include opened with The CTA renders as markup again. Heading, paragraphs and links, rather than the literal Two things these also confirm, which I could not check on the stale preview: the browser titles are back ( |
The 38 core-node pages were assembled during the build by core-nodes-sync.mjs out
of three things that were not content: a JSON catalogue, 41 markdown fragments in an
Eleventy includes directory, and an HTTP fetch of the Node-RED project's help. Almost
every defect this branch has fixed came from that machinery rather than from the
move: an H1 the generator added on top of one the fragment already had, mustaches
code-spanned inside a code sample because a fence was indented, asset URLs that
resolved against whatever depth the generated page happened to sit at, a category and
a node contending for one URL, hand-computed metaTitle and navOrder, and a catalogue
entry that pointed a page at the wrong node's help for years without erroring.
Each page is now an ordinary file under nuxt/content-guides/, carrying its own
frontmatter and its own prose, ending with the node it wants help for:
::node-red-help{category="network" file="10-mqtt" node="mqtt out" name="MQTT Out"}
The fetch is unchanged in substance: server/api/node-red-help.get.ts asks GitHub for
the locale file through the existing cachedFetch, so it runs during prerender and the
help is part of the static HTML rather than something a reader or a crawler waits for.
Responses are still cached per locale file, since one file serves several nodes. The
help is third-party HTML, so it is sanitised against an allowlist before it reaches
v-html, which the generated markdown never did.
Deleted: core-nodes-sync.mjs and library-markdown.mjs with their tests, the catalogue,
and the Eleventy global data nothing had read since the templates went. The selection
rules were worth keeping and move to node-red-help.mjs, which is the whole of what
remains: exact name wins, otherwise every name with that prefix joined in order.
It also adds the reverse of that lookup. unclaimedHelpNames reports help a locale file
offers that nothing asked for, which is the only check that catches being wrong rather
than stale: a request for the wrong block succeeds, so nothing errors. It found three
already. http-response, tcp-out and mqtt-broker have prose written for them and real
help upstream, and have never been published, one of them because its filename was
typed tpc-out. Those three fragments stay put rather than being deleted with the
others; publishing them is a content decision, not part of this move.
|
The core node pages were written by a build script ( Most of the defects found on this PR were in that script rather than in the move, so |
…found Five of the thirty-eight pages were shipping with no help at all. isSafeHelpRef guarded the locale file name with a lower-case-only pattern, and five of the parser files are upper case upstream: 70-CSV, 70-HTML, 70-JSON, 70-XML, 70-YAML. The route answered 400 before it ever fetched, the component fell back to its "could not be read" notice, and the page prerendered green, so the CSV, HTML, JSON, XML and YAML pages each carried that notice where their built-in help belongs. Nitro's prerender has no failOnError, so nothing was ever going to catch this at build time. The guard still allows a single path segment and nothing else. The sanitiser allowlist was dropping content, not just styling. It was written from guesswork; it is now the enumerated set of every tag and attribute upstream actually uses inside a help block, across all 36 locale files. What had been going missing: `class`, which is the only thing distinguishing an optional message property from a required one (`<dt class="optional">`) and a type from prose (`<span class="property-type">`); `target` on all seven of upstream's outbound links, now with a `rel` the transform adds since upstream sets none; `center` in the Range node's table cells. `style` stays out: upstream's one use of it is malformed and no text depends on it. sanitize-html keeps a dropped tag's text, which is why an over-tight allowlist reads as a styling glitch rather than an error. Three guides lost their CTA tracking. They carried a hand-written `onclick="capture(...)"` as Eleventy pages and MDC strips it, so the event stopped firing the moment they became content files. They use the existing cta-image component now, with a `reference` prop that reproduces the payload they were already reporting rather than introducing a new one. The checks that would have caught these run offline. node-red-help.test.mjs validates every ::node-red-help reference in the tree against the route's own guard, and asserts what the allowlist keeps and drops. mdc-bindings.mjs replaces a regex that could not see a multi-line or triple mustache: it parses the real tree through MDC and walks the AST for binding nodes, over the guides, the handbook and the flowfuse docs. All three are clean. Verified live over HTTP across all 38 pages: no error notices, no page missing its help, the property-type class on 23 and target preserved on 7.
`::component{...}` opens a container and the closing `::` is not optional. The three
guides whose inline-image CTA moved from a raw `<a onclick=...>` to `::cta-image{...}`
were missing it, so the container swallowed the rest of each file and every heading
after the CTA stopped being a heading. The component still rendered and the build
still succeeded; the only symptom was the pages' own tables of contents pointing at
anchors that no longer existed, which is what the link checker reported.
Every other `::cta-image` in the repo closes on the next line. These three now do too.
unclosedMdcBlocks reports an opener with no closer, and runs over the guides. Adding
the check first and reintroducing the defect confirms it fails rather than passing on
a tree that happens to be clean.







Description
Moves the Node-RED library from
/node-red/**into/docs/, off Eleventy. Same mechanism as #5745.Stacked on #5745 (needs
guides-sync.mjs).nuxt/content-guides/, Nunjucks → MDC/node-red/flowfuse/**→/docs/flowfuse-nodes/(our node reference, under FlowFuse User Manuals)/node-red/itself. It's a marketing page, not docs/node-red/learn/("100+ Tutorials (2026)") → becomes the group indexWorth a look
library-markdown.mjs— reusesdocs-markdown's callout/HTML repair. Two orderings are pinned by tests because both fail silently:{% renderFlow %}must convert before leftover Nunjucks is stripped (77 diagrams), and Node-RED's{{ msg.payload }}must be code-spanned so MDC doesn't bind it.core-nodes-sync.mjs— the 39 core-node pages were never files. Still fetched from the Node-RED repo per build, but a missing help name now fails the build instead of rendering an empty section. 5xx is retried, 404 isn't.coreNodes.jsonasked formqtt-in/mqtt-out(upstream saysmqtt in/mqtt out) andupdfor both UDP nodes, a typo./node-red/.Not in scope
Whether any of these pages should be removed rather than moved is a separate decision.
Related Issue(s)
None.
Checklist