Skip to content

landing: serve /landing/ from Nuxt - #5786

Merged
dimitrieh merged 5 commits into
mainfrom
nuxt/landing
Sep 16, 2026
Merged

dimitrieh merged 5 commits into
mainfrom
nuxt/landing

Conversation

@dimitrieh

Copy link
Copy Markdown
Contributor

Description

Moves /landing/ to Nuxt. The campaign pages that were pure frontmatter become a content collection; the four with their own layout become .vue pages.

The HubSpot meeting embed stays behind the analytics-consent gate: it registers the same global the consent banner calls, rather than loading on mount.

Related Issue(s)

Part of #5777

Checklist

  • I have read the contribution guidelines
  • I have considered the performance impact of these changes
  • Suitable unit/system level tests have been added and they pass
  • Documentation has been updated
  • For blog PRs, an Art Request has been created (instructions)

@netlify

netlify Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for flowfuse-website ready!

Name Link
🔨 Latest commit 229765b
🔍 Latest deploy log https://app.netlify.com/projects/flowfuse-website/deploys/6aaa7beaf1bbec00081d591a
😎 Deploy Preview https://deploy-preview-5786--flowfuse-website.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 46 (🟢 up 8 from production)
Accessibility: 95 (no change from production)
Best Practices: 92 (no change from production)
SEO: 92 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@dimitrieh

Copy link
Copy Markdown
Contributor Author

Rebased on main and fixed the failing build.

The prerender 500s on /landing/tulip/ and /landing/factory-efficiency/ were a missing auto-import: both call useScrollToAnchor(), and nuxt/composables/useScrollToAnchor.ts only exists on the /use-cases/ branch. Nuxt auto-imports mean there is no import line to break, so it compiles and throws at prerender, on exactly the pages that call it. The composable is carried here too, byte-identical to the copy in #5780, so whichever lands first the other merges cleanly.

