From 0a84cbd4d171aec8f1ceaf8ce8bf165be30c4583 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Thu, 10 Sep 2026 17:24:37 +0200 Subject: [PATCH 1/3] events: serve /events/ from Nuxt 3 routes move off 11ty. These are three one-off pages on layouts/base.njk, which added only the header and footer, so each stays its own page with its former frontmatter as local data. The three "book a demo" buttons were hand-written ff-btn links to /book-demo and fired no event; they are now, so they carry the registry's copy and tracking. The two in-page jump links used an inline onclick calling scrollIntoView and go through useScrollToAnchor, the same helper the pages that included smooth-scroll.njk use. /events/hannover-messe-2025/ had two

s closed with a stray , and a heading using
, which is not a tag; the markup is well-formed here. Verified all 3 against production. The only differences are the demo buttons' label and href, which now come from the CTA registry ("Book a Demo", uppercased by CSS, and a trailing slash) rather than being typed per page. --- nuxt/components/SiteArt.vue | 20 ++ nuxt/composables/useScrollToAnchor.ts | 9 + nuxt/nuxt.config.ts | 4 + nuxt/pages/events/hannover-messe-2025.vue | 182 +++++++++++++ nuxt/pages/events/hannover-messe-2026.vue | 317 ++++++++++++++++++++++ nuxt/pages/events/proveit-2026.vue | 158 +++++++++++ nuxt/server/middleware/legacy.ts | 2 +- nuxt/utils/siteArt.ts | 196 +++++++++++++ scripts/gen-site-art.mjs | 103 +++++++ src/events/hannover-messe-2025.njk | 171 ------------ src/events/hannover-messe-2026.njk | 307 --------------------- src/events/proveit-2026.njk | 145 ---------- 12 files changed, 990 insertions(+), 624 deletions(-) create mode 100644 nuxt/components/SiteArt.vue create mode 100644 nuxt/composables/useScrollToAnchor.ts create mode 100644 nuxt/pages/events/hannover-messe-2025.vue create mode 100644 nuxt/pages/events/hannover-messe-2026.vue create mode 100644 nuxt/pages/events/proveit-2026.vue create mode 100644 nuxt/utils/siteArt.ts create mode 100755 scripts/gen-site-art.mjs delete mode 100644 src/events/hannover-messe-2025.njk delete mode 100644 src/events/hannover-messe-2026.njk delete mode 100644 src/events/proveit-2026.njk diff --git a/nuxt/components/SiteArt.vue b/nuxt/components/SiteArt.vue new file mode 100644 index 0000000000..7d0d8acee0 --- /dev/null +++ b/nuxt/components/SiteArt.vue @@ -0,0 +1,20 @@ + + + diff --git a/nuxt/composables/useScrollToAnchor.ts b/nuxt/composables/useScrollToAnchor.ts new file mode 100644 index 0000000000..501e1c3b45 --- /dev/null +++ b/nuxt/composables/useScrollToAnchor.ts @@ -0,0 +1,9 @@ +// src/_includes/smooth-scroll.njk, which four pages pulled in to make one in-page link +// scroll rather than jump. The .njk defined a global function and wired it through an +// inline onclick; this is the same behaviour as a handler the template can bind. +export function useScrollToAnchor() { + return function scrollToAnchor(event: Event, anchorId: string) { + event.preventDefault() + document.getElementById(anchorId)?.scrollIntoView({ behavior: 'smooth' }) + } +} diff --git a/nuxt/nuxt.config.ts b/nuxt/nuxt.config.ts index fbcfd8847f..0da01f976b 100644 --- a/nuxt/nuxt.config.ts +++ b/nuxt/nuxt.config.ts @@ -428,6 +428,10 @@ export default defineNuxtConfig({ '/pricing/request-quote/', // The homepage itself: nothing links to it that the crawler starts from. '/', + // The event pages are .vue files with no listing to crawl from. + '/events/proveit-2026/', + '/events/hannover-messe-2026/', + '/events/hannover-messe-2025/', // /ai is only linked from 11ty-generated HTML (nav, homepage), which the // Nuxt prerender crawler never parses, so it has to be listed explicitly // or the route is missing from nuxt/dist and every link to it breaks. diff --git a/nuxt/pages/events/hannover-messe-2025.vue b/nuxt/pages/events/hannover-messe-2025.vue new file mode 100644 index 0000000000..88cbb87b7d --- /dev/null +++ b/nuxt/pages/events/hannover-messe-2025.vue @@ -0,0 +1,182 @@ + + + diff --git a/nuxt/pages/events/hannover-messe-2026.vue b/nuxt/pages/events/hannover-messe-2026.vue new file mode 100644 index 0000000000..3c4e68b621 --- /dev/null +++ b/nuxt/pages/events/hannover-messe-2026.vue @@ -0,0 +1,317 @@ + + + diff --git a/nuxt/pages/events/proveit-2026.vue b/nuxt/pages/events/proveit-2026.vue new file mode 100644 index 0000000000..24bbf2c491 --- /dev/null +++ b/nuxt/pages/events/proveit-2026.vue @@ -0,0 +1,158 @@ + + + diff --git a/nuxt/server/middleware/legacy.ts b/nuxt/server/middleware/legacy.ts index e55d21ae74..616f1f77e3 100644 --- a/nuxt/server/middleware/legacy.ts +++ b/nuxt/server/middleware/legacy.ts @@ -19,7 +19,7 @@ const NUXT_PREFIXES = ['/handbook', '/ebooks', '/whitepaper', '/pricing', '/docs // Top-level routes still on 11ty, not yet ported to Nuxt (everything not listed above // already falls through to the 11ty proxy by default). Remove entries here as they migrate: -// /about, /blueprints, /careers, /community, /events +// /about, /blueprints, /careers, /community // /free-consultation, /industries, /landing, /node-red, /partners, /platform, // /use-cases, /webinars diff --git a/nuxt/utils/siteArt.ts b/nuxt/utils/siteArt.ts new file mode 100644 index 0000000000..3404c74719 --- /dev/null +++ b/nuxt/utils/siteArt.ts @@ -0,0 +1,196 @@ +// SVG files that page content included raw, with no wrapper: {% include "components/icons/ +// x.svg" %} rather than the `navoption`/`ffIconLg` shortcodes that NavIcon reproduces. +// +// They cannot go through NavIcon or an : +// - NavIcon wraps its payload in an outer carrying a fixed ff-icon size class, +// which would override the size the caller's box sets. +// - breaks `currentColor`, and most of these are coloured by their container. +// +// GENERATED by scripts/gen-site-art.mjs from the keys the pages and content collections +// actually reference, so the bundle carries only what is used - an eager glob of that +// directory inlines ~137 KB, most of it unused art. Re-run it after adding a page that +// names a new icon. + +import aAcademicCap from '../../src/_includes/components/icons/academic-cap.svg?raw' +import aAdjustmentsHorizontal from '../../src/_includes/components/icons/adjustments-horizontal.svg?raw' +import aAdjustmentsVertical from '../../src/_includes/components/icons/adjustments-vertical.svg?raw' +import aAirGappedDeployment from '../../src/_includes/components/icons/air-gapped-deployment.svg?raw' +import aAirplane from '../../src/_includes/components/icons/airplane.svg?raw' +import aArchiveBoxXMark from '../../src/_includes/components/icons/archive-box-x-mark.svg?raw' +import aArrowLongRight from '../../src/_includes/components/icons/arrow-long-right.svg?raw' +import aArrowPath from '../../src/_includes/components/icons/arrow-path.svg?raw' +import aArrowPathRoundedSquare from '../../src/_includes/components/icons/arrow-path-rounded-square.svg?raw' +import aArrowSmallDown from '../../src/_includes/components/icons/arrow-small-down.svg?raw' +import aArrowTopRightOnSquare from '../../src/_includes/components/icons/arrow-top-right-on-square.svg?raw' +import aArrowTrendingUp from '../../src/_includes/components/icons/arrow-trending-up.svg?raw' +import aArrows from '../../src/_includes/components/icons/arrows.svg?raw' +import aArrowsPointingOut from '../../src/_includes/components/icons/arrows-pointing-out.svg?raw' +import aArrowsRightLeft from '../../src/_includes/components/icons/arrows-right-left.svg?raw' +import aAuditLogs from '../../src/_includes/components/icons/audit-logs.svg?raw' +import aBellAlert from '../../src/_includes/components/icons/bell-alert.svg?raw' +import aBolt from '../../src/_includes/components/icons/bolt.svg?raw' +import aBookOpen from '../../src/_includes/components/icons/book-open.svg?raw' +import aBuildingOffice2 from '../../src/_includes/components/icons/building-office-2.svg?raw' +import aCalendar from '../../src/_includes/components/icons/calendar.svg?raw' +import aCamera from '../../src/_includes/components/icons/camera.svg?raw' +import aCar from '../../src/_includes/components/icons/car.svg?raw' +import aCertificate from '../../src/_includes/components/icons/certificate.svg?raw' +import aCertifiedNode from '../../src/_includes/components/icons/certified-node.svg?raw' +import aChart from '../../src/_includes/components/icons/chart.svg?raw' +import aChatBubbleBottomCenterText from '../../src/_includes/components/icons/chat-bubble-bottom-center-text.svg?raw' +import aChatBubbleLeftRightSm from '../../src/_includes/components/icons/chat-bubble-left-right-sm.svg?raw' +import aCheckBadge from '../../src/_includes/components/icons/check-badge.svg?raw' +import aCheckCircle from '../../src/_includes/components/icons/check-circle.svg?raw' +import aChevronDown from '../../src/_includes/components/icons/chevron-down.svg?raw' +import aChevronLeft from '../../src/_includes/components/icons/chevron-left.svg?raw' +import aChevronRight from '../../src/_includes/components/icons/chevron-right.svg?raw' +import aChevronRightSm from '../../src/_includes/components/icons/chevron-right-sm.svg?raw' +import aChip from '../../src/_includes/components/icons/chip.svg?raw' +import aCircleStack from '../../src/_includes/components/icons/circle-stack.svg?raw' +import aClipList from '../../src/_includes/components/icons/clip-list.svg?raw' +import aClipboardDocumentCheck from '../../src/_includes/components/icons/clipboard-document-check.svg?raw' +import aClock from '../../src/_includes/components/icons/clock.svg?raw' +import aCloud from '../../src/_includes/components/icons/cloud.svg?raw' +import aCodeBracket from '../../src/_includes/components/icons/code-bracket.svg?raw' +import aCodeblock from '../../src/_includes/components/icons/codeblock.svg?raw' +import aCog from '../../src/_includes/components/icons/cog.svg?raw' +import aCog6Tooth from '../../src/_includes/components/icons/cog-6-tooth.svg?raw' +import aComputerDesktop from '../../src/_includes/components/icons/computer-desktop.svg?raw' +import aCubeTransparent from '../../src/_includes/components/icons/cube-transparent.svg?raw' +import aCursorArrowRays from '../../src/_includes/components/icons/cursor-arrow-rays.svg?raw' +import aData from '../../src/_includes/components/icons/data.svg?raw' +import aDatabase from '../../src/_includes/components/icons/database.svg?raw' +import aDocumentArrowUp from '../../src/_includes/components/icons/document-arrow-up.svg?raw' +import aDocumentChartBar from '../../src/_includes/components/icons/document-chart-bar.svg?raw' +import aDocumentCheck from '../../src/_includes/components/icons/document-check.svg?raw' +import aDocumentText from '../../src/_includes/components/icons/document-text.svg?raw' +import aEye from '../../src/_includes/components/icons/eye.svg?raw' +import aEyeSlash from '../../src/_includes/components/icons/eye-slash.svg?raw' +import aFactory from '../../src/_includes/components/icons/factory.svg?raw' +import aGlobeAlt from '../../src/_includes/components/icons/globe-alt.svg?raw' +import aHandRaised from '../../src/_includes/components/icons/hand-raised.svg?raw' +import aLayers from '../../src/_includes/components/icons/layers.svg?raw' +import aLifebuoy from '../../src/_includes/components/icons/lifebuoy.svg?raw' +import aLightBulb from '../../src/_includes/components/icons/light-bulb.svg?raw' +import aLink from '../../src/_includes/components/icons/link.svg?raw' +import aLinkSlash from '../../src/_includes/components/icons/link-slash.svg?raw' +import aLock from '../../src/_includes/components/icons/lock.svg?raw' +import aLockClosed from '../../src/_includes/components/icons/lock-closed.svg?raw' +import aLockOpen from '../../src/_includes/components/icons/lock-open.svg?raw' +import aPin from '../../src/_includes/components/icons/pin.svg?raw' +import aPinSlash from '../../src/_includes/components/icons/pin-slash.svg?raw' +import aPulse from '../../src/_includes/components/icons/pulse.svg?raw' +import aPuzzlePiece from '../../src/_includes/components/icons/puzzle-piece.svg?raw' +import aQueueList from '../../src/_includes/components/icons/queue-list.svg?raw' +import aRectangleStack from '../../src/_includes/components/icons/rectangle-stack.svg?raw' +import aRocketLaunch from '../../src/_includes/components/icons/rocket-launch.svg?raw' +import aRoleBasedAccess from '../../src/_includes/components/icons/role-based-access.svg?raw' +import aServerStack from '../../src/_includes/components/icons/server-stack.svg?raw' +import aShare from '../../src/_includes/components/icons/share.svg?raw' +import aShieldCheck from '../../src/_includes/components/icons/shield-check.svg?raw' +import aShieldExclamation from '../../src/_includes/components/icons/shield-exclamation.svg?raw' +import aSingleSignOn from '../../src/_includes/components/icons/single-sign-on.svg?raw' +import aSnowflake from '../../src/_includes/components/icons/snowflake.svg?raw' +import aSparkles from '../../src/_includes/components/icons/sparkles.svg?raw' +import aSquares2x2 from '../../src/_includes/components/icons/squares-2x2.svg?raw' +import aSquaresPlus from '../../src/_includes/components/icons/squares-plus.svg?raw' +import aTargetView from '../../src/_includes/components/icons/target-view.svg?raw' +import aUns from '../../src/_includes/components/icons/uns.svg?raw' +import aUserGroup from '../../src/_includes/components/icons/user-group.svg?raw' +import aUsers from '../../src/_includes/components/icons/users.svg?raw' +import aWifi from '../../src/_includes/components/icons/wifi.svg?raw' +import aWindTurbine from '../../src/_includes/components/icons/wind-turbine.svg?raw' +import aWrenchScrewdriver from '../../src/_includes/components/icons/wrench-screwdriver.svg?raw' + +export const siteArt: Record = { + 'academic-cap': aAcademicCap, + 'adjustments-horizontal': aAdjustmentsHorizontal, + 'adjustments-vertical': aAdjustmentsVertical, + 'air-gapped-deployment': aAirGappedDeployment, + 'airplane': aAirplane, + 'archive-box-x-mark': aArchiveBoxXMark, + 'arrow-long-right': aArrowLongRight, + 'arrow-path': aArrowPath, + 'arrow-path-rounded-square': aArrowPathRoundedSquare, + 'arrow-small-down': aArrowSmallDown, + 'arrow-top-right-on-square': aArrowTopRightOnSquare, + 'arrow-trending-up': aArrowTrendingUp, + 'arrows': aArrows, + 'arrows-pointing-out': aArrowsPointingOut, + 'arrows-right-left': aArrowsRightLeft, + 'audit-logs': aAuditLogs, + 'bell-alert': aBellAlert, + 'bolt': aBolt, + 'book-open': aBookOpen, + 'building-office-2': aBuildingOffice2, + 'calendar': aCalendar, + 'camera': aCamera, + 'car': aCar, + 'certificate': aCertificate, + 'certified-node': aCertifiedNode, + 'chart': aChart, + 'chat-bubble-bottom-center-text': aChatBubbleBottomCenterText, + 'chat-bubble-left-right-sm': aChatBubbleLeftRightSm, + 'check-badge': aCheckBadge, + 'check-circle': aCheckCircle, + 'chevron-down': aChevronDown, + 'chevron-left': aChevronLeft, + 'chevron-right': aChevronRight, + 'chevron-right-sm': aChevronRightSm, + 'chip': aChip, + 'circle-stack': aCircleStack, + 'clip-list': aClipList, + 'clipboard-document-check': aClipboardDocumentCheck, + 'clock': aClock, + 'cloud': aCloud, + 'code-bracket': aCodeBracket, + 'codeblock': aCodeblock, + 'cog': aCog, + 'cog-6-tooth': aCog6Tooth, + 'computer-desktop': aComputerDesktop, + 'cube-transparent': aCubeTransparent, + 'cursor-arrow-rays': aCursorArrowRays, + 'data': aData, + 'database': aDatabase, + 'document-arrow-up': aDocumentArrowUp, + 'document-chart-bar': aDocumentChartBar, + 'document-check': aDocumentCheck, + 'document-text': aDocumentText, + 'eye': aEye, + 'eye-slash': aEyeSlash, + 'factory': aFactory, + 'globe-alt': aGlobeAlt, + 'hand-raised': aHandRaised, + 'layers': aLayers, + 'lifebuoy': aLifebuoy, + 'light-bulb': aLightBulb, + 'link': aLink, + 'link-slash': aLinkSlash, + 'lock': aLock, + 'lock-closed': aLockClosed, + 'lock-open': aLockOpen, + 'pin': aPin, + 'pin-slash': aPinSlash, + 'pulse': aPulse, + 'puzzle-piece': aPuzzlePiece, + 'queue-list': aQueueList, + 'rectangle-stack': aRectangleStack, + 'rocket-launch': aRocketLaunch, + 'role-based-access': aRoleBasedAccess, + 'server-stack': aServerStack, + 'share': aShare, + 'shield-check': aShieldCheck, + 'shield-exclamation': aShieldExclamation, + 'single-sign-on': aSingleSignOn, + 'snowflake': aSnowflake, + 'sparkles': aSparkles, + 'squares-2x2': aSquares2x2, + 'squares-plus': aSquaresPlus, + 'target-view': aTargetView, + 'uns': aUns, + 'user-group': aUserGroup, + 'users': aUsers, + 'wifi': aWifi, + 'wind-turbine': aWindTurbine, + 'wrench-screwdriver': aWrenchScrewdriver, +} diff --git a/scripts/gen-site-art.mjs b/scripts/gen-site-art.mjs new file mode 100755 index 0000000000..cf57a0dbaf --- /dev/null +++ b/scripts/gen-site-art.mjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node +// Regenerate nuxt/utils/siteArt.ts from the icon keys the site actually references. +// +// inlines an SVG from src/_includes/components/icons/ verbatim, which is what +// page content that used a bare {% include %} needs. The registry is written out rather +// than globbed, so the bundle carries only the icons in use - an eager glob of that +// directory inlines ~137 KB, most of it unused art. +// +// Keeping it hand-maintained across ~50 keys is not realistic, so this scans for every +// key that is referenced and rewrites the file. Run it after adding a page that names a +// new icon; the build fails loudly on a missing import, and warns in dev on a +// key the registry does not have. +// +// Usage: node scripts/gen-site-art.mjs [repo-root] +import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' + +const ICON_DIR = 'src/_includes/components/icons' + +function filesUnder (dir, ext) { + if (!existsSync(dir)) return [] + return readdirSync(dir, { recursive: true, withFileTypes: true }) + .filter(entry => entry.isFile() && entry.name.endsWith(ext)) + .map(entry => join(entry.parentPath ?? entry.path, entry.name)) +} + +function matchAll (text, pattern) { + return [...text.matchAll(pattern)].map(match => match[1]) +} + +// `explicit` are literal uses - a missing file there is a broken +// page, so it fails the run. `harvested` are svgPath:/icon: values in content and page +// consts, which a renderer *may* pass to SiteArt but may equally resolve some other way +// (contact-us has its own icon map, for one), so a miss there is only reported. +function referencedKeys (root) { + const vues = [...filesUnder(join(root, 'nuxt/pages'), '.vue'), ...filesUnder(join(root, 'nuxt/components'), '.vue')] + const explicit = new Set() + for (const file of vues) { + for (const key of matchAll(readFileSync(file, 'utf8'), / name resolved from an + // installed icon set, not a file in this repo, so it is not SiteArt's to provide. + if (!key.startsWith('i-')) harvested.add(key) + } + } + return { explicit, harvested } +} + +const ident = key => 'a' + key.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join('') + +const HEADER = `// SVG files that page content included raw, with no wrapper: {% include "components/icons/ +// x.svg" %} rather than the \`navoption\`/\`ffIconLg\` shortcodes that NavIcon reproduces. +// +// They cannot go through NavIcon or an : +// - NavIcon wraps its payload in an outer carrying a fixed ff-icon size class, +// which would override the size the caller's box sets. +// - breaks \`currentColor\`, and most of these are coloured by their container. +// +// GENERATED by scripts/gen-site-art.mjs from the keys the pages and content collections +// actually reference, so the bundle carries only what is used - an eager glob of that +// directory inlines ~137 KB, most of it unused art. Re-run it after adding a page that +// names a new icon. +` + +const root = resolve(process.argv[2] ?? '.') +const { explicit, harvested } = referencedKeys(root) + +// Keys that resolve through NavIcon (the navoption/ffIconLg wrapper) or a hand-written +// Vue icon component are not SiteArt's job. +const navFile = join(root, 'nuxt/utils/navIcons.ts') +const navKeys = existsSync(navFile) ? new Set(matchAll(readFileSync(navFile, 'utf8'), /^ {4}'([a-z0-9-]+)':/gm)) : new Set() + +const usable = [] +const missing = [] +for (const key of [...new Set([...explicit, ...harvested])].sort()) { + (existsSync(join(root, ICON_DIR, `${key}.svg`)) ? usable : missing).push(key) +} + +const lines = [HEADER] +for (const key of usable) lines.push(`import ${ident(key)} from '../../${ICON_DIR}/${key}.svg?raw'`) +lines.push('\nexport const siteArt: Record = {') +for (const key of usable) lines.push(` '${key}': ${ident(key)},`) +lines.push('}\n') + +const out = join(root, 'nuxt/utils/siteArt.ts') +writeFileSync(out, lines.join('\n')) +console.log(`${out}: ${usable.length} icons`) + +const alsoInNav = usable.filter(key => navKeys.has(key)) +if (alsoInNav.length) console.log(` (also in navIcons, which is fine - different wrapper: ${alsoInNav.slice(0, 6).join(', ')}…)`) +if (missing.length) console.log(` no SVG file (resolved elsewhere, not bundled here): ${missing.join(', ')}`) + +const broken = missing.filter(key => explicit.has(key)) +if (broken.length) { + console.log(` ERROR: is asked for these and there is no file: ${broken.join(', ')}`) + process.exit(1) +} diff --git a/src/events/hannover-messe-2025.njk b/src/events/hannover-messe-2025.njk deleted file mode 100644 index fb8187a1a6..0000000000 --- a/src/events/hannover-messe-2025.njk +++ /dev/null @@ -1,171 +0,0 @@ ---- -layout: layouts/base.njk -tags: - - trade-show -title: FlowFuse at Hannover Messe 2025 -meta: - description: Visit us at Hall 15, Stand H71 and experience live demos, explore real-time data visualisations, and see how open source innovation is shaping the future of industrial operations. -heroSection: - image: events/images/hm25/hm-25-hero.png - imgAlt: Image of people at the FlowFuse booth - logo: events/images/hm25/hm_logo.png - logoAlt: Hannover Messe Logo - content: - - Discover how FlowFuse empowers you to build, deploy, and scale industrial automation, your way. - - Visit us at Hall 15, Stand H71 and experience live demos, explore - real-time data visualisations, and see how open source innovation is shaping the future of - industrial operations. - - Let’s shape the future of industrial automation, together. - cta: CLAIM FREE TICKET - ctaUrl: "https://www.hannovermesse.de/en/application/registration/direct-entry-tickets-passes?code=rkGXe" -demoSection: - title: Visit Us and Unlock the Future of Modular Industrial Software - image: events/images/hm25/hm-25-demo.png - imgAlt: Image of people attending the FlowFuse demo. - contentIntro: "At Hannover Messe 2025, you’ll see how FlowFuse empowers you to enhance your existing MES and explore an alternative to traditional SCADA systems. You’ll experience:" - contentHiglights: - - Real-time production monitoring for instant insights - - Seamless integration with your existing systems - - Fully customisable dashboards and alerts - - Hands-on demos that put you in control of your operations - contentOutro: - - At FlowFuse, we believe in empowering engineers to take charge, designing, adapting, and scaling solutions to meet unique operational challenges. - - Visit us at Hall 15, Stand H71 and unlock the potential of smarter, more efficient manufacturing. -liveSession: - title: "On-Site Live Session:
How FlowFuse enables a future-proof UNS IT/OT architecture" - image: events/images/hm25/mayker-session-banner.jpg - imgAlt: "Banner indicating place and time of the live session" - description: - - Join us at our booth on April 1st at 11:30 am for an exclusive live session presented by Jef Lamerant and Diederik Vermeersch from the Mayker team on how to simplify IT/OT integration with a Unified Namespace (UNS). - - With deep expertise in digital manufacturing and industrial automation, Mayker helps companies bridge the gap between IT and OT systems for more connected, efficient operations. - highlights: - - The most common challenges in IT/OT integration, and how to overcome them - - Why a Unified Namespace (UNS) is essential for scalable, future-proof data management - - How FlowFuse enables low-code integration, edge/cloud connectivity, and real-time event processing - - Real-world case studies showcasing efficiency gains and reduced downtime - - Practical steps to implement a UNS, plus key considerations to keep in mind - ctaTitle: Don’t miss out! - ctaCopy: - - Stop by our booth, catch the session, and chat with our team to discover how we can help you unlock the full potential of your industrial data. - - After the session, we’ll be giving away three plug-and-play UNS-in-a-Box, be one of the lucky attendees to take one home! - ctaUrl: "/events/hm25-invite.ics" - ctaCta: ADD IT TO YOUR CALENDAR -bookDemo: - title: Can’t make it to Hannover Messe 2025? - content: - - No problem! You can still experience FlowFuse’s cutting-edge modular MES solution from the comfort of your own space. Book a personalised online demo with us and see how our flexible, real-time industrial software can optimise your operations. - - Our team is ready to walk you through the features that matter most to your business and answer any questions you may have. - - Take control of your industrial data and improve efficiency across your operations. - cta: BOOK A DEMO - ctaUrl: "/book-demo" ---- - -
-
-
-
-
-
- {% set imageSrc = ["./", heroSection.image ] | join %} - {% set imageDescription = heroSection.imgAlt %} - {% image imageSrc, imageDescription, [644] %} -
-
- {% set logoSrc = ["./", heroSection.logo ] | join %} - {% set logoDescription = heroSection.logoAlt %} - {% image logoSrc, logoDescription, [50] %} -
-
-
- -

