diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md
index cdac3713c1..5851d1ca9b 100644
--- a/.claude/CLAUDE.md
+++ b/.claude/CLAUDE.md
@@ -268,9 +268,9 @@ This also covers old `/docs/**` paths left behind by a rename in `flowfuse/flowf
## Call-to-Action components
-**Nuxt only** — 11ty pages still use hand-written `` links; these components have no 11ty equivalent yet.
+Both frameworks have their own implementation of the same system — Vue components on Nuxt, Nunjucks macros on 11ty (see **11ty equivalent** below). The rendering/styling code itself is kept in sync by hand (Vue and Nunjucks share nothing at that level), but the *data* — event name, href, and fixed label per destination — lives in one file, `src/_data/ctaDestinations.json`, imported by both the Vue components and the Nunjucks macros (via a Nunjucks global, see below) so the two can't drift apart on copy or event names without both sides being touched. `site.json`'s `appURL` is combined with `ctaDestinations`' `hrefSuffix` for `signUp`/`signIn`, since the base URL differs per environment and doesn't belong duplicated a third time in `ctaDestinations.json`.
-There are exactly four CTA destinations, each with its own component with **fixed copy and href** (a PostHog audit found dozens of different button texts pointing at the same four URLs, which made it impossible to tell which copy converted best — see `/handbook/marketing/website#call-to-action-buttons` for the non-engineer-facing explanation and a live gallery of every variant):
+There are exactly five CTA destinations, each with its own component with **fixed copy and href** (a PostHog audit found dozens of different button texts pointing at the same handful of URLs, which made it impossible to tell which copy converted best — see `/handbook/marketing/website#call-to-action-buttons` for the non-engineer-facing explanation and a live gallery of every variant):
| Component | href | Fixed label |
|---|---|---|
@@ -278,8 +278,9 @@ There are exactly four CTA destinations, each with its own component with **fixe
| `nuxt/components/CtaSignIn.vue` | `site.appURL` | "Sign In" |
| `nuxt/components/CtaContactUs.vue` | `/contact-us/` | "Contact Us" |
| `nuxt/components/CtaBookDemo.vue` | `/book-demo/` | "Book a Demo" |
+| `nuxt/components/CtaPricing.vue` | `/pricing/` | "View Pricing" |
-All four are thin wrappers around `nuxt/components/cta/CtaButton.vue`, which does the actual styling/tracking and isn't meant to be used directly. If a page needs different wording, that's a sign a fifth destination-specific component is needed — not a prop that lets callers override copy on these four.
+All five are thin wrappers around `nuxt/components/cta/CtaButton.vue`, which does the actual styling/tracking and isn't meant to be used directly. If a page needs different wording, that's a sign a sixth destination-specific component is needed — not a prop that lets callers override copy on these five.
**Props** (all optional except `variant`/`position`): `variant` (`primary` | `primary-outlined` | `highlight` | `highlight-outlined` | `ghost` | `nav-text`), `position` (free string, sent to PostHog — describes where on the page, e.g. `hero`, `pricing-card`), `plan` (e.g. `edge`/`hub`/`fleet`, sent to PostHog), `color` (`primary`|`highlight`|`white`, only for `variant="ghost"`, which has no background of its own), `icon` (Nuxt Icon name for a trailing icon), `uppercase`, `padded` (only for `variant="nav-text"` — whether it has the header-` Bridge the gap between OT and IT teams using FlowFuse, the only comprehensive application platform with industrial AI and governance baked in. Your first operational application could be running this week. Request a demo to see how, or explore pricing to find the right fit. {{ cta.title }} {{ cta.description }} Explore our pricing Our managed hosted solution, maintained and supported by the FlowFuse team. Experience flexibility, scalability, and expert assistance to drive your business forward. Deploy FlowFuse on your hardware for enhanced flexibility. Opt for self-hosted deployment with FlowFuse, enabling on-premises installation and full control over your deployment process. {{ ctaSection.description }}` link padding or is true zero-padding inline text), `preview` (renders identically but doesn't navigate or call `capture()` — used by the handbook's live example gallery so clicking a doc example can't send a real event or leave the page).
@@ -287,7 +288,7 @@ All four are thin wrappers around `nuxt/components/cta/CtaButton.vue`, which doe
There's no `size` prop — every real-button variant's padding/font-size is hardcoded to match `.ff-btn` exactly (see the Computed-tab note in the gotchas below), so a size knob would only ever have affected icon dimensions. It was removed once confirmed nothing used a non-default value.
-Click tracking: `capture(event, { position, variant, plan? })` via `nuxt/composables/useCapture.ts`, which wraps the global `window.capture()` from `src/_includes/analytics/body.html` (shared with 11ty, no-ops without analytics consent). Event names: `cta-sign-up`, `cta-sign-in`, `cta-contact-us`, `cta-book-demo`.
+Click tracking: `capture(event, { position, variant, plan? })` via `nuxt/composables/useCapture.ts`, which wraps the global `window.capture()` from `src/_includes/analytics/body.html` (shared with 11ty, no-ops without analytics consent). Event names: `cta-sign-up`, `cta-sign-in`, `cta-contact-us`, `cta-book-demo`, `cta-pricing`.
### Gotchas already solved here (don't re-discover them)
@@ -299,6 +300,16 @@ Click tracking: `capture(event, { position, variant, plan? })` via `nuxt/composa
- **The `ui` prop override doesn't reach compoundVariants-driven classes.** The gotcha above (full-string replacement) only applies to the app.config-level base extension; UButton's own `variant`/`color`-driven classes (e.g. `ghost`'s `hover:bg-{color}/10`) are computed separately and still get merged in via `tv()` regardless of what `ui.base` says. `CtaButton.vue`'s ghost color classes explicitly add `hover:bg-transparent` to cancel that default hover background, since a ghost CTA should have none at all.
- **UButton's `to` prop treats any same-origin-looking path as a Nuxt route, even if Nuxt doesn't serve it.** `CtaContactUs`/`CtaBookDemo` point at `/contact-us/` and `/book-demo/`, which are still 11ty — without `external`, clicking does a client-side Vue Router navigation instead of a real page load, and 404s instead of reaching the server-side 11ty proxy. `CtaButton.vue` takes a fixed (non-caller-configurable) `external` prop per destination; `CtaContactUs`/`CtaBookDemo` set it `true`, `CtaSignUp`/`CtaSignIn` set it `false` (moot — those hrefs are already cross-origin). **When `/contact-us/` or `/book-demo/` actually migrates to Nuxt, flip that destination's `external` to `false`** — leaving it `true` would keep forcing a full page reload where a client-side nav would work fine.
+### 11ty equivalent
+
+`src/_includes/components/cta/` holds five Nunjucks macros (`ctaSignUp`, `ctaSignIn`, `ctaContactUs`, `ctaBookDemo`, `ctaPricing`) mirroring the Vue components above — same fixed copy/href/event per destination, same `{ position, variant }` capture payload (no `preview` or `external` prop: 11ty has no handbook gallery to guard against, and every `` is a real page load already, no Vue Router to fight). All five delegate to a shared `ctaButton` macro in the same folder (`cta-button.njk`) for the actual class-building/tracking, same relationship as `CtaButton.vue` to its wrappers.
+
+No `plan` param on the 11ty side — that's only used on the pricing table, which is Nuxt-only (there's no 11ty pricing page to call it from). If 11ty ever grows a caller that needs it, add it back rather than passing it unused today.
+
+`ctaSignUp`/`ctaSignIn` take `site` as their first argument (`site.appURL` for the href) instead of reading it from the calling template's context — Nunjucks macros don't inherit the caller's context unless explicitly imported `with context`, and passing `site` explicitly avoids relying on that import mode everywhere the macro is used.
+
+Every hand-written `` pointing at one of the five destinations has been migrated to these macros — there should be no new ones. A link to a URL outside the five fixed destinations (e.g. `/ai/`) is not part of this system and stays hand-written.
+
## Naming conventions
- All slugs: **kebab-case**
diff --git a/.eleventy.js b/.eleventy.js
index 01105ef345..18e6ed43e4 100644
--- a/.eleventy.js
+++ b/.eleventy.js
@@ -150,6 +150,31 @@ module.exports = function(eleventyConfig) {
watch: ["_site/**/*.css", "_site/**/*.js"],
})
+ // CTA event/href/label data (single source of truth shared with the Nuxt
+ // Cta* components, which import the same src/_data/ctaDestinations.json)
+ // registered as a Nunjucks *global* rather than page data: the five
+ // ctaSignUp/ctaSignIn/ctaContactUs/ctaBookDemo/ctaPricing macros need to
+ // read it, and Nunjucks macros don't see page-context data
+ // (addGlobalData) unless it's explicitly passed in as an argument -
+ // addNunjucksGlobal makes it visible everywhere, macros included, with no
+ // per-call-site plumbing.
+ // Registered once with a stable object reference, then refreshed IN PLACE
+ // on every build (not re-registered via a second addNunjucksGlobal call,
+ // which only affected later builds - templates already held a reference
+ // to the first, now-stale value, so mid-watch edits rendered `undefined`
+ // until the whole process restarted). Mutating the same object's keys
+ // means every template holding a reference to it sees the update too.
+ const ctaDestinationsPath = require.resolve('./src/_data/ctaDestinations.json')
+ const ctaDestinations = require(ctaDestinationsPath)
+ eleventyConfig.addNunjucksGlobal('ctaDestinations', ctaDestinations)
+ eleventyConfig.addWatchTarget(ctaDestinationsPath)
+ eleventyConfig.on('eleventy.before', () => {
+ delete require.cache[ctaDestinationsPath]
+ const fresh = require(ctaDestinationsPath)
+ Object.keys(ctaDestinations).forEach(key => delete ctaDestinations[key])
+ Object.assign(ctaDestinations, fresh)
+ })
+
// make global accessible in src/_includes/layouts/base.njk for loading of PH scripts
eleventyConfig.addGlobalData('POSTHOG_APIKEY', () => process.env.POSTHOG_APIKEY || '' )
eleventyConfig.addGlobalData('DEV_MODE', () => DEV_MODE || DEV_MODE_POSTS)
diff --git a/nuxt/components/BlogPostCta.vue b/nuxt/components/BlogPostCta.vue
index 7a00f80ac6..a8d5500557 100644
--- a/nuxt/components/BlogPostCta.vue
+++ b/nuxt/components/BlogPostCta.vue
@@ -4,10 +4,9 @@ const props = defineProps<{
cta?: { type?: string, title?: string, description?: string } | null
}>()
-// 'sign-up' / 'demo' / 'contact' now render one of the unified Cta* components
-// (fixed copy/href/event) instead of a local buttonText/buttonUrl pair, so the
-// blog CTA can't drift from the rest of the site's copy. 'pricing' isn't one
-// of the four unified destinations, so it keeps its own link.
+// All four types now render one of the unified Cta* components (fixed
+// copy/href/event) instead of a local buttonText/buttonUrl pair, so the blog
+// CTA can't drift from the rest of the site's copy.
const CTA_VARIANTS: Record
Get Started with FlowFuse
FlowFuse Cloud
On Premise
{{ ctaSection.title }}
Whether you're extending an existing system, standardizing a proven solution across sites, or connecting OT and IT data flows, FlowFuse gives your team the platform to build once and run everywhere.
That's how a small team manages operations across dozens of sites, without rebuilding from scratch every time.
Ready to build yours?
Speed without governance creates new debt. FlowFuse provides the production layer where AI-assisted work becomes visible, versioned, secure, and reusable, across the enterprise.
- + {#- No plain-text-link variant exists yet in the shared Cta* + system (CLAUDE.md reserves `text` for that), and ghost is + too heavy (bold/uppercase/button padding) for this inline + sentence-style link - kept hand-written until that variant + exists, but still fires the same unified event/props + shape as the real CtaBookDemo would, tagged variant="text" + so it's identifiable in PostHog as this one-off style. -#} + Book a demo {% include "components/icons/arrow-long-right.svg" %} diff --git a/src/industries.njk b/src/industries.njk index 84e78f343f..3bb97cb45b 100644 --- a/src/industries.njk +++ b/src/industries.njk @@ -6,6 +6,8 @@ meta: title: "Industries" description: "FlowFuse helps industrial teams connect, automate, and standardize operations across sectors. Explore how FlowFuse is applied in your industry." --- +{% from "components/cta/cta-sign-up.njk" import ctaSignUp %} +{% from "components/cta/cta-contact-us.njk" import ctaContactUs %}FlowFuse meets manufacturers where they are, connecting OT and IT systems, automating workflows, and standardizing operations across every site.
Talk to an expert, or get started with FlowFuse today.
{{ hero.description }}
FlowFuse enhances Node-RED with enterprise features to accelerate digitalization and optimize industrial processes.
Ready to get started building with Node-RED?
- + {{ ctaSignUp(site, 'primary', 'nodered', extraClass='hidden min-h-[40px] md:inline') }}After successfully navigating the technical qualification, take the next step by booking a meeting with our team. Together, we'll work towards ensuring your device is fully compatible with FlowFuse and Node-RED.
Operational workflow patterns that follow Event, Decision, Action. Explore how each one is operationalized with FlowFuse.
Talk to an expert, or get started with FlowFuse today.
Data integration can be a challenging task, especially when dealing with multiple data formats, protocols, and sources. FlowFuse's low-code industrial application platform allows anyone to collect data from multiple sources, apply the data, and update, store, and transmit data to a new service.
--- - +{% from "components/cta/cta-contact-us.njk" import ctaContactUs %}FlowFuse simplifies the management of deployments to edge devices in manufacturing, from equipment to PLCs. Our platform ensures secure and reliable handling of large-scale deployments, providing seamless edge connectivity solutions.
--- - +{% from "components/cta/cta-contact-us.njk" import ctaContactUs %}FlowFuse streamlines manufacturing automation, from production monitoring to supply chain management. Our intuitive, customizable platform enables simple creation of custom solutions for unique manufacturing needs.
--- - +{% from "components/cta/cta-sign-up.njk" import ctaSignUp %} +{% from "components/cta/cta-book-demo.njk" import ctaBookDemo %}