From the review pass over the whole migration:

  • Adding /landing to NUXT_PREFIXES stopped src/landing/images/** resolving in dev: those 13 files only reach nuxt/public/ through the 11ty passthrough in a production build. Added a carve-out beside the two this file already has for /resources/images/ and the blog and changelog image paths. Production was never affected.
  • The seven [slug].vue pages were in neither sitemap (a data collection is invisible to the module's static-route discovery and to content-urls.get.ts), having left sitemap-legacy.xml when their .njk files were deleted. Added to sitemap.urls, minus the skipIndex entries, which are noindex.
  • Three <Cta* ... icon /> rendered no icon: icon is typed string, so a value-less attribute is "", not true.
  • HubSpotMeetings.vue had replaced <CtaCustom destination-key="hubspotMeeting"> with a hand-written <a> that re-typed the URL and the event name already held in nuxt/lib/custom-cta-destinations.ts. That registry exists so one destination cannot drift between call sites; CtaCustom restored.

nuxt/lib/rich-text.mjs is dropped and scripts/gen-site-art.py is now scripts/gen-site-art.mjs, both per the review on #5784.

@dimitrieh

Copy link
Copy Markdown
Contributor Author

Rebased onto main. The conflicts in nuxt/server/middleware/legacy.ts and nuxt/nuxt.config.ts are resolved and CI is green.

Comment thread nuxt/utils/siteArt.ts Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dimitrieh please remove these siteArt files, let's keep it consistent with the approaches we've taken on previous PRs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed: SiteArt.vue, siteArt.ts and gen-site-art.mjs are all gone, call sites are <UIcon>.

Of the 59 keys these pages use, 48 are stock Heroicons. Three were the same glyph under another name, checked against the set path by path: chip is cpu-chip, chart is chart-bar-square, chevron-right-sm is chevron-right at w-4.

The other eight have no Heroicons equivalent (uns, layers, pin, pin-slash, pulse, snowflake, target-view, arrows) and nearly all arrive as strings from the .yml, so they are a local ff collection in nuxt/assets/icons/ that UIcon reads by name. The content stays uniform that way, i-heroicons-x next to i-ff-x, with nothing left to resolve a bare key.

Each call site carries its own w-/h- now, per your note on #5784: the raw <svg> filled its wrapper through the intrinsic ratio, and one wrapper here set only w-6.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified on the preview, CI green.

All 59 glyphs render at the same size and position as live. One visible difference, and it is a fix rather than a regression: the uns icon in the right-hand card renders as a solid red blob on live, because the raw inline SVG carried a clip-path referencing an internal id. Iconify normalises that, so the preview shows the actual glyph.

Compared /landing/building-and-scaling-industrial-applications/ against live: same 976x706 box, same 14 glyphs, otherwise pixel-identical. Four of the custom ones (snowflake, pin-slash, target-view, layers) paint via background-image rather than mask-image because their SVGs have hardcoded colours, which is the same reason they never followed currentColor under SiteArt either.

Also corrected the preview links in my earlier comment: they pointed at flowforge-website, which now 404s after the Netlify project rename.

Comment thread nuxt/composables/useScrollToAnchor.ts Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dimitrieh I'd say we don't need this either, scroll-behavior: smooth is already global, defined in style.css

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. All three CTAs already carry href="#form" with a matching target, so the handler was only calling preventDefault to do what the browser does anyway. They keep their capture() call and the global scroll-behavior: smooth animates the jump.

Comment thread nuxt/public/images/walter-icon.png Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dimitrieh it seems like the customer stories assets are being duplicated here... I don't see them removed from the 11ty folder, and if we did that, then it would also affect the customer stories pages. Moving them now seems out of scope for the landing pages migration.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that moving them is out of scope, so nothing moved out of the 11ty tree. The copies go instead: seven images this PR added under nuxt/public are referenced by nothing in the Nuxt tree, and src/ already has all seven at the same URLs via the passthrough. That is arch-icon.png, home/home-arch.png, home/pidd.png, home/power.png, pidd-icon.png, stories/walter.png and walter-icon.png. Customer-stories pages are untouched. It also matches what an already-merged Nuxt page does: /images/stories/un-wmo.jpg is referenced from nuxt/ on main and is not in nuxt/public.

@dimitrieh

Copy link
Copy Markdown
Contributor Author

Pushed:

  • The three CTAs already carry href="#form" and the targets exist, so the handler only had to preventDefault to do what the browser does anyway. They keep their capture() call and lose the scroll, which retires the composable.
  • On the duplicated assets: seven of the images copied into nuxt/public here are referenced by nothing in the Nuxt tree, and src/ already has all seven, which 11ty passthrough-copies to the same URLs. So the copies just go. Nothing moves out of the 11ty tree and the customer-stories pages are untouched, which keeps it in scope. The seven are arch-icon.png, home/home-arch.png, home/pidd.png, home/power.png, pidd-icon.png, stories/walter.png and walter-icon.png.

That matches what an already-merged Nuxt page does: /images/stories/un-wmo.jpg is referenced from nuxt/ on main and is not in nuxt/public.

The /landing registration and the /landing/images/ dev carve-out those pages need are in #5815.

@dimitrieh

Copy link
Copy Markdown
Contributor Author

Correction to my earlier comment: #5815 is closed, the registration belongs in each migration PR instead. Pushed here. It also carries the /landing/images/ carve-out, since those images stay in src/.

@dimitrieh dimitrieh mentioned this pull request Sep 16, 2026
5 tasks
@dimitrieh
dimitrieh requested a review from Yndira-E September 16, 2026 09:55
@Yndira-E

Copy link
Copy Markdown
Contributor

The icon for the secondary cta is not properly aligned and on mobile it becomes stacked

Screenshot 2026-09-16 at 12 32 38

dimitrieh and others added 4 commits September 16, 2026 13:18
10 routes move off 11ty, in three shapes:

- The four ABM pages (layouts/abm-landing.njk) and the three gated briefs
  (layouts/landing-comparison.njk) were pure frontmatter, so they become one
  `landingPages` collection with a `kind`, routed by pages/landing/[slug].vue
  through <LandingAbm> and <LandingComparison>.
- /landing/plc/, /tulip/ and /factory-efficiency/ had real markup bodies and
  stay hand-written Vue pages.

The gated briefs' `skipIndex` kept them out of 11ty's sitemap; they carry a
robots noindex now, which is what that flag was for.

<TestimonialCarousel> replaces testimonials.njk, whose script queried the DOM
and ran a 10-second interval with no teardown - fine for a page that never
unmounts, a leak in an SPA. It also clears the interval on unmount.

<HubSpotMeetings> replaces hs-book-meeting.njk and the non-form branch of
consent-fallback.njk. The embed stays gated on analytics consent: the component
registers window._ffLoadMeetings for cookieconsent-config.js to call on accept
rather than loading the script on mount, so declining analytics still means no
third-party scheduler. The "Choose a time to talk" panel is what a visitor
without consent sees, not an error state, so it renders until the embed is up.

scripts/gen-site-art.py generates nuxt/utils/siteArt.ts from the icon keys the
pages and content actually name. 87 keys was past hand-maintaining, and an
eager glob of that directory would inline ~137 KB of mostly unused art. It
fails only on a literal <SiteArt name="…"> with no file; a key some other
renderer resolves is reported, not fatal.

One content fix: /landing/edge-connectivity/'s heading carried "&#8209;".
rich-text.mjs escapes & by design, so that rendered as literal text; the
content holds the U+2011 character it meant.

Verified all 10 against production, with no structural differences.
SiteArt inlined raw SVGs from src/_includes/components/icons/ through a
generated import map, so the pages carried a component, a 196-line generated
file and the script that wrote it. The call sites are <UIcon> now and all
three files go.

Of the 59 keys the landing pages reference, 48 are stock Heroicons and become
i-heroicons-<key>. Three were the same glyph under another name, verified
against the icon set path by path: chip is cpu-chip, chart is chart-bar-square,
and chevron-right-sm is chevron-right at w-4. The remaining eight have no
Heroicons equivalent, so they stay in the repo as a local `ff` collection that
<UIcon> reads by name. That keeps the .yml uniform: a glyph is named the same
way whether it is ours or Heroicons', and nothing has to resolve a bare key.

Each call site now carries its own width and height. The raw <svg> filled its
wrapper through the intrinsic ratio, which a masked <span> does not have, so
the one wrapper that set only w-6 would otherwise have collapsed to no height.
The three CTAs already carry href="#form" and the targets exist, so the JS
handler only had to preventDefault to do what the browser does anyway. They keep
their capture() call and let the site-wide scroll-behavior: smooth animate the
jump, which retires the composable.

Seven images had been copied into nuxt/public although nothing in the Nuxt tree
references them and 11ty already serves all seven from src/. Dropping the copies
leaves the customer-stories pages untouched, which is what makes this in scope:
nothing moves out of the 11ty tree, the duplicates just go.
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 landing images stay in src/ and only reach nuxt/public through the 11ty
passthrough in a production build, so dev needs the same carve-out.
The .njk's extraClass came over verbatim, but it was written against a
different DOM. cta-button.njk wrapped the label and the arrow in one
<span class="inline-flex items-center gap-2">, so the <a> had a single child
and `inline` / `flex flex-col` changed nothing that was drawn. UButton renders
the label and its trailing-icon as siblings, so `flex flex-col` stacks them and
`inline` drops the flex context that centres the arrow.

Dropping the two classes restores UButton's own inline-flex; the mobile CTA
keeps w-full and its margins.

pages/vs/[slug].vue carries the same string from #5783 and is already on main,
so /vs/ignition/, /vs/kepware/ and /vs/litmus/ stack their mobile CTA on live
today. Same one-line fix, included here because it is the same root cause.
@dimitrieh

Copy link
Copy Markdown
Contributor Author

Fixed, and both symptoms had one cause.

The .njk's extraClass came over verbatim, but it was written against a different DOM. cta-button.njk wrapped the label and the arrow in a single <span class="inline-flex items-center gap-2">, so the <a> had one child and inline / flex flex-col changed nothing that was drawn. UButton renders the label and its trailing-icon as siblings, so flex flex-col stacks them and inline drops the flex context that centres the arrow. Copying the strings across was faithful to the source and wrong on the page.

Dropping the two classes restores UButton's own inline-flex. The mobile CTA keeps w-full and its margins.

pages/vs/[slug].vue carries the same string from #5783 and is already on main, so /vs/ignition/, /vs/kepware/ and /vs/litmus/ stack their mobile CTA on live today. Same one-line fix, included here because it is the same cause.

I checked the other migration branches for the pattern. The rest are flex flex-col on a CTA with no icon, where there is only one child and nothing stacks, so I left those alone. The tell is icon plus flex flex-col together.

@dimitrieh
dimitrieh merged commit bffd97c into main Sep 16, 2026
8 checks passed
@dimitrieh
dimitrieh deleted the nuxt/landing branch September 16, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants