2026-05-31 12:08:05 +02:00
|
|
|
// Server-only fetchers for the public template marketplace. Used by the
|
|
|
|
|
// server-rendered template detail page (SEO metadata + JSON-LD) and the
|
|
|
|
|
// sitemap. Never import this into a client component.
|
|
|
|
|
|
|
|
|
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000';
|
|
|
|
|
|
|
|
|
|
export interface TemplateTool {
|
|
|
|
|
name: string;
|
|
|
|
|
description: string;
|
|
|
|
|
inputSchema: Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface TemplateSecretHint {
|
|
|
|
|
key: string;
|
|
|
|
|
description: string;
|
|
|
|
|
howToGetUrl?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface TemplateDetail {
|
|
|
|
|
id: string;
|
|
|
|
|
slug: string;
|
|
|
|
|
title: string;
|
|
|
|
|
shortDescription: string;
|
|
|
|
|
longDescription: string | null;
|
|
|
|
|
category: string;
|
|
|
|
|
status: 'draft' | 'public' | 'hidden' | 'takedown';
|
|
|
|
|
verified: boolean;
|
|
|
|
|
forkCount: number;
|
|
|
|
|
activeDeployments: number;
|
|
|
|
|
toolsSchema: TemplateTool[];
|
|
|
|
|
generatedCode: string;
|
|
|
|
|
requiredSecrets: TemplateSecretHint[];
|
|
|
|
|
scopes: string[];
|
|
|
|
|
ownerName: string | null;
|
|
|
|
|
ownerOrgName: string | null;
|
|
|
|
|
sourceServerId: string | null;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fetch a single public template by slug for server rendering. Returns null
|
|
|
|
|
* for missing / non-public templates so the page can `notFound()` — we only
|
|
|
|
|
* want `public` templates indexed.
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchTemplate(slug: string): Promise<TemplateDetail | null> {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API_BASE}/v1/templates/${encodeURIComponent(slug)}`, {
|
|
|
|
|
// Cache server-side for 5 min so crawler hits don't hammer the API.
|
|
|
|
|
next: { revalidate: 300 },
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) return null;
|
|
|
|
|
const data = (await res.json()) as { template?: TemplateDetail };
|
|
|
|
|
const t = data.template;
|
|
|
|
|
if (!t || t.status !== 'public') return null;
|
|
|
|
|
return t;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-08 23:00:25 +02:00
|
|
|
export interface TemplateSummary {
|
|
|
|
|
id: string;
|
|
|
|
|
slug: string;
|
|
|
|
|
title: string;
|
|
|
|
|
shortDescription: string;
|
|
|
|
|
category: string;
|
|
|
|
|
status: 'draft' | 'public' | 'hidden' | 'takedown';
|
|
|
|
|
verified: boolean;
|
|
|
|
|
forkCount: number;
|
|
|
|
|
activeDeployments: number;
|
|
|
|
|
ownerName: string | null;
|
|
|
|
|
ownerOrgName: string | null;
|
|
|
|
|
sourceServerId: string | null;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Full public template list for server-rendering the marketplace grid so
|
|
|
|
|
* crawlers see the templates in the initial HTML. Best-effort: returns empty
|
|
|
|
|
* lists on error so the page still renders (the client component refetches
|
|
|
|
|
* on filter changes anyway).
|
|
|
|
|
*/
|
|
|
|
|
export async function fetchPublicTemplates(): Promise<{
|
|
|
|
|
templates: TemplateSummary[];
|
|
|
|
|
categories: string[];
|
|
|
|
|
}> {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API_BASE}/v1/templates?sort=trending`, {
|
|
|
|
|
next: { revalidate: 300 },
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) return { templates: [], categories: [] };
|
|
|
|
|
const data = (await res.json()) as { templates?: TemplateSummary[]; categories?: string[] };
|
|
|
|
|
return { templates: data.templates ?? [], categories: data.categories ?? [] };
|
|
|
|
|
} catch {
|
|
|
|
|
return { templates: [], categories: [] };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-31 12:08:05 +02:00
|
|
|
/** Slugs of public templates, for the sitemap. Best-effort: returns [] on error. */
|
|
|
|
|
export async function fetchPublicTemplateSlugs(): Promise<string[]> {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${API_BASE}/v1/templates?limit=100&sort=newest`, {
|
|
|
|
|
next: { revalidate: 600 },
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) return [];
|
|
|
|
|
const data = (await res.json()) as
|
|
|
|
|
| { templates?: Array<{ slug?: string }> }
|
|
|
|
|
| Array<{ slug?: string }>;
|
|
|
|
|
const list = Array.isArray(data) ? data : (data.templates ?? []);
|
|
|
|
|
return list.map((t) => t.slug).filter((s): s is string => typeof s === 'string');
|
|
|
|
|
} catch {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|