diff --git a/packages/web/app/layout.tsx b/packages/web/app/layout.tsx index fdb3c8e75..5fcc996e3 100644 --- a/packages/web/app/layout.tsx +++ b/packages/web/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next" +import type { JSX, ReactNode } from "react" import { GeistSans } from "geist/font/sans" import { GeistMono } from "geist/font/mono" import Script from "next/script" @@ -32,21 +33,31 @@ export const metadata: Metadata = { ], authors: [{ name: "Yeongyu Kim", url: "https://github.com/code-yeongyu" }], creator: "Yeongyu Kim", + alternates: { + canonical: "/", + languages: { + en: "/", + ko: "/ko", + ja: "/ja", + zh: "/zh", + }, + }, openGraph: { type: "website", locale: "en_US", + alternateLocale: ["ko_KR", "ja_JP", "zh_CN"], url: primarySiteUrl, siteName: "Oh My OpenAgent", title: "Oh My OpenAgent — The Best Agent Harness", description: "Meet Sisyphus: The batteries-included agent that codes like you. Multi-model orchestration, Team Mode, background agents, 54+ lifecycle hooks.", - images: [{ url: "/images/hero.webp", width: 1024, height: 683, alt: "Oh My OpenAgent" }], + // og:image is supplied by app/opengraph-image.tsx via Next.js file-based metadata convention. }, twitter: { card: "summary_large_image", title: "Oh My OpenAgent — The Best Agent Harness", description: "Meet Sisyphus: The batteries-included agent that codes like you.", - images: ["/images/hero.webp"], + // twitter:image is supplied by app/twitter-image.tsx via the file-based convention. }, robots: { index: true, @@ -54,6 +65,8 @@ export const metadata: Metadata = { googleBot: { index: true, follow: true, + "max-image-preview": "large", + "max-snippet": -1, }, }, } @@ -82,7 +95,7 @@ const jsonLd = { const gaMeasurementId = "G-S0QJFKT46Q" const gaTrackedDomain = "ohmyopenagent.com" -export default function RootLayout({ children }: { children: React.ReactNode }) { +export default function RootLayout({ children }: { readonly children: ReactNode }): JSX.Element { return ( { + const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@${weight}&display=swap` + const css = await (await fetch(url)).text() + const match = css.match(/src: url\((.+?)\) format\('(?:opentype|truetype)'\)/) + if (!match?.[1]) throw new Error(`Font URL not found for ${family} ${weight}`) + const fontResponse = await fetch(match[1]) + return fontResponse.arrayBuffer() +} + +export default async function OpenGraphImage(): Promise { + const [geistBold, geistRegular, geistMonoMedium] = await Promise.all([ + loadGoogleFont("Geist", 700), + loadGoogleFont("Geist", 400), + loadGoogleFont("Geist Mono", 500), + ]) + + return new ImageResponse( +
+ {/* Dotted grid background: flexbox-only, no grid */} +
+ + {/* Top-left: brand wordmark */} +
+
+
+ oh-my-openagent +
+
+ + {/* Center stack: headline + subtitle */} +
+
+ The Best Agent + Harness +
+
+ Sisyphus orchestrates the team. Type ultrawork. Done. +
+
+ + {/* Bottom row: install command + cursor */} +
+
+ $ + bunx oh-my-openagent install +
+
+ ▌ +
+
+
, + { + ...size, + fonts: [ + { name: "Geist", data: geistRegular, weight: 400, style: "normal" }, + { name: "Geist", data: geistBold, weight: 700, style: "normal" }, + { name: "Geist Mono", data: geistMonoMedium, weight: 500, style: "normal" }, + ], + }, + ) +} diff --git a/packages/web/app/twitter-image.tsx b/packages/web/app/twitter-image.tsx new file mode 100644 index 000000000..96df64adc --- /dev/null +++ b/packages/web/app/twitter-image.tsx @@ -0,0 +1,167 @@ +import { ImageResponse } from "next/og" + +export const alt = "oh-my-openagent — The Best Agent Harness (Twitter)" +export const size = { width: 1200, height: 630 } +export const contentType = "image/png" + +async function loadGoogleFont(family: string, weight: number): Promise { + const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(family)}:wght@${weight}&display=swap` + const css = await (await fetch(url)).text() + const match = css.match(/src: url\((.+?)\) format\('(?:opentype|truetype)'\)/) + if (!match?.[1]) throw new Error(`Font URL not found for ${family} ${weight}`) + const fontResponse = await fetch(match[1]) + return fontResponse.arrayBuffer() +} + +export default async function TwitterImage(): Promise { + const [geistBold, geistRegular, geistMonoMedium] = await Promise.all([ + loadGoogleFont("Geist", 700), + loadGoogleFont("Geist", 400), + loadGoogleFont("Geist Mono", 500), + ]) + + return new ImageResponse( +
+ {/* Dotted grid background: flexbox-only, no grid */} +
+ + {/* Top-left: brand wordmark */} +
+
+
+ oh-my-openagent +
+
+ + {/* Center stack: headline + subtitle */} +
+
+ The Best Agent + Harness +
+
+ Sisyphus orchestrates the team. Type ultrawork. Done. +
+
+ + {/* Bottom row: install command + cursor */} +
+
+ $ + bunx oh-my-openagent install +
+
+ ▌ +
+
+
, + { + ...size, + fonts: [ + { name: "Geist", data: geistRegular, weight: 400, style: "normal" }, + { name: "Geist", data: geistBold, weight: 700, style: "normal" }, + { name: "Geist Mono", data: geistMonoMedium, weight: 500, style: "normal" }, + ], + }, + ) +}