diff --git a/site/package.json b/site/package.json index ab3c1dc..04c78cc 100644 --- a/site/package.json +++ b/site/package.json @@ -7,6 +7,7 @@ "dev": "astro dev", "build": "astro build", "preview": "astro preview", + "og": "node scripts/gen-og.mjs", "astro": "astro" }, "devDependencies": { diff --git a/site/public/og.png b/site/public/og.png new file mode 100644 index 0000000..c1cb95d Binary files /dev/null and b/site/public/og.png differ diff --git a/site/scripts/gen-og.mjs b/site/scripts/gen-og.mjs new file mode 100644 index 0000000..3444289 --- /dev/null +++ b/site/scripts/gen-og.mjs @@ -0,0 +1,71 @@ +// Generate the social / link-unfurl card: a real screenshot of the hero card, +// written to public/og.png (referenced only by the OG/Twitter tags in +// src/pages/index.astro, so it never appears in the visible page). +// +// Needs a running site server. Easiest: +// bun run dev # in another terminal (serves http://localhost:4321/maximal) +// bun run og # this script +// Override the target with OG_URL=... if your dev server is elsewhere. +// +// Uses Playwright's bundled Chromium (installed at the repo root). Renders with +// reduced motion so the tagline is fully shown (no typing animation) and the +// WebGL paint is captured as a single static frame. +import { chromium } from "playwright"; +import { fileURLToPath } from "node:url"; + +const TARGET = process.env.OG_URL ?? "http://localhost:4321/maximal"; +const OUT = fileURLToPath(new URL("../public/og.png", import.meta.url)); +const W = 1200; +const H = 630; // 1.91:1 — the standard Open Graph / large-summary card ratio + +const browser = await chromium.launch(); +try { + const page = await browser.newPage({ + viewport: { width: W, height: H }, + deviceScaleFactor: 1, // output exactly 1200x630 to match the og:image meta + colorScheme: "dark", // richer: the god-ray backdrop frames the hero card + reducedMotion: "reduce", + }); + await page.goto(TARGET, { waitUntil: "networkidle" }); + await page.waitForSelector(".hero"); + + // Compose a clean card for the capture only (the live page is untouched): + // - hide the download buttons (visibility:hidden keeps the card's height), + // the typing caret, the other sections, and the dock; + // - pin the hero dead-centre at a generous size. + // Centering is load-bearing: the god-rays light source is anchored to the + // hero's centre, so a large card centred on that point fully covers the + // shader's central fade zone — otherwise it peeks out below the card as a + // dark box. The card half-height must exceed the fade radius (~0.26 * H). + await page.addStyleTag({ + content: ` + .hero-cta { visibility: hidden !important; } + .hero-typed__caret { display: none !important; } + main article section, .dock { display: none !important; } + .hero { + position: fixed !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + width: 880px !important; + min-height: 392px !important; + margin: 0 !important; + display: flex !important; + flex-direction: column !important; + justify-content: center !important; + z-index: 5 !important; + } + `, + }); + + // The hero just moved + resized. Force the god-rays backdrop to re-anchor its + // light to the new (centred) hero — under reduced motion it only redraws on a + // resize/scroll, not a rAF loop — so its fade zone re-centres on the card. + await page.evaluate(() => window.dispatchEvent(new Event("resize"))); + await page.waitForTimeout(1000); // reflow + webfonts + the WebGL frame settle + + await page.screenshot({ path: OUT }); + console.log(`wrote ${OUT} (${W}x${H})`); +} finally { + await browser.close(); +} diff --git a/site/src/components/markdoc/Hero.astro b/site/src/components/markdoc/Hero.astro index ddbc8bb..8763508 100644 --- a/site/src/components/markdoc/Hero.astro +++ b/site/src/components/markdoc/Hero.astro @@ -13,7 +13,8 @@ interface Props { const { tagline, downloadLabel = "Download for macOS" } = Astro.props; // Markdoc doesn't forward $variables into tag attributes, so pull the resolved // release straight from the shared (memoized) build-time module. -const { macDmg: downloadUrl, versionLabel, winSetup } = await getDownloadInfo(); +const { macDmg: downloadUrl, versionLabel, winSetup, hasWindows } = + await getDownloadInfo(); ---
@@ -28,12 +29,7 @@ const { macDmg: downloadUrl, versionLabel, winSetup } = await getDownloadInfo();
{ downloadUrl && ( - + @@ -43,6 +39,28 @@ const { macDmg: downloadUrl, versionLabel, winSetup } = await getDownloadInfo(); ) } + { + hasWindows ? ( + + + + + Download for Windows + + {versionLabel && {versionLabel}} + + ) : ( +
+ + + Windows + + coming soon +
+ ) + }
@@ -141,37 +159,3 @@ const { macDmg: downloadUrl, versionLabel, winSetup } = await getDownloadInfo(); tick(); })(); - - diff --git a/site/src/lib/downloads.ts b/site/src/lib/downloads.ts index e1d02d8..4641c0e 100644 --- a/site/src/lib/downloads.ts +++ b/site/src/lib/downloads.ts @@ -42,20 +42,25 @@ async function compute(): Promise { ? `${REPO_URL}/releases/download/${tag}/${filename}` : RELEASES_URL; const versionForAsset = tag ?? "latest"; - const macDmgFile = `maximal-${versionForAsset}-darwin-arm64.dmg`; + const conventionDmg = `maximal-${versionForAsset}-darwin-arm64.dmg`; - // Windows: only advertise the installer when the release actually ships a - // *-setup.exe (the Tauri NSIS artifact). We pick it up from the resolved - // asset list rather than guessing a filename, so the button stays "coming - // soon" until a real Windows build is attached. + // Resolve both downloads from the release's actual asset list rather than + // guessing filenames, so each button links to the real artifact for the + // latest build. macOS prefers an arm64 .dmg; Windows takes the NSIS + // *-setup.exe. A pinned version carries no asset list, so macOS falls back to + // the conventional .dmg filename and Windows stays "coming soon". + const macAsset = + assets.find((a) => /\.dmg$/i.test(a.name) && /arm64|aarch64/i.test(a.name)) ?? + assets.find((a) => /\.dmg$/i.test(a.name)) ?? + null; const winAsset = assets.find((a) => /-setup\.exe$/i.test(a.name)) ?? null; return { repo: REPO, repoUrl: REPO_URL, releasesUrl: RELEASES_URL, - macDmg: assetUrl(macDmgFile), - macDmgFile, + macDmg: macAsset?.url ?? assetUrl(conventionDmg), + macDmgFile: macAsset?.name ?? conventionDmg, winSetup: winAsset?.url ?? null, winSetupFile: winAsset?.name ?? null, hasWindows: winAsset !== null, diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index 5755615..1cac76b 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -13,6 +13,17 @@ const REPO_URL = `https://github.com/${REPO}`; // wraps the unchanged lib/version.ts logic — so this shell stays presentational. const entry = await getEntry("landing", "index"); const { Content } = await render(entry); + +// Social / link-unfurl card. A generated screenshot of the hero card (see +// scripts/gen-og.mjs → public/og.png) advertised via Open Graph + Twitter meta +// only — invisible to the rendered page, but it's what unfurlers use instead of +// guessing at the first inline (which was grabbing the dashboard shot). +const siteBase = (Astro.site?.href ?? "https://stuffbucket.github.io/maximal/") + .replace(/\/$/, ""); +const ogImage = `${siteBase}/og.png`; +const ogTitle = "maximal · your AI tools, your Copilot models, one connection"; +const ogDescription = + "Connect the AI tools you use to the models in GitHub Copilot."; --- @@ -49,6 +60,24 @@ const { Content } = await render(entry); media="(prefers-color-scheme: dark)" /> + + + + + + + + + + + + + +