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.
Unified · Split
| @@ -8,6 +8,7 @@ import cloudflare from "@astrojs/cloudflare"; · 7 unmodified lines | |||
| 8 | 8 | ||
| 9 | // https://astro.build/config | 9 | // https://astro.build/config |
| 10 | export default defineConfig({ | 10 | export default defineConfig({ |
| 11 | site: "https://executor.sh", | ||
| 11 | output: "server", | 12 | output: "server", |
| 12 | integrations: [react()], | 13 | integrations: [react()], |
| 13 | vite: { | 14 | vite: { |
Binary file not shown.
| @@ -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.props | 12 | } = Astro.props |
| 13 | |||
| 14 | const ogImage = new URL('/og-image.png', Astro.site ?? Astro.url).toString() | ||
| 15 | const 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 /> |
| @@ -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, |