Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"og": "node scripts/gen-og.mjs",
"astro": "astro"
},
"devDependencies": {
Expand Down
Binary file added site/public/og.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions site/scripts/gen-og.mjs
Original file line number Diff line number Diff line change
@@ -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 <meta> 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();
}
66 changes: 25 additions & 41 deletions site/src/components/markdoc/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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();
---

<header class="card card--scarlet hero" aria-labelledby="title">
Expand All @@ -28,12 +29,7 @@ const { macDmg: downloadUrl, versionLabel, winSetup } = await getDownloadInfo();
<div class="hero-cta">
{
downloadUrl && (
<a
class="btn btn--primary hero-cta__primary"
href={downloadUrl}
rel="noopener"
data-win-setup={winSetup ?? ""}
>
<a class="btn btn--primary hero-cta__primary" href={downloadUrl} rel="noopener">
<Paint base="#15656f" mode="clearcoat" class="btn-paint" />
<span class="btn-label">
<Spark class="btn-spark" />
Expand All @@ -43,6 +39,28 @@ const { macDmg: downloadUrl, versionLabel, winSetup } = await getDownloadInfo();
</a>
)
}
{
hasWindows ? (
<a class="btn btn--primary hero-cta__primary" href={winSetup} rel="noopener">
<Paint base="#15656f" mode="clearcoat" class="btn-paint" />
<span class="btn-label">
<Spark class="btn-spark" />
<span>Download for Windows</span>
</span>
{versionLabel && <span class="btn-meta">{versionLabel}</span>}
</a>
) : (
<div class="btn btn--soon hero-cta__primary" aria-disabled="true">
<span class="btn-label">
<svg class="btn-spark" viewBox="0 0 22 22" fill="currentColor" aria-hidden="true">
<path d="M11 1 L13 9 L21 11 L13 13 L11 21 L9 13 L1 11 L9 9 Z" />
</svg>
<span>Windows</span>
</span>
<span class="btn-meta">coming soon</span>
</div>
)
}
</div>
</header>

Expand Down Expand Up @@ -141,37 +159,3 @@ const { macDmg: downloadUrl, versionLabel, winSetup } = await getDownloadInfo();
tick();
})();
</script>

<script is:inline>
// OS-aware primary download: macOS is the live build; Windows is "coming
// soon", so Windows visitors get an honest inert chip instead of a Mac
// download. The server-rendered default is the macOS download (no-JS safe).
(function () {
var nav = navigator;
var uad = nav.userAgentData;
var plat = (uad && uad.platform) || nav.platform || "";
var isWindows = /win/i.test(plat) || /windows/i.test(nav.userAgent || "");
if (!isWindows) return;
var btn = document.querySelector(".hero-cta__primary");
if (!btn) return;
var label = btn.querySelector(".btn-label span:last-child");
var winSetup = btn.getAttribute("data-win-setup");
if (winSetup) {
// A Windows installer exists in the release → keep the primary affordance
// lit and point it at the .exe.
btn.setAttribute("href", winSetup);
if (label) label.textContent = "Download for Windows";
return;
}
// No Windows build yet → an honest inert "coming soon" chip.
btn.classList.remove("btn--primary");
btn.classList.add("btn--soon");
btn.removeAttribute("href");
btn.setAttribute("aria-disabled", "true");
var paint = btn.querySelector(".btn-paint");
if (paint) paint.style.display = "none";
if (label) label.textContent = "Windows";
var meta = btn.querySelector(".btn-meta");
if (meta) meta.textContent = "coming soon";
})();
</script>
19 changes: 12 additions & 7 deletions site/src/lib/downloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,25 @@ async function compute(): Promise<DownloadInfo> {
? `${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,
Expand Down
29 changes: 29 additions & 0 deletions site/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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 <img> (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.";
---

<!doctype html>
Expand Down Expand Up @@ -49,6 +60,24 @@ const { Content } = await render(entry);
media="(prefers-color-scheme: dark)"
/>
<meta name="color-scheme" content="light dark" />

<!-- Open Graph / Twitter: a generated hero-card screenshot for link unfurls.
Head metadata only; does not affect the visible page. -->
<meta property="og:type" content="website" />
<meta property="og:url" content={`${siteBase}/`} />
<meta property="og:title" content={ogTitle} />
<meta property="og:description" content={ogDescription} />
<meta property="og:image" content={ogImage} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
property="og:image:alt"
content="The maximal hero card: the maximal wordmark on a glossy candy-red panel."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={ogTitle} />
<meta name="twitter:description" content={ogDescription} />
<meta name="twitter:image" content={ogImage} />
</head>
<body>
<a href="#main" class="skip-link">Skip to content</a>
Expand Down
Loading