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();
@@ -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)"
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Skip to content