Skip to content

Commit eeeb28f

Browse files
committed
Restore SEO meta parity and route the install image through Netlify Image CDN on /integrations/
1 parent 69f61de commit eeeb28f

5 files changed

Lines changed: 165 additions & 15 deletions

File tree

nuxt/components/NetlifyImg.vue

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup>
2+
const props = defineProps({
3+
src: { type: String, required: true },
4+
alt: { type: String, default: '' },
5+
width: { type: [String, Number], default: undefined },
6+
height: { type: [String, Number], default: undefined },
7+
format: { type: String, default: 'webp' },
8+
quality: { type: Number, default: 80 },
9+
loading: { type: String, default: 'lazy' },
10+
sizes: { type: String, default: undefined }
11+
})
12+
13+
// /.netlify/* doesn't exist in dev — serve the original src.
14+
const isDev = import.meta.dev
15+
16+
const buildUrl = (w, h) => {
17+
if (isDev) return props.src
18+
const params = new URLSearchParams({ url: props.src, fm: props.format, q: String(props.quality) })
19+
if (w) params.set('w', String(w))
20+
if (h) params.set('h', String(h))
21+
return `/.netlify/images?${params.toString()}`
22+
}
23+
24+
const baseW = computed(() => (props.width ? Number(props.width) : null))
25+
const baseH = computed(() => (props.height ? Number(props.height) : null))
26+
const src = computed(() => buildUrl(baseW.value, baseH.value))
27+
const srcset = computed(() => {
28+
if (isDev || !baseW.value) return undefined
29+
return [1, 2]
30+
.map(d => `${buildUrl(baseW.value * d, baseH.value ? baseH.value * d : null)} ${d}x`)
31+
.join(', ')
32+
})
33+
</script>
34+
35+
<template>
36+
<img
37+
:src="src"
38+
:srcset="srcset"
39+
:sizes="sizes"
40+
:alt="alt"
41+
:width="width"
42+
:height="height"
43+
:loading="loading"
44+
/>
45+
</template>

nuxt/components/integrations/InstallBox.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const installPngUrl = '/images/integrations/palette-manager-install.png'
2727
rel="noopener noreferrer"
2828
>palette manager</a>.
2929
</p>
30+
<!-- GIF stays as <img> so animation survives. -->
3031
<img
3132
:src="installGifUrl"
3233
alt="Animation of the Node-RED palette manager: open Manage Palette, search for the node, then click Install."
@@ -35,12 +36,11 @@ const installPngUrl = '/images/integrations/palette-manager-install.png'
3536
height="720"
3637
class="motion-reduce:hidden block w-full rounded border border-gray-200"
3738
/>
38-
<img
39+
<NetlifyImg
3940
:src="installPngUrl"
4041
alt="Node-RED Palette Manager dialog with the node selected and the Install button visible."
41-
loading="lazy"
42-
width="707"
43-
height="376"
42+
:width="707"
43+
:height="376"
4444
class="motion-safe:hidden block w-full rounded border border-gray-200"
4545
/>
4646
</div>

nuxt/pages/integrations/[...id].vue

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup>
22
import { cleanRepoUrl, formatNumber, shortDate } from '~/utils/formatters'
3+
import { SITE_URL, OG_IMAGE, buildJsonLd } from '~/utils/seo'
34
45
const route = useRoute()
56
const router = useRouter()
@@ -33,14 +34,35 @@ const authorIsLinkable = computed(() => {
3334
return Boolean(url) && (url.includes('github.com') || url.includes('npmjs.com'))
3435
})
3536
36-
// Function form so title/meta react if the integration ever updates after mount.
37+
const seoTitle = computed(() => `${node.value._id} • FlowFuse Integrations`)
38+
const seoDescription = computed(() => node.value.description || '')
39+
// id can end in `/` from trailing-slash catch-all routes.
40+
const pageUrl = computed(() => `${SITE_URL}/integrations/${id.value.replace(/\/+$/, '')}/`)
41+
42+
useSeoMeta({
43+
title: () => seoTitle.value,
44+
description: () => seoDescription.value,
45+
ogTitle: () => seoTitle.value,
46+
ogDescription: () => seoDescription.value,
47+
ogUrl: () => pageUrl.value,
48+
ogImage: OG_IMAGE,
49+
ogType: 'website',
50+
twitterCard: 'summary_large_image',
51+
twitterSite: '@FlowFuseinc',
52+
twitterImage: OG_IMAGE,
53+
twitterDescription: ''
54+
})
55+
3756
useHead(() => ({
38-
title: `${node.value._id} • FlowFuse Integrations`,
39-
meta: [
40-
{ name: 'description', content: node.value.description },
41-
{ property: 'og:title', content: node.value._id },
42-
{ property: 'og:description', content: node.value.description }
43-
]
57+
link: [{ rel: 'canonical', href: pageUrl.value }],
58+
script: [{
59+
type: 'application/ld+json',
60+
innerHTML: JSON.stringify(buildJsonLd({
61+
url: pageUrl.value,
62+
title: seoTitle.value,
63+
description: seoDescription.value
64+
}))
65+
}]
4466
}))
4567
4668
// Tab state for nodes with examples

