← back to Homesonspec
apps/web/src/app/builders/page.tsx
107 lines
import Link from "next/link";
import { prisma } from "@homesonspec/database";
export const dynamic = "force-dynamic";
export const metadata = {
title: "Home Builders Directory | HomesOnSpec",
description:
"Home builders indexed on HomesOnSpec, with each builder's coverage area, website, and live verified inventory count. New builders added continuously.",
};
export default async function BuildersDirectoryPage() {
const builders = await prisma.builder.findMany({
where: { isDemo: false },
include: {
_count: {
select: {
communities: true,
homes: { where: { status: "PUBLISHED", isDemo: false } },
},
},
},
});
const rows = builders
.map((b) => ({
slug: b.slug,
name: b.name,
website: b.websiteUrl,
coverage: b.coverageArea,
communities: b._count.communities,
homeCount: b._count.homes,
}))
.sort((a, b) => b.homeCount - a.homeCount || a.name.localeCompare(b.name));
const totalHomes = rows.reduce((s, r) => s + r.homeCount, 0);
const withInventory = rows.filter((r) => r.homeCount > 0).length;
return (
<div className="mx-auto max-w-6xl px-4 py-8">
<h1 className="text-3xl font-bold">Home Builders Directory</h1>
<p className="mt-2 text-neutral-600">
Every builder on HomesOnSpec — national, regional, and local. {rows.length} builders ·{" "}
{withInventory} with live inventory · {totalHomes.toLocaleString()} verified homes.
</p>
<p className="mt-1 text-xs text-neutral-400">
Listing does not imply sponsorship or endorsement by the builder. Inventory is verified from each
builder's own website.
</p>
<div className="mt-6 overflow-x-auto rounded-xl border border-neutral-200">
<table className="w-full text-left text-sm">
<thead className="border-b border-neutral-200 bg-neutral-50 text-xs uppercase tracking-wide text-neutral-500">
<tr>
<th className="px-4 py-3">Builder</th>
<th className="px-4 py-3">Coverage area</th>
<th className="px-4 py-3">Website</th>
<th className="px-4 py-3 text-right">Homes</th>
</tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.slug} className="border-b border-neutral-100 hover:bg-neutral-50">
<td className="px-4 py-3">
<Link href={`/builders/${r.slug}`} className="font-medium text-teal-800 hover:underline">
{r.name}
</Link>
{r.communities > 0 && (
<span className="ml-2 text-xs text-neutral-400">{r.communities} communities</span>
)}
</td>
<td className="px-4 py-3 text-neutral-600">{r.coverage ?? "—"}</td>
<td className="px-4 py-3">
{r.website ? (
<a
href={r.website}
target="_blank"
rel="nofollow noopener noreferrer"
className="text-teal-700 hover:underline"
>
{r.website.replace(/^https?:\/\/(www\.)?/, "").replace(/\/$/, "")}
</a>
) : (
<span className="text-neutral-400">—</span>
)}
</td>
<td className="px-4 py-3 text-right">
{r.homeCount > 0 ? (
<Link
href={`/search?builder=${r.slug}`}
className="rounded-full bg-teal-50 px-3 py-1 text-xs font-semibold text-teal-800 hover:bg-teal-100"
>
{r.homeCount.toLocaleString()} homes →
</Link>
) : (
<span className="text-xs text-neutral-400">coming soon</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}