diff --git a/nuxt/components/DeviceAgentInstallCommands.vue b/nuxt/components/DeviceAgentInstallCommands.vue new file mode 100644 index 0000000000..88c14f517a --- /dev/null +++ b/nuxt/components/DeviceAgentInstallCommands.vue @@ -0,0 +1,36 @@ + + + diff --git a/nuxt/lib/certified-nodes.mjs b/nuxt/lib/certified-nodes.mjs new file mode 100644 index 0000000000..921132dc72 --- /dev/null +++ b/nuxt/lib/certified-nodes.mjs @@ -0,0 +1,51 @@ +// Turns the two FlowFuse Certified catalogue feeds into the cards /node-red/ renders. +// Kept free of Nuxt imports so it can be unit tested with `node --test`; +// server/api/certified-nodes.get.ts is the only caller and does the fetching. + +// The catalogues carry no display `name`, and the ids are lowercase slugs, so +// title-casing alone gives "Opcua" and "Rtsp". Acronyms cannot be inferred. +const NAME_OVERRIDES = { + '@flowfuse-certified-nodes/opcua': 'OPC UA', + '@flowfuse-certified-nodes/rtsp': 'RTSP', + '@flowfuse-certified-nodes/cip-suite': 'CIP Suite', +} + +// Last resort when there is no override: "@flowfuse-certified-nodes/google-sheets" +// becomes "Google Sheets", which reads correctly for names that are ordinary words +// ("Redis", "Modbus", "Kafka"). +function displayNameFromId (id) { + return (id.split('/').pop() ?? id) + .split('-') + .map(word => word.replace(/^\w/, char => char.toUpperCase())) + .join(' ') +} + +// Not entry.name: normalizeCatalogueModule falls back to the id's last segment when the +// feed carries no name, which every one of these does, so trusting it would print +// "modbus" and "rtsp" rather than "Modbus" and "RTSP". +export function certifiedNodeName (id) { + return NAME_OVERRIDES[id] ?? displayNameFromId(id) +} + +// One card per (node, collection) pair, because the badge names the collection and a node +// can be certified in both. That is what the .njk rendered. `feeds` is one entry list per +// catalogue, as fetchCertifiedCatalogues() returns them. +export function certifiedNodeCards (feeds) { + const cards = feeds.flat().flatMap(entry => (entry.collections ?? []).map(collection => ({ + id: entry._id, + name: certifiedNodeName(entry._id), + description: entry.description, + version: entry.version, + collection, + // docsUrl is already normalised (flowfuse.com paths only, trailing slash added, + // per-node overrides applied); the collection index is the fallback, as it was in + // the .njk. Some publisher URLs still point at /node-red/flowfuse/… paths that now + // 301 into /docs/ - that is true of production too, and it is the catalogue + // entries that need updating, not this. + docsPath: entry.docsUrl ?? `/docs/flowfuse-nodes/${collection}/`, + }))) + + // Stable, so a node certified in both collections keeps feed order: Hub, then Edge. + cards.sort((a, b) => a.name.localeCompare(b.name)) + return cards +} diff --git a/nuxt/lib/certified-nodes.test.mjs b/nuxt/lib/certified-nodes.test.mjs new file mode 100644 index 0000000000..936144774b --- /dev/null +++ b/nuxt/lib/certified-nodes.test.mjs @@ -0,0 +1,72 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' + +import { certifiedNodeCards, certifiedNodeName } from './certified-nodes.mjs' + +const SCOPE = '@flowfuse-certified-nodes' + +function entry (slug, collections, extra = {}) { + return { + _id: `${SCOPE}/${slug}`, + name: slug, + description: `${slug} description`, + version: '1.0.0', + collections, + ...extra, + } +} + +test('acronyms come from the override map', () => { + assert.equal(certifiedNodeName(`${SCOPE}/opcua`), 'OPC UA') + assert.equal(certifiedNodeName(`${SCOPE}/rtsp`), 'RTSP') + assert.equal(certifiedNodeName(`${SCOPE}/cip-suite`), 'CIP Suite') +}) + +test('other slugs are title-cased word by word', () => { + assert.equal(certifiedNodeName(`${SCOPE}/modbus`), 'Modbus') + assert.equal(certifiedNodeName(`${SCOPE}/google-sheets`), 'Google Sheets') + assert.equal(certifiedNodeName('redis'), 'Redis') +}) + +test('the catalogue name is ignored, since it is only the lowercase slug', () => { + const [card] = certifiedNodeCards([[entry('rtsp', ['edge'])]]) + assert.equal(card.name, 'RTSP') +}) + +test('a node certified in two collections gets one card per collection', () => { + const cards = certifiedNodeCards([[entry('modbus', ['hub', 'edge'])]]) + assert.deepEqual(cards.map(card => card.collection), ['hub', 'edge']) + assert.ok(cards.every(card => card.id === `${SCOPE}/modbus`)) +}) + +test('an entry with no collections gets no card', () => { + assert.deepEqual(certifiedNodeCards([[entry('kafka', undefined), entry('redis', [])]]), []) +}) + +test('docsPath keeps the normalised docsUrl and falls back to the collection index', () => { + const [withDocs, withoutDocs] = certifiedNodeCards([[ + entry('kafka', ['hub'], { docsUrl: '/docs/flowfuse-nodes/hub/kafka/' }), + entry('modbus', ['edge']), + ]]) + assert.equal(withDocs.docsPath, '/docs/flowfuse-nodes/hub/kafka/') + assert.equal(withoutDocs.docsPath, '/docs/flowfuse-nodes/edge/') +}) + +test('cards from both feeds are sorted together by display name', () => { + const hub = [entry('redis', ['hub']), entry('opcua', ['hub'])] + const edge = [entry('modbus', ['edge']), entry('cip-suite', ['edge'])] + assert.deepEqual( + certifiedNodeCards([hub, edge]).map(card => card.name), + ['CIP Suite', 'Modbus', 'OPC UA', 'Redis'], + ) +}) + +test('the same node in both feeds stays in feed order after sorting', () => { + const cards = certifiedNodeCards([[entry('kafka', ['hub'])], [entry('kafka', ['edge'])]]) + assert.deepEqual(cards.map(card => card.collection), ['hub', 'edge']) +}) + +test('no feeds, or empty feeds after a fetch failure, give no cards', () => { + assert.deepEqual(certifiedNodeCards([]), []) + assert.deepEqual(certifiedNodeCards([[], []]), []) +}) diff --git a/nuxt/nuxt.config.ts b/nuxt/nuxt.config.ts index fa5c1e3a32..1bca58edd2 100644 --- a/nuxt/nuxt.config.ts +++ b/nuxt/nuxt.config.ts @@ -539,6 +539,8 @@ export default defineNuxtConfig({ ...collectSlugRoutes(join(__dirname, 'content/vs'), '/vs'), + '/node-red/', + '/about/', ...collectSlugRoutes(join(__dirname, 'content/landing'), '/landing'), diff --git a/nuxt/pages/node-red/index.vue b/nuxt/pages/node-red/index.vue new file mode 100644 index 0000000000..03fd1b4f13 --- /dev/null +++ b/nuxt/pages/node-red/index.vue @@ -0,0 +1,349 @@ + + + diff --git a/nuxt/public/images/learning_nr.png b/nuxt/public/images/learning_nr.png new file mode 100644 index 0000000000..226bb519a5 Binary files /dev/null and b/nuxt/public/images/learning_nr.png differ diff --git a/nuxt/public/images/pseudo-editor.png b/nuxt/public/images/pseudo-editor.png new file mode 100644 index 0000000000..a37ea7df27 Binary files /dev/null and b/nuxt/public/images/pseudo-editor.png differ diff --git a/nuxt/server/api/certified-nodes.get.ts b/nuxt/server/api/certified-nodes.get.ts new file mode 100644 index 0000000000..402099c8d8 --- /dev/null +++ b/nuxt/server/api/certified-nodes.get.ts @@ -0,0 +1,34 @@ +// The FlowFuse Certified Nodes list, in the shape /node-red/ renders it. +// +// This replaces src/_data/certifiedNodes.js, whose own comment said it "deliberately +// duplicates the fetch rather than importing across the boundary" while the site was +// mid-migration. That boundary is gone: fetchCatalogue() already reads the same two +// certified feeds (CERTIFIED_HUB_API and CERTIFIED_EDGE_API), so this derives the display +// shape from it instead of fetching the feeds a second time. The derivation itself (names, +// one card per collection, docs fallback, order) is in lib/certified-nodes.mjs, where +// `npm test` covers it. +// +// A feed being unreachable returns an empty list rather than failing: fetchCatalogueFeed +// already swallows per-feed errors, and the page skips its whole section when the list is +// empty, as the .njk's `{% if certifiedNodes.all.length %}` did. +import { certifiedNodeCards } from '../../lib/certified-nodes.mjs' +import { fetchCertifiedCatalogues } from '../../utils/integrations' +import { COLLECTION_LABELS, type CertifiedCollection } from '../../types/integrations' + +export interface CertifiedNode { + id: string + name: string + description: string + version: string + collection: CertifiedCollection + collectionLabel: string + docsPath: string +} + +export default defineEventHandler(async (): Promise => { + const feeds = await fetchCertifiedCatalogues() + return certifiedNodeCards(feeds).map((card: Omit) => ({ + ...card, + collectionLabel: COLLECTION_LABELS[card.collection], + })) +}) diff --git a/nuxt/server/middleware/legacy.ts b/nuxt/server/middleware/legacy.ts index 902fae8b3e..dc8ccb13e3 100644 --- a/nuxt/server/middleware/legacy.ts +++ b/nuxt/server/middleware/legacy.ts @@ -15,11 +15,11 @@ const NUXT_ROUTE_PREFIXES = ['/integrations/', '/raw/'] // left once its one referring blog post pointed at /contact-us/ instead) - but they stay // listed so their 301s in nuxt/redirects.ts are served by Nitro in dev rather than being // proxied to 11ty, which has nothing there either. -const NUXT_PREFIXES = ['/handbook', '/ebooks', '/whitepaper', '/pricing', '/docs', '/changelog', '/application-guide', '/blog', '/product', '/customer-stories', '/thank-you', '/resources', '/webinars', '/free-consultation', '/vs', '/landing', '/use-cases', '/partners', '/industries', '/blueprints', '/events', '/platform'] +const NUXT_PREFIXES = ['/handbook', '/ebooks', '/whitepaper', '/pricing', '/docs', '/changelog', '/application-guide', '/blog', '/product', '/customer-stories', '/thank-you', '/resources', '/webinars', '/free-consultation', '/vs', '/landing', '/use-cases', '/partners', '/industries', '/blueprints', '/events', '/platform', '/node-red'] // Top-level routes still on 11ty, not yet ported to Nuxt (everything not listed above // already falls through to the 11ty proxy by default). Remove entries here as they migrate: -// /careers, /community, /events, /free-consultation, /node-red, /platform, /webinars +// /careers, /community, /events, /free-consultation, /platform, /webinars // New pages should never grow that fallback set: nuxt/lib/legacy-pages.test.mjs fails // `npm test` if a PR adds a new .njk file under src/ that doesn't already exist on main, @@ -54,10 +54,8 @@ export default defineEventHandler(async (event) => { // The documentation below /node-red/ moved into /docs/, and every old URL now 301s // from nuxt/redirects-node-red.ts. Those are Nitro route rules, so the request has to // reach Nitro in dev rather than being proxied to 11ty, which no longer has the pages. - // /node-red itself is deliberately excluded and keeps falling through to 11ty: it is - // still a marketing page there, which is why the redirect map is explicit paths rather - // than a splat. - if (normalised !== '/node-red' && normalised.startsWith('/node-red/')) return + // /node-red itself is a Nuxt page now, so the whole prefix is listed above and the + // carve-out that used to keep just that path on 11ty is gone. // src/vs/images/** are still 11ty-owned files: they only reach nuxt/public/ through the // passthrough in a production build, so dev has to ask 11ty for them even though /vs is diff --git a/nuxt/utils/integrations.ts b/nuxt/utils/integrations.ts index fe3abfdc1a..7eb9f11a3c 100644 --- a/nuxt/utils/integrations.ts +++ b/nuxt/utils/integrations.ts @@ -71,11 +71,30 @@ async function fetchCatalogueFeed ( try { const data = await ofetch(url) return (data.modules ?? []).map(m => normalizeCatalogueModule(m, tier, collection)) - } catch { + } catch (error) { + // An empty list degrades to a hidden section rather than a broken page, which is + // what the .njk did too. /node-red/ is prerendered now, so that hidden section + // stays hidden until the next deploy - say which feed failed in the build log + // rather than leave a missing page section as the only evidence. + console.warn(`[integrations] catalogue feed ${url} is unavailable:`, error instanceof Error ? error.message : error) return [] } } +/* + Just the two FlowFuse Certified catalogues, each entry tagged with the one collection + it came from. fetchCatalogue() below merges these with the npm catalogue and the + flowfuse-nodes feed, and tags that last group `certified` too - so filtering its + result by tier returns more than these two feeds hold. /node-red/ lists exactly these, + which is what src/_data/certifiedNodes.js fetched before it was retired. +*/ +export function fetchCertifiedCatalogues (): Promise { + return Promise.all([ + fetchCatalogueFeed(CERTIFIED_HUB_API, 'certified', 'hub'), + fetchCatalogueFeed(CERTIFIED_EDGE_API, 'certified', 'edge') + ]) +} + export async function fetchCatalogue (): Promise { const [api, hub, edge, flowfuseNodes] = await Promise.all([ ofetch(INTEGRATIONS_API).catch(() => ({ catalogue: [] as IntegrationCatalogEntry[] })), diff --git a/src/_includes/components/device-agent-install-commands.njk b/src/_includes/components/device-agent-install-commands.njk deleted file mode 100644 index 59dd0fcfbe..0000000000 --- a/src/_includes/components/device-agent-install-commands.njk +++ /dev/null @@ -1,68 +0,0 @@ - -
-