nuxt/pages/integrations/index.vue

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
<script setup>
22
import { INTEGRATION_CATEGORIES } from '~/types/integrations'
33
import { fetchCatalogue } from '~/utils/integrations'
4+
import { SITE_URL, OG_IMAGE, buildJsonLd } from '~/utils/seo'
5+
6+
const PAGE_URL = `${SITE_URL}/integrations/`
7+
const TITLE = 'Integrations • FlowFuse'
8+
const DESCRIPTION = 'Explore the list of integrations and modules available for your Node-RED projects. Created (and curated) by FlowFuse and the Node-RED community.'
9+
10+
useSeoMeta({
11+
title: TITLE,
12+
description: DESCRIPTION,
13+
ogTitle: TITLE,
14+
ogDescription: DESCRIPTION,
15+
ogUrl: PAGE_URL,
16+
ogImage: OG_IMAGE,
17+
ogType: 'website',
18+
twitterCard: 'summary_large_image',
19+
twitterSite: '@FlowFuseinc',
20+
twitterImage: OG_IMAGE,
21+
twitterDescription: ''
22+
})
423
524
useHead({
6-
title: 'Integrations • FlowFuse',
7-
meta: [
8-
{ name: 'description', content: 'Explore the list of integrations and modules available for your Node-RED projects. Created (and curated) by FlowFuse and the Node-RED community.' }
9-
]
25+
link: [{ rel: 'canonical', href: PAGE_URL }],
26+
script: [{
27+
type: 'application/ld+json',
28+
innerHTML: JSON.stringify(buildJsonLd({ url: PAGE_URL, title: TITLE, description: DESCRIPTION }))
29+
}]
1030
})
1131
1232
const route = useRoute()

nuxt/utils/seo.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Parity with src/_includes/jsonld.njk.
2+
3+
export const SITE_URL = 'https://flowfuse.com'
4+
export const OG_IMAGE = `${SITE_URL}/images/og-social-tile.jpg`
5+
6+
const SITE_TAGLINE = 'Build workflows and integrations that optimize your industrial operations'
7+
8+
export function buildJsonLd (opts: { url: string, title: string, description: string, image?: string }) {
9+
const pageImage = opts.image || OG_IMAGE
10+
return {
11+
'@context': 'https://schema.org',
12+
'@graph': [
13+
{
14+
'@type': 'Organization',
15+
'@id': `${SITE_URL}/#organization`,
16+
name: 'FlowFuse',
17+
url: SITE_URL,
18+
description: SITE_TAGLINE,
19+
logo: { '@type': 'ImageObject', url: `${SITE_URL}/handbook/images/logos/ff-logo--square--dark.png` },
20+
sameAs: [
21+
'https://www.g2.com/products/flowfuse/reviews',
22+
'https://www.linkedin.com/company/flowfuse',
23+
'https://www.youtube.com/@FlowFuseInc',
24+
'https://github.com/FlowFuse'
25+
]
26+
},
27+
{
28+
'@type': 'WebSite',
29+
'@id': `${SITE_URL}/#website`,
30+
url: SITE_URL,
31+
name: 'FlowFuse',
32+
description: SITE_TAGLINE,
33+
publisher: { '@id': `${SITE_URL}/#organization` }
34+
},
35+
{
36+
'@type': 'WebPage',
37+
'@id': `${opts.url}#webpage`,
38+
url: opts.url,
39+
name: opts.title,
40+
headline: opts.title,
41+
description: opts.description,
42+
isPartOf: { '@id': `${SITE_URL}/#website` },
43+
primaryImageOfPage: { '@type': 'ImageObject', url: pageImage },
44+
publisher: { '@id': `${SITE_URL}/#organization` }
45+
},
46+
{
47+
'@type': 'SoftwareApplication',
48+
'@id': `${SITE_URL}/#software`,
49+
name: 'FlowFuse',
50+
url: SITE_URL,
51+
applicationCategory: 'DeveloperApplication',
52+
publisher: { '@id': `${SITE_URL}/#organization` },
53+
aggregateRating: {
54+
'@type': 'AggregateRating',
55+
ratingValue: '4.8',
56+
ratingCount: '3',
57+
bestRating: '5',
58+
worstRating: '0'
59+
}
60+
}
61+
]
62+
}
63+
}

0 commit comments

Comments
 (0)