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
48 changes: 48 additions & 0 deletions web/app/docs/file/[slug]/og.png/route.tsx
Original file line number Diff line number Diff line change
@@ -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(
<DefaultVariant
headingFamily={headingFamily}
bodyFamily={bodyFamily}
eyebrow={fileSection.label}
title={doc.frontmatter.title}
subtitle={doc.frontmatter.description}
/>,
{
...OG_SIZE,
...(fonts.length > 0 ? { fonts } : {}),
}
);
}
16 changes: 16 additions & 0 deletions web/app/docs/file/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';

import { ProductDocPage } from '../../../../components/docs/ProductDocPage';
import { ogImage } from '../../../../lib/og-meta';
import {
fileSection,
getProductDoc,
Expand All @@ -27,13 +28,28 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
return { title: 'Not Found' };
}

const ogPath = `/docs/${SECTION_ID}/${slug}/og.png`;

return {
title: `${doc.frontmatter.title} — Relayfile`,
description: doc.frontmatter.description,
alternates: {
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)],
},
};
}

Expand Down
4 changes: 2 additions & 2 deletions web/app/docs/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,7 +24,7 @@ export default function DocsLayout({ children }: { children: ReactNode }) {
<div className={styles.docsPage}>
<SiteNav
center={<DocsSearch index={searchIndex} productScopes={productScopes} />}
actions={<GitHubStarsBadge />}
actions={<DocsGitHubStarsBadgeServer />}
mobileMenuContent={<DocsNav variant="mobileMenu" />}
hideMobileDocsLink
/>
Expand Down
48 changes: 48 additions & 0 deletions web/app/docs/loop/[slug]/og.png/route.tsx
Original file line number Diff line number Diff line change
@@ -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(
<DefaultVariant
headingFamily={headingFamily}
bodyFamily={bodyFamily}
eyebrow={loopSection.label}
title={doc.frontmatter.title}
subtitle={doc.frontmatter.description}
/>,
{
...OG_SIZE,
...(fonts.length > 0 ? { fonts } : {}),
}
);
}
16 changes: 16 additions & 0 deletions web/app/docs/loop/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from 'next';

import { ProductDocPage } from '../../../../components/docs/ProductDocPage';
import { ogImage } from '../../../../lib/og-meta';
import {
getProductDoc,
getProductDocMarkdownUrl,
Expand All @@ -27,13 +28,28 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
return { title: 'Not Found' };
}

const ogPath = `/docs/${SECTION_ID}/${slug}/og.png`;

return {
title: `${doc.frontmatter.title} — Relayloop`,
description: doc.frontmatter.description,
alternates: {
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)],
},
};
}

Expand Down
59 changes: 59 additions & 0 deletions web/components/DocsGitHubStarsBadge.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
);
}

/**
* 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 (
<a
href={entry.href}
target="_blank"
rel="noopener noreferrer"
className={s.badge}
aria-label={
entry.count
? `View ${entry.label} on GitHub (${entry.count} stars)`
: `View ${entry.label} on GitHub`
}
>
<GithubIcon />
{entry.count ? (
<span className={s.meta}>
<span className={s.divider} />
<span className={s.count}>{entry.count}</span>
</span>
) : null}
</a>
);
}
34 changes: 32 additions & 2 deletions web/components/GitHubStars.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
Expand All @@ -16,9 +20,9 @@ function formatStarCount(stars: number): string {
return stars >= 1000 ? `${(stars / 1000).toFixed(1)}k` : String(stars);
}

async function getGitHubStars(): Promise<string | null> {
async function getGitHubStars(repo: string = DEFAULT_REPO): Promise<string | null> {
try {
const response = await fetch('https://api.github.com/repos/agentworkforce/relay', {
const response = await fetch(`https://api.github.com/repos/${repo}`, {
headers: {
Accept: 'application/vnd.github+json',
'User-Agent': 'agentrelay-web',
Expand All @@ -35,6 +39,32 @@ async function getGitHubStars(): Promise<string | null> {
}
}

/**
* Docs header badge that follows the active section: Relayfile under
* `/docs/file`, Relayloop under `/docs/loop`, Agent Relay elsewhere. Fetches
* every section's star count on the server, then the client picks by path.
*/
export async function DocsGitHubStarsBadgeServer() {
const targets: { id: string | null; repo: string; label: string }[] = [
{ id: null, repo: DEFAULT_REPO, label: 'Agent Relay' },
...productSections.map((section) => ({
id: section.id,
repo: section.repo,
label: section.label,
})),
];

const counts = await Promise.all(targets.map((t) => getGitHubStars(t.repo)));
const repos: DocsStarRepo[] = targets.map((t, i) => ({
id: t.id,
href: `https://github.com/${t.repo}`,
label: t.label,
count: counts[i],
}));

return <DocsGitHubStarsBadge repos={repos} />;
}

export async function GitHubStarsBadge() {
const count = await getGitHubStars();

Expand Down
4 changes: 4 additions & 0 deletions web/components/docs/DocsNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const productNavIcons: Record<string, NavIcon> = {
'local-development': Terminal,
mounting: FolderOpen,
sdk: SiTypescript,
'python-sdk': SiPython,
agents: Bot,
'adapters-and-providers': Plug,
comparison: BookOpen,
Expand Down Expand Up @@ -197,6 +198,9 @@ export function DocsNav({ variant = 'sidebar' }: { variant?: 'sidebar' | 'mobile
<Link href={docsBasePath} className={styles.productHeaderName}>
<ProductIcon className={styles.productHeaderIcon} aria-hidden="true" />
<span>{productSection.label}</span>
{productSection.version && (
<span className={styles.productHeaderVersion}>v{productSection.version}</span>
)}
</Link>
<p className={styles.productHeaderTagline}>{productSection.tagline}</p>
</div>
Expand Down
12 changes: 11 additions & 1 deletion web/components/docs/TableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
'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';

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[];
Expand Down Expand Up @@ -47,7 +51,13 @@ export function TableOfContents({ items }: { items: TocItem[] }) {
</select>
</label>
</div>
<DocsVersionSelect />
{!productSection && <DocsVersionSelect />}
{productSection?.version && (
<div className={styles.versionControl}>
<span className={styles.versionLabel}>Version</span>
<span className={styles.versionStatic}>v{productSection.version}</span>
</div>
)}

{items.length > 0 && (
<nav className={styles.tocNav} aria-label="On this page">
Expand Down
Loading
Loading