Categories
How projects are grouped — two-level taxonomy with spheres, categories, multi-membership and SEO-friendly URLs.
Why this matters
Categories are the spine of every directory. They drive your sitemap structure, your internal linking graph, your URL hierarchy, and ultimately how Google decides which pages to surface for niche queries. A directory with thoughtful categories ranks for "best [niche tool] for [use case]" long-tail searches automatically; one with sloppy categories spends years fighting flat keyword competition.
DirectoryLaunch ships a two-level taxonomy (spheres → categories) with multi-membership: a single project can sit in multiple categories simultaneously, which means you can have both "Developer Tools → CLI utilities" and "Open Source → Trending" pointing to the same project without duplicating it. This page covers the data shape, admin workflow, and the SEO patterns that emerge from it.
DirectoryKit uses a two-level hierarchy:
- Spheres — broad buckets (e.g. "Software", "Business", "Creative")
- Categories — specific topics inside a sphere (e.g. "SaaS", "Developer Tools", "UI Kits")
Every project belongs to one or more categories. Spheres come along for the ride (they're inferred from the category).
Seeding on first install
The first time the app runs against an empty categories table, it inserts the list from config/directory.config.ts → seedCategories. Edit that list before your first deploy so your directory starts with sensible categories.
After that, manage everything through the admin panel.
Admin view
Powered by @dnd-kit/*. Drag to reorder, drag between spheres to re-home a category. CSV import also supported for bulk.
Frontend components
| Component | Where it's used |
|---|---|
CategorySelector | Dropdown filter on listing pages |
CategoryBadge | Label on project cards |
/categories | Full listing of all spheres + categories |
/categories/[slug] | Single-category page with all projects in it |
Database shape
The categories table columns you care about:
| Column | Meaning |
|---|---|
name | Display name |
slug | URL-friendly ID (used in /categories/[slug]) |
sphere | Which sphere this category belongs to |
icon | Optional Lucide icon name |
color | Optional brand color for the badge |
sort_order | Controls display order |
Projects (apps table) store their categories as a JSONB array of slugs in categories. This lets a single project appear in multiple categories without a join table.
How "Sphere" pages work
There isn't a separate sphere page by default — /categories groups categories under their sphere header. If you want dedicated sphere landing pages, add a route under app/(marketing)/spheres/[slug]/page.tsx that queries categories by sphere.
A real-world walkthrough: planning categories for a SaaS-for-agencies directory
Suppose you're launching a directory for SaaS tools agencies use. You'd shape the taxonomy like this:
- Sphere "Operations" — Project Management, CRM, Time Tracking, Invoicing, Proposals, Client Portals
- Sphere "Marketing" — SEO Tools, Content Tools, Analytics, Email Marketing, Social Media
- Sphere "Creative" — Design Tools, Asset Management, Video Production, Copy Generation
- Sphere "Internal" — HR Software, Hiring, Internal Wiki, Time-off Tracking
Twenty categories under four spheres. Each category page becomes a long-tail SEO target ("best CRM for agencies", "agency time tracking software"). A single project — say, Notion — can sit in three of these at once: "Project Management", "Internal Wiki", and "Client Portals". Multi-membership means each appearance gives Notion an internal link from a relevant category page, strengthening its ranking signals.
Avoid going deeper than two levels: three-level taxonomies create thin leaf pages that dilute crawl signals. Avoid more than ~30 total categories for the same reason — every category page needs at least 5–8 listings to feel substantive to Google.
Common pitfalls
- Editing seed categories after first deploy. Once the table is populated,
seedCategoriesis ignored. Add new categories from the admin panel, not from the config file. (The seed only runs against an empty table.) - Renaming category slugs in production. Slugs are part of the URL (
/categories/saas). Renaming breaks every backlink and every internal link from project pages. If you must rename, ship a 301 redirect from old slug to new innext.config.jsfirst. - Letting projects sit in zero categories. A project with no category renders fine on its own page but won't appear on any category listing — which is your primary discovery surface. Validate at submission: at least one category required.
- Over-categorizing a single project. Listing one project in 8 categories looks like spam to Google, dilutes the relevance signal of each category page, and confuses users. Cap at 3 categories per project (enforce via Zod validation in submission).
- Skipping the sphere field. The
spherecolumn drives the grouped display on/categories. Empty spheres mean ungrouped categories, which makes the index page look like a flat 30-item dump. Always assign a sphere — even if you only use one.
FAQ
Can a category have a custom landing page (above the project list)?
Yes — extend app/categories/[slug]/page.tsx to read a content column from the categories table and render it as MDX above the project grid. This is the cheapest way to get a 600+ word SEO landing page for each category without rewriting the listing logic.
How do I migrate from a single-category to multi-category schema?
If you started with category_id (single FK), migrate to JSONB with: UPDATE apps SET categories = jsonb_build_array((SELECT slug FROM categories WHERE id = category_id)). Then drop the old column. Existing project URLs are unchanged; only the listing-page query layer needs updating.
Should I expose all categories in the main nav, or just spheres?
Spheres only, with categories revealed on hover or on the /categories page. 30+ links in the main nav is overwhelming and dilutes your nav-link equity. Many directories on DirectoryLaunch put one "Browse" link in the nav that points to /categories and let the index page do the heavy lifting.