{{ title | safe }}

- {% for item in heroSection.content %} -

{{ item | safe }}

- {% endfor %} - -
-
-
-
- -
-
-
-

{{ demoSection.title | safe }}

-

{{ demoSection.contentIntro | safe }}

-
-
    - {% for highlight in demoSection.contentHiglights %} -
  • {{ highlight | safe }}
  • - {% endfor %} -
-
- {% for item in demoSection.contentOutro %} -

{{ item | safe }}

- {% endfor %} -
-
-
- {% set imageSrc = ["./", demoSection.image ] | join %} - {% set imageDescription = demoSection.imgAlt %} - {% image imageSrc, imageDescription, [644] %} -
-
-
- -
-
-

{{ liveSession.title | safe }}

-
- {% set imageSrc = ["./", liveSession.image ] | join %} - {% set imageDescription = liveSession.imgAlt %} - {% image imageSrc, imageDescription, [1024] %} -
- {% for item in liveSession.description %} -

{{ item | safe }}

- {% endfor %} -
-
-
-

In this session, you’ll learn:

-
    - {% for highlight in liveSession.highlights %} -
  • {{ highlight | safe }}
  • - {% endfor %} -
-
-
-

{{ liveSession.ctaTitle | safe }}

- {% for item in liveSession.ctaCopy %} -

