diff --git a/web/app/docs/file/[slug]/og.png/route.tsx b/web/app/docs/file/[slug]/og.png/route.tsx new file mode 100644 index 0000000..2757e8d --- /dev/null +++ b/web/app/docs/file/[slug]/og.png/route.tsx @@ -0,0 +1,48 @@ +import { ImageResponse } from 'next/og'; +import { notFound } from 'next/navigation'; + +import { DefaultVariant, loadBrandFonts, OG_SIZE } from '../../../../../lib/og/template'; +import { getProductDoc, getProductDocSlugs } from '../../../../../lib/product-docs'; +import { fileSection } from '../../../../../lib/product-docs-nav'; + +export const runtime = 'nodejs'; +// Prerender one card per Relayfile doc at build time. +export const dynamic = 'force-static'; + +type RouteContext = { + params: Promise<{ slug: string }>; +}; + +export function generateStaticParams() { + return getProductDocSlugs(fileSection).map((slug) => ({ slug })); +} + +/** + * Per-doc Open Graph card for Relayfile: the default variant with a "Relayfile" + * eyebrow plus the doc title and description — so these pages get a product card + * instead of the site-wide Agent Relay default. + */ +export async function GET(_request: Request, { params }: RouteContext) { + const { slug } = await params; + const doc = getProductDoc(fileSection.id, slug); + + if (!doc) { + notFound(); + } + + const { fonts, headingFamily, bodyFamily } = await loadBrandFonts(); + + return new ImageResponse( + , + { + ...OG_SIZE, + ...(fonts.length > 0 ? { fonts } : {}), + } + ); +} diff --git a/web/app/docs/file/[slug]/page.tsx b/web/app/docs/file/[slug]/page.tsx index fa325e9..eda8aa4 100644 --- a/web/app/docs/file/[slug]/page.tsx +++ b/web/app/docs/file/[slug]/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import { ProductDocPage } from '../../../../components/docs/ProductDocPage'; +import { ogImage } from '../../../../lib/og-meta'; import { fileSection, getProductDoc, @@ -27,6 +28,8 @@ export async function generateMetadata({ params }: PageProps): Promise return { title: 'Not Found' }; } + const ogPath = `/docs/${SECTION_ID}/${slug}/og.png`; + return { title: `${doc.frontmatter.title} — Relayfile`, description: doc.frontmatter.description, @@ -34,6 +37,19 @@ export async function generateMetadata({ params }: PageProps): Promise canonical: absoluteUrl(`/docs/${SECTION_ID}/${slug}`), types: { 'text/markdown': getProductDocMarkdownUrl(SECTION_ID, slug) }, }, + openGraph: { + title: `${doc.frontmatter.title} — Relayfile`, + description: doc.frontmatter.description, + url: absoluteUrl(`/docs/${SECTION_ID}/${slug}`), + type: 'article', + images: [ogImage(ogPath, `${doc.frontmatter.title} — Relayfile docs`)], + }, + twitter: { + card: 'summary_large_image', + title: `${doc.frontmatter.title} — Relayfile`, + description: doc.frontmatter.description, + images: [absoluteUrl(ogPath)], + }, }; } diff --git a/web/app/docs/layout.tsx b/web/app/docs/layout.tsx index b79cdc5..68f71ff 100644 --- a/web/app/docs/layout.tsx +++ b/web/app/docs/layout.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from 'react'; import { DocsLanguageProvider } from '../../components/docs/DocsLanguageContext'; import { DocsNav } from '../../components/docs/DocsNav'; import { DocsSearch } from '../../components/docs/DocsSearch'; -import { GitHubStarsBadge } from '../../components/GitHubStars'; +import { DocsGitHubStarsBadgeServer } from '../../components/GitHubStars'; import { SiteFooter } from '../../components/SiteFooter'; import { SiteNav } from '../../components/SiteNav'; import styles from '../../components/docs/docs.module.css'; @@ -24,7 +24,7 @@ export default function DocsLayout({ children }: { children: ReactNode }) {
} - actions={} + actions={} mobileMenuContent={} hideMobileDocsLink /> diff --git a/web/app/docs/loop/[slug]/og.png/route.tsx b/web/app/docs/loop/[slug]/og.png/route.tsx new file mode 100644 index 0000000..a4557da --- /dev/null +++ b/web/app/docs/loop/[slug]/og.png/route.tsx @@ -0,0 +1,48 @@ +import { ImageResponse } from 'next/og'; +import { notFound } from 'next/navigation'; + +import { DefaultVariant, loadBrandFonts, OG_SIZE } from '../../../../../lib/og/template'; +import { getProductDoc, getProductDocSlugs } from '../../../../../lib/product-docs'; +import { loopSection } from '../../../../../lib/product-docs-nav'; + +export const runtime = 'nodejs'; +// Prerender one card per Relayloop doc at build time. +export const dynamic = 'force-static'; + +type RouteContext = { + params: Promise<{ slug: string }>; +}; + +export function generateStaticParams() { + return getProductDocSlugs(loopSection).map((slug) => ({ slug })); +} + +/** + * Per-doc Open Graph card for Relayloop: the default variant with a "Relayloop" + * eyebrow plus the doc title and description — so these pages get a product card + * instead of the site-wide Agent Relay default. + */ +export async function GET(_request: Request, { params }: RouteContext) { + const { slug } = await params; + const doc = getProductDoc(loopSection.id, slug); + + if (!doc) { + notFound(); + } + + const { fonts, headingFamily, bodyFamily } = await loadBrandFonts(); + + return new ImageResponse( + , + { + ...OG_SIZE, + ...(fonts.length > 0 ? { fonts } : {}), + } + ); +} diff --git a/web/app/docs/loop/[slug]/page.tsx b/web/app/docs/loop/[slug]/page.tsx index 224e9cf..5b9bba9 100644 --- a/web/app/docs/loop/[slug]/page.tsx +++ b/web/app/docs/loop/[slug]/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import { ProductDocPage } from '../../../../components/docs/ProductDocPage'; +import { ogImage } from '../../../../lib/og-meta'; import { getProductDoc, getProductDocMarkdownUrl, @@ -27,6 +28,8 @@ export async function generateMetadata({ params }: PageProps): Promise return { title: 'Not Found' }; } + const ogPath = `/docs/${SECTION_ID}/${slug}/og.png`; + return { title: `${doc.frontmatter.title} — Relayloop`, description: doc.frontmatter.description, @@ -34,6 +37,19 @@ export async function generateMetadata({ params }: PageProps): Promise canonical: absoluteUrl(`/docs/${SECTION_ID}/${slug}`), types: { 'text/markdown': getProductDocMarkdownUrl(SECTION_ID, slug) }, }, + openGraph: { + title: `${doc.frontmatter.title} — Relayloop`, + description: doc.frontmatter.description, + url: absoluteUrl(`/docs/${SECTION_ID}/${slug}`), + type: 'article', + images: [ogImage(ogPath, `${doc.frontmatter.title} — Relayloop docs`)], + }, + twitter: { + card: 'summary_large_image', + title: `${doc.frontmatter.title} — Relayloop`, + description: doc.frontmatter.description, + images: [absoluteUrl(ogPath)], + }, }; } diff --git a/web/components/DocsGitHubStarsBadge.tsx b/web/components/DocsGitHubStarsBadge.tsx new file mode 100644 index 0000000..7c5153e --- /dev/null +++ b/web/components/DocsGitHubStarsBadge.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { usePathname } from 'next/navigation'; + +import { getProductSectionForPath } from '../lib/product-docs-nav'; +import s from './github-stars.module.css'; + +export type DocsStarRepo = { + /** Product section id (`file`, `loop`) or `null` for the default Agent Relay repo. */ + id: string | null; + href: string; + label: string; + count: string | null; +}; + +function GithubIcon() { + return ( + + ); +} + +/** + * GitHub stars badge that targets the repo for the docs section currently being + * viewed: Relayfile under `/docs/file`, Relayloop under `/docs/loop`, and Agent + * Relay everywhere else. Star counts are fetched on the server and handed in; + * this component just picks the right one for the active path. + */ +export function DocsGitHubStarsBadge({ repos }: { repos: DocsStarRepo[] }) { + const pathname = usePathname() ?? '/docs'; + const section = getProductSectionForPath(pathname); + const entry = + repos.find((r) => r.id === (section?.id ?? null)) ?? repos.find((r) => r.id === null) ?? repos[0]; + + if (!entry) return null; + + return ( + + + {entry.count ? ( + + + {entry.count} + + ) : null} + + ); +} diff --git a/web/components/GitHubStars.tsx b/web/components/GitHubStars.tsx index 4409f07..cb734ee 100644 --- a/web/components/GitHubStars.tsx +++ b/web/components/GitHubStars.tsx @@ -1,9 +1,13 @@ +import { productSections } from '../lib/product-docs-nav'; +import { DocsGitHubStarsBadge, type DocsStarRepo } from './DocsGitHubStarsBadge'; import s from './github-stars.module.css'; type GitHubRepoResponse = { stargazers_count?: number; }; +const DEFAULT_REPO = 'agentworkforce/relay'; + function GithubIcon() { return ( {productSection.label} + {productSection.version && ( + v{productSection.version} + )}

