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", + }); +} 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, + ); +}