{{ item | safe }}

- {% endfor %} - -
-
-
- -
-

{{ bookDemo.title | safe }}

-
- {% for item in bookDemo.content %} -

{{ item | safe}}

- {% endfor %} -
- -
-
-
\ No newline at end of file diff --git a/src/events/hannover-messe-2026.njk b/src/events/hannover-messe-2026.njk deleted file mode 100644 index 83d24b7d75..0000000000 --- a/src/events/hannover-messe-2026.njk +++ /dev/null @@ -1,307 +0,0 @@ ---- -layout: layouts/base.njk -tags: - - trade-show -meta: - title: FlowFuse at Hannover Messe 2026 - description: Visit FlowFuse at Hannover Messe 2026 in Hall 014, Stand K26 for live demos, special sessions, and industrial connectivity in action. -heroSection: - image: events/images/hm26/hm-26-hero.png - imgAlt: Image of people at the FlowFuse booth - logo: events/images/hm25/hm_logo.png - logoAlt: Hannover Messe Logo -demoSection: - image: events/images/hm26/hm-26-demo.png - imgAlt: Image of people attending the FlowFuse demo. -specialSessions: - - image: events/images/hm26/nick-presenting.png - imgAlt: Nick O'Leary presenting - imageClass: center - labelClass: text-indigo-600 - label: Masterclass - title: 'Bridging IT and OT: How AI-Powered Industrial Platforms Transform Digital Transformation Initiatives' - date: "Monday, April 20 2026 | 16:45 - 17:45" - location: "Hall 26, Solution Lab (E43), Masterclass 1" - description: "FlowFuse founder Nick O'Leary will deliver an in-depth masterclass on how AI-powered industrial platforms connect IT and OT, unlock data silos, and enable scalable digital transformation." - cta: SAVE YOUR SPOT - ctaUrl: "https://www.hannovermesse.de/event/bridging-it-and-ot-how-ai-powered-industrial-platforms-transform-digital-transformation-initiatives/mc/85826#" - - image: events/images/hm26/ff-talks.png - imgAlt: FlowFuse Talks at Hannover Messe - imageClass: top - labelClass: text-indigo-600 - label: Live at our booth - id: flowfuse-talks - title: "FlowFuse Talks" - date: "Monday, April 20 to Thursday, April 23 2026" - location: "Hall 014 Stand K26" - description: "Join FlowFuse talks to see how teams connect machines, move data, and build and scale real-world industrial applications, all on a single platform, with insights from practitioners working on production systems, partners and the FlowFuse team." - cta: SAVE YOUR SPOT - secondaryCta: VIEW AGENDA - secondaryCtaTarget: "#flowfuse-talks-agenda" - ctaUrl: "https://www.eventbrite.com/e/flowfuse-talks-at-hannover-messe-2026-tickets-1985896052552?aff=oddtdtcreator" -flowFuseTalksAgenda: - ctaUrl: "https://www.eventbrite.com/e/flowfuse-talks-at-hannover-messe-2026-tickets-1985896052552?aff=oddtdtcreator" - days: - - dateLabel: "Monday, April 20" - talks: - - time: "10:00 - 11:00" - title: "Bringing Intelligence to the Edge - Leveraging FlowFuse for Local Context" - speaker: "Kristopher Sandoval" - - time: "13:00 - 14:00" - title: "Mayker Presents: From OT Connectivity to MES - A Practical Path for Batch Manufacturers" - speaker: "Mayker" - - time: "15:00 - 16:00" - title: "The Intelligence Imperative: Turning Manufacturing Data into Business Value" - speaker: "ZJ van de Weg" - - dateLabel: "Tuesday, April 21" - talks: - - time: "10:00 - 11:00" - title: "Making Industry Work - Leveraging NodeOPCUA with Sterfive and FlowFuse" - speaker: "Sterfive" - - time: "13:00 - 14:00" - title: "Containerised Success - Leveraging Docker and FlowFuse for Agile Development" - speaker: "Kristopher Sandoval" - - time: "15:00 - 16:00" - title: "FlowFuse & Tulip: Better Together - A Conversation on Modern Industrial Innovation" - speaker: "Tulip" - - dateLabel: "Wednesday, April 22" - talks: - - time: "10:00 - 11:00" - title: "IT/OT Without the Hangover - A Breakfast with FlowFuse and Mayker" - speaker: "Mayker" - - time: "13:00 - 14:00" - title: "Ensuring Data Sovereignty and Security in Industrial Spaces" - speaker: "Kristopher Sandoval" - - time: "15:00 - 16:00" - title: "Architecting AI at Scale with FlowFuse" - speaker: "Nick O'Leary" - - dateLabel: "Thursday, April 23" - talks: - - time: "10:00 - 11:00" - title: "Live Edge AI Demo (RevPi + FlowFuse)" - speaker: "Revolution Pi" - - time: "13:00 - 14:00" - title: "Deploying MCP and AI Connectivity at Scale with FlowFuse" - speaker: "Kristopher Sandoval" - - time: "15:00 - 16:00" - title: "Evaluating and Controlling Hallucinations in Industrial AI Applications" - speaker: "Kristopher Sandoval" -bookDemo: - cta: BOOK A DEMO - ctaUrl: "/book-demo" ---- - -
-
-
-
-
-
- {% set imageSrc = ["./", heroSection.image ] | join %} - {% set imageDescription = heroSection.imgAlt %} - {% image imageSrc, imageDescription, [644] %} -
-
- {% set logoSrc = ["./", heroSection.logo ] | join %} - {% set logoDescription = heroSection.logoAlt %} - {% image logoSrc, logoDescription, [50] %} -
-
-
- -

