Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions nuxt/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,14 @@ export default defineContentConfig({
highlight: z.boolean().optional(),
order: z.number(),
features: z.array(z.string()),
bestFitFor: z.array(z.string()).optional(),
button: z.object({
label: z.string(),
to: z.string(),
external: z.boolean().optional(),
color: z.enum(['primary', 'secondary', 'highlight']).optional(),
variant: z.enum(['solid', 'outline', 'soft', 'subtle', 'ghost', 'link']).optional(),
event: z.string().optional(),
}),
})
}),
Expand Down
16 changes: 8 additions & 8 deletions nuxt/content/feature-catalog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ sections:
- id: secure
title: Secure
features:
- id: certified-nodes-it
title: Certified Nodes - IT
description: "IT certified node bundle includes: Redis, MQTT, HTTP Request, AI nodes (Gemini, Claude, ChatGPT, Ollama), MCP Server"
tiers: { edge: false, hub: true }
- id: certified-nodes-ot
title: Certified Nodes - OT
description: "OT certified node bundle includes: OPC-UA, Modbus TCP & RTU, RTSP, EtherNet/IP, AI nodes (Gemini, Claude, ChatGPT, Ollama), MCP Server"
tiers: { edge: true, hub: false }
- id: audit-log
title: Audit Log
tiers: { edge: true, hub: true }
Expand All @@ -110,14 +118,6 @@ sections:
- id: single-sign-on
title: Single Sign-On (SSO)
tiers: { edge: true, hub: true }
- id: certified-nodes-it
title: Certified Nodes - IT
description: "IT certified node bundle includes: Redis, MQTT, HTTP Request, AI nodes (Gemini, Claude, ChatGPT, Ollama), MCP Server"
tiers: { edge: false, hub: true }
- id: certified-nodes-ot
title: Certified Nodes - OT
description: "OT certified node bundle includes: OPC-UA, Modbus TCP & RTU, RTSP, EtherNet/IP, AI nodes (Gemini, Claude, ChatGPT, Ollama), MCP Server"
tiers: { edge: true, hub: false }
- id: baa-for-hipaa
title: BAA for HIPAA
tiers: { edge: false, hub: true }
Expand Down
17 changes: 10 additions & 7 deletions nuxt/content/plans/edge.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
title: Edge
tierId: edge
order: 1
description: Deploy, manage and scale industrial applications across production sites with centralized governance and certified industrial connectivity.
description: Deploy, manage, and scale industrial applications across your sites with centralized governance and certified industrial connectivity.
badge: For OT teams
features:
- Industrial applications
- PLC & machine connectivity
- OT-certified nodes
- MQTT Broker
- DevOps Pipelines
- Device Fleet Updates
- Device Group Management
- "Certified Nodes: OT"
- Blueprints - OT APPS
bestFitFor:
- OT teams connecting PLCs, machines, and systems
- Businesses standardising and deploying automation across multiple sites
- Standard deployments across multiple sites.
button:
label: Contact us
to: /contact-us/
external: true
variant: solid
variant: solid
event: cta-contact-us
21 changes: 13 additions & 8 deletions nuxt/content/plans/hub.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
title: Hub
tierId: hub
order: 2
description: Integrate and orchestrate data across enterprise systems, APIs and databases with centralized governance.
description: Integrate and orchestrate data across enterprise systems, APIs and databases with centralised governance.
badge: For IT teams
features:
- Business system integration
- Data orchestration
- IT-certified nodes
- Git Integration
- High Availability
- DevOps Pipelines
- BAA for HIPAA
- Protected Instances
- Certified Nodes - IT
- Blueprints - IT APPS
bestFitFor:
- IT teams moving data between ERPs, databases, and cloud APIs.
- Complex data sets that need governed integration and orchestration.
- Custom deployments at the data and orchestration level.
button:
label: Contact us
to: /contact-us/
label: START FREE TRIAL
to: https://app.flowfuse.com/account/create
external: true
variant: solid
variant: solid
event: cta-join
100 changes: 95 additions & 5 deletions nuxt/pages/pricing/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,37 @@ const tableTiers = computed(() => (plans.value ?? []).map(p => ({
id: p.tierId,
title: p.title,
highlight: p.highlight,
button: p.button,
bestFitFor: p.bestFitFor,
})))

