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/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 2f4384df38..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,
     },
 
@@ -316,6 +317,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/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 37f799bcd7..25f2a1aef2 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'])
+// 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', '/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
@@ -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.
 })
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 ----