import { CodeBlock } from '@/components/code-block'; import { JsonLd } from '@/components/json-ld'; import { Logo } from '@/components/logo'; import { type TemplateDetail, fetchTemplate } from '@/lib/templates-server'; import { pageMetadata, templateJsonLd } from '@/lib/seo'; import type { Metadata } from 'next'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { Activity, ExternalLink, GitFork, ShieldCheck } from 'lucide-react'; import { CollapsibleCode } from './collapsible-code'; // Server-rendered for SEO: per-template , description, OpenGraph and // SoftwareApplication JSON-LD. The only interactive piece (the code-audit // toggle) lives in a client island. interface PageProps { params: Promise<{ slug: string }>; } export async function generateMetadata({ params }: PageProps): Promise<Metadata> { const { slug } = await params; const t = await fetchTemplate(slug); if (!t) { return pageMetadata({ title: 'Template not found', description: 'This MCP server template is not available.', path: `/templates/${slug}`, }); } return pageMetadata({ title: `${t.title} — MCP server for Claude, Cursor & ChatGPT`, description: t.shortDescription.length > 0 ? t.shortDescription : `Fork the ${t.title} MCP server and deploy your own OAuth-protected copy in seconds.`, path: `/templates/${slug}`, }); } export default async function TemplateDetailPage({ params }: PageProps) { const { slug } = await params; const template: TemplateDetail | null = await fetchTemplate(slug); if (!template) notFound(); const forkHref = `/servers/new?template=${template.slug}`; return ( <div className="flex min-h-screen flex-col"> <JsonLd data={templateJsonLd({ slug: template.slug, title: template.title, description: template.shortDescription, category: template.category, tools: template.toolsSchema.map((t) => t.name), author: template.ownerName ?? template.ownerOrgName, })} /> <header className="sticky top-0 z-50 border-b border-[--color-border] bg-[--color-bg]/85 backdrop-blur-md"> <div className="mx-auto flex h-12 max-w-5xl items-center justify-between px-6"> <div className="flex items-center gap-3"> <Logo /> <span className="text-[12.5px] text-[--color-fg-subtle]"> /{' '} <Link href="/templates" className="hover:text-[--color-fg]"> templates </Link>{' '} / {template.slug} </span> </div> <Link href="/login" className="rounded-md bg-[--color-accent] px-3 py-1.5 text-[12.5px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8]" > Start building </Link> </div> </header> <main className="mx-auto w-full max-w-5xl flex-1 px-6 py-10"> <div className="grid gap-10 lg:grid-cols-[1fr_300px]"> <div> <div className="flex items-center gap-2"> <span className="mono rounded-full border border-[--color-border] bg-[--color-bg-subtle] px-2 py-0.5 text-[11px] text-[--color-fg-muted]"> {template.category} </span> {template.verified && ( <span className="inline-flex items-center gap-1 rounded-full border border-[--color-accent]/40 bg-[--color-accent]/10 px-2 py-0.5 text-[11px] font-medium text-[--color-accent]"> <ShieldCheck size={10} /> verified </span> )} </div> <h1 className="mt-3 text-[28px] font-semibold tracking-tight">{template.title}</h1> <p className="mt-2 text-[14px] leading-relaxed text-[--color-fg-muted]"> {template.shortDescription} </p> {template.longDescription && ( <p className="mt-4 whitespace-pre-wrap text-[13.5px] leading-relaxed text-[--color-fg]"> {template.longDescription} </p> )} <section className="mt-10"> <h2 className="text-[16px] font-semibold tracking-tight"> Tools ({template.toolsSchema.length}) </h2> <div className="mt-3 space-y-3"> {template.toolsSchema.map((tool) => { const paramCount = Object.keys(tool.inputSchema ?? {}).length; return ( <div key={tool.name} className="panel p-3"> <div className="flex items-baseline gap-2"> <span className="mono text-[13px] font-semibold">{tool.name}</span> <span className="mono text-[10.5px] text-[--color-fg-subtle]"> {paramCount} param{paramCount === 1 ? '' : 's'} </span> </div> <p className="mt-1.5 text-[12.5px] text-[--color-fg-muted]"> {tool.description} </p> {paramCount > 0 && ( <div className="mt-2"> <CodeBlock label="input schema" code={JSON.stringify(tool.inputSchema, null, 2)} /> </div> )} </div> ); })} </div> </section> {template.requiredSecrets.length > 0 && ( <section className="mt-10"> <h2 className="text-[16px] font-semibold tracking-tight"> Credentials you'll need </h2> <p className="mt-1 text-[12.5px] text-[--color-fg-muted]"> When you fork, the wizard asks you for these. Your values stay in your container — the template author never sees them. </p> <div className="mt-3 space-y-2"> {template.requiredSecrets.map((s) => ( <div key={s.key} className="panel p-3"> <div className="flex items-baseline justify-between gap-2"> <span className="mono text-[13px] font-semibold">{s.key}</span> {s.howToGetUrl && ( <a href={s.howToGetUrl} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 text-[11.5px] text-[--color-accent] hover:underline" > How to get one <ExternalLink size={10} /> </a> )} </div> <p className="mt-1.5 text-[12.5px] text-[--color-fg-muted]">{s.description}</p> </div> ))} </div> </section> )} <CollapsibleCode code={template.generatedCode} /> </div> <aside className="space-y-3"> <div className="panel p-4"> {template.status === 'public' ? ( <> <Link href={forkHref} className="inline-flex h-11 w-full items-center justify-center rounded-md bg-[--color-accent] text-[13.5px] font-medium text-white transition-colors duration-200 hover:bg-[#5557e8]" > Fork this template → </Link> <p className="mt-2 text-[11.5px] text-[--color-fg-muted]"> One click → your own isolated container. </p> </> ) : ( <div className="rounded-md border border-amber-400/30 bg-amber-400/5 p-2.5 text-[12px] text-amber-200/90"> This template is <span className="mono">{template.status}</span> — not forkable. </div> )} </div> <div className="panel p-3 text-[12px]"> <Row label="Forks" value={template.forkCount} icon={<GitFork size={11} />} /> <Row label="Live deployments" value={template.activeDeployments} icon={<Activity size={11} />} /> <Row label="Category" value={template.category} mono /> <Row label="Published" value={new Date(template.createdAt).toLocaleDateString()} /> <Row label="Author" value={template.ownerName ?? template.ownerOrgName ?? 'anonymous'} /> </div> <div className="panel p-3 text-[11.5px] leading-relaxed text-[--color-fg-muted]"> <strong className="text-[--color-fg]">Forking is safe.</strong> Your fork gets its own Docker container, its own port, its own AES-256-encrypted secrets. The template author has no visibility into your traffic or data. </div> </aside> </div> </main> </div> ); } function Row({ label, value, mono, icon, }: { label: string; value: string | number; mono?: boolean; icon?: React.ReactNode; }) { return ( <div className="flex items-baseline justify-between gap-3 py-1"> <span className="text-[--color-fg-subtle]">{label}</span> <span className={`inline-flex items-center gap-1.5 ${mono ? 'mono' : ''} text-[--color-fg]`}> {icon} {value} </span> </div> ); }