Skip to content

Commit 54d2f80

Browse files
committed
feat: add Integrations marketing page
Add a /integrations page listing the discovery integrations (Docker, Podman, SNMP) and their transports from the integrations fixture, with an integration- request CTA. Wire up the footer link, sitemap entry, Integration types, and the integration_request_clicked analytics event.
1 parent 30cb7f8 commit 54d2f80

5 files changed

Lines changed: 229 additions & 5 deletions

File tree

src/lib/analytics.svelte.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ export const analytics = {
161161
capture('service_request_clicked');
162162
},
163163

164+
/**
165+
* Track integration request button clicks
166+
*/
167+
integrationRequestClicked: () => {
168+
capture('integration_request_clicked');
169+
},
170+
164171
/**
165172
* Track interest in roadmap items (clicks to expand/view details)
166173
*/

src/lib/components/Footer.svelte

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,6 @@
7373
<li>
7474
<a href="/changelog" class="text-sm text-gray-400 hover:text-white">Changelog</a>
7575
</li>
76-
<li>
77-
<a href="/services" class="text-sm text-gray-400 hover:text-white"
78-
>Discoverable Services</a
79-
>
80-
</li>
8176
<li>
8277
<a href="/docs/reference/security" class="text-sm text-gray-400 hover:text-white"
8378
>Security</a
@@ -90,6 +85,14 @@
9085
<div>
9186
<span class="mb-4 block font-semibold text-white">Resources</span>
9287
<ul class="space-y-2">
88+
<li>
89+
<a href="/integrations" class="text-sm text-gray-400 hover:text-white">Integrations</a>
90+
</li>
91+
<li>
92+
<a href="/services" class="text-sm text-gray-400 hover:text-white"
93+
>Discoverable Services</a
94+
>
95+
</li>
9396
<li>
9497
<a href="/docs" class="text-sm text-gray-400 hover:text-white">Documentation</a>
9598
</li>
@@ -109,6 +112,18 @@
109112
<ul class="space-y-2">
110113
<li><a href="/about" class="text-sm text-gray-400 hover:text-white">About</a></li>
111114
<li><a href="/press" class="text-sm text-gray-400 hover:text-white">Press</a></li>
115+
<li>
116+
<a
117+
href="https://www.linkedin.com/company/scanopynet/"
118+
target="_blank"
119+
rel="noopener noreferrer"
120+
class="text-sm text-gray-400 hover:text-white"
121+
onclick={() =>
122+
trackExternalLink('linkedin', 'https://www.linkedin.com/company/scanopynet/')}
123+
>
124+
LinkedIn
125+
</a>
126+
</li>
112127
<li>
113128
<a href="mailto:hello@scanopy.net" class="text-sm text-gray-400 hover:text-white"
114129
>hello@scanopy.net</a

src/lib/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,28 @@ export interface ServiceDefinition {
5858
logo_needs_white_background?: boolean;
5959
}
6060