{{ installCommandsHeading | default("Install on edge, straight from your terminal") }}

-

Linux and macOS

-
- /bin/bash -c "$(curl -fsSL https://flowfuse.github.io/device-agent/get.sh)" && ./flowfuse-device-agent-installer - -
-

Windows (run elevated)

-
- Set-Location $env:USERPROFILE; powershell -c "irm https://flowfuse.github.io/device-agent/get.ps1 | iex"; .\flowfuse-device-agent-installer.exe - -
-

- Installing with npm or Docker instead? The - installation docs cover every route. -

-
- - diff --git a/src/node-red/index.njk b/src/node-red/index.njk deleted file mode 100644 index 1383293854..0000000000 --- a/src/node-red/index.njk +++ /dev/null @@ -1,385 +0,0 @@ ---- -layout: page -nohero: true -meta: - title: "What is Node-RED?" - keywords: what is node-red, what is Node-RED, how does node-red works, low-code programming, visual programming, flow-based programming, node-red enterprise, node-red for production - faq: - - question: "What is Node-RED and how does it work?" - answer: > - Node-RED is a low-code programming environment that allows developers to connect a wide variety of endpoints, such as industrial PLCs, APIs, databases, enterprise applications and online services. Users of Node-RED visually drag ‘nodes’ from a palette that represent the end points and then connect them together into flows to accomplish the desired task. Node-RED applications are accessible via a web browser. - - question: "Who uses Node-RED?" - answer: > - Node-RED is popular among IoT and individuals creating DIY home automation systems. However, Node-RED is mostly being used in professional use cases in a variety of industries. Apart from software engineers, Node-RED’s graphical approach to unlocking and effectively using data has empowered industrial engineers or non developers to apply their domain knowledge easily and accelerate projects. - - question: "Which industrial sectors use Node-RED in production environments?" - answer: > - Yes, Node-RED is widely used in real-world production environments across industries like manufacturing, energy, smart buildings, and more. While Node-RED provides a powerful low-code runtime, FlowFuse makes it easier and safer to use in professional settings. Many companies are already using FlowFuse to run Node-RED in production. FlowFuse adds everything needed to manage and scale Node-RED reliably, including user access control, version control for flows, automated deployments, remote device management, and secure team collaboration. This gives organizations the confidence to build and maintain robust industrial applications using Node-RED. - - question: "How do I deploy Node-RED online or in the Cloud?" - answer: > - FlowFuse Cloud is a hosted Node-RED service that developers can use to access Node-RED online. This makes it much quicker to get started using Node-RED since you don't need to install it locally. - - question: "What is Node-RED used for?" - answer: > - Node-RED can be used for a wide variety of use cases, including: -
    -
  • The manufacturing industry uses Node-RED for collecting data from different pieces of factory equipment.
  • -
  • Creating dashboards that visualize data and allow for event triggers to occur based on the data.
  • -
  • Developing chatbot platforms that can collect interactions of a wide variety of social media platforms.
  • -
  • Extract, transform and integrate data from many different sources in the enterprise.
  • -
  • Lots of hobbyists use Node-RED for home automation.
  • -
  • Integrating data with machine learning models
  • -
- - question: "How much does Node-RED cost?" - answer: > - Node-RED is an open source project hosted at the OpenJS Foundation. It is made available under the Apache Software License so individuals can use Node-RED free of charge. Participation in the open source project is encouraged to help support the Node-RED community. - - question: "What language is Node-RED?" - answer: > - Node-RED is a visual programming environment that allows you to do a lot without writing a single line of code. An important benefit of Node-RED is that it also allows for developers to write JavaScript and use the NodeJS API to extend the functionality of a node or flow. - - question: "How can I get started using Node-RED?" - answer: > - There are three routes. FlowFuse Cloud needs no installation at all: sign up and you have a Node-RED editor straight away. To run Node-RED on your own machine, a single command installs Node-RED, sets it up as a service so it restarts on boot, and offers to import flows you already have on that machine. If you would rather install Node-RED by hand, the Getting Started guide on nodered.org covers every platform. - - question: "What port number does Node-RED use?" - answer: > - Node-RED is accessed via port 1880. You can access the Node-RED editor running locally at http://localhost:1880 or on another computer at http://domain name:1880. - - question: "How can I secure a Node-RED installation?" - answer: > - There are three aspects of Node-RED that need to be secured. -
    -
  1. Node-RED is a web application so you need to enable HTTPS access to the Node-RED runtime. The default Node-RED settings include a commented out section that can be used for local certificates.
  2. -
  3. The Node-RED editor and administration require authentication to access it. Node-RED supports username/password and OAuth/OpenID authentication. The adminAuth property in the setting file is used to specify the authentication credentials.
  4. -
  5. Securing the HTTP Node can be done using basic authentication. This can be set using the httpNodeAuth property in the settings file.
  6. -
