Skip to content

Commit 1251973

Browse files
Merge branch 'main' into seo/dashboard-page-optimization
2 parents d3db82f + a5f4c68 commit 1251973

55 files changed

Lines changed: 1721 additions & 324 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eleventy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ module.exports = function(eleventyConfig) {
141141
eleventyConfig.addPassthroughCopy("src/js/ai-expert-modal.js");
142142
eleventyConfig.addPassthroughCopy("src/js/hm-promo-banner.js");
143143
eleventyConfig.addPassthroughCopy("src/js/nav-tracking.js");
144+
eleventyConfig.addPassthroughCopy("src/js/signup-popup.js");
144145

145146
// Watch content images for the image pipeline
146147
eleventyConfig.addWatchTarget("src/**/*.{svg,webp,png,jpeg,gif}");

nuxt/components/AppHeader.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ onMounted(() => {
149149
150150
<!-- Mobile hamburger -->
151151
<div class="flex items-center gap-2 md:hidden relative z-20">
152-
<CtaContactUs variant="primary" position="header-mobile" />
152+
<CtaBookDemo variant="primary" position="header-mobile" />
153153
<button id="nav-toggle" class="text-gray-700 flex items-center text-red-hero">
154154
<svg class="burger fill-current h-4 w-4" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Menu</title><path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/></svg>
155155
<svg class="close fill-current h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
@@ -187,7 +187,7 @@ onMounted(() => {
187187
<ul class="cta hidden md:flex flex-row items-center justify-end font-medium text no-underline z-10 bg-transparent w-auto">
188188
<li class="hidden md:flex"><CtaSignUp variant="nav-text" position="main-nav" padded class="ff-nav-freetrial text-base" /></li>
189189
<li class="flex">
190-
<CtaContactUs variant="primary" position="main-nav" class="ml-2" />
190+
<CtaBookDemo variant="primary" position="main-nav" class="ml-2" />
191191
</li>
192192
</ul>
193193
</nav>
@@ -198,7 +198,7 @@ onMounted(() => {
198198
<CtaSignIn variant="primary-outlined" position="mobile-cta-bar" />
199199
<CtaSignUp variant="primary-outlined" position="mobile-cta-bar" />
200200
</div>
201-
<CtaContactUs variant="primary" position="mobile-cta-bar" />
201+
<CtaBookDemo variant="primary" position="mobile-cta-bar" />
202202
</div>
203203
</header>
204204
</template>

nuxt/components/BlogPostCta.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function onCtaClick (event: MouseEvent) {
5151
</script>
5252

5353
<template>
54-
<div class="ff-blue-card blog-post-cta p-8 sm:p-12 m-auto max-w-prose">
54+
<div class="ff-blue-card blog-post-cta p-8 sm:p-12 m-auto">
5555
<div class="flex flex-col gap-6 sm:gap-8 text-center sm:text-left">
5656
<h3 class="mt-0 mb-0 !text-3xl text-indigo-800">{{ cta?.title || currentCta.title }}</h3>
5757
<p class="mt-0 mb-0 max-w-4xl mx-auto sm:mx-0 leading-relaxed">{{ cta?.description || currentCta.description }}</p>

nuxt/components/ChangelogListing.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,16 @@ onUnmounted(() => {
101101
<!-- The release label lives in a column beside its own entries rather than in a
102102
separate rail, so it lines up with the entries it belongs to, and sticks while
103103
you read through them. Two columns per release, not one list plus one nav. -->
104+
<!-- -scroll-mt-7 cancels the label column's pt-7 below, so a deep link such as
105+
/changelog/#release-3-0 rests with the release label itself just under the
106+
header, at the site-wide 75px inset, rather than the section box that starts
107+
28px above it. That also tucks the previous release's sticky label behind the
108+
header, instead of leaving it poking out above the release you asked for. -->
104109
<section
105110
v-for="group in visibleGroups"
106111
:id="anchorId(group.release)"
107112
:key="group.release"
108-
class="flex"
113+
class="flex -scroll-mt-7"
109114
>
110115
<!-- pt-7 so the label lines up with the first entry's title rather than sitting
111116
above it: each entry carries `my-2 py-6` before its title, which this column
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<script setup lang="ts" generic="T extends { id: string; label: string }">
2+
// Sticky left nav + stacked, always-visible content sections, with the nav
3+
// link highlighted for whichever section the reader has scrolled to. Shared by
4+
// /product ("Find the product for how you work") and /integrations/opcua
5+
// ("What is OPC UA").
6+
//
7+
// Deliberately not @nuxt/ui's UContentToc/useScrollspy: that tracks active
8+
// headings via IntersectionObserver on the heading elements themselves, which
9+
// works for dense docs TOCs but breaks here - when the gap between sections
10+
// (padding + divider) is wider than the heading, scrolling through that gap
11+
// leaves no heading intersecting and the indicator sticks on the previous
12+
// section. Walking sections in DOM order and taking whichever one's top has
13+
// crossed the viewport's vertical midpoint (overwriting on each match, so the
14+
// deepest/furthest-scrolled section wins) doesn't have that gap.
15+
//
16+
// Generic over T (not just { id, label }) so a scoped slot can read whatever
17+
// extra fields a caller's item carries (e.g. /product's heading/image/slug)
18+
// without a cast.
19+
const props = withDefaults(defineProps<{
20+
items: T[]
21+
ariaLabel?: string
22+
gapClass?: string
23+
dividerClass?: string
24+
// Viewport width at which the nav switches from stacked-above-content to
25+
// beside it. 'lg' is /product's original width (its item content is
26+
// wider - text + image per step); opcua's items are text-only and have
27+
// room to go side-by-side sooner, at 'md'. Literal class strings below
28+
// (not a template-interpolated breakpoint) since Tailwind's build-time
29+
// scanner only picks up classes it can see written out in full.
30+
breakpoint?: 'md' | 'lg'
31+
}>(), {
32+
ariaLabel: undefined,
33+
gapClass: 'gap-16',
34+
dividerClass: 'pt-16 border-t border-gray-200',
35+
breakpoint: 'lg',
36+
})
37+
38+
const rowClass = computed(() => props.breakpoint === 'md' ? 'flex flex-col md:flex-row gap-10' : 'flex flex-col lg:flex-row gap-10')
39+
const navClass = computed(() => props.breakpoint === 'md' ? 'hidden md:block md:w-44 shrink-0' : 'hidden lg:block lg:w-44 shrink-0')
40+
41+
const active = ref(props.items[0]?.id ?? '')
42+
const stepRefs = ref<Record<string, HTMLElement | null>>({})
43+
44+
function updateActive () {
45+
const steps = Object.entries(stepRefs.value).filter((entry): entry is [string, HTMLElement] => !!entry[1])
46+
if (!steps.length) return
47+
const mid = window.innerHeight / 2
48+
let current = steps[0][0]
49+
for (const [id, el] of steps) {
50+
if (el.getBoundingClientRect().top <= mid) current = id
51+
}
52+
active.value = current
53+
}
54+
55+
let ticking = false
56+
function onScroll () {
57+
if (!ticking) { ticking = true; requestAnimationFrame(() => { ticking = false; updateActive() }) }
58+
}
59+
60+
onMounted(() => {
61+
window.addEventListener('scroll', onScroll, { passive: true })
62+
window.addEventListener('resize', onScroll, { passive: true })
63+
updateActive()
64+
})
65+
66+
onUnmounted(() => {
67+
window.removeEventListener('scroll', onScroll)
68+
window.removeEventListener('resize', onScroll)
69+
})
70+
</script>
71+
72+
<template>
73+
<div :class="rowClass">
74+
<nav :class="navClass" :aria-label="ariaLabel">
75+
<ul class="sticky top-24 flex flex-col border-l border-gray-200">
76+
<li v-for="item in items" :key="item.id">
77+
<a
78+
:href="`#${item.id}`"
79+
class="block py-[0.6rem] pl-5 -ml-px border-l-2 font-medium transition-colors duration-200"
80+
:class="active === item.id ? 'text-indigo-600 border-indigo-600' : 'border-transparent text-gray-500 hover:text-indigo-600'"
81+
>{{ item.label }}</a>
82+
</li>
83+
</ul>
84+
</nav>
85+
86+
<div class="flex-1 min-w-0 flex flex-col" :class="gapClass">
87+
<div
88+
v-for="(item, index) in items"
89+
:id="item.id"
90+
:key="item.id"
91+
:ref="(el) => { stepRefs[item.id] = el as HTMLElement | null }"
92+
class="scroll-mt-24"
93+
:class="index > 0 ? dividerClass : ''"
94+
>
95+
<slot :name="item.id" :item="item" :index="index" />
96+
</div>
97+
</div>
98+
</div>
99+
</template>

nuxt/components/cta/CtaButton.vue

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,16 @@ const VARIANT_MAP: Record<string, { color: string, variant: string, hover: strin
9090
ghost: { color: 'primary', variant: 'ghost', hover: '' },
9191
}
9292
93-
// UButton's own "ghost" variant compoundVariants add a faint hover background
94-
// (hover:bg-{color}/10) - that's Nuxt UI's default ghost affordance, but a
95-
// ghost CTA here should have no background at any point, hover included, so
96-
// every entry explicitly cancels it with hover:bg-transparent.
93+
// UButton's own "ghost" variant compoundVariants add a faint hover AND active
94+
// background (hover:bg-{color}/10, active:bg-{color}/10) - that's Nuxt UI's
95+
// default ghost affordance, but a ghost CTA here should have no background at
96+
// any point, hover and click both, so every entry cancels both explicitly.
97+
// Missing active:bg-transparent here used to leave a visible fill flash on
98+
// click/tap even though hover looked fine.
9799
const GHOST_COLOR_CLASSES: Record<string, string> = {
98-
primary: 'text-primary hover:text-primary/75 hover:bg-transparent',
99-
highlight: 'text-highlight hover:text-highlight/75 hover:bg-transparent',
100-
white: 'text-white hover:text-gray-200 hover:bg-transparent',
100+
primary: 'text-primary hover:text-primary/75 hover:bg-transparent active:bg-transparent',
101+
highlight: 'text-highlight hover:text-highlight/75 hover:bg-transparent active:bg-transparent',
102+
white: 'text-white hover:text-gray-200 hover:bg-transparent active:bg-transparent',
101103
}
102104
103105
const uiVariant = computed(() => VARIANT_MAP[props.variant])

nuxt/content/faq/pricing.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ items:
44
- question: "What's the difference between Edge and Hub?"
55
answer: "Edge is a product designed specifically for OT teams and the unique challenges they manage. With Edge, you can connect PLCs, machines, and systems, applying a standardised methodology for deploying automation at scale. This tier comes with everything in the FlowFuse core platform, but also includes support for Device Fleet Updates and Device Group Management. Additionally, this tier includes the OT Certified Nodes collection and deployable OT Application Blueprints.<br><br>Hub is a product better suited for IT teams moving data between their systems and orchestrating multi-plant installations. For this reason, it includes everything in the FlowFuse core platform, as well as some particularly useful features for this use case. It includes Git Integration, High Availability, BAA for HIPAA, and Protected Instances. It also includes the IT Certified Nodes collection and deployable IT Application Blueprints.<br><br>For customers requiring something between these two featuresets, please <a href='/contact-us'>Contact Us</a> so we can help build a tailored solution for your environment!"
66
- question: "How does customer support work for FlowFuse?"
7-
answer: "FlowFuse provides active support for Certified Nodes and the FlowFuse platform per the <a href='/handbook/sales/subscription-agreement-1.5'>SLA</a>. Customers with these features can get support by <a href='/support'>filing a ticket</a> or via help information in the <a href='/support'>FlowFuse Support Portal</a>. For general issues related to Node-RED or third party modules, raise issues in the <a href='https://community.flowfuse.com/'>community forum</a>."
7+
answer: "FlowFuse provides active support for Certified Nodes and the FlowFuse platform per the <a href='/handbook/sales/customer-success/#sla'>SLA</a>. Customers with these features can get support by <a href='/support'>filing a ticket</a> or via help information in the <a href='/support'>FlowFuse Support Portal</a>. For general issues related to Node-RED or third party modules, raise issues in the <a href='https://community.flowfuse.com/'>community forum</a>."
88
- question: "How does FlowFuse secure my data?"
99
answer: "FlowFuse employs a layered approach to securing customer data. For a fuller understanding of the mechanisms behind this approach, please read our <a href='/platform/security/'>security statement</a> as well as our <a href='/docs/contribute/architecture/'>architecture documentation</a>.<br><br>FlowFuse secures user access through username/password, SAML-based SSO, or LDAP, with support for multi-factor authentication and automatic session expiry after 14 days. Access is governed by role-based controls within Teams - Owner, Member, Viewer, and Dashboard roles - while platform-wide admin access is tightly restricted (on FlowFuse Cloud, limited to vetted employees on a need-to-have basis). All administrative and team-level actions are captured in audit logs. On FlowFuse Cloud, data is encrypted at rest with AES-256 (PostgreSQL and AWS EFS), stored in the EU (Ireland), and protected in transit with TLS 1.2 or higher terminated at the edge for both HTTP and MQTT traffic.<br><br>Organizationally, FlowFuse holds SOC 2 Type 1 and Type 2 certifications (audited by Advantage Partners) and maintains formal information security, acceptable use, and data management policies alongside regular security awareness training. The platform is also built to be GxP validation-ready: features like role-based access control, audit logging, instance versioning and snapshots, infrastructure-as-code, and SSO/LDAP integration align with the traceability and change-control expectations of regulated environments such as FDA 21 CFR Part 11 and EU GMP Annex 11, enabling customers to perform their own validation and supplier qualification."
1010
- question: "Does FlowFuse offer annual discounts for FlowFuse Cloud?"

nuxt/content/feature-catalog.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ sections:
375375
- id: baa-for-hipaa
376376
title: BAA for HIPAA
377377
description: "FlowFuse can sign a Business Associate Agreement to ensure proper safeguarding of protected health information handled on your behalf."
378-
docsLink: /handbook/sales/subscription-agreement-1.5/
378+
docsLink: /handbook/sales/subscription-agreement-1.6/
379379
tiers: { edge: false, hub: true, fleet: false }
380380
- id: sbom
381381
title: Software Bill of Materials

nuxt/content/handbook/engineering/support/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Support tickets can be opened via the [FlowFuse Support Portal](/support). FlowF
1010

1111
## SLAs
1212

13-
For all FlowFuse customers, we have an [SLA](/handbook/sales/subscription-agreement-1.5) defined as part of the Subscription Agreement. Initial responses to support tickets must be sent within the SLA time frame defined in the Subscription Agreement.
13+
All paying FlowFuse customers get a Standard Support SLA by default; a faster Enterprise-level SLA can be purchased and negotiated as an upgrade. See the [SLA definitions](/handbook/sales/customer-success/#sla) for the current response-time commitments. Initial responses to support tickets must be sent within the applicable SLA time frame.
1414

1515
## Triage Duty
1616

nuxt/content/handbook/engineering/support/triage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ Some customers have response-time SLAs based on their subscription.
8989

9090
Triage leads are not responsible for enforcing SLAs directly, but are responsible for ensuring tickets are routed and acknowledged in a way that supports them.
9191

92-
Current SLA definitions and response expectations are documented in the [Subscription Agreement](/handbook/sales/subscription-agreement-1.5/).
92+
Current SLA definitions and response expectations are documented in [Customer Success](/handbook/sales/customer-success/#sla).
9393

9494
When triaging:
9595

0 commit comments

Comments
 (0)