const faqAccordionItems = computed(() => (faq.value?.items ?? []).map(item => ({
label: item.question,
content: item.answer,
})))

// The comparison table should only show features that differ between tiers.
// Features shared by every tier don't belong in a comparison — they're listed
// separately below as a plain "core features" dump, grouped by their original
// category so ~37 shared features don't read as one undifferentiated list.
const comparisonSections = computed(() => (featureCatalog.value?.sections ?? [])
.map(section => ({ ...section, features: section.features.filter(f => f.tiers.edge !== f.tiers.hub) }))
.filter(section => section.features.length > 0))

const coreFeatureSections = computed(() => (featureCatalog.value?.sections ?? [])
.map(section => ({ ...section, features: section.features.filter(f => f.tiers.edge === f.tiers.hub) }))
.filter(section => section.features.length > 0))

// window.capture is injected by the site's analytics script (see
// src/_includes/analytics/body.html) and is a no-op wrapper around
// posthog.capture — it's absent outside production, hence the guard.
function capture (eventName?: string, props?: Record<string, unknown>) {
if (!eventName) return
if (typeof (window as any).capture === 'function') {
(window as any).capture(eventName, props)
}
}

// UPricingTable's feature-title slot types `feature` without `description`,
// even though the featureCatalog content schema does define it.
interface CatalogFeature {
Expand All @@ -24,6 +48,13 @@ function featureDescription (feature: CatalogFeature) {
return feature.description
}

useSeoMeta({
title: 'FlowFuse Pricing',
description: 'Packages built around the job, deploying and scaling automation across plants, or integrating the business systems and data sources you run on. All governed on one platform.',
ogUrl: 'https://flowfuse.com/pricing/',
twitterSite: '@FlowFuseinc',
})

useSchemaOrg([
defineWebPage({ '@type': 'FAQPage' }),
...(faq.value?.items ?? []).map(item => defineQuestion({
Expand All @@ -38,27 +69,70 @@ useSchemaOrg([
<div class="max-w-5xl mx-auto py-16 px-4">
<h1 class="text-center"><span class="text-indigo-600">FlowFuse</span> Pricing</h1>
<h2 class="text-center text-gray-500 text-2xl -mt-3 mb-10">Choose the package that fits your team</h2>
<p class="text-center text-lg max-w-2xl mx-auto mb-16">Machines on the shop floor, scaling to more plants? Or business systems and data sources to bring under governance? There’s a FlowFuse package for both.</p>
<p class="text-center text-lg max-w-2xl mx-auto mb-16">Whether you’re connecting one plant or standardizing across hundreds of business systems, FlowFuse has a product for you.</p>
<UPricingPlans>
<UPricingPlan v-for="plan in plans" :key="plan.id" v-bind="plan" :ui="{ button: 'text-base font-bold', featureTitle: 'whitespace-normal overflow-visible text-clip', titleWrapper: 'mb-4' }" />
<UPricingPlan v-for="plan in plans" :key="plan.id" v-bind="plan" :ui="{ root: 'bg-radial-[at_bottom_right] from-indigo-50 to-white', button: 'text-base font-bold', titleWrapper: 'mb-4', features: 'mt-2' }">
<template #description>
<p>{{ plan.description }}</p>
<p class="mt-6 text-sm font-semibold text-gray-900">Everything in the <a href="#core-features" class="text-indigo-600 hover:underline">core FlowFuse platform</a> plus:</p>
</template>
<template #features>
<li v-for="feature in plan.features" :key="feature" class="flex items-center gap-2 min-w-0">
<UIcon name="i-lucide-check" class="size-5 shrink-0 text-primary" />
<span class="text-muted text-sm whitespace-normal overflow-visible text-clip">{{ feature }}</span>
</li>
<li class="flex items-center gap-2 min-w-0">
<a href="#comparison" class="text-sm font-semibold text-indigo-600 hover:underline">See all</a>
</li>
</template>
<template #button>
<UButton
block size="lg" v-bind="plan.button" class="text-base font-bold"
@click="capture(plan.button.event, { position: 'pricing-card', tier: plan.tierId, page: 'pricing' })"
/>
</template>
</UPricingPlan>
</UPricingPlans>
<SocialProof class="mt-16" />

<CertifiedNodes />

<h2 class="text-center mt-28 mb-10"><span class="text-indigo-600">FlowFuse</span> Comparison</h2>
<h2 id="comparison" class="text-center mt-28 mb-10"><span class="text-indigo-600">FlowFuse</span> Comparison</h2>
<UPricingTable
v-if="featureCatalog"
class="mt-16"
:tiers="tableTiers"
:sections="featureCatalog.sections"
:sections="comparisonSections"
:ui="{
tier: 'border-x border-t border-b border-default rounded-t-lg bg-radial-[at_bottom_right] from-indigo-50 to-white',
td: 'border-x border-default',
th: 'px-6 border-l border-default',
tr: '*:py-4',
tbody: '[&>tr[data-slot]]:bg-indigo-50/50 [&>tr:first-child>th]:border-t [&>tr:first-child>th]:border-default [&>tr:first-child>th]:rounded-t-lg [&>tr:last-child>th]:rounded-b-lg [&>tr:last-child>td]:rounded-b-lg',
tierWrapper: 'items-center text-center',
tierTitleWrapper: 'justify-center',
tierDescription: 'hidden',
tierDescription: 'w-full',
tierPriceWrapper: 'hidden',
tierButton: 'w-full',
}"
>
<template #tier-button="{ tier }">
<UButton
v-if="tier.button" block size="lg" v-bind="tier.button" class="text-base font-bold"
@click="capture(tier.button.event, { position: 'pricing-comparison', tier: tier.id, page: 'pricing' })"
/>
</template>
<template #tier-description="{ tier }">
<div v-if="tier.bestFitFor?.length" class="w-full mt-4 text-left">
<p class="text-sm font-semibold text-gray-900">Best Fit For:</p>
<ul class="mt-1.5 flex flex-col gap-1.5 text-sm text-gray-600">
<li v-for="fit in tier.bestFitFor" :key="fit" class="flex items-start gap-1.5">
<UIcon name="i-lucide-check" class="size-4 shrink-0 mt-0.5 text-indigo-600" />
<span>{{ fit }}</span>
</li>
</ul>
</div>
</template>
<template #feature-title="{ feature }">
<UPopover v-if="featureDescription(feature)" :content="{ side: 'top' }">
<button type="button" class="inline-flex items-center gap-1 text-left hover:text-indigo-600">
Expand All @@ -73,6 +147,22 @@ useSchemaOrg([
</template>
</UPricingTable>

<div v-if="coreFeatureSections.length" id="core-features" class="mt-28">
<h2 class="text-center mb-2"><span class="text-indigo-600">FlowFuse</span> Core Features</h2>
<h3 class="text-center text-gray-500 text-2xl mb-10">Included in every FlowFuse plan</h3>
<div class="max-w-4xl mx-auto space-y-8">
<div v-for="section in coreFeatureSections" :key="section.id">
<h3 class="font-semibold text-sm text-gray-900 mb-3">{{ section.title }}</h3>
<ul class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-3">
<li v-for="feature in section.features" :key="feature.id" class="flex items-center gap-2">
<UIcon name="i-lucide-check" class="size-4 shrink-0 text-indigo-600" />
<span class="text-sm text-gray-700">{{ feature.title }}</span>
</li>
</ul>
</div>
</div>
</div>

<div v-if="faq" class="mt-28 mx-auto">
<h2 class="text-center mb-10" v-html="faq.title" />
<UAccordion :items="faqAccordionItems">
Expand Down
1 change: 1 addition & 0 deletions src/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ html, body {
font-size: 16px;
font-family: var(--font-sans);
scroll-padding-top: 75px;
scroll-behavior: smooth;
background-color: white;
}

Expand Down