FlowFuse at Hannover Messe 2026

-

Discover how FlowFuse empowers you to build, deploy, and scale industrial automation, your way.

-

Visit us at Hannover Messe at Hall 014 Stand K26 and experience live demonstrations of FlowFuse connecting the entire industrial stack, from PLCs on the shop floor to MES, ERP, and cloud services, enabling real-time industrial connectivity, data integration, and AI-powered operations.

-

Let’s transform industrial data together, live, integrated, and in real time.

- -
-
-
-
- -
-
-
-

Join Us and Experience Connectivity in Action

-

At Hannover Messe 2026, you’ll see how FlowFuse empowers you to enhance your existing systems and build truly integrated industrial solutions. At our booth, you’ll experience:

-
-
    -
  • A live connected production line demo (PLCs, MES, ERP, and cloud fully integrated via Node-RED)
  • -
  • Real-time data flow across the complete ISA-95 architecture
  • -
  • Seamless integration with your existing IT and OT systems
  • -
  • AI-powered development capabilities with FlowFuse Expert
  • -
  • Practical demonstrations showing how to make your industrial data AI-ready
  • -
-
-
-
-
- {% set imageSrc = ["./", demoSection.image ] | join %} - {% set imageDescription = demoSection.imgAlt %} - {% image imageSrc, imageDescription, [644] %} -
-
-
-
-
- -
-
-