61+
export interface IntegrationTransport {
62+
id: string;
63+
name: string;
64+
description: string;
65+
requires_config: boolean;
66+
single_endpoint_per_host: boolean;
67+
targets: string[];
68+
}
69+
70+
export interface Integration {
71+
id: string;
72+
name: string;
73+
category: string;
74+
has_logo: boolean;
75+
logo_ext: string;
76+
logo_slug: string;
77+
logo_needs_white_background: boolean;
78+
discovers: string;
79+
summary: string;
80+
transports: IntegrationTransport[];
81+
}
82+
6183
export interface GalleryAuthor {
6284
name: string;
6385
url?: string;
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<script lang="ts">
2+
import type { Integration } from '$lib/types';
3+
import integrationsData from '$lib/fixtures/integrations.json';
4+
import { analytics } from '$lib/analytics.svelte';
5+
6+
const integrations: Integration[] = integrationsData;
7+
8+
// Logos are shared with the service catalog under /static/logos/services/services/*
9+
const logoGlob = import.meta.glob('/static/logos/services/services/*', {
10+
eager: true,
11+
query: '?url',
12+
import: 'default'
13+
}) as Record<string, string>;
14+
const logoBySlug = new Map<string, string>();
15+
for (const [path, url] of Object.entries(logoGlob)) {
16+
const filename = path.split('/').pop()!;
17+
const slug = filename.replace(/\.[^.]+$/, '');
18+
logoBySlug.set(slug, url.replace(/^\/static\//, '/'));
19+
}
20+
21+
function logoUrl(integration: Integration): string | null {
22+
if (!integration.has_logo) return null;
23+
return logoBySlug.get(integration.logo_slug) ?? null;
24+
}
25+
26+
const categories = [...new Set(integrations.map((i) => i.category))].sort((a, b) =>
27+
a.localeCompare(b)
28+
);
29+
30+
function byCategory(category: string): Integration[] {
31+
return integrations
32+
.filter((i) => i.category === category)
33+
.sort((a, b) => a.name.localeCompare(b.name));
34+
}
35+
36+
const description = `Scanopy integrates with ${integrations
37+
.map((i) => i.name)
38+
.join(', ')} to discover and document what's running on your infrastructure.`;
39+
</script>
40+
41+
<svelte:head>
42+
<title>Integrations - Scanopy</title>
43+
<meta name="description" content={description} />
44+
<link rel="canonical" href="https://scanopy.net/integrations" />
45+
46+
<meta property="og:title" content="Integrations - Scanopy" />
47+
<meta property="og:description" content={description} />
48+
<meta property="og:url" content="https://scanopy.net/integrations" />
49+
<meta property="og:image" content="https://scanopy.net/social.webp" />
50+
<meta name="twitter:card" content="summary_large_image" />
51+
<meta name="twitter:title" content="Integrations - Scanopy" />
52+
<meta name="twitter:description" content={description} />
53+
<meta name="twitter:image" content="https://scanopy.net/social.webp" />
54+
55+
{@html `<script type="application/ld+json">${JSON.stringify({
56+
'@context': 'https://schema.org',
57+
'@type': 'WebPage',
58+
name: 'Integrations - Scanopy',
59+
description,
60+
url: 'https://scanopy.net/integrations',
61+
isPartOf: {
62+
'@type': 'WebSite',
63+
'@id': 'https://scanopy.net/#website'
64+
},
65+
mainEntity: {
66+
'@type': 'ItemList',
67+
name: 'Scanopy integrations',
68+
numberOfItems: integrations.length,
69+
itemListElement: integrations.map((integration, i) => ({
70+
'@type': 'ListItem',
71+
position: i + 1,
72+
name: integration.name
73+
}))
74+
}
75+
})}</script>`}
76+
</svelte:head>
77+
78+
<section class="py-20">
79+
<div class="container mx-auto px-4">
80+
<div class="mb-12 text-center">
81+
<h1 class="mb-4 text-4xl font-bold text-white lg:text-5xl">Integrations</h1>
82+
<p class="mx-auto max-w-2xl text-xl text-gray-400">
83+
Connect Scanopy to the platforms you already run. Each integration discovers what's there and
84+
keeps your network documentation current. More are added regularly.
85+
</p>
86+
</div>
87+
88+
<div class="mx-auto max-w-5xl space-y-12">
89+
{#each categories as category (category)}
90+
<div>
91+
<h2 class="mb-6 text-sm font-semibold uppercase tracking-wide text-gray-500">
92+
{category}
93+
</h2>
94+
95+
<div class="grid gap-6 md:grid-cols-2">
96+
{#each byCategory(category) as integration (integration.id)}
97+
{@const logo = logoUrl(integration)}
98+
<div
99+
class="flex flex-col rounded-xl border border-gray-700 bg-gray-800/50 p-6 transition-colors hover:border-gray-600 hover:bg-gray-800"
100+
>
101+
<div class="flex items-start gap-4">
102+
<div
103+
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-lg {integration.logo_needs_white_background
104+
? 'bg-paper'
105+
: 'bg-gray-700/50'}"
106+
>
107+
{#if logo}
108+
<img
109+
src={logo}
110+
alt="{integration.name} logo"
111+
class="h-7 w-7 object-contain"
112+
width="28"
113+
height="28"
114+
/>
115+
{:else}
116+
<span class="text-xl font-bold text-gray-400">
117+
{integration.name.charAt(0)}
118+
</span>
119+
{/if}
120+
</div>
121+
122+
<div class="min-w-0 flex-1">
123+
<h3 class="text-lg font-semibold text-white">{integration.name}</h3>
124+
<p class="mt-1 text-sm text-gray-400">{integration.discovers}</p>
125+
</div>
126+
</div>
127+
128+
<div class="mt-5 border-t border-gray-700/70 pt-4">
129+
<span class="mb-3 block text-xs font-semibold uppercase tracking-wide text-gray-500">
130+
Connection methods
131+
</span>
132+
<ul class="space-y-3">
133+
{#each integration.transports as transport (transport.id)}
134+
<li class="flex items-baseline gap-2">
135+
<span class="font-medium text-white">{transport.name}</span>
136+
<span class="text-sm text-gray-400">{transport.description}</span>
137+
</li>
138+
{/each}
139+
</ul>
140+
</div>
141+
</div>
142+
{/each}
143+
</div>
144+
</div>
145+
{/each}
146+
</div>
147+
148+
<div class="mt-16 text-center">
149+
<p class="mb-4 text-gray-400">Need Scanopy to connect to something else?</p>
150+
<a
151+
href="https://github.com/scanopy/scanopy/issues/new?template=feature_request.md"
152+
target="_blank"
153+
rel="noopener noreferrer"
154+
class="btn-secondary"
155+
onclick={() => analytics.integrationRequestClicked()}
156+
>
157+
Request an Integration
158+
</a>
159+
</div>
160+
161+
<div class="mx-auto mt-16 max-w-3xl text-center">
162+
<p class="mb-4 text-gray-400">
163+
Once connected, an integration runs as part of your scheduled scans — no manual exports, no
164+
stale diagrams. See the
165+
<a href="/docs/using-scanopy/discovery/" class="text-blue-400 hover:text-blue-300"
166+
>discovery documentation</a
167+
>
168+
for setup details, or browse the
169+
<a href="/services" class="text-blue-400 hover:text-blue-300">services Scanopy detects</a>
170+
during a scan.
171+
</p>
172+
<p class="text-gray-400">
173+
Want to <a href="/pricing" class="text-blue-400 hover:text-blue-300">compare plans</a>? You
174+
can <a href="/community" class="text-blue-400 hover:text-blue-300">self-host free</a> or get a
175+
<a href="/commercial" class="text-blue-400 hover:text-blue-300">commercial license</a>.
176+
</p>
177+
</div>
178+
</div>
179+
</section>

src/routes/sitemap.xml/+server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export async function GET() {
3737
{ loc: '/', src: 'src/routes/+page.svelte' },
3838
{ loc: '/pricing', src: 'src/routes/pricing/+page.svelte' },
3939
{ loc: '/services', src: 'src/routes/services/+page.svelte' },
40+
{ loc: '/integrations', src: 'src/routes/integrations/+page.svelte' },
4041
{ loc: '/changelog', src: 'src/routes/changelog/+page.svelte' },
4142
{ loc: '/roadmap', src: 'src/routes/roadmap/+page.svelte' },
4243
{ loc: '/about', src: 'src/routes/about/+page.svelte' },

0 commit comments

Comments
 (0)