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.

2 min read

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.

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.

Product updates for developers shipping with Designyff kits.

Purchased source may be used in personal, commercial, and client projects. Do not redistribute or resell the original source. Terms of Service

Designyff © 2026 | Starter kits for developers | Powered by  Tacko