From ba67fae062de52a63773e3891491cfece6d01b92 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 8 Nov 2026 16:01:13 +0000 Subject: [PATCH 1/2] fix(web): real social preview cards (og:image 404ed); per-page og for /r and /nodes /og-image.png never existed, so every link shared to Reddit/X/Slack showed no image, and every page carried the homepage's og:title/url. Cards are now generated by next/og (opengraph-image + twitter-image): a site-wide card, an invite card for /r/ (" invited you"), and a live count for /nodes. metadataBase set; pageOg() keeps type/site name when a page overrides. Co-Authored-By: Claude Opus 5.5 --- apps/web/src/app/layout.tsx | 3 +- apps/web/src/app/nodes/opengraph-image.tsx | 19 +++++++ apps/web/src/app/nodes/page.tsx | 8 ++- apps/web/src/app/nodes/twitter-image.tsx | 16 ++++++ apps/web/src/app/opengraph-image.tsx | 13 +++++ apps/web/src/app/r/[code]/opengraph-image.tsx | 15 ++++++ apps/web/src/app/r/[code]/page.tsx | 10 ++-- apps/web/src/app/r/[code]/twitter-image.tsx | 15 ++++++ apps/web/src/app/twitter-image.tsx | 13 +++++ apps/web/src/lib/og.tsx | 54 +++++++++++++++++++ 10 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/app/nodes/opengraph-image.tsx create mode 100644 apps/web/src/app/nodes/twitter-image.tsx create mode 100644 apps/web/src/app/opengraph-image.tsx create mode 100644 apps/web/src/app/r/[code]/opengraph-image.tsx create mode 100644 apps/web/src/app/r/[code]/twitter-image.tsx create mode 100644 apps/web/src/app/twitter-image.tsx create mode 100644 apps/web/src/lib/og.tsx diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 1e20c985..d5ff13df 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -15,6 +15,7 @@ const DESCRIPTION = export const revalidate = 3600; export const metadata: Metadata = { + metadataBase: new URL(SITE), title: "c0mpute — the open compute network", description: DESCRIPTION, manifest: "/manifest.json", @@ -25,13 +26,11 @@ export const metadata: Metadata = { title: "c0mpute — the open compute network", description: DESCRIPTION, url: SITE, - images: [{ url: `${SITE}/og-image.png`, width: 1200, height: 630, alt: "c0mpute" }], }, twitter: { card: "summary_large_image", title: "c0mpute — the open compute network", description: DESCRIPTION, - images: [`${SITE}/og-image.png`], }, alternates: { canonical: SITE }, }; diff --git a/apps/web/src/app/nodes/opengraph-image.tsx b/apps/web/src/app/nodes/opengraph-image.tsx new file mode 100644 index 00000000..c4a13181 --- /dev/null +++ b/apps/web/src/app/nodes/opengraph-image.tsx @@ -0,0 +1,19 @@ +import { ogCard, OG_SIZE } from "@/lib/og"; +import { getNodesPayload } from "@/lib/nodes"; + +export const alt = "c0mpute nodes online"; +export const size = OG_SIZE; +export const contentType = "image/png"; +export const dynamic = "force-dynamic"; + +export default async function Image() { + const p = await getNodesPayload(); + return ogCard({ + kicker: "every node online now", + title: + p.source === "aggregator" && p.workers_online > 0 + ? `${p.workers_online.toLocaleString()} nodes online` + : "The c0mpute node board", + sub: "goal: 100,000 nodes", + }); +} diff --git a/apps/web/src/app/nodes/page.tsx b/apps/web/src/app/nodes/page.tsx index 57e01a3b..50d2cfc9 100644 --- a/apps/web/src/app/nodes/page.tsx +++ b/apps/web/src/app/nodes/page.tsx @@ -1,12 +1,16 @@ import Link from "next/link"; import { CodeBlock } from "@/components/CodeBlock"; import { getNodesPayload, NODE_GOAL } from "@/lib/nodes"; +import { pageOg } from "@/lib/og"; + +const DESCRIPTION = + "Every c0mpute node online right now, ranked by its c0mpute bench score, with who runs each one and who invited them. How to name your node. The goal is 100,000 nodes."; export const metadata = { title: "nodes — c0mpute", - description: - "Every c0mpute node online right now, ranked by its c0mpute bench score, with who runs each one and who invited them. How to name your node. The goal is 100,000 nodes.", + description: DESCRIPTION, alternates: { canonical: "https://c0mpute.com/nodes" }, + ...pageOg("c0mpute nodes online", DESCRIPTION, "/nodes"), }; // A live board must not be a build-time snapshot. The aggregator read is a diff --git a/apps/web/src/app/nodes/twitter-image.tsx b/apps/web/src/app/nodes/twitter-image.tsx new file mode 100644 index 00000000..457f30ea --- /dev/null +++ b/apps/web/src/app/nodes/twitter-image.tsx @@ -0,0 +1,16 @@ +import { ogCard, OG_SIZE } from "@/lib/og"; +import { getNodesPayload } from "@/lib/nodes"; + +export const alt = "c0mpute nodes online"; +export const size = OG_SIZE; +export const contentType = "image/png"; +export const dynamic = "force-dynamic"; + +export default async function Image() { + const p = await getNodesPayload(); + return ogCard({ + kicker: "every node online now", + title: `${p.workers_online.toLocaleString()} nodes online`, + sub: "goal: 100,000 nodes", + }); +} diff --git a/apps/web/src/app/opengraph-image.tsx b/apps/web/src/app/opengraph-image.tsx new file mode 100644 index 00000000..5fc2e4e8 --- /dev/null +++ b/apps/web/src/app/opengraph-image.tsx @@ -0,0 +1,13 @@ +import { ogCard, OG_SIZE } from "@/lib/og"; + +export const alt = "c0mpute: the open compute network"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function Image() { + return ogCard({ + kicker: "run jobs anywhere, sell unused compute", + title: "The open compute network", + sub: "CPUs + GPUs from PCs to data centers", + }); +} diff --git a/apps/web/src/app/r/[code]/opengraph-image.tsx b/apps/web/src/app/r/[code]/opengraph-image.tsx new file mode 100644 index 00000000..33d61877 --- /dev/null +++ b/apps/web/src/app/r/[code]/opengraph-image.tsx @@ -0,0 +1,15 @@ +import { ogCard, OG_SIZE } from "@/lib/og"; +import { sanitizeRef } from "@/lib/nodes"; + +export const alt = "Run a c0mpute node"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default async function Image({ params }: { params: Promise<{ code: string }> }) { + const code = sanitizeRef((await params).code); + return ogCard({ + kicker: code ? `${code} invited you` : "you're invited", + title: "Run a c0mpute node", + sub: "one command · CPU or GPU · goal: 100,000 nodes", + }); +} diff --git a/apps/web/src/app/r/[code]/page.tsx b/apps/web/src/app/r/[code]/page.tsx index b8baaeca..46484cf3 100644 --- a/apps/web/src/app/r/[code]/page.tsx +++ b/apps/web/src/app/r/[code]/page.tsx @@ -3,17 +3,21 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { CodeBlock } from "@/components/CodeBlock"; import { installLine, sanitizeRef } from "@/lib/nodes"; +import { pageOg } from "@/lib/og"; type Props = { params: Promise<{ code: string }> }; export async function generateMetadata({ params }: Props): Promise { const code = sanitizeRef((await params).code); + const title = code ? `${code} invited you to run a c0mpute node` : "Run a c0mpute node"; + const description = + "Run a c0mpute node with one command. The open compute network is aiming for 100,000 nodes so it can train and serve open-weight models in the open."; return { - title: code ? `${code} invited you to run a c0mpute node` : "c0mpute", - description: - "Run a c0mpute node with one command. The open compute network is aiming for 100,000 nodes so it can train and serve open-weight models in the open.", + title, + description, robots: { index: false, follow: true }, alternates: { canonical: "https://c0mpute.com/" }, + ...pageOg(title, description, `/r/${code ?? ""}`), }; } diff --git a/apps/web/src/app/r/[code]/twitter-image.tsx b/apps/web/src/app/r/[code]/twitter-image.tsx new file mode 100644 index 00000000..33d61877 --- /dev/null +++ b/apps/web/src/app/r/[code]/twitter-image.tsx @@ -0,0 +1,15 @@ +import { ogCard, OG_SIZE } from "@/lib/og"; +import { sanitizeRef } from "@/lib/nodes"; + +export const alt = "Run a c0mpute node"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default async function Image({ params }: { params: Promise<{ code: string }> }) { + const code = sanitizeRef((await params).code); + return ogCard({ + kicker: code ? `${code} invited you` : "you're invited", + title: "Run a c0mpute node", + sub: "one command · CPU or GPU · goal: 100,000 nodes", + }); +} diff --git a/apps/web/src/app/twitter-image.tsx b/apps/web/src/app/twitter-image.tsx new file mode 100644 index 00000000..5fc2e4e8 --- /dev/null +++ b/apps/web/src/app/twitter-image.tsx @@ -0,0 +1,13 @@ +import { ogCard, OG_SIZE } from "@/lib/og"; + +export const alt = "c0mpute: the open compute network"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function Image() { + return ogCard({ + kicker: "run jobs anywhere, sell unused compute", + title: "The open compute network", + sub: "CPUs + GPUs from PCs to data centers", + }); +} diff --git a/apps/web/src/lib/og.tsx b/apps/web/src/lib/og.tsx new file mode 100644 index 00000000..75ad7ec7 --- /dev/null +++ b/apps/web/src/lib/og.tsx @@ -0,0 +1,54 @@ +import { ImageResponse } from "next/og"; +import type { Metadata } from "next"; + +export const SITE = "https://c0mpute.com"; +export const OG_SIZE = { width: 1200, height: 630 }; + +/** + * Open Graph for one page. Next replaces the layout's `openGraph` object + * wholesale when a page sets its own, so every page that wants its own title + * goes through here to keep type/site name. The image comes from the nearest + * `opengraph-image.tsx`, never from a static file (the old /og-image.png 404ed + * and left every social preview blank). + */ +export function pageOg(title: string, description: string, path: string): Pick { + return { + openGraph: { type: "website", siteName: "c0mpute", title, description, url: `${SITE}${path}` }, + twitter: { card: "summary_large_image", title, description }, + }; +} + +/** The c0mpute card: dark terminal, green accent, a kicker, a title, a footer line. */ +export function ogCard({ kicker, title, sub }: { kicker: string; title: string; sub: string }) { + return new ImageResponse( + ( +
+
c0mpute
+
+
{`// ${kicker}`}
+
40 ? 64 : 76, fontWeight: 700, lineHeight: 1.1, color: "#f5f5f5" }}> + {title} +
+
+
+ {sub} + c0mpute.com +
+
+ ), + OG_SIZE, + ); +} From e61ea60428761a52445888423f7b4dab341d0a03 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sun, 8 Nov 2026 16:23:54 +0000 Subject: [PATCH 2/2] fix(web): /chat preview card + og title Co-Authored-By: Claude Opus 5.5 --- apps/web/src/app/chat/opengraph-image.tsx | 13 +++++++++++++ apps/web/src/app/chat/page.tsx | 8 ++++++-- apps/web/src/app/chat/twitter-image.tsx | 13 +++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/chat/opengraph-image.tsx create mode 100644 apps/web/src/app/chat/twitter-image.tsx diff --git a/apps/web/src/app/chat/opengraph-image.tsx b/apps/web/src/app/chat/opengraph-image.tsx new file mode 100644 index 00000000..3b7e84c6 --- /dev/null +++ b/apps/web/src/app/chat/opengraph-image.tsx @@ -0,0 +1,13 @@ +import { ogCard, OG_SIZE } from "@/lib/og"; + +export const alt = "c0mpute chat: open models on the open compute network"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function Image() { + return ogCard({ + kicker: "open models, served by people's own hardware", + title: "Chat on c0mpute", + sub: "browser · CLI · OpenAI-compatible API · MCP", + }); +} diff --git a/apps/web/src/app/chat/page.tsx b/apps/web/src/app/chat/page.tsx index 37dcf0d1..faa87650 100644 --- a/apps/web/src/app/chat/page.tsx +++ b/apps/web/src/app/chat/page.tsx @@ -1,10 +1,14 @@ import { ChatApp } from "./chat-app"; +import { pageOg } from "@/lib/og"; + +const DESCRIPTION = + "Chat with open models running on the c0mpute network. Sign in with CoinPay (DID required). Same chat from the browser, the c0mpute CLI, an OpenAI-compatible API and MCP."; export const metadata = { title: "chat — c0mpute", - description: - "Chat with open models running on the c0mpute network. Sign in with CoinPay (DID required). Same chat from the browser, the c0mpute CLI, an OpenAI-compatible API and MCP.", + description: DESCRIPTION, alternates: { canonical: "https://c0mpute.com/chat" }, + ...pageOg("Chat with open models on c0mpute", DESCRIPTION, "/chat"), }; export const dynamic = "force-dynamic"; diff --git a/apps/web/src/app/chat/twitter-image.tsx b/apps/web/src/app/chat/twitter-image.tsx new file mode 100644 index 00000000..3b7e84c6 --- /dev/null +++ b/apps/web/src/app/chat/twitter-image.tsx @@ -0,0 +1,13 @@ +import { ogCard, OG_SIZE } from "@/lib/og"; + +export const alt = "c0mpute chat: open models on the open compute network"; +export const size = OG_SIZE; +export const contentType = "image/png"; + +export default function Image() { + return ogCard({ + kicker: "open models, served by people's own hardware", + title: "Chat on c0mpute", + sub: "browser · CLI · OpenAI-compatible API · MCP", + }); +}