- You can also follow our guide on how to secure Node-RED. - - question: "How is Node-RED used in IoT?" - answer: > - Node-RED is very popular in the manufacturing and industrial automation industries (IIoT) as an edge computing solution. Often deployed on PLCs and IoT gateways, Node-RED supports collecting data from legacy and proprietary systems (Siemens S7, Modbus, OPC-UA, etc), integrating the industrial data with other data sources (weather data, enterprise data), filtering data on the edge, and then sending the data to the cloud. FlowFuse offers remote device deployment to IIoT edge devices. Node-RED is also very popular with DIY home automation enthusiasts, in particular the Home Assistant community. -installCommandsHeading: "Or install Node-RED on your own machine" -description: Node-RED is the low-code programming language of choice for industrial applications. Industrial engineers use Node-RED to collect, transform, and integrate data through visualized dashboards. -sitemapPriority: 0.8 ---- -{% from "components/cta/cta-sign-up.njk" import ctaSignUp %} -{% from "components/cta/cta-book-demo.njk" import ctaBookDemo %} - -
- -
-
-
-

- What is Node-RED? -

-

-

- Co-created by Nick O'Leary, CTO of FlowFuse, Node-RED is the low-code programming language of choice for industrial applications. Industrial engineers use Node-RED to collect, transform, and integrate data through visualized dashboards. -

