History

feat(marketing): og:image meta tags and URL-synced hero tab

Ship a real OG/Twitter card image for executor.sh and persist the hero Cloud/Local toggle to `?try=`, so the choice is shareable and survives back/forward navigation.

Rhys Sullivan committed 6 months ago

Commit 42674bb1 parent 4cfa2994 files · +31 −0Browse files

Unified · Split

4 files
  1. apps/marketing/astro.config.mjs +1 −0
  2. apps/marketing/public/og-image.png +0 −0
  3. apps/marketing/src/layouts/Layout.astro +14 −0
  4. apps/marketing/src/pages/index.astro +17 −1
apps/marketing/astro.config.mjs+1−0
@@ -8,6 +8,7 @@ import cloudflare from "@astrojs/cloudflare"; · 7 unmodified lines
8 8
9// https://astro.build/config9// https://astro.build/config
10export default defineConfig({10export default defineConfig({
11 site: "https://executor.sh",
11 output: "server",12 output: "server",
12 integrations: [react()],13 integrations: [react()],
13 vite: {14 vite: {
apps/marketing/public/og-image.png+0−0

Binary file not shown.

apps/marketing/src/layouts/Layout.astro+14−0
@@ -10,6 +10,9 @@ const { · 9 unmodified lines
10 title = 'Executor — The gateway to connect your agent to everything',10 title = 'Executor — The gateway to connect your agent to everything',
11 description = 'One place every agent plugs into every tool you already use. Wire it up once, bring the whole team.'11 description = 'One place every agent plugs into every tool you already use. Wire it up once, bring the whole team.'
12} = Astro.props12} = Astro.props
13
14const ogImage = new URL('/og-image.png', Astro.site ?? Astro.url).toString()
15const canonical = new URL(Astro.url.pathname, Astro.site ?? Astro.url).toString()
13---16---
14 17
15<html lang="en">18<html lang="en">
@@ -22,8 +25,19 @@ const { · 6 unmodified lines
22 <meta name="viewport" content="width=device-width, initial-scale=1" />25 <meta name="viewport" content="width=device-width, initial-scale=1" />
23 <meta name="color-scheme" content="light" />26 <meta name="color-scheme" content="light" />
24 <meta name="description" content={description} />27 <meta name="description" content={description} />
28 <link rel="canonical" href={canonical} />
29 <meta property="og:type" content="website" />
30 <meta property="og:url" content={canonical} />
25 <meta property="og:title" content={title} />31 <meta property="og:title" content={title} />
26 <meta property="og:description" content={description} />32 <meta property="og:description" content={description} />
33 <meta property="og:image" content={ogImage} />
34 <meta property="og:image:width" content="3800" />
35 <meta property="og:image:height" content="2360" />
36 <meta property="og:image:alt" content={title} />
37 <meta name="twitter:card" content="summary_large_image" />
38 <meta name="twitter:title" content={title} />
39 <meta name="twitter:description" content={description} />
40 <meta name="twitter:image" content={ogImage} />
27 <title>{title}</title>41 <title>{title}</title>
28 <link rel="preconnect" href="https://fonts.googleapis.com" />42 <link rel="preconnect" href="https://fonts.googleapis.com" />
29 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />43 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
apps/marketing/src/pages/index.astro+17−1
@@ -377,7 +377,16 @@ import { SelfHostContactModal } from "../components/self-host-contact-modal"; · 376 unmodified lines
377 // visitors still see the recommended path.377 // visitors still see the recommended path.
378 const heroToggle = document.querySelector<HTMLElement>(".hero-toggle");378 const heroToggle = document.querySelector<HTMLElement>(".hero-toggle");
379 const tabToggle = heroToggle?.querySelector<HTMLElement>(".tab-toggle");379 const tabToggle = heroToggle?.querySelector<HTMLElement>(".tab-toggle");
380 const setActiveTab = (tab: "cloud" | "local") => {380 const TAB_PARAM = "try";
381 const isHeroTab = (v: unknown): v is "cloud" | "local" =>
382 v === "cloud" || v === "local";
383 const syncTabToUrl = (tab: "cloud" | "local") => {
384 const url = new URL(window.location.href);
385 if (url.searchParams.get(TAB_PARAM) === tab) return;
386 url.searchParams.set(TAB_PARAM, tab);
387 window.history.replaceState(window.history.state, "", url);
388 };
389 const setActiveTab = (tab: "cloud" | "local", updateUrl = true) => {
381 if (!heroToggle || !tabToggle) return;390 if (!heroToggle || !tabToggle) return;
382 heroToggle.dataset.active = tab;391 heroToggle.dataset.active = tab;
383 tabToggle.dataset.active = tab;392 tabToggle.dataset.active = tab;
@@ -389,8 +398,11 @@ import { SelfHostContactModal } from "../components/self-host-contact-modal"; · 5 unmodified lines
389 );398 );
390 },399 },
391 );400 );
401 if (updateUrl) syncTabToUrl(tab);
392 };402 };
393 if (heroToggle && tabToggle) {403 if (heroToggle && tabToggle) {
404 const initial = new URLSearchParams(window.location.search).get(TAB_PARAM);
405 if (isHeroTab(initial)) setActiveTab(initial, false);
394 tabToggle.querySelectorAll<HTMLButtonElement>("button[data-tab]").forEach(406 tabToggle.querySelectorAll<HTMLButtonElement>("button[data-tab]").forEach(
395 (btn) => {407 (btn) => {
396 btn.addEventListener("click", () => {408 btn.addEventListener("click", () => {
@@ -399,6 +411,10 @@ import { SelfHostContactModal } from "../components/self-host-contact-modal"; · 2 unmodified lines
399 });411 });
400 },412 },
401 );413 );
414 window.addEventListener("popstate", () => {
415 const next = new URLSearchParams(window.location.search).get(TAB_PARAM);
416 setActiveTab(isHeroTab(next) ? next : "cloud", false);
417 });
402 }418 }
403 419
404 // Desktop card "Download →" CTA jumps to the hero, switches to Local,420 // Desktop card "Download →" CTA jumps to the hero, switches to Local,