From d8b64f1006000deef38ce447f875b978c19009a2 Mon Sep 17 00:00:00 2001
From: gusmar2017
Date: Mon, 13 Jul 2026 18:10:34 -0500
Subject: [PATCH] feat: internal linking hubs for gated servers (indexing
recovery slice 4)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Ensures every isIndexable() (Slice 2 gated) server is reachable in <=3
clicks from the homepage:
- Category hubs (/categories/[category]) now link the FULL gated set per
category via new getIndexableServersByCategory(), not the old 200-row
raw-active-servers query.
- New /categories index page (crawlable hub-of-hubs; fills the
previously-dangling breadcrumb target referenced by generateCategoryJsonLd).
- RelatedServersForCategory now sources from the gated query and is wired
onto every server detail page (previously only used on category hubs and
blog posts) — self-excludes the current server, category-scoped.
- Homepage "Featured Servers" now uses new getIndexableTopServers() instead
of the unfiltered getTopServers(), so the top-N-by-stars linking surface
never resurfaces a thin/non-gated page.
- /servers pagination confirmed already SSR-crawlable (real next/link
hrefs via PaginationLink, passed through the client filter wrapper as
children) — documented with a comment, no behavior change needed.
- Homepage footer links to /categories in two places for redundant crawl
paths.
Click-depth: home -> category hub (1) -> server (2); home -> Featured
Servers top-6 (1); home -> /servers paginated, stars-desc default fronts
the gated cluster (2-3). Category-less gated servers fall back to the
/servers and Featured Servers paths.
Co-Authored-By: Claude Fable 5
---
apps/web/app/categories/[category]/page.tsx | 14 +-
apps/web/app/categories/page.tsx | 144 ++++++++++++++++++
apps/web/app/page.tsx | 24 ++-
apps/web/app/servers/[slug]/page.tsx | 49 ++----
apps/web/app/servers/page.tsx | 10 +-
.../components/RelatedServersForCategory.tsx | 22 ++-
apps/web/lib/queries.ts | 85 +++++++++++
7 files changed, 300 insertions(+), 48 deletions(-)
create mode 100644 apps/web/app/categories/page.tsx
diff --git a/apps/web/app/categories/[category]/page.tsx b/apps/web/app/categories/[category]/page.tsx
index 3cb8884..db5e876 100644
--- a/apps/web/app/categories/[category]/page.tsx
+++ b/apps/web/app/categories/[category]/page.tsx
@@ -1,4 +1,4 @@
-import { getServersByCategory, getCategoryCount } from '@/lib/queries';
+import { getIndexableServersByCategory, getCategoryCount } from '@/lib/queries';
import { generateCategoryMetadata, generateCategoryJsonLd } from '@/lib/metadata';
import { getQualityStatus } from '@/lib/quality-status';
import { safeJsonLd } from '@/lib/json-ld';
@@ -45,10 +45,14 @@ export default async function CategoryPage({
if (!(CATEGORIES as readonly string[]).includes(category)) notFound();
const label = CATEGORY_LABELS[category as keyof typeof CATEGORY_LABELS] || category;
- // Fetch servers (up to 200 for display) and the true total count in parallel.
- // The true count feeds JSON-LD numberOfItems; servers feeds the card grid.
+ // Indexing-recovery Slice 4: this hub must link every isIndexable() (gated)
+ // server in the category — getIndexableServersByCategory() is the full,
+ // uncapped, pre-filtered set (not getServersByCategory()'s 200-row raw
+ // cap), so a large category can never silently drop gated servers from
+ // its own hub page. categoryCount (JSON-LD numberOfItems) intentionally
+ // still reflects the true active-server count, not just the gated subset.
const [servers, categoryCount] = await Promise.all([
- getServersByCategory(category),
+ getIndexableServersByCategory(category),
getCategoryCount(category),
]);
@@ -90,7 +94,7 @@ export default async function CategoryPage({
- {categoryCount} servers in this category
+ {servers.length} servers in this category