From d81ddc3b87824dc14ce9b6b6939bef53c3e7002e Mon Sep 17 00:00:00 2001 From: ZJ van de Weg Date: Fri, 7 Aug 2026 10:58:33 -0700 Subject: [PATCH 1/3] Migrate /contact-us and /book-demo from 11ty to Nuxt Ports the shared mql-contact layout, HubSpot form, and a new meetings embed component, then flips CtaContactUs/CtaBookDemo's `external` prop to false now that both routes are served by Nuxt directly. --- .claude/CLAUDE.md | 2 +- nuxt/components/CtaBookDemo.vue | 4 +- nuxt/components/CtaContactUs.vue | 4 +- nuxt/components/HubSpotForm.vue | 15 ++++++- nuxt/components/HubSpotMeetings.vue | 62 +++++++++++++++++++++++++++++ nuxt/components/MqlContactPage.vue | 58 +++++++++++++++++++++++++++ nuxt/nuxt.config.ts | 2 + nuxt/pages/book-demo/index.vue | 34 ++++++++++++++++ nuxt/pages/contact-us/index.vue | 35 ++++++++++++++++ nuxt/server/middleware/legacy.ts | 2 +- src/book-demo.njk | 23 ----------- src/contact-us.njk | 24 ----------- 12 files changed, 208 insertions(+), 57 deletions(-) create mode 100644 nuxt/components/HubSpotMeetings.vue create mode 100644 nuxt/components/MqlContactPage.vue create mode 100644 nuxt/pages/book-demo/index.vue create mode 100644 nuxt/pages/contact-us/index.vue delete mode 100644 src/book-demo.njk delete mode 100644 src/contact-us.njk diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 693f8b12ef..6ad0354ac0 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -297,7 +297,7 @@ Click tracking: `capture(event, { position, variant, plan? })` via `nuxt/composa - **`.handbook-content a` must exclude Nuxt UI components, or use `:where()`.** A plain `.handbook-content a { color: ... }` rule (even layered) beats a `UButton`'s own utility classes if it has higher specificity, flattening any Cta* button rendered inside handbook markdown to a plain link color. Fixed by moving the rule into `@layer base` and using `:where(a)` to zero out its added specificity, so any component's own classes win normally (same file as above). - **`not-prose` on a wrapper also strips Tailwind Typography's code-block styling** (the dark background on `
`) for anything nested inside it, not just its own prose text styling. `nuxt/components/content/CtaExample.vue` (the handbook's live example gallery) doesn't use `not-prose` for this reason, even though it also renders non-prose button/grid markup.
 - **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.
+- **UButton's `to` prop treats any same-origin-looking path as a Nuxt route, even if Nuxt doesn't serve it.** `CtaButton.vue` takes a fixed (non-caller-configurable) `external` prop per destination, set by each `Cta*` wrapper — `true` when the href still points at an 11ty-served route (would otherwise 404 via client-side Vue Router instead of reaching the 11ty proxy), `false` once that route is served by Nuxt. `CtaContactUs`/`CtaBookDemo` now set `false` — `/contact-us` and `/book-demo` are Nuxt routes (`nuxt/pages/contact-us/index.vue`, `nuxt/pages/book-demo/index.vue`). `CtaSignUp`/`CtaSignIn` set `false` too, moot since those hrefs are already cross-origin.
 
 ## Naming conventions
 
diff --git a/nuxt/components/CtaBookDemo.vue b/nuxt/components/CtaBookDemo.vue
index c22e704d5a..1667b0fe17 100644
--- a/nuxt/components/CtaBookDemo.vue
+++ b/nuxt/components/CtaBookDemo.vue
@@ -17,10 +17,8 @@ withDefaults(defineProps<{
 const EVENT = 'cta-book-demo'
 const HREF = '/book-demo/'
 const LABEL = 'Book a Demo'
-// /book-demo/ is still served by 11ty, not a Nuxt route - see CtaButton.vue's
-// `external` prop.
 
 
 
diff --git a/nuxt/components/CtaContactUs.vue b/nuxt/components/CtaContactUs.vue
index e6d5f03185..39d056bcd7 100644
--- a/nuxt/components/CtaContactUs.vue
+++ b/nuxt/components/CtaContactUs.vue
@@ -17,10 +17,8 @@ withDefaults(defineProps<{
 const EVENT = 'cta-contact-us'
 const HREF = '/contact-us/'
 const LABEL = 'Contact Us'
-// /contact-us/ is still served by 11ty, not a Nuxt route - see CtaButton.vue's
-// `external` prop.
 
 
 
diff --git a/nuxt/components/HubSpotForm.vue b/nuxt/components/HubSpotForm.vue
index 0e6627749e..e0f35ad08c 100644
--- a/nuxt/components/HubSpotForm.vue
+++ b/nuxt/components/HubSpotForm.vue
@@ -10,6 +10,7 @@ const props = defineProps<{
 const containerId = `hs-form-${props.formId.replace(/-/g, '')}`
 const region = props.region ?? 'eu1'
 const portalId = props.portalId ?? '26586079'
+const showFallback = ref(false)
 
 function loadScript(): Promise {
     return new Promise((resolve, reject) => {
@@ -48,11 +49,21 @@ onMounted(async () => {
             } : {}),
         })
     } catch {
-        // silently fail if HubSpot cannot load
+        showFallback.value = true
     }
 })
 
 
 
diff --git a/nuxt/components/HubSpotMeetings.vue b/nuxt/components/HubSpotMeetings.vue
new file mode 100644
index 0000000000..5a570fb0ec
--- /dev/null
+++ b/nuxt/components/HubSpotMeetings.vue
@@ -0,0 +1,62 @@
+
+
+
diff --git a/nuxt/components/MqlContactPage.vue b/nuxt/components/MqlContactPage.vue
new file mode 100644
index 0000000000..9ee0257864
--- /dev/null
+++ b/nuxt/components/MqlContactPage.vue
@@ -0,0 +1,58 @@
+
+
+
diff --git a/nuxt/nuxt.config.ts b/nuxt/nuxt.config.ts
index 2f4384df38..a813fc3082 100644
--- a/nuxt/nuxt.config.ts
+++ b/nuxt/nuxt.config.ts
@@ -316,6 +316,8 @@ export default defineNuxtConfig({
                     '/integrations',
                     '/pricing',
                     '/product',
+                    '/contact-us',
+                    '/book-demo',
                     '/ebooks/beginner-guide-to-a-professional-nodered/',
                     '/ebooks/ultimate-guide-to-building-applications-with-flowfuse-dashboard-for-node-red/',
                     '/whitepaper/uns-decoupling-data-producers-and-consumers/',
diff --git a/nuxt/pages/book-demo/index.vue b/nuxt/pages/book-demo/index.vue
new file mode 100644
index 0000000000..64cf20573f
--- /dev/null
+++ b/nuxt/pages/book-demo/index.vue
@@ -0,0 +1,34 @@
+
+
+
diff --git a/nuxt/pages/contact-us/index.vue b/nuxt/pages/contact-us/index.vue
new file mode 100644
index 0000000000..a38b139d64
--- /dev/null
+++ b/nuxt/pages/contact-us/index.vue
@@ -0,0 +1,35 @@
+
+
+
diff --git a/nuxt/server/middleware/legacy.ts b/nuxt/server/middleware/legacy.ts
index 37f799bcd7..51ce6b9ae3 100644
--- a/nuxt/server/middleware/legacy.ts
+++ b/nuxt/server/middleware/legacy.ts
@@ -4,7 +4,7 @@ import { defineEventHandler, proxyRequest } from 'h3'
 // Extend this list as pages are migrated. Trailing slashes are matched automatically.
 // Note: /sitemap-legacy.xml is deliberately NOT listed here — it only exists in
 // nuxt/public/ after a production build, so in dev it must keep proxying to 11ty's _site/.
-const NUXT_ROUTES = new Set(['/terms', '/privacy-policy', '/integrations', '/resources/publications', '/sitemap.xml', '/robots.txt', '/llms.txt', '/llms-full.txt'])
+const NUXT_ROUTES = new Set(['/terms', '/privacy-policy', '/integrations', '/resources/publications', '/sitemap.xml', '/robots.txt', '/llms.txt', '/llms-full.txt', '/contact-us', '/book-demo'])
 
 // Path prefixes handled by Nuxt. Used for dynamic routes like /integrations/{id}.
 const NUXT_ROUTE_PREFIXES = ['/integrations/', '/raw/']
diff --git a/src/book-demo.njk b/src/book-demo.njk
deleted file mode 100644
index 0838cda7df..0000000000
--- a/src/book-demo.njk
+++ /dev/null
@@ -1,23 +0,0 @@
----
-layout: layouts/mql-contact.njk
-title: Walk through your setup with our team
-description: Walk through your setup, discuss your use case, and get guidance on the right approach for your production setup.
-sitemapPriority: 0.8
-image: /images/og-book-demo.jpeg
-meta:
-  description: Book a demo to understand how FlowFuse helps you professionalize your Node-RED deployment.
-hubspot:
-    script: "hubspot/hs-book-meeting.njk"
-    dataSrc: https://meetings-eu1.hubspot.com/michael-davis/round-robin-sales-team?embed=true
-otherChannels:
-  - title: Prefer to start with a message?
-    description: Share your use case and we’ll get back to you shortly.
-    buttonText: Contact us
-    buttonLink: /contact-us
-    icon: mail
-  - title: Looking for technical help?
-    description: Visit our Help Center for docs, guides, and community support.
-    buttonText: Help center
-    buttonLink: /support
-    icon: lifebuoy
----
diff --git a/src/contact-us.njk b/src/contact-us.njk
deleted file mode 100644
index 27b01ba645..0000000000
--- a/src/contact-us.njk
+++ /dev/null
@@ -1,24 +0,0 @@
----
-layout: layouts/mql-contact.njk
-title: Tell us about your setup
-description: 

Whether you're connecting systems, automating workflows, or scaling industrial data, we'll help you figure out the right approach for your production setup.

-sitemapPriority: 0.9 -meta: - description: Whether you're connecting systems, automating workflows, or scaling industrial data, we'll help you figure out the right approach for your production setup. -hubspot: - script: "hubspot/hs-form.njk" - formId: 734455e5-4cda-4329-97da-07e40cda791c - cta: "cta-contact-us" - reference: "contact-us" -otherChannels: - - title: Want to talk it through live? - description: Book a session and we'll go through your setup together. - buttonText: Book a session - buttonLink: https://meetings-eu1.hubspot.com/michael-davis/round-robin-sales-team - icon: calendar - - title: Need technical help instead? - description: Visit our Help Center for docs, guides and access to our support team. - buttonText: Help center - buttonLink: /support - icon: lifebuoy ---- From c53e678d85d76d43bc4c72a5c51b10d15d2cda69 Mon Sep 17 00:00:00 2001 From: ZJ van de Weg Date: Fri, 7 Aug 2026 11:39:55 -0700 Subject: [PATCH 2/3] Invert dev proxy split to a shrinking legacy list, migrate /thank-you to Nuxt legacy.ts now lists what's still on 11ty instead of what's migrated, so the list shrinks toward empty as pages move over. /thank-you is fully ported: 4 route variants, new stories/webinar/blog preview components, and stories/ webinars content collections sourced in place from src/. --- nuxt/components/ThankYouExploreMore.vue | 110 ++++++++++++++++++ nuxt/components/ThankYouStoriesBlock.vue | 39 +++++++ nuxt/content.config.ts | 32 +++++ nuxt/nuxt.config.ts | 1 + nuxt/pages/thank-you/contact.vue | 30 +++++ .../thank-you/download-platform-overview.vue | 30 +++++ nuxt/pages/thank-you/download.vue | 30 +++++ .../download_ebook-flowfuse-dashboard.vue | 30 +++++ nuxt/server/middleware/legacy.ts | 29 ++--- 9 files changed, 314 insertions(+), 17 deletions(-) create mode 100644 nuxt/components/ThankYouExploreMore.vue create mode 100644 nuxt/components/ThankYouStoriesBlock.vue create mode 100644 nuxt/pages/thank-you/contact.vue create mode 100644 nuxt/pages/thank-you/download-platform-overview.vue create mode 100644 nuxt/pages/thank-you/download.vue create mode 100644 nuxt/pages/thank-you/download_ebook-flowfuse-dashboard.vue diff --git a/nuxt/components/ThankYouExploreMore.vue b/nuxt/components/ThankYouExploreMore.vue new file mode 100644 index 0000000000..89b5946217 --- /dev/null +++ b/nuxt/components/ThankYouExploreMore.vue @@ -0,0 +1,110 @@ + + + diff --git a/nuxt/components/ThankYouStoriesBlock.vue b/nuxt/components/ThankYouStoriesBlock.vue new file mode 100644 index 0000000000..272f0dda1b --- /dev/null +++ b/nuxt/components/ThankYouStoriesBlock.vue @@ -0,0 +1,39 @@ + + + diff --git a/nuxt/content.config.ts b/nuxt/content.config.ts index b82fc55749..cf2d38bfb6 100644 --- a/nuxt/content.config.ts +++ b/nuxt/content.config.ts @@ -144,6 +144,38 @@ export default defineContentConfig({ sitemap: defineSitemapSchema(), }) }), + // Source files stay at src/customer-stories/ (still served by 11ty, see + // LEGACY_PREFIXES in nuxt/server/middleware/legacy.ts) - this collection only + // feeds the ThankYouStoriesBlock tiles, it isn't rendered as Nuxt pages, so it + // deliberately has no `sitemap` field. + stories: defineCollection({ + type: 'page', + source: { + cwd: join(__dirname, '../src'), + include: 'customer-stories/*.md', + }, + schema: z.object({ + image: z.string().optional(), + logo: z.string().optional(), + story: z.object({ + brand: z.string(), + }), + }) + }), + // Source files stay at src/webinars/ (still served by 11ty, see LEGACY_PREFIXES + // in nuxt/server/middleware/legacy.ts) - only queried for the "Latest/Upcoming + // Webinar" tile on the thank-you pages, so no `sitemap` field either. + webinars: defineCollection({ + type: 'page', + source: { + cwd: join(__dirname, '../src'), + include: 'webinars/**/*.md', + }, + schema: z.object({ + date: z.coerce.date(), + time: z.string().optional(), + }) + }), ebooks: defineCollection({ type: 'page', source: 'ebooks/*.md', diff --git a/nuxt/nuxt.config.ts b/nuxt/nuxt.config.ts index a813fc3082..d9b2bd185a 100644 --- a/nuxt/nuxt.config.ts +++ b/nuxt/nuxt.config.ts @@ -278,6 +278,7 @@ export default defineNuxtConfig({ routeRules: { '/terms': { robots: false }, '/privacy-policy': { robots: false }, + '/thank-you/**': { robots: false }, ...redirects, }, diff --git a/nuxt/pages/thank-you/contact.vue b/nuxt/pages/thank-you/contact.vue new file mode 100644 index 0000000000..243b6efb06 --- /dev/null +++ b/nuxt/pages/thank-you/contact.vue @@ -0,0 +1,30 @@ + + + diff --git a/nuxt/pages/thank-you/download-platform-overview.vue b/nuxt/pages/thank-you/download-platform-overview.vue new file mode 100644 index 0000000000..e9deb813f4 --- /dev/null +++ b/nuxt/pages/thank-you/download-platform-overview.vue @@ -0,0 +1,30 @@ + + + diff --git a/nuxt/pages/thank-you/download.vue b/nuxt/pages/thank-you/download.vue new file mode 100644 index 0000000000..8cadfa5975 --- /dev/null +++ b/nuxt/pages/thank-you/download.vue @@ -0,0 +1,30 @@ + + + diff --git a/nuxt/pages/thank-you/download_ebook-flowfuse-dashboard.vue b/nuxt/pages/thank-you/download_ebook-flowfuse-dashboard.vue new file mode 100644 index 0000000000..be12b6e385 --- /dev/null +++ b/nuxt/pages/thank-you/download_ebook-flowfuse-dashboard.vue @@ -0,0 +1,30 @@ + + + diff --git a/nuxt/server/middleware/legacy.ts b/nuxt/server/middleware/legacy.ts index 51ce6b9ae3..af4a78cf90 100644 --- a/nuxt/server/middleware/legacy.ts +++ b/nuxt/server/middleware/legacy.ts @@ -1,16 +1,12 @@ import { defineEventHandler, proxyRequest } from 'h3' -// Routes that are handled by Nuxt pages (not proxied to 11ty). -// Extend this list as pages are migrated. Trailing slashes are matched automatically. -// Note: /sitemap-legacy.xml is deliberately NOT listed here — it only exists in -// nuxt/public/ after a production build, so in dev it must keep proxying to 11ty's _site/. -const NUXT_ROUTES = new Set(['/terms', '/privacy-policy', '/integrations', '/resources/publications', '/sitemap.xml', '/robots.txt', '/llms.txt', '/llms-full.txt', '/contact-us', '/book-demo']) +// Routes still served by 11ty (not yet migrated to Nuxt). +// Shrink this list as pages are migrated; once empty, this middleware can be deleted. +// Trailing slashes are matched automatically. +const LEGACY_ROUTES = new Set(['/', '/sitemap-legacy.xml']) -// Path prefixes handled by Nuxt. Used for dynamic routes like /integrations/{id}. -const NUXT_ROUTE_PREFIXES = ['/integrations/', '/raw/'] - -// Route prefixes handled by Nuxt (all paths starting with these are served by Nuxt). -const NUXT_PREFIXES = ['/handbook', '/ebooks', '/whitepaper', '/pricing', '/docs', '/changelog', '/application-guide', '/blog', '/product'] +// Path prefixes still served by 11ty (all paths starting with these are proxied). +const LEGACY_PREFIXES = ['/customer-stories', '/industries', '/jobs', '/landing', '/node-red', '/partners', '/platform', '/use-cases', '/vs', '/webinars', '/events', '/community', '/ask-me-anything', '/sign-up'] export default defineEventHandler(async (event) => { if (process.env.NODE_ENV !== 'development') return @@ -24,17 +20,16 @@ export default defineEventHandler(async (event) => { const queryIndex = path.indexOf('?') const pathWithoutQuery = queryIndex === -1 ? path : path.slice(0, queryIndex) const normalised = pathWithoutQuery.replace(/\/$/, '') || '/' - if (NUXT_ROUTES.has(normalised)) return - if (NUXT_ROUTE_PREFIXES.some(prefix => pathWithoutQuery.startsWith(prefix))) return // Changelog and blog post images still live alongside their markdown in src/{changelog,blog}/**/images // and are only synced into nuxt/public by the 11ty passthrough during a production build - - // proxy them to 11ty in dev even though /changelog and /blog are Nuxt-handled prefixes. + // proxy them to 11ty in dev even though /changelog and /blog are otherwise Nuxt-handled. if (/^\/(changelog|blog)\/\d{4}\/\d{2}\/images\//.test(normalised)) return proxyRequest(event, `http://localhost:8080${path}`) - // Let Nuxt handle migrated path prefixes - if (NUXT_PREFIXES.some(prefix => normalised === prefix || normalised.startsWith(prefix + '/'))) return + // Note: /sitemap-legacy.xml is deliberately not routed to Nuxt — it only exists in + // nuxt/public/ after a production build, so in dev it must keep proxying to 11ty's _site/. + if (LEGACY_ROUTES.has(normalised)) return proxyRequest(event, `http://localhost:8080${path}`) + if (LEGACY_PREFIXES.some(prefix => normalised === prefix || normalised.startsWith(prefix + '/'))) return proxyRequest(event, `http://localhost:8080${path}`) - // Proxy everything else to the 11ty dev server - return proxyRequest(event, `http://localhost:8080${path}`) + // Everything else is a migrated Nuxt route. }) From efc82bf8ba130d4f4dc15dc928fe680b575247da Mon Sep 17 00:00:00 2001 From: Zeger-Jan van de Weg Date: Wed, 12 Aug 2026 07:29:49 -0700 Subject: [PATCH 3/3] Update nuxt/server/middleware/legacy.ts Co-authored-by: Yndira Escobar <129537638+Yndira-E@users.noreply.github.com> --- nuxt/server/middleware/legacy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuxt/server/middleware/legacy.ts b/nuxt/server/middleware/legacy.ts index af4a78cf90..25f2a1aef2 100644 --- a/nuxt/server/middleware/legacy.ts +++ b/nuxt/server/middleware/legacy.ts @@ -6,7 +6,7 @@ import { defineEventHandler, proxyRequest } from 'h3' const LEGACY_ROUTES = new Set(['/', '/sitemap-legacy.xml']) // Path prefixes still served by 11ty (all paths starting with these are proxied). -const LEGACY_PREFIXES = ['/customer-stories', '/industries', '/jobs', '/landing', '/node-red', '/partners', '/platform', '/use-cases', '/vs', '/webinars', '/events', '/community', '/ask-me-anything', '/sign-up'] +const LEGACY_PREFIXES = ['/customer-stories', '/industries', '/landing', '/node-red', '/partners', '/platform', '/use-cases', '/vs', '/webinars', '/events', '/community', '/about', '/ai', '/blueprints', '/careers', '/email-signature', '/free-consultation', '/professional-services', '/support'] export default defineEventHandler(async (event) => { if (process.env.NODE_ENV !== 'development') return