{productSection.tagline}

diff --git a/web/components/docs/TableOfContents.tsx b/web/components/docs/TableOfContents.tsx index a9b1e2f..b52d38c 100644 --- a/web/components/docs/TableOfContents.tsx +++ b/web/components/docs/TableOfContents.tsx @@ -1,8 +1,10 @@ 'use client'; import { useEffect, useState } from 'react'; +import { usePathname } from 'next/navigation'; import type { TocItem } from '../../lib/docs'; +import { getProductSectionForPath } from '../../lib/product-docs-nav'; import { useDocsLanguage } from './DocsLanguageContext'; import { DocsVersionSelect } from './DocsVersionSelect'; import styles from './docs.module.css'; @@ -10,6 +12,8 @@ import styles from './docs.module.css'; export function TableOfContents({ items }: { items: TocItem[] }) { const [activeId, setActiveId] = useState(''); const { language, setLanguage } = useDocsLanguage(); + const pathname = usePathname() ?? '/docs'; + const productSection = getProductSectionForPath(pathname); useEffect(() => { const headings = items.map((item) => document.getElementById(item.id)).filter(Boolean) as HTMLElement[]; @@ -47,7 +51,13 @@ export function TableOfContents({ items }: { items: TocItem[] }) { - + {!productSection && } + {productSection?.version && ( +
+ Version + v{productSection.version} +
+ )} {items.length > 0 && (