-

FlowFuse enhances Node-RED with enterprise features to accelerate digitalization and optimize industrial processes.

-

Ready to get started building with Node-RED? There are two ways in.

-
- {{ ctaSignUp(site, 'primary', 'nodered', extraClass='min-h-[40px] md:inline') }} -

- Nothing to install, you get an editor in the browser. Or - install Node-RED on your own machine - with one command. -

-
-
-
- {% image "../images/pseudo-editor.png", "Node-RED pseudo UI", [500] %} -
-
- {{ ctaSignUp(site, 'primary', 'nodered-mobile', extraClass='flex flex-col w-full') }} -

- Nothing to install, you get an editor in the browser. Or - install Node-RED on your own machine - with one command. -

-
-
-
- - -
-
- {% include "components/device-agent-install-commands.njk" %} -

- One command installs Node.js, Node-RED and the FlowFuse Device Agent, sets it up as a - service so it restarts on boot, and opens your browser to connect it. It creates a - FlowFuse account for you if you do not have one. If the machine already runs Node-RED, - it offers to import those flows. -

-
-
- - -
-
-
-

Harness the Power of Low-Code Integration

- - {% set sections = [ - { - "svgPath": "components/icons/light-bulb.svg", - "title": "Flow Based Programming", - "description": "Node-RED’s visual programming makes it easy for non-developers to build robust applications." - }, - { - "svgPath": "components/icons/puzzle-piece.svg", - "title": "Digital Services Integration", - "description": 'Node-RED facilitates seamless integration with a wide array of digital services, including protocols (OPC-UA, Modbus, Siemens S7), databases, APIs, and devices. Browse the full integrations catalog.' - }, - { - "svgPath": "components/icons/document-chart-bar.svg", - "title": "Data Visualization", - "description": 'Node-RED Dashboard makes it easy - to visualize your data in charts that integrate directly with actions and events that respond to - the data.' - } - ] %} - - {% for section in sections %} -
-
-
-
- {% include section.svgPath %} -
-
-
-
{{ section.title }}
-
-
-
-
-

