From f96885cedb57e09b27fdd5908c12e22d8434dbab Mon Sep 17 00:00:00 2001 From: Nilesh Raj Date: Wed, 21 Oct 2026 13:12:25 +0530 Subject: [PATCH] Fix category metadata and homepage SEO --- app/categories/[slug]/page.tsx | 9 ++------- app/feedback/page.tsx | 13 ++++++++----- app/page.tsx | 13 +++++++++++-- app/sitemap.ts | 3 --- components/DirectoryPreview.tsx | 2 +- components/FeyCards.tsx | 6 +++--- components/HomeCatalogSections.tsx | 3 ++- components/ProfileAvatars.tsx | 12 ++++-------- lib/category-metadata.ts | 21 +++++++++++++++++++++ lib/homepage-preview.ts | 10 ++++++++++ lib/sitemap.ts | 2 -- tests/category-metadata.test.ts | 30 ++++++++++++++++++++++++++++++ tests/homepage-preview.test.ts | 30 ++++++++++++++++++++++++++++++ tests/sitemap-generation.test.ts | 8 ++------ 14 files changed, 124 insertions(+), 38 deletions(-) create mode 100644 lib/category-metadata.ts create mode 100644 lib/homepage-preview.ts create mode 100644 tests/category-metadata.test.ts create mode 100644 tests/homepage-preview.test.ts diff --git a/app/categories/[slug]/page.tsx b/app/categories/[slug]/page.tsx index af65fb3..e776feb 100644 --- a/app/categories/[slug]/page.tsx +++ b/app/categories/[slug]/page.tsx @@ -3,6 +3,7 @@ import { AgentCard } from "@/components/AgentCard"; import { CategoryNav } from "@/components/CategoryNav"; import { CatalogState } from "@/components/CatalogState"; import { getAgents, getCategories, getCategory, getCategoryAgents } from "@/lib/data"; +import { getCategoryMetadata } from "@/lib/category-metadata"; import { serializeJsonLd } from "@/lib/json-ld"; import { createPublicPageMetadata } from "@/lib/seo"; import { SITE_URL } from "@/lib/site"; @@ -20,13 +21,7 @@ export async function generateStaticParams(): Promise<{ slug: string }[]> { export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; const category = await getCategory(slug); - const title = category?.name - ? `Open-source ${category.name.toLowerCase()} AI agents: setup guides` - : "Open-source AI agent category"; - const description = category?.description - ? `Find open-source ${category.name.toLowerCase()} AI agents. Review project sources, setup requirements, and documented access. ${category.description}` - : "Explore open-source AI agents by category, with source repositories, setup guidance, and documented system access."; - return createPublicPageMetadata({ title, description, path: `/categories/${slug}` }); + return createPublicPageMetadata({ ...getCategoryMetadata(category), path: `/categories/${slug}` }); } export default async function CategoryPage({ params }: { params: Promise<{ slug: string }> }) { diff --git a/app/feedback/page.tsx b/app/feedback/page.tsx index 4437562..fc8d170 100644 --- a/app/feedback/page.tsx +++ b/app/feedback/page.tsx @@ -15,8 +15,11 @@ export default function FeedbackPage() { ); } -export const metadata = createPublicPageMetadata({ - title: "Share feedback", - description: "Send feedback about using AgentNine. Public display requires your permission and team approval.", - path: "/feedback", -}); +export const metadata = { + ...createPublicPageMetadata({ + title: "Share feedback", + description: "Send feedback about using AgentNine. Public display requires your permission and team approval.", + path: "/feedback", + }), + robots: { index: false, follow: true }, +}; diff --git a/app/page.tsx b/app/page.tsx index 8c273a3..2c294b4 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,6 +10,9 @@ import { serializeJsonLd } from "@/lib/json-ld"; import { createPublicPageMetadata } from "@/lib/seo"; import { SITE_URL } from "@/lib/site"; +const homepageDescription = + "Explore open-source AI agents. Compare setup requirements, source repositories, and documented access across coding, research, content, and automation projects."; + export default function Home() { const jsonLd = { "@context": "https://schema.org", @@ -19,7 +22,7 @@ export default function Home() { "@id": `${SITE_URL}/#website`, url: SITE_URL, name: "AgentNine", - description: "Find open-source AI agents in AgentNine's directory. Browse by category, compare listings, and review setup, source, and access details.", + description: homepageDescription, publisher: { "@id": `${SITE_URL}/#organization` }, }, { @@ -28,6 +31,12 @@ export default function Home() { url: SITE_URL, name: "AgentNine", logo: { "@type": "ImageObject", url: `${SITE_URL}/icon.svg` }, + sameAs: [ + "https://github.com/Nilesh1735/AgentNine", + "https://www.linkedin.com/company/agentnine/", + "https://x.com/agentninepro", + "https://www.instagram.com/agentnine.pro", + ], }, ], }; @@ -89,6 +98,6 @@ export default function Home() { export const metadata = createPublicPageMetadata({ title: "Open-Source AI Agent Directory", - description: "Find open-source AI agents in AgentNine's directory. Browse by category, compare listings, and review setup guides, source links, and access details.", + description: homepageDescription, path: "/", }); diff --git a/app/sitemap.ts b/app/sitemap.ts index d833cfa..8df8858 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -2,7 +2,6 @@ import type { MetadataRoute } from "next"; import { getAgents, getCategories } from "@/lib/data"; import { getPublishedNewsPosts } from "@/lib/news"; import { - FEEDBACK_PAGE_LAST_MODIFIED, getLatestSitemapLastModified, getSitemapLastModified, STATIC_PAGE_LAST_MODIFIED, @@ -47,7 +46,6 @@ export default async function sitemap(): Promise { newsPosts.map((post) => post.updated_at ?? post.published_at ?? post.created_at), ); const legalLastModified = getSitemapLastModified(legalConfig.lastUpdated); - const feedbackPageLastModified = getSitemapLastModified(FEEDBACK_PAGE_LAST_MODIFIED); const staticPageLastModified = { about: getSitemapLastModified(STATIC_PAGE_LAST_MODIFIED.about), faq: getSitemapLastModified(STATIC_PAGE_LAST_MODIFIED.faq), @@ -65,7 +63,6 @@ export default async function sitemap(): Promise { { url: `${base}/join`, ...(staticPageLastModified.join ? { lastModified: staticPageLastModified.join } : {}), changeFrequency: "monthly", priority: 0.4 }, { url: `${base}/about`, ...(staticPageLastModified.about ? { lastModified: staticPageLastModified.about } : {}), changeFrequency: "monthly", priority: 0.5 }, { url: `${base}/faq`, ...(staticPageLastModified.faq ? { lastModified: staticPageLastModified.faq } : {}), changeFrequency: "monthly", priority: 0.5 }, - { url: `${base}/feedback`, ...(feedbackPageLastModified ? { lastModified: feedbackPageLastModified } : {}), changeFrequency: "monthly", priority: 0.7 }, { url: `${base}/contact`, ...(staticPageLastModified.contact ? { lastModified: staticPageLastModified.contact } : {}), changeFrequency: "monthly", priority: 0.3 }, { url: `${base}/privacy`, ...(legalLastModified ? { lastModified: legalLastModified } : {}), changeFrequency: "yearly", priority: 0.2 }, { url: `${base}/cookies`, ...(legalLastModified ? { lastModified: legalLastModified } : {}), changeFrequency: "yearly", priority: 0.2 }, diff --git a/components/DirectoryPreview.tsx b/components/DirectoryPreview.tsx index 6f79a5f..ce7bd61 100644 --- a/components/DirectoryPreview.tsx +++ b/components/DirectoryPreview.tsx @@ -15,7 +15,7 @@ type PreviewCategory = Pick; export function DirectoryPreview({ agents, categories }: { agents: PreviewAgent[]; categories: PreviewCategory[] }) { const categoryById = new Map(categories.map((category) => [category.id, category])); - const collection = agents.slice(0, 40); + const collection = agents; const ringCollection = [...collection, ...collection]; const loopDuration = Math.max(120, collection.length * 2); const controls = useAnimationControls(); diff --git a/components/FeyCards.tsx b/components/FeyCards.tsx index 7dce73b..cb70aef 100644 --- a/components/FeyCards.tsx +++ b/components/FeyCards.tsx @@ -241,12 +241,12 @@ export function FeyCards() { return (
-

OPEN-SOURCE AI AGENT DIRECTORY

-

+

OPEN-SOURCE AI AGENT DIRECTORY

+

Find Your {" "} Next Agent - +

{ + const previewAgents = selectHomepagePreviewAgents(agents.data, categories.data).map((agent) => { const trust = getAgentTrustState(agent); return { id: agent.id, diff --git a/components/ProfileAvatars.tsx b/components/ProfileAvatars.tsx index 5248d1c..225ceb2 100644 --- a/components/ProfileAvatars.tsx +++ b/components/ProfileAvatars.tsx @@ -1,8 +1,5 @@ import { MaskedAvatars } from "@/components/ui/masked-avatars"; -const emptyAvatar = - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 160'%3E%3C/svg%3E"; - export function ProfileAvatars({ size = 88, className, @@ -13,11 +10,10 @@ export function ProfileAvatars({ return ( | undefined) { + const name = category?.name.trim().replace(/\s+/g, " "); + if (!name) return { title: FALLBACK_TITLE, description: FALLBACK_DESCRIPTION }; + + const categoryType = name.replace(/\s+agents?$/i, "").trim() || name; + const subject = `${categoryType.toLowerCase().replace(/\bai\b/g, "AI")} agents`; + const description = category?.description?.trim(); + + return { + title: `Open-source ${subject}: setup guides`, + description: description + ? `Explore ${subject} by source, setup requirements, and documented access. ${description}` + : `Explore ${subject} by source, setup requirements, and documented access.`, + }; +} diff --git a/lib/homepage-preview.ts b/lib/homepage-preview.ts new file mode 100644 index 0000000..a01cb46 --- /dev/null +++ b/lib/homepage-preview.ts @@ -0,0 +1,10 @@ +import type { Agent, Category } from "./types"; + +export function selectHomepagePreviewAgents>( + agents: readonly T[], + categories: readonly Pick[], +): T[] { + return categories.flatMap(({ id }) => + agents.filter((agent) => agent.category_id === id).slice(0, 6), + ); +} diff --git a/lib/sitemap.ts b/lib/sitemap.ts index 053a8fc..2afe17e 100644 --- a/lib/sitemap.ts +++ b/lib/sitemap.ts @@ -1,5 +1,3 @@ -export const FEEDBACK_PAGE_LAST_MODIFIED = "2026-10-19"; - export const STATIC_PAGE_LAST_MODIFIED = { about: "2026-10-19", faq: "2026-10-17", diff --git a/tests/category-metadata.test.ts b/tests/category-metadata.test.ts new file mode 100644 index 0000000..ca6ffe0 --- /dev/null +++ b/tests/category-metadata.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { getCategoryMetadata } from "@/lib/category-metadata"; + +describe("getCategoryMetadata", () => { + it.each([ + ["Automation agents", "automation agents"], + ["Coding agents", "coding agents"], + ["Content agents", "content agents"], + ["Research agents", "research agents"], + ])("normalizes the %s category without repeating agents", (name, subject) => { + expect(getCategoryMetadata({ name, description: `${name} description.` })).toEqual({ + title: `Open-source ${subject}: setup guides`, + description: `Explore ${subject} by source, setup requirements, and documented access. ${name} description.`, + }); + }); + + it("keeps agent in category labels singular or already included in a longer name", () => { + expect(getCategoryMetadata({ name: "AI research agents", description: "Research." }).title) + .toBe("Open-source AI research agents: setup guides"); + expect(getCategoryMetadata({ name: "Research agent", description: "Research." }).title) + .toBe("Open-source research agents: setup guides"); + }); + + it("uses the generic fallback when category data is unavailable", () => { + expect(getCategoryMetadata(undefined)).toEqual({ + title: "Open-source AI agent category", + description: "Explore open-source AI agents by category, with source repositories, setup guidance, and documented system access.", + }); + }); +}); diff --git a/tests/homepage-preview.test.ts b/tests/homepage-preview.test.ts new file mode 100644 index 0000000..64c2f7c --- /dev/null +++ b/tests/homepage-preview.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { selectHomepagePreviewAgents } from "@/lib/homepage-preview"; + +describe("selectHomepagePreviewAgents", () => { + it("selects up to six agents from each category in category order", () => { + const categories = ["automation", "coding", "content", "research"].map((id) => ({ id })); + const agents = categories.flatMap(({ id }) => + Array.from({ length: 8 }, (_, index) => ({ category_id: id, name: `${id}-${index + 1}` })), + ); + + const selected = selectHomepagePreviewAgents(agents, categories); + + expect(selected).toHaveLength(24); + expect(categories.map(({ id }) => selected.filter((agent) => agent.category_id === id))).toEqual( + categories.map(({ id }) => agents.filter((agent) => agent.category_id === id).slice(0, 6)), + ); + expect(selected.map((agent) => agent.name)).not.toContain("automation-7"); + }); + + it("keeps a category below six agents without filling from another category", () => { + const categories = [{ id: "automation" }, { id: "coding" }]; + const agents = [ + { category_id: "automation", name: "agent-a" }, + { category_id: "coding", name: "agent-b" }, + { category_id: "coding", name: "agent-c" }, + ]; + + expect(selectHomepagePreviewAgents(agents, categories)).toEqual(agents); + }); +}); diff --git a/tests/sitemap-generation.test.ts b/tests/sitemap-generation.test.ts index 51b72b7..7380079 100644 --- a/tests/sitemap-generation.test.ts +++ b/tests/sitemap-generation.test.ts @@ -46,13 +46,9 @@ describe("sitemap generation", () => { mocks.getLatestPublishedSiteFeedbackUpdate.mockResolvedValue(new Date("2026-10-19T09:00:00.000Z")); }); - it("sets the requested feedback priority", async () => { + it("does not include the noindex feedback form", async () => { const entries = await sitemap(); - expect(entries.find((entry) => entry.url.endsWith("/feedback"))).toMatchObject({ - changeFrequency: "monthly", - priority: 0.7, - lastModified: new Date("2026-10-19T00:00:00.000Z"), - }); + expect(entries.find((entry) => entry.url.endsWith("/feedback"))).toBeUndefined(); }); it("uses the newest underlying content date for collection pages", async () => {