Directories
Building a directory with Next.js
Model listings and categories in Prisma and render public, SEO-friendly listing pages in the Next.js App Router.
A directory is a list of records with their own URLs. The Directory Starter uses Next.js 15, Prisma 6, and SQLite. Search indexing for a large catalog is a separate problem, covered in SEO architecture for large Next.js directories.
Models
model Category {
id String @id @default(cuid())
slug String @unique
name String
listings Listing[]
}
model Listing {
id String @id @default(cuid())
slug String @unique
title String
summary String
url String
categoryId String
category Category @relation(fields: [categoryId], references: [id])
status String @default("pending")
createdAt DateTime @default(now())
}
slug is the public id. status defaults to pending, so a submission is not public until you publish it. The listing page loads by slug and calls notFound() unless status === "published".
The listing route
src/app/listings/[slug]/page.tsx is an async server component. In Next.js 15 the params prop is a promise:
export default async function ListingPage({ params }: { params: Promise<{ slug: string }> }) {
const listing = await prisma.listing.findUnique({
where: { slug: (await params).slug },
include: { category: true }
});
if (!listing || listing.status !== "published") notFound();
return (
<main>
<p>{listing.category.name}</p>
<h1>{listing.title}</h1>
<p>{listing.summary}</p>
<a href={listing.url}>Visit</a>
</main>
);
}
generateMetadata uses the same lookup and returns title and description from the row. A missing listing still needs a fallback title because metadata runs even when you later call notFound().
Submission
The home page lists published rows and offers a submit form. New rows stay pending. Review them before they get a public URL. The kit does not take payment for a listing. A paid submission flow is what the Job Board Starter adds for jobs; you can copy that Checkout pattern if a directory listing should be paid.
Search
Filtering is a query against title and category. Do it in Prisma (contains on SQLite is case-sensitive for some builds; test the collation you care about). Do not load every listing into the client and filter there once the table is large.
Want to skip the setup? The Directory Starter already includes categories, listing pages, search, and a pending submission state.