{{ section.description | safe }}

-
-
-
- {% endfor %} - -
-
-
-
- - - - -
-
-

Why Choose FlowFuse for Node-RED

-
- {% set sections = [ - { - "iconPath": "components/icons/shield-check.svg", - "title": "Enhanced Security", - "description": "Security and compliance guardrails ensure adherence to security protocols." - }, - { - "iconPath": "components/icons/user-group.svg", - "title": "Seamless Collaboration", - "description": "Cloud-based solutions and dev-ops pipelines streamline development with user-friendly interfaces and safeguard against errors." - }, - { - "iconPath": "components/icons/arrows-pointing-out.svg", - "title": "Scalable Architecture ", - "description": "Start small and scale to thousands of self–built and self–maintained applications. Reduce reliance on point solutions and vendor lock-in." - }, - { - "iconPath": "components/icons/academic-cap.svg", - "title": "Expert Support", - "description": "Get access to our dedicated support team who are creators and experts in Node-RED, ready to assist you at every step." - } - ] %} - - {% for section in sections %} -
-
-
- {% include section.iconPath %} -
-

{{ section.title }}

-

{{ section.description }}

-
-
- {% endfor %} -
-
-
- - -{# Driven by the live catalogues via src/_data/certifiedNodes.js, so newly - certified nodes appear here without editing this page. The section is - skipped entirely if neither catalogue could be fetched at build time. - Positioned beside the "Why FlowFuse" block: both make the production/trust case. #} -{% if certifiedNodes.all.length %} -
-
- - {% include "components/icons/certified-node.svg" %} - FlowFuse Certified - -

- Nodes certified for production -

-

- The Node-RED library is enormous, and a README cannot tell you whether a package is still - maintained, whether it is secure, or whether anyone will answer when it breaks. FlowFuse - certifies a set of nodes against those questions: each one has a named owner accountable - for it, is vetted for reliability, security and current documentation, and is patched by - FlowFuse after release. -

- - -
-
-{% endif %} - - -
-
-

- Node-RED Learning Resources -

-
-
- {% image "../images/learning_nr.png", "Node-RED pseudo UI", [406] %} -
-
-

- Jumpstart your Node-RED journey with FlowFuse’s comprehensive learning resources, including tutorials, case studies, and community forums. Whether you’re new to Node-RED or looking to refine your skills, our resources are designed to accelerate your learning curve. -

-

- Node-RED Academy - | - Core Nodes - | - Variables - | - Quick Tips -

- SEE MORE - GET IMPLEMENTATION SUPPORT -
-
-
-
- - -
- -
-
-

Flexible Deployment with FlowFuse

-
-
-
- {% set cards = [ - { - "title": "FlowFuse Cloud", - "description": "Get started with Node-RED easily! No installation needed – just sign up for instant editor access.", - "buttonText": "MORE INFO", - "buttonLink": "/pricing/" - }, - { - "title": "On your own machine", - "description": "One command installs Node-RED, keeps it running as a service, and connects it to FlowFuse. It imports flows you already have.", - "buttonText": "INSTALL NODE-RED", - "buttonLink": "#install-node-red-from-your-terminal" - }, - { - "title": "Self managed", - "description": "Run FlowFuse where you prefer; that's possible too! In your cloud of preference, or even on-site.", - "buttonText": "TALK TO SALES", - "buttonLink": "/book-demo/" - } - ] %} - - {% for card in cards %} -
-
-
-

- {{ card.title }} -

-
-

- {{ card.description }} -

-
- {% if card.buttonLink == '/book-demo/' %} - {{ ctaBookDemo('primary-outlined', 'nodered-deployment', extraClass='md:self-end align-baseline w-full mt-3') }} - {% else %} - {{ card.buttonText }} - {% endif %} -
- {% endfor %} -
-
-
- -
-

- Frequently Asked Questions -

- {% include "faq.njk" %} -
-