Skip to content

Make the Node-RED library part of the documentation, and convert it to Nuxt - #5750

Merged
KristopherLeads merged 18 commits into
mainfrom
docs/node-red-library-into-docs
Sep 8, 2026
Merged

KristopherLeads merged 18 commits into
mainfrom
docs/node-red-library-into-docs

Conversation

@dimitrieh

@dimitrieh dimitrieh commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Description

Moves the Node-RED library from /node-red/** into /docs/, off Eleventy. Same mechanism as #5745.

Stacked on #5745 (needs guides-sync.mjs).

Moves 81 pages + 433 assets → nuxt/content-guides/, Nunjucks → MDC
Also moves /node-red/flowfuse/** → /docs/flowfuse-nodes/ (our node reference, under FlowFuse User Manuals)
Stays put /node-red/ itself. It's a marketing page, not docs
Retires /node-red/learn/ ("100+ Tutorials (2026)") → becomes the group index
New sidebar group "Node-RED", ranked last, after Contributing
Removes 9 header nav slots + their footer mirrors

Worth a look

  • library-markdown.mjs — reuses docs-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.
  • Fixed 4 pages that have been shipping empty help. coreNodes.json asked for mqtt-in/mqtt-out (upstream says mqtt in/mqtt out) and upd for both UDP nodes, a typo.
  • Redirects are explicit paths, not a splat — a splat would swallow /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

  • I have read the contribution guidelines
  • I have considered the performance impact of these changes
  • Suitable unit/system level tests have been added and they pass
  • Documentation has been updated
  • For blog PRs, an Art Request has been created (instructions)

@netlify

netlify Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for flowforge-website ready!

Name Link
🔨 Latest commit 5fc9bbf
🔍 Latest deploy log https://app.netlify.com/projects/flowforge-website/deploys/6aa02321c1cda20008faaddb
😎 Deploy Preview https://deploy-preview-5750--flowforge-website.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 62 (🟢 up 7 from production)
Accessibility: 96 (no change from production)
Best Practices: 100 (no change from production)
SEO: 92 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@dimitrieh
dimitrieh marked this pull request as ready for review September 7, 2026 15:49
@dimitrieh dimitrieh changed the title Make the Node-RED library part of the documentation Make the Node-RED library part of the documentation, and convert it to Nuxt Sep 7, 2026
@dimitrieh
dimitrieh force-pushed the docs/node-red-library-into-docs branch from b451b5a to 63d57a2 Compare September 7, 2026 16:10
@dimitrieh

dimitrieh commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Code review

Found 4 issues:

  1. protectMustaches splits an existing code span in two and leaves the mustache exposed, which is the exact failure it exists to prevent. The backtick check only looks at the character immediately either side of the match, so a mustache in the middle of a longer span fails it and gets re-wrapped. Verified against real content: src/_includes/core-nodes/template-use-case.md goes from {% raw %}\

    Hello {{payload.name}}!

    `{% endraw %}to`

    Hello `{{payload.name}}`!

    `, so {{payload.name}}reaches MDC as bare text.http-request-use-case.mdcascades worse and also wraps two runs of ordinary prose as bogus inline code. Both feed the generated Template and HTTP request pages viacore-nodes-sync.mjs`.

return segment
.replace(/\{%-?\s*(?:end)?raw\s*-?%\}\r?\n?/g, '')
.replace(/(`?)(\{\{[^{}\n]*\}\})(`?)/g, (whole, before, mustache, after) =>
(before && after) ? whole : `\`${mustache}\``)
}).join('')
}

  1. The Function node's page and the Function category index resolve to the same URL. slugFor('Function') is function, which is also its category directory, so the generator writes both core-nodes/function.md (the node's mirrored help, ~13KB) and core-nodes/function/index.md (the category listing, ~650B), and both become /docs/node-red/core-nodes/function/. Whichever @nuxt/content resolves last wins. The legacy redirect for that path pointed at the node page, so a visitor following it may land on the category list instead. No build error, so CI is green on it.

? processLibraryMarkdown(readFileSync(useCasePath, 'utf8'), { title: node.name })
: ''
const dest = join(outDir, `${node.slug}.md`)
mkdirSync(dirname(dest), { recursive: true })
writeFileSync(dest, renderCoreNodePage(node, {

  1. {% caution %} callouts are silently downgraded to plain paragraphs. convertCallouts in docs-markdown.mjs handles only note, warning and critical, then strips every remaining Nunjucks tag, but .eleventy.js defines caution as a first-class callout with its own icon. The OPC UA guide used it for "Server hosting is not supported on FlowFuse Cloud", which now renders with no visual distinction from body text.

out = joinHtmlBlocks(out)
// Last: this strips every Nunjucks tag still standing.
return convertCallouts(out)
}

  1. Two redirects still point at moved paths, so they now double-hop. Their targets became redirect sources in the new map: /node-red/core-nodes/mqtt/ → /node-red/core-nodes/mqtt-in/ → /docs/node-red/core-nodes/mqtt-in/, and /education/ → /node-red/learn/ → /docs/node-red/.

'/blog/2025/10/the-ai-orchestation-hype/': { redirect: { to: '/blog/2025/10/the-ai-orchestration-hype/', statusCode: 301 } },
'/node-red/core-nodes/mqtt/': { redirect: { to: '/node-red/core-nodes/mqtt-in/', statusCode: 301 } },
'/blueprints/manufacturing/manufacturing-support-request/': { redirect: { to: '/blueprints/manufacturing/andon-system/', statusCode: 301 } },

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.

@dimitrieh

Copy link
Copy Markdown
Contributor Author

All four resolved in 02d2826.

  1. protectMustaches now splits on fences and code spans and only rewrites what falls between them, instead of checking adjacent backticks. Two committed pages carried the damage and are regenerated.
  2. Per-category pages are gone, so nothing shadows the Function node's URL; their grouping is now headings on the section index. The five bare category paths come out of the redirect map too, since those were never pages under Eleventy either.
  3. {% caution %} is converted locally, in the same markup shape, before the tag-stripping pass.
  4. /node-red/core-nodes/mqtt/ and /education/ go straight to /docs/.

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.

@dimitrieh

Copy link
Copy Markdown
Contributor Author

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.

/node-red/learn/ /docs/node-red/
/node-red/protocol/modbus/ /docs/node-red/protocol/modbus/
/node-red/core-nodes/function/ /docs/node-red/core-nodes/function/
/node-red/flowfuse/mcp/ /docs/flowfuse-nodes/mcp/
/node-red/ 200, unmoved: it is the marketing page

Two notes:

  • The five bare category paths (/node-red/core-nodes/common/ and siblings) 404 rather than redirect. That is intended: they were never pages, since the Eleventy templates were pagination-only.
  • These are Nitro route rules, so they are served by the app rather than at the edge. The existing TODO in nuxt/redirects.ts about moving redirects to netlify.toml still stands and predates this PR.

@dimitrieh

Copy link
Copy Markdown
Contributor Author

Follow-up opened: #5752, which answers the sync-script objection from #5495 (#5495 (comment)) for the part where it actually applies.

It gives the docs collection a second source pointing straight at nuxt/content-guides/, so the guides are read rather than copied in. guides-sync.mjs shrinks to copying non-markdown assets plus a collision check, and the editUrl/updated stamping moves into content:file:beforeParse. Small change: 7 code files, plus 27 README.md to index.md renames that @nuxt/content requires.

The other two generators stay, for reasons rather than convenience:

  • docs-sync keeps its own clone. @nuxt/content's native git source is shallow, which would flatten every page's updated date, which is already the documented reason that clone is not shallow.
  • core-nodes-sync cannot be a collection source at all: its input is an HTTP fetch, not files on disk.

Draft, and stacked on this PR. Merge order is #5745, then this, then #5752.

@dimitrieh

Copy link
Copy Markdown
Contributor Author

FYI: this PR stackes on #5745 which when merged, will reduce file count in this PR.

@sumitshinde-84

sumitshinde-84 commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

Hey @dimitrieh, this looks good. There are two things I noticed, though:

  • The Getting Started nav item is duplicated several times in the navbar.
gettting-started-duplicated
  • The page this PR removes was getting traffic. I'm not against removing it, but I think it's worth making sure the new "Using Node-RED" page is also optimized for the old keyword so we don't lose that traffic. I'll add a suggestion for this in the PR.

Also, one question: apart from node-red/learn, is there any other page being removed by this PR? And have any titles or meta titles been changed?

Also, I find this PR really big. Would it be possible to split it into smaller chunks? If not, that's fine.

Comment thread nuxt/content-guides/node-red/README.md
@dimitrieh
dimitrieh changed the base branch from main to docs/application-guide-into-docs September 8, 2026 08:46
…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 }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@dimitrieh
dimitrieh force-pushed the docs/application-guide-into-docs branch from 820d71c to d1a2528 Compare September 8, 2026 09:03
@dimitrieh
dimitrieh force-pushed the docs/node-red-library-into-docs branch from 02d2826 to 35d1a80 Compare September 8, 2026 09:04
@sumitshinde-84

Copy link
Copy Markdown
Contributor

@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.

@sumitshinde-84

Copy link
Copy Markdown
Contributor

@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.
mqtt-core-node-title-duplication

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.
@dimitrieh

Copy link
Copy Markdown
Contributor Author

All four fixed, in 35d1a80 and 88b3b98.

  • Duplicated "Getting Started": every nested directory index took its parent's eleventyNavigation key instead of its own. Editor, Node-RED Library and Programming are back.
  • Core node categories: back as a directory each, so the sidebar nests the way the editor's palette does. That also resolves the URL collision which removed them last round, the other way round: the category owns core-nodes/function/ and the node is core-nodes/function/function/. The old flat URLs still reach a node in one hop.
  • Titles: yes, and on every moved page, not only /node-red/learn/. Eleventy titled a page metaTitle || navTitle || meta.title; the migration dropped metaTitle, and the docs page titles navTitle || title, so each title collapsed to its sidebar label. "Using MySQL with Node-RED (2026 Updated)" became "MySQL", "Node-RED - Inject Node" became "Inject". Every page now carries its Eleventy title in metaTitle, which the docs page prefers. Product docs titles are untouched, nothing from FlowFuse/flowfuse sets the field.
  • Other pages removed: only /node-red/learn/. /node-red/ itself stays put, it is the marketing page.

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.
@dimitrieh

Copy link
Copy Markdown
Contributor Author

Fixed in 11cde53. Both includes open with # {{ meta.title }}, and the page is assembled with its own # <node name>, so resolving that interpolation put two identical headings next to each other.

Eleventy had the same duplicate, but empty (meta.title was undefined on a paginated core-node page, so it rendered as a bare <h1 tabindex="-1">), which is why it went unnoticed. A heading that just repeats the node name is now dropped, and any other H1 in an include becomes an H2. Three other includes carried a second H1 and fold the same way. Checked over every committed include, not a fixture.

…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.
@sumitshinde-84

sumitshinde-84 commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor

@dimitrieh cta looks weird

cta-node-red

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.
@dimitrieh

Copy link
Copy Markdown
Contributor Author

Good catch, fixed in e583b3d. That CTA is a hand-written wrapper <div>, and its contents only parse as markdown because of the blank lines around them. One of the migration transforms joins adjacent raw-HTML blocks by removing blank lines, which turned the whole thing into a single HTML block and made the markdown inside render as literal text. Blank lines restored, and I scanned every migrated page for markdown wedged against an HTML tag: this was the only one.

While in there, a review pass turned up five more of the same shape, all of which built green:

  • The link and websocket pages were showing a fraction of their help. Those catalogue entries name a whole node family, and Eleventy joined every matching help block where the new code took only the first, while still captioning it as the node's complete built-in help.
  • /docs/node-red/core-nodes/read-file/ has been showing the Watch node's help, in production too. The catalogue pointed it at the wrong file.
  • The Template node's page had backticks injected into the code sample that teaches Mustache syntax, because the fence was indented under a list item and the transform only recognised fences at column 0.
  • The Function category page and the Function node inside it both read "Function" in the sidebar, one directly above the other.
  • Two guides linked the Function node at what is now the category's URL.

…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.
@dimitrieh

Copy link
Copy Markdown
Contributor Author

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 docs/application-guide-into-docs rather than main, so its diff is its own 8 commits instead of the whole move). Netlify only builds previews for PRs targeting main, so it has stopped building this one. The link at the top still resolves, but it is serving 02d2826, from before any of today's fixes: the duplicated "Getting Started", the raw-markdown CTA and the flat core-nodes list are all still in it. Anything you screenshot from there now is already fixed.

Merge order is #5745, then this, then #5752. The preview comes back on its own once #5745 merges, because GitHub retargets this PR to main at that point.

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 main again.

@dimitrieh

Copy link
Copy Markdown
Contributor Author

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.

Docs sidebar with the Getting Started subtree correctly labelled

The MQTT Out page has one H1. The use-case include opened with # {{ meta.title }}, which resolved to the node's own name next to the heading the generator adds. Note the breadcrumb too: the palette categories are back, so this page sits under Core Nodes / Network Nodes.

MQTT Out docs page with a single H1 heading

The CTA renders as markup again. Heading, paragraphs and links, rather than the literal ### and [text](url) you screenshotted.

Firebase page CTA rendering as a heading, paragraphs and links

Two things these also confirm, which I could not check on the stale preview: the browser titles are back (Node-RED - MQTT Out Node and Using Firebase with Node-RED (2026 Updated) respectively, rather than MQTT Out and Firebase), and the nested core-node URLs resolve.

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.
@dimitrieh

dimitrieh commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor Author

The core node pages were written by a build script (core-nodes-sync.mjs) out of three things that were not content: a JSON catalogue, 41 markdown fragments in an Eleventy includes folder, and a fetch of Node-RED's help. They are now ordinary content files, one per node, each carrying its own frontmatter and prose and ending with ::node-red-help{category="network" file="10-mqtt" node="mqtt out"}. Same URLs, same titles, same help: the fetch still runs during the build, so the help is in the static HTML, and it is sanitised before rendering now.

Most of the defects found on this PR were in that script rather than in the move, so core-nodes-sync.mjs and library-markdown.mjs are both deleted and the change is net smaller than what it replaces. A new check reports help that Node-RED offers but nothing asks for, and it found three: http-response, tcp-out and mqtt-broker have prose written and real help upstream but have never been published, one because its file was named tpc-out. Those three fragments stay in place, since publishing them is a content decision rather than part of this move.

…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.
Base automatically changed from docs/application-guide-into-docs to main September 8, 2026 15:00
@KristopherLeads
KristopherLeads merged commit d4184fb into main Sep 8, 2026
8 checks passed
@KristopherLeads
KristopherLeads deleted the docs/node-red-library-into-docs branch September 8, 2026 15:22

This branch was successfully deployed

1 active deployment
Preview — 5fc9bbf4 Deployed Sep 8, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants