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
7 changes: 6 additions & 1 deletion src/app/[locale]/agents/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
const t = await getTranslations({ locale, namespace: "AgentDetail" });
const agent = await prisma.agent.findFirst({
where: { slug, status: "APPROVED" },
select: { name: true, description: true },
select: { name: true, description: true, kind: true, _count: { select: { skills: true } } },
});
if (!agent) return { title: t("notFound") };
const description = (agent.description || `${agent.name} on ${SITE_NAME}`).slice(0, 200);
// Thin: a scraped PROJECT entry with no declared skills and a barely-there
// description adds little unique value over the GitHub repo itself — keep it out
// of the index to limit scaled-content exposure. Substantive entries stay indexed.
const thin = agent.kind === "PROJECT" && agent._count.skills === 0 && (agent.description ?? "").trim().length < 80;
return {
metadataBase: new URL(absoluteUrl("")),
title: agent.name,
description,
...(thin ? { robots: { index: false, follow: true } } : {}),
alternates: localizedAlternates(locale, `/agents/${slug}`),
openGraph: { title: `${agent.name} — ${SITE_NAME}`, description, url: absoluteUrl(`/agents/${slug}`), type: "website", locale: localeOg(locale), images: [absoluteUrl("/opengraph-image")] },
twitter: { card: "summary_large_image", title: `${agent.name} — ${SITE_NAME}`, description },
Expand Down
28 changes: 26 additions & 2 deletions src/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
import createMiddleware from "next-intl/middleware";
import { routing } from "@/i18n/routing";
import type { NextRequest } from "next/server";

// In Next.js 16 the request interceptor file is `proxy.ts` (formerly
// `middleware.ts`). next-intl's middleware factory returns a plain
// `(NextRequest) => NextResponse` handler, so it works unchanged here.
export default createMiddleware(routing);
// `(NextRequest) => NextResponse` handler, which we wrap to add SEO headers.
const handle = createMiddleware(routing);

const NON_DEFAULT_LOCALES = new Set<string>(routing.locales.filter((l) => l !== routing.defaultLocale));

// Entity DETAIL pages (/{locale}/agents/{slug}, /{locale}/skills/{slug}) carry
// English-only bodies (scraped repo descriptions / AgentCards / READMEs); the 14
// non-default locales are translated UI chrome around identical content. Emit
// `X-Robots-Tag: noindex` on those so the 15-locale fan-out isn't read as scaled /
// duplicate content. `follow` keeps link equity flowing, and hreflang still points
// crawlers at the canonical English page (which stays indexable). Listing,
// scenario, and marketing pages are genuinely translated, so they keep indexing in
// every locale.
function isNonDefaultLocaleEntityDetail(pathname: string): boolean {
const seg = pathname.split("/").filter(Boolean);
return seg.length >= 3 && NON_DEFAULT_LOCALES.has(seg[0]) && (seg[1] === "agents" || seg[1] === "skills");
}

export default function proxy(req: NextRequest) {
const res = handle(req);
if (isNonDefaultLocaleEntityDetail(req.nextUrl.pathname)) {
res.headers.set("X-Robots-Tag", "noindex, follow");
}
return res;
}

export const config = {
// Run on page routes only. Exclude:
Expand Down
Loading