Don't Miss our Special Sessions

- {% for session in specialSessions %} -
-
- {% set imageSrc = ["./", session.image ] | join %} - {% set imageDescription = session.imgAlt %} - {% image imageSrc, imageDescription, [263] %} -
-
- -

{{ session.title }}

-
-
- -

{{ session.date }}

-
-
- -

{{ session.location }}

-
-
-

{{ session.description }}

-
- - {{ session.cta }} - - {% if session.secondaryCta and session.secondaryCtaTarget %} - - {{ session.secondaryCta }} - {% include "components/icons/arrow-small-down.svg" %} - - {% endif %} -
-
-
- {% endfor %} -
-
- -
-
-

FlowFuse Talks Agenda

-
- {% for day in flowFuseTalksAgenda.days %} -
-
-
- {% include "components/icons/calendar.svg" %} -
- {{ day.dateLabel }} -
- {% for talk in day.talks %} - {% if talk.tbd %} -
-

{{ talk.time }}

-

{{ talk.title }}

-

{{ talk.speaker }}

-
- {% else %} - -
-

{{ talk.time }}

-
- {% include "components/icons/arrow-top-right-on-square.svg" %} -
-
-

{{ talk.title }}

-

{{ talk.speaker }}

-
- {% endif %} - {% endfor %} -
- {% endfor %} -
-
-
- -
-
-
- -

Intelligence and Connectivity Across Every Layer

-
-

Join us throughout Hannover Messe for exclusive live demonstrations featuring FlowFuse's complete industrial connectivity stack.

-
-
-
-
-

Our live setup shows how:

-
    -
  • The most common challenges in IT/OT integration, and how to overcome them
  • -
  • Why a Unified Namespace (UNS) is essential for scalable, future-proof data management
  • -
  • How FlowFuse enables low-code integration, edge/cloud connectivity, and real-time event processing
  • -
  • Real-world case studies showcasing efficiency gains and reduced downtime
  • -
  • Practical steps to implement a UNS, plus key considerations to keep in mind
  • -
-
-
-

Meet the Team On Site

-

Stop by the booth, see the demos in action, and talk with our team about how FlowFuse can support your digital transformation journey.

- -
-
-
-
- -
-
-
-
-

Can’t make it to Hannover Messe?

- {% macro bookDemoButton() %} - - {{ bookDemo.cta }} - - {% endmacro %} - -
-
-

No problem! You can still experience FlowFuse’s industrial connectivity platform remotely.

-

Book a personalised online demo and discover how our flexible, real-time industrial software can optimise your operations and make your data AI-ready.

-
-
- {{ bookDemoButton() }} -
-
-
-
diff --git a/src/events/proveit-2026.njk b/src/events/proveit-2026.njk deleted file mode 100644 index 8cfea4099f..0000000000 --- a/src/events/proveit-2026.njk +++ /dev/null @@ -1,145 +0,0 @@ ---- -layout: layouts/base.njk -tags: - - trade-show -title: FlowFuse at ProveIt! 2026 -meta: - description: Join FlowFuse at ProveIt 2026 to learn how to turn manufacturing data into business value. Explore our session, workshop, demos, and meeting opportunities. -demoHighlights: - - Live demonstrations connected to the ProveIt! Virtual Factory - - Real-time data flow across the complete ISA-95 architecture - - Seamless integration with your existing systems - - AI-powered development capabilities with FlowFuse Expert - - Hands-on demos that put you in control of your operation -sessionHighlights: - - How we move data through the complete ISA-95 industrial stack (Levels 0-4) - - Why FlowFuse is essential for scalable, future-proof data management - - How our platform enables low-code integration, edge/cloud connectivity, and real-time event processing - - Real-world integration with the ProveIt! ecosystem showcasing efficiency gains - - Practical steps to implement unified industrial architectures ---- -{% from "components/cta/cta-book-demo.njk" import ctaBookDemo %} -
- -
-
-
-
-
- {% image "./events/images/pi26/proveit26-hero.png", "Image of people at the FlowFuse booth", [644] %} -
-
- {% image "./events/images/pi26/pi_logo.png", "ProveIt! 2026 Logo", [170] %} -
-
-
- -

{{ title | safe }}

-

Discover how FlowFuse empowers you to build, deploy, and scale industrial automation, your way.

-

Visit us at Booth #17 and experience live demonstrations of FlowFuse connecting to the ProveIt! Virtual Factory, showcasing real-time industrial connectivity, data integration, and AI-powered operations.

-

Let's prove it together, live, integrated, and in real-time.

- -
-
-
-
- -
-
-
-

Join Us at Booth #17 and Experience Industrial Connectivity in Action

-

Join Us at Booth #17 and Experience Industrial Connectivity in Action

-
-
    - {% for highlight in demoHighlights %} -
  • {{ highlight | safe }}
  • - {% endfor %} -
-
-

At FlowFuse, we believe in empowering engineers to take charge, designing, adapting, and scaling solutions to meet unique operational challenges.

-

Visit us at Booth #17 and unlock the potential of smarter, more efficient manufacturing.

-
-
-
- {% image "./events/images/pi26/proveit26-booth.png", "Image of people attending the FlowFuse demo.", [644] %} -
-
-
-
- -
- -

The Intelligence Imperative: Turning Manufacturing Data into Business Value

-
- {% image "./events/images/pi26/proveit-session.jpg", "Banner with details about the on-stage session", [1024] %} -
-

Data is everywhere in manufacturing. What about intelligence? How do we collect this data? How do we manage it? How do we make the most of it? This session examines why traditional approaches fall short and introduces a new framework for creating intelligent, data-driven manufacturing operations.

-

Discover real-world strategies to move from passive monitoring to proactive, predictive, and prescriptive decision-making.

-
- -
- -

Building and Deploying Industrial AI with FlowFuse

-
- {% image "./events/images/pi26/proveit-workshop.jpg", "Banner with details about the hands-on workshop", [1024] %} -
-

Although it might seem like it at times, AI isn't magic - it requires significant infrastructure to deploy and manage. Luckily FlowFuse makes it easy to develop and oversee this process, enabling you to deploy in minutes what otherwise may take hours.

-

Join Kristopher Sandoval and Drew Gatti in this workshop as we build out an MCP flow in FlowFuse and demonstrate how it all works under the hood. This is a "bring your own" workshop - so bring your own laptop, problem, and end goal.

- -
- -
- -

Intelligence and Connectivity Across Every Layer

-

Join us at our booth from February 16–20, 2026 for exclusive live demonstrations featuring FlowFuse's complete industrial connectivity stack.

-

Our demonstrations showcase how FlowFuse integrates with the ProveIt! Virtual Factory, demonstrating real-world industrial interoperability.

-
-
-
-

In these sessions, you'll see:

-
    - {% for highlight in sessionHighlights %} -
  • {{ highlight | safe }}
  • - {% endfor %} -
-
-
-

Don't miss out!

-

Stop by our booth, meet our leadership team and technical experts throughout the event, including Zeger-Jan van de Weg (CEO), Michael Davis, Kristopher Sandoval, Drew Gatti, and Omar Ahmed.

- -
-
-
- -
-

Can't make it to ProveIt! 2026?

-
-

No problem! You can still experience FlowFuse's cutting-edge industrial connectivity solution from the comfort of your own space. Book a personalised online demo with us and see how our flexible, real-time industrial software can optimise your operations.

-

Our team is ready to walk you through the features that matter most to your business and answer any questions you may have.

-

Take control of your industrial data and improve efficiency across your operations.

-
-
- {{ ctaBookDemo('primary', 'hero', extraClass='items-center') }} -
-
-
-
From 43a8ed288c02f0c634d72b807cd0f158b2084d11 Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Wed, 16 Sep 2026 10:59:46 +0200 Subject: [PATCH 2/3] events: serve /events/ from Nuxt in dev too Without the prefix the dev middleware keeps proxying to 11ty, so npm run dev shows the old page. The middleware returns early unless NODE_ENV is development, which is why CI and the deploy preview both looked correct. The event images and the Hannover .ics stay in src/ and only reach nuxt/public through the 11ty passthrough in a production build, so dev needs the carve-out /resources and /vs already have. --- nuxt/server/middleware/legacy.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/nuxt/server/middleware/legacy.ts b/nuxt/server/middleware/legacy.ts index 616f1f77e3..2ebf066a03 100644 --- a/nuxt/server/middleware/legacy.ts +++ b/nuxt/server/middleware/legacy.ts @@ -15,7 +15,7 @@ const NUXT_ROUTE_PREFIXES = ['/integrations/', '/raw/'] // left once its one referring blog post pointed at /contact-us/ instead) - but they stay // listed so their 301s in nuxt/redirects.ts are served by Nitro in dev rather than being // proxied to 11ty, which has nothing there either. -const NUXT_PREFIXES = ['/handbook', '/ebooks', '/whitepaper', '/pricing', '/docs', '/changelog', '/application-guide', '/blog', '/product', '/customer-stories', '/thank-you', '/resources', '/webinars', '/free-consultation', '/vs'] +const NUXT_PREFIXES = ['/handbook', '/ebooks', '/whitepaper', '/pricing', '/docs', '/changelog', '/application-guide', '/blog', '/product', '/customer-stories', '/thank-you', '/resources', '/webinars', '/free-consultation', '/vs', '/events'] // Top-level routes still on 11ty, not yet ported to Nuxt (everything not listed above // already falls through to the 11ty proxy by default). Remove entries here as they migrate: @@ -52,6 +52,13 @@ export default defineEventHandler(async (event) => { // from still-11ty pages like use-cases/uns.njk) - /resources is otherwise a Nuxt prefix. if (normalised.startsWith('/resources/images/')) return proxyRequest(event, `http://localhost:8080${path}`) + // Same story for src/events/images/** and the .ics the Hannover page links: 11ty-owned + // files that only reach nuxt/public/ through the passthrough in a production build, so + // dev has to ask 11ty for them even though /events is a Nuxt prefix now. + if (normalised.startsWith('/events/images/') || normalised === '/events/hm25-invite.ics') { + return proxyRequest(event, `http://localhost:8080${path}`) + } + // The documentation below /node-red/ moved into /docs/, and every old URL now 301s // from nuxt/redirects-node-red.ts. Those are Nitro route rules, so the request has to // reach Nitro in dev rather than being proxied to 11ty, which no longer has the pages. From 497c644474a5c20152adb518b69d73cc358b50fb Mon Sep 17 00:00:00 2001 From: Dimitrie Hoekstra Date: Wed, 16 Sep 2026 11:07:40 +0200 Subject: [PATCH 3/3] events: use Heroicons directly, drop SiteArt and the scroll handler The three glyphs are stock Heroicons, so they go through and SiteArt.vue, siteArt.ts and gen-site-art.mjs come out. Each call site carries its own size: UIcon renders a mask span with no intrinsic ratio, so a width-only wrapper would have collapsed it. Both anchors already carry the href they scrolled to, so the handler only had to preventDefault to do what the browser does anyway. The site-wide scroll-behavior: smooth animates the jump, which retires the composable. --- nuxt/components/SiteArt.vue | 20 --- nuxt/composables/useScrollToAnchor.ts | 9 - nuxt/pages/events/hannover-messe-2026.vue | 17 +- nuxt/utils/siteArt.ts | 196 ---------------------- scripts/gen-site-art.mjs | 103 ------------ 5 files changed, 8 insertions(+), 337 deletions(-) delete mode 100644 nuxt/components/SiteArt.vue delete mode 100644 nuxt/composables/useScrollToAnchor.ts delete mode 100644 nuxt/utils/siteArt.ts delete mode 100755 scripts/gen-site-art.mjs diff --git a/nuxt/components/SiteArt.vue b/nuxt/components/SiteArt.vue deleted file mode 100644 index 7d0d8acee0..0000000000 --- a/nuxt/components/SiteArt.vue +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/nuxt/composables/useScrollToAnchor.ts b/nuxt/composables/useScrollToAnchor.ts deleted file mode 100644 index 501e1c3b45..0000000000 --- a/nuxt/composables/useScrollToAnchor.ts +++ /dev/null @@ -1,9 +0,0 @@ -// src/_includes/smooth-scroll.njk, which four pages pulled in to make one in-page link -// scroll rather than jump. The .njk defined a global function and wired it through an -// inline onclick; this is the same behaviour as a handler the template can bind. -export function useScrollToAnchor() { - return function scrollToAnchor(event: Event, anchorId: string) { - event.preventDefault() - document.getElementById(anchorId)?.scrollIntoView({ behavior: 'smooth' }) - } -} diff --git a/nuxt/pages/events/hannover-messe-2026.vue b/nuxt/pages/events/hannover-messe-2026.vue index 3c4e68b621..d99de4c96d 100644 --- a/nuxt/pages/events/hannover-messe-2026.vue +++ b/nuxt/pages/events/hannover-messe-2026.vue @@ -7,11 +7,12 @@ // // What the port changes on purpose: // - Two "jump to section" links used an inline onclick calling scrollIntoView; both go -// through useScrollToAnchor, the same helper smooth-scroll.njk's pages use. -// - The section glyphs were {% include %}d raw, so they become . +// natively: both anchors already carry the href they scroll to, and the site-wide +// scroll-behavior: smooth in style.css animates the jump. +// - The section glyphs were {% include %}d raw; they are stock Heroicons, so they +// become , each carrying its own size since the mask span has no ratio. // - The book-a-demo button was a Nunjucks macro rendering a hand-written ff-btn link to // /book-demo, so it fired no event. It is . -const scrollToAnchor = useScrollToAnchor() const HERO = { image: '/events/images/hm26/hm-26-hero.png', @@ -153,10 +154,9 @@ useSeoMeta({ VIEW EVENTS - + @@ -227,10 +227,9 @@ useSeoMeta({ v-if="session.secondaryCta && session.secondaryCtaTarget" class="inline-flex items-center justify-center gap-1.5 text-base font-semibold uppercase text-indigo-600 hover:text-indigo-800 w-full md:w-auto no-underline hover:no-underline" :href="session.secondaryCtaTarget" - @click="scrollToAnchor($event, 'flowfuse-talks-agenda')" > {{ session.secondaryCta }} - + @@ -244,7 +243,7 @@ useSeoMeta({
-
+ {{ day.dateLabel }}

{{ talk.time }}

-
+

{{ talk.title }}

{{ talk.speaker }}

diff --git a/nuxt/utils/siteArt.ts b/nuxt/utils/siteArt.ts deleted file mode 100644 index 3404c74719..0000000000 --- a/nuxt/utils/siteArt.ts +++ /dev/null @@ -1,196 +0,0 @@ -// SVG files that page content included raw, with no wrapper: {% include "components/icons/ -// x.svg" %} rather than the `navoption`/`ffIconLg` shortcodes that NavIcon reproduces. -// -// They cannot go through NavIcon or an : -// - NavIcon wraps its payload in an outer carrying a fixed ff-icon size class, -// which would override the size the caller's box sets. -// - breaks `currentColor`, and most of these are coloured by their container. -// -// GENERATED by scripts/gen-site-art.mjs from the keys the pages and content collections -// actually reference, so the bundle carries only what is used - an eager glob of that -// directory inlines ~137 KB, most of it unused art. Re-run it after adding a page that -// names a new icon. - -import aAcademicCap from '../../src/_includes/components/icons/academic-cap.svg?raw' -import aAdjustmentsHorizontal from '../../src/_includes/components/icons/adjustments-horizontal.svg?raw' -import aAdjustmentsVertical from '../../src/_includes/components/icons/adjustments-vertical.svg?raw' -import aAirGappedDeployment from '../../src/_includes/components/icons/air-gapped-deployment.svg?raw' -import aAirplane from '../../src/_includes/components/icons/airplane.svg?raw' -import aArchiveBoxXMark from '../../src/_includes/components/icons/archive-box-x-mark.svg?raw' -import aArrowLongRight from '../../src/_includes/components/icons/arrow-long-right.svg?raw' -import aArrowPath from '../../src/_includes/components/icons/arrow-path.svg?raw' -import aArrowPathRoundedSquare from '../../src/_includes/components/icons/arrow-path-rounded-square.svg?raw' -import aArrowSmallDown from '../../src/_includes/components/icons/arrow-small-down.svg?raw' -import aArrowTopRightOnSquare from '../../src/_includes/components/icons/arrow-top-right-on-square.svg?raw' -import aArrowTrendingUp from '../../src/_includes/components/icons/arrow-trending-up.svg?raw' -import aArrows from '../../src/_includes/components/icons/arrows.svg?raw' -import aArrowsPointingOut from '../../src/_includes/components/icons/arrows-pointing-out.svg?raw' -import aArrowsRightLeft from '../../src/_includes/components/icons/arrows-right-left.svg?raw' -import aAuditLogs from '../../src/_includes/components/icons/audit-logs.svg?raw' -import aBellAlert from '../../src/_includes/components/icons/bell-alert.svg?raw' -import aBolt from '../../src/_includes/components/icons/bolt.svg?raw' -import aBookOpen from '../../src/_includes/components/icons/book-open.svg?raw' -import aBuildingOffice2 from '../../src/_includes/components/icons/building-office-2.svg?raw' -import aCalendar from '../../src/_includes/components/icons/calendar.svg?raw' -import aCamera from '../../src/_includes/components/icons/camera.svg?raw' -import aCar from '../../src/_includes/components/icons/car.svg?raw' -import aCertificate from '../../src/_includes/components/icons/certificate.svg?raw' -import aCertifiedNode from '../../src/_includes/components/icons/certified-node.svg?raw' -import aChart from '../../src/_includes/components/icons/chart.svg?raw' -import aChatBubbleBottomCenterText from '../../src/_includes/components/icons/chat-bubble-bottom-center-text.svg?raw' -import aChatBubbleLeftRightSm from '../../src/_includes/components/icons/chat-bubble-left-right-sm.svg?raw' -import aCheckBadge from '../../src/_includes/components/icons/check-badge.svg?raw' -import aCheckCircle from '../../src/_includes/components/icons/check-circle.svg?raw' -import aChevronDown from '../../src/_includes/components/icons/chevron-down.svg?raw' -import aChevronLeft from '../../src/_includes/components/icons/chevron-left.svg?raw' -import aChevronRight from '../../src/_includes/components/icons/chevron-right.svg?raw' -import aChevronRightSm from '../../src/_includes/components/icons/chevron-right-sm.svg?raw' -import aChip from '../../src/_includes/components/icons/chip.svg?raw' -import aCircleStack from '../../src/_includes/components/icons/circle-stack.svg?raw' -import aClipList from '../../src/_includes/components/icons/clip-list.svg?raw' -import aClipboardDocumentCheck from '../../src/_includes/components/icons/clipboard-document-check.svg?raw' -import aClock from '../../src/_includes/components/icons/clock.svg?raw' -import aCloud from '../../src/_includes/components/icons/cloud.svg?raw' -import aCodeBracket from '../../src/_includes/components/icons/code-bracket.svg?raw' -import aCodeblock from '../../src/_includes/components/icons/codeblock.svg?raw' -import aCog from '../../src/_includes/components/icons/cog.svg?raw' -import aCog6Tooth from '../../src/_includes/components/icons/cog-6-tooth.svg?raw' -import aComputerDesktop from '../../src/_includes/components/icons/computer-desktop.svg?raw' -import aCubeTransparent from '../../src/_includes/components/icons/cube-transparent.svg?raw' -import aCursorArrowRays from '../../src/_includes/components/icons/cursor-arrow-rays.svg?raw' -import aData from '../../src/_includes/components/icons/data.svg?raw' -import aDatabase from '../../src/_includes/components/icons/database.svg?raw' -import aDocumentArrowUp from '../../src/_includes/components/icons/document-arrow-up.svg?raw' -import aDocumentChartBar from '../../src/_includes/components/icons/document-chart-bar.svg?raw' -import aDocumentCheck from '../../src/_includes/components/icons/document-check.svg?raw' -import aDocumentText from '../../src/_includes/components/icons/document-text.svg?raw' -import aEye from '../../src/_includes/components/icons/eye.svg?raw' -import aEyeSlash from '../../src/_includes/components/icons/eye-slash.svg?raw' -import aFactory from '../../src/_includes/components/icons/factory.svg?raw' -import aGlobeAlt from '../../src/_includes/components/icons/globe-alt.svg?raw' -import aHandRaised from '../../src/_includes/components/icons/hand-raised.svg?raw' -import aLayers from '../../src/_includes/components/icons/layers.svg?raw' -import aLifebuoy from '../../src/_includes/components/icons/lifebuoy.svg?raw' -import aLightBulb from '../../src/_includes/components/icons/light-bulb.svg?raw' -import aLink from '../../src/_includes/components/icons/link.svg?raw' -import aLinkSlash from '../../src/_includes/components/icons/link-slash.svg?raw' -import aLock from '../../src/_includes/components/icons/lock.svg?raw' -import aLockClosed from '../../src/_includes/components/icons/lock-closed.svg?raw' -import aLockOpen from '../../src/_includes/components/icons/lock-open.svg?raw' -import aPin from '../../src/_includes/components/icons/pin.svg?raw' -import aPinSlash from '../../src/_includes/components/icons/pin-slash.svg?raw' -import aPulse from '../../src/_includes/components/icons/pulse.svg?raw' -import aPuzzlePiece from '../../src/_includes/components/icons/puzzle-piece.svg?raw' -import aQueueList from '../../src/_includes/components/icons/queue-list.svg?raw' -import aRectangleStack from '../../src/_includes/components/icons/rectangle-stack.svg?raw' -import aRocketLaunch from '../../src/_includes/components/icons/rocket-launch.svg?raw' -import aRoleBasedAccess from '../../src/_includes/components/icons/role-based-access.svg?raw' -import aServerStack from '../../src/_includes/components/icons/server-stack.svg?raw' -import aShare from '../../src/_includes/components/icons/share.svg?raw' -import aShieldCheck from '../../src/_includes/components/icons/shield-check.svg?raw' -import aShieldExclamation from '../../src/_includes/components/icons/shield-exclamation.svg?raw' -import aSingleSignOn from '../../src/_includes/components/icons/single-sign-on.svg?raw' -import aSnowflake from '../../src/_includes/components/icons/snowflake.svg?raw' -import aSparkles from '../../src/_includes/components/icons/sparkles.svg?raw' -import aSquares2x2 from '../../src/_includes/components/icons/squares-2x2.svg?raw' -import aSquaresPlus from '../../src/_includes/components/icons/squares-plus.svg?raw' -import aTargetView from '../../src/_includes/components/icons/target-view.svg?raw' -import aUns from '../../src/_includes/components/icons/uns.svg?raw' -import aUserGroup from '../../src/_includes/components/icons/user-group.svg?raw' -import aUsers from '../../src/_includes/components/icons/users.svg?raw' -import aWifi from '../../src/_includes/components/icons/wifi.svg?raw' -import aWindTurbine from '../../src/_includes/components/icons/wind-turbine.svg?raw' -import aWrenchScrewdriver from '../../src/_includes/components/icons/wrench-screwdriver.svg?raw' - -export const siteArt: Record = { - 'academic-cap': aAcademicCap, - 'adjustments-horizontal': aAdjustmentsHorizontal, - 'adjustments-vertical': aAdjustmentsVertical, - 'air-gapped-deployment': aAirGappedDeployment, - 'airplane': aAirplane, - 'archive-box-x-mark': aArchiveBoxXMark, - 'arrow-long-right': aArrowLongRight, - 'arrow-path': aArrowPath, - 'arrow-path-rounded-square': aArrowPathRoundedSquare, - 'arrow-small-down': aArrowSmallDown, - 'arrow-top-right-on-square': aArrowTopRightOnSquare, - 'arrow-trending-up': aArrowTrendingUp, - 'arrows': aArrows, - 'arrows-pointing-out': aArrowsPointingOut, - 'arrows-right-left': aArrowsRightLeft, - 'audit-logs': aAuditLogs, - 'bell-alert': aBellAlert, - 'bolt': aBolt, - 'book-open': aBookOpen, - 'building-office-2': aBuildingOffice2, - 'calendar': aCalendar, - 'camera': aCamera, - 'car': aCar, - 'certificate': aCertificate, - 'certified-node': aCertifiedNode, - 'chart': aChart, - 'chat-bubble-bottom-center-text': aChatBubbleBottomCenterText, - 'chat-bubble-left-right-sm': aChatBubbleLeftRightSm, - 'check-badge': aCheckBadge, - 'check-circle': aCheckCircle, - 'chevron-down': aChevronDown, - 'chevron-left': aChevronLeft, - 'chevron-right': aChevronRight, - 'chevron-right-sm': aChevronRightSm, - 'chip': aChip, - 'circle-stack': aCircleStack, - 'clip-list': aClipList, - 'clipboard-document-check': aClipboardDocumentCheck, - 'clock': aClock, - 'cloud': aCloud, - 'code-bracket': aCodeBracket, - 'codeblock': aCodeblock, - 'cog': aCog, - 'cog-6-tooth': aCog6Tooth, - 'computer-desktop': aComputerDesktop, - 'cube-transparent': aCubeTransparent, - 'cursor-arrow-rays': aCursorArrowRays, - 'data': aData, - 'database': aDatabase, - 'document-arrow-up': aDocumentArrowUp, - 'document-chart-bar': aDocumentChartBar, - 'document-check': aDocumentCheck, - 'document-text': aDocumentText, - 'eye': aEye, - 'eye-slash': aEyeSlash, - 'factory': aFactory, - 'globe-alt': aGlobeAlt, - 'hand-raised': aHandRaised, - 'layers': aLayers, - 'lifebuoy': aLifebuoy, - 'light-bulb': aLightBulb, - 'link': aLink, - 'link-slash': aLinkSlash, - 'lock': aLock, - 'lock-closed': aLockClosed, - 'lock-open': aLockOpen, - 'pin': aPin, - 'pin-slash': aPinSlash, - 'pulse': aPulse, - 'puzzle-piece': aPuzzlePiece, - 'queue-list': aQueueList, - 'rectangle-stack': aRectangleStack, - 'rocket-launch': aRocketLaunch, - 'role-based-access': aRoleBasedAccess, - 'server-stack': aServerStack, - 'share': aShare, - 'shield-check': aShieldCheck, - 'shield-exclamation': aShieldExclamation, - 'single-sign-on': aSingleSignOn, - 'snowflake': aSnowflake, - 'sparkles': aSparkles, - 'squares-2x2': aSquares2x2, - 'squares-plus': aSquaresPlus, - 'target-view': aTargetView, - 'uns': aUns, - 'user-group': aUserGroup, - 'users': aUsers, - 'wifi': aWifi, - 'wind-turbine': aWindTurbine, - 'wrench-screwdriver': aWrenchScrewdriver, -} diff --git a/scripts/gen-site-art.mjs b/scripts/gen-site-art.mjs deleted file mode 100755 index cf57a0dbaf..0000000000 --- a/scripts/gen-site-art.mjs +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env node -// Regenerate nuxt/utils/siteArt.ts from the icon keys the site actually references. -// -// inlines an SVG from src/_includes/components/icons/ verbatim, which is what -// page content that used a bare {% include %} needs. The registry is written out rather -// than globbed, so the bundle carries only the icons in use - an eager glob of that -// directory inlines ~137 KB, most of it unused art. -// -// Keeping it hand-maintained across ~50 keys is not realistic, so this scans for every -// key that is referenced and rewrites the file. Run it after adding a page that names a -// new icon; the build fails loudly on a missing import, and warns in dev on a -// key the registry does not have. -// -// Usage: node scripts/gen-site-art.mjs [repo-root] -import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' -import { join, resolve } from 'node:path' - -const ICON_DIR = 'src/_includes/components/icons' - -function filesUnder (dir, ext) { - if (!existsSync(dir)) return [] - return readdirSync(dir, { recursive: true, withFileTypes: true }) - .filter(entry => entry.isFile() && entry.name.endsWith(ext)) - .map(entry => join(entry.parentPath ?? entry.path, entry.name)) -} - -function matchAll (text, pattern) { - return [...text.matchAll(pattern)].map(match => match[1]) -} - -// `explicit` are literal uses - a missing file there is a broken -// page, so it fails the run. `harvested` are svgPath:/icon: values in content and page -// consts, which a renderer *may* pass to SiteArt but may equally resolve some other way -// (contact-us has its own icon map, for one), so a miss there is only reported. -function referencedKeys (root) { - const vues = [...filesUnder(join(root, 'nuxt/pages'), '.vue'), ...filesUnder(join(root, 'nuxt/components'), '.vue')] - const explicit = new Set() - for (const file of vues) { - for (const key of matchAll(readFileSync(file, 'utf8'), / name resolved from an - // installed icon set, not a file in this repo, so it is not SiteArt's to provide. - if (!key.startsWith('i-')) harvested.add(key) - } - } - return { explicit, harvested } -} - -const ident = key => 'a' + key.split('-').map(part => part.charAt(0).toUpperCase() + part.slice(1)).join('') - -const HEADER = `// SVG files that page content included raw, with no wrapper: {% include "components/icons/ -// x.svg" %} rather than the \`navoption\`/\`ffIconLg\` shortcodes that NavIcon reproduces. -// -// They cannot go through NavIcon or an : -// - NavIcon wraps its payload in an outer carrying a fixed ff-icon size class, -// which would override the size the caller's box sets. -// - breaks \`currentColor\`, and most of these are coloured by their container. -// -// GENERATED by scripts/gen-site-art.mjs from the keys the pages and content collections -// actually reference, so the bundle carries only what is used - an eager glob of that -// directory inlines ~137 KB, most of it unused art. Re-run it after adding a page that -// names a new icon. -` - -const root = resolve(process.argv[2] ?? '.') -const { explicit, harvested } = referencedKeys(root) - -// Keys that resolve through NavIcon (the navoption/ffIconLg wrapper) or a hand-written -// Vue icon component are not SiteArt's job. -const navFile = join(root, 'nuxt/utils/navIcons.ts') -const navKeys = existsSync(navFile) ? new Set(matchAll(readFileSync(navFile, 'utf8'), /^ {4}'([a-z0-9-]+)':/gm)) : new Set() - -const usable = [] -const missing = [] -for (const key of [...new Set([...explicit, ...harvested])].sort()) { - (existsSync(join(root, ICON_DIR, `${key}.svg`)) ? usable : missing).push(key) -} - -const lines = [HEADER] -for (const key of usable) lines.push(`import ${ident(key)} from '../../${ICON_DIR}/${key}.svg?raw'`) -lines.push('\nexport const siteArt: Record = {') -for (const key of usable) lines.push(` '${key}': ${ident(key)},`) -lines.push('}\n') - -const out = join(root, 'nuxt/utils/siteArt.ts') -writeFileSync(out, lines.join('\n')) -console.log(`${out}: ${usable.length} icons`) - -const alsoInNav = usable.filter(key => navKeys.has(key)) -if (alsoInNav.length) console.log(` (also in navIcons, which is fine - different wrapper: ${alsoInNav.slice(0, 6).join(', ')}…)`) -if (missing.length) console.log(` no SVG file (resolved elsewhere, not bundled here): ${missing.join(', ')}`) - -const broken = missing.filter(key => explicit.has(key)) -if (broken.length) { - console.log(` ERROR: is asked for these and there is no file: ${broken.join(', ')}`) - process.exit(1) -}