1,104 Static Pages to Connect Plumbers: Why I Built a Local Directory in 2025

Projects· 6 min read

1,104 Static Pages to Connect Plumbers: Why I Built a Local Directory in 2025

The idea nobody wants to touch

Look, I'll be direct: when I told other developers I was going to build an emergency plumber directory in 2025, most looked at me like I was crazy.

"Local directories have been dead since 2015," they said. "Google My Business dominates everything," they insisted.

And they were right... to a point.

But there's something nobody mentions: when your pipe bursts at 3 AM and your house is flooding, you're not going to spend 20 minutes reading Google reviews. You need a phone number for someone available. Now.

That's the real problem I wanted to solve with [Find Emergency Plumber](https://github.com/brianMena/find-emergency-plumber).

The real numbers

Before diving into the code, let me show you what I built:

→ 1,104 static pages generated at build time → 90 cities across the United States covered → 1,251 verified plumbers with quality scores → Next.js 16 with App Router for maximum speed → Supabase as PostgreSQL database → Sanity CMS for blog content management

All deployed on Vercel, with a Telegram-style dark theme I love.

Why build for the US market from Spain? Because the market is larger and the problem is universal. A broken pipe in Miami is as urgent as one in Madrid.

The technical decision that changed everything

The interesting part of this project isn't *what* I built, but how I built it.

Most local directories use dynamic rendering: you search, the server queries the database, renders the page, sends it to you. It's slow and bad for SEO.

I went in the opposite direction: massive static generation.

Why 1,104 static pages

Each city + service combination is generated as a static HTML page at build time:

```typescript // Simplified example of the structure export async function generateStaticParams() { const cities = await getCities(); // 90 cities const services = ['emergency', '24-7', 'repair', 'installation'];

return cities.flatMap(city => services.map(service => ({ city: city.slug, service: service })) ); } ```

Each page is generated once during build and served instantly. Google loves this because:

1. Brutal speed: No database query time 2. Pure HTML: Crawlers can index everything immediately 3. Cache-friendly: Vercel can serve everything from edge

The stack I chose (and why)

Next.js 16 with App Router: Next.js 16 App Router makes static generation trivial. I used TypeScript in strict mode because type errors in production are expensive.

Supabase (PostgreSQL): I needed a relational database for plumbers, cities, and their relationships. Supabase gives me PostgreSQL with an excellent SDK and edge functions if I need them later.

Sanity CMS: For the blog I needed something more flexible than simple markdown. Sanity v3 lets me edit content with real-time preview and has a fast API.

Tailwind CSS 4: The new Tailwind with native CSS is significantly faster in development. No more conflicts with `py-*` utilities (yes, this happened to me and I had to fix it in a recent commit).

```typescript // package.json main dependencies { "next": "16.x", "react": "19.2", "@supabase/supabase-js": "latest", "next-sanity": "latest", "tailwindcss": "4.x" } ```

The real problems nobody tells you about

Building this wasn't a straight path. Here are the real problems I faced:

1. Google Search Console hated me

For weeks, Google wasn't indexing my pages. The problem: conflicts between `www` and non-www in canonical tags. I had to:

  • Force consistent 301 redirects
  • Fix canonical URLs in the sitemap
  • Block static assets from crawling to not waste crawl budget

Real commit from the project: ``` fix: GSC indexation issues - www canonical, redirects, and static asset blocking ```

This is reality: technical SEO matters as much as content.

2. Vercel Image Optimization vs Sanity

Vercel charges for image optimization beyond the free tier. Sanity images were going through Vercel's optimizer and burning through my budget.

Solution: bypass Vercel's optimizer for Sanity images, using Sanity's CDN directly which already optimizes images.

```typescript // Use Sanity's loader directly import imageUrlBuilder from '@sanity/image-url';

const builder = imageUrlBuilder(sanityClient);

function urlFor(source: any) { return builder.image(source); } ```

3. Null-checking everywhere

When you integrate an external CMS like Sanity, you assume the data is well-formatted. Spoiler: it's not always.

I had to add defensive null-checking in all components that render Sanity content:

```typescript // Before (crashed in production) const imageUrl = post.mainImage.asset.url;

// After (defensive) const imageUrl = post.mainImage?.asset?.url ?? '/fallback.jpg'; ```

What's next: Real-time SMS and calls

The project works, but it's missing the key piece: instant communication.

When someone needs an emergency plumber at 3 AM, they don't want to fill out a form and wait for an email. They want to:

1. Click "Call now" 2. Have the system call the nearest available plumber 3. Connect the call in seconds

This requires integration with Twilio or another telephony provider. The architecture would be:

→ User clicks "Emergency" → Webhook to Twilio API → Twilio calls plumbers in order of proximity → First plumber who answers connects with user

That's the next phase of the project. The interesting thing is that static SEO is already working to drive traffic; now it's time to optimize conversion.

Takeaways for your next project

1. Static generation > Dynamic for directories If your content doesn't change every second, generate static pages. SEO and speed are worth it.

2. Small problems kill you in production Canonical URLs, null-checking, image budgets... these "details" can sink a project.

3. Build for large markets from wherever you are I'm in Spain, building for USA. The internet has no borders, use that to your advantage.

4. Solve an urgent problem, not a convenient one A broken pipe is an urgent problem. People pay for urgency, not convenience.

5. Ship fast, iterate later This project has 5 months of commits. I didn't wait to have Twilio integrated to launch. I shipped with the essentials and I'm iterating.

The local directory isn't dead

What's dead are poorly built local directories: slow, poorly optimized, not solving real problems.

But if you build something fast, well-optimized, and that solves a real urgent problem, there's space.

1,104 static pages. 1,251 plumbers. 90 cities.

This is building in public: I show you real numbers, commits that fixed bugs, technical decisions I made.

What are you going to build?

---

*You can see the complete project code on [GitHub](https://github.com/brianMena/find-emergency-plumber). All the stack, all the decisions, everything documented.*

Brian Mena

Brian Mena

Software engineer building profitable digital products: SaaS, directories and AI agents. All from scratch, all in production.

LinkedIn