← back to Small Business Builder

src/render/proprietors.js

157 lines

// /proprietors — every sole-proprietor barbershop / salon flagged by the
// owner-name heuristic, grouped by neighborhood. The differentiating angle:
// chains, franchises, and salon collectives are filtered OUT. What remains
// is the "the chair IS the brand" cohort — 5,000+ chairs across LA whose
// state license reads as a person's name.
//
// Editorial framing: each section is a neighborhood, each row is "{NAME} ·
// {category}" — no chain logos, no marketing copy. Reads like a phone book
// of LA's actual sole-prop chairs, surfaced cleanly.

import { esc, escJsonLd } from '../lib/escape.js';

function slugify(s) {
  return String(s || '').toLowerCase().replace(/[^\w]+/g, '-').replace(/^-|-$/g, '');
}

export function renderProprietors({ businesses = [], hoodCounts = [] } = {}) {
  const baseUrl = process.env.SITE_URL || 'https://localhost';

  // Group by neighborhood for the editorial run.
  const byHood = new Map();
  for (const b of businesses) {
    const k = b.neighborhood || 'Unspecified';
    if (!byHood.has(k)) byHood.set(k, []);
    byHood.get(k).push(b);
  }
  // Sort neighborhoods by row count descending (biggest sole-prop counts lead).
  const orderedHoods = [...byHood.entries()]
    .sort((a, b) => b[1].length - a[1].length);

  const itemList = {
    '@context': 'https://schema.org',
    '@type': 'ItemList',
    name: `Sole-Proprietor Salons & Barbers · ${businesses.length}`,
    description: 'Every sole-proprietor salon or barbershop in the LA Salon Directory whose state-licensed name reads as a person, not a brand.',
    url: `${baseUrl}/proprietors`,
    numberOfItems: businesses.length,
    itemListElement: businesses.slice(0, 100).map((b, i) => ({
      '@type': 'ListItem',
      position: i + 1,
      item: {
        '@type': 'HairSalon',
        name: b.name,
        url: `${baseUrl}/biz/${b.slug}`,
      },
    })),
  };

  return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>The Proprietors · ${businesses.length} sole-prop chairs · LA Salon Directory</title>
<meta name="description" content="The LA salons and barbershops whose state-licensed name reads as a person's name. ${businesses.length.toLocaleString()} sole-proprietor chairs across ${orderedHoods.length} neighborhoods.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<script type="application/ld+json">${escJsonLd(JSON.stringify(itemList))}</script>
<style>
:root{
  --ink:#1a1a1a;--paper:#f5f5f5;--rule:#e6e3dc;--mute:#7a766f;
  --accent:#b8945a; /* brushed-gold accent — the "proprietor" mark */
  --gold:#b8945a;
  --serif:"Cormorant Garamond",Georgia,serif;
  --sans:"Inter",-apple-system,BlinkMacSystemFont,system-ui,sans-serif;
  --mono:ui-monospace,"SF Mono",Menlo,monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{background:var(--paper);color:var(--ink);font-family:var(--sans);font-weight:400;-webkit-font-smoothing:antialiased}
a{color:inherit;text-decoration:none}

.topbar{padding:18px 32px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--rule);position:sticky;top:0;background:var(--paper);z-index:60;flex-wrap:wrap;gap:12px}
.brand{font-family:var(--serif);font-size:20px;letter-spacing:-.01em;font-weight:500}
.brand em{font-style:italic;color:var(--accent)}
.topnav{display:flex;gap:18px;font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute)}
.topnav a:hover{color:var(--ink)}

.hero{padding:80px 32px 56px;border-bottom:1px solid var(--rule);max-width:1240px;margin:0 auto}
.hero-eyebrow{font-family:var(--mono);font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--accent);margin-bottom:18px}
.hero h1{font-family:var(--serif);font-weight:500;font-size:clamp(48px,8vw,108px);line-height:.96;letter-spacing:-.02em;margin-bottom:22px;max-width:14ch}
.hero h1 em{font-style:italic;color:var(--accent)}
.hero-lede{font-size:18px;line-height:1.6;color:#3a3a3a;max-width:62ch;margin-bottom:32px}
.hero-lede em{font-family:var(--serif);font-style:italic;font-size:21px;color:var(--ink)}
.hero-stats{display:flex;gap:48px;font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute);flex-wrap:wrap}
.hero-stats b{display:block;font-family:var(--serif);font-style:italic;font-size:34px;letter-spacing:-.01em;color:var(--ink);font-weight:500;margin-top:4px;text-transform:none}

.hood-block{padding:64px 32px;max-width:1240px;margin:0 auto;border-top:1px solid var(--rule)}
.hood-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:32px;padding-bottom:14px;border-bottom:1px solid var(--rule)}
.hood-head h2{font-family:var(--serif);font-weight:500;font-size:clamp(36px,4.5vw,64px);letter-spacing:-.015em;line-height:1}
.hood-head .meta{font-family:var(--mono);font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--mute)}

.row-grid{column-count:3;column-gap:48px;column-rule:1px solid var(--rule)}
@media (max-width:880px){.row-grid{column-count:1}}
.prop-row{break-inside:avoid;display:grid;grid-template-columns:1fr auto;gap:14px;align-items:baseline;padding:9px 0;border-bottom:1px dotted var(--rule);transition:padding 150ms ease}
.prop-row:hover{padding-left:6px;background:#fff}
.prop-name{font-family:var(--serif);font-size:18px;font-weight:500;letter-spacing:-.005em;line-height:1.25}
.prop-cat{font-family:var(--mono);font-size:9px;letter-spacing:.16em;color:var(--accent);text-transform:uppercase;text-align:right;white-space:nowrap}

.footer{padding:48px 32px;border-top:1px solid var(--rule);font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--mute);display:flex;justify-content:space-between;flex-wrap:wrap;gap:16px}
@media (max-width:880px){
  .topbar{padding:14px 20px}
  .hero{padding:48px 20px 32px}
  .hood-block{padding:48px 20px}
}
</style>
</head>
<body>
<header class="topbar">
  <div class="brand"><a href="/">LA <em>Salon</em> Directory</a> <span style="color:var(--mute);font-style:italic">/ the proprietors</span></div>
  <nav class="topnav">
    <a href="/neighborhoods">Neighborhoods</a>
    <a href="/fresh">Just Opened</a>
    <a href="/corridor/ventura-blvd">Ventura</a>
    <a href="/corridor/sunset-blvd">Sunset</a>
    <a href="/map">Map</a>
    <a href="/">Home</a>
  </nav>
</header>

<section class="hero">
  <div class="hero-eyebrow">The Proprietors · Sole-License Chairs</div>
  <h1>The chair <em>is</em> the brand.</h1>
  <p class="hero-lede">In <em>${businesses.length.toLocaleString()}</em> chairs across LA County, the state license isn't issued to a corporation, an LLC, or a salon collective — it's issued to a person. This is the directory of those people: the sole-proprietor barbers, stylists, and colorists whose name on the door is the same as the name on the license.</p>
  <div class="hero-stats">
    <div>Sole-prop chairs<b>${businesses.length.toLocaleString()}</b></div>
    <div>Neighborhoods<b>${orderedHoods.length.toLocaleString()}</b></div>
    <div>State-licensed<b>100%</b></div>
  </div>
</section>

${orderedHoods.map(([hood, list]) => `
  <section class="hood-block">
    <div class="hood-head">
      <h2>${esc(hood)}</h2>
      <span class="meta">${list.length} chair${list.length===1?'':'s'}</span>
    </div>
    <div class="row-grid">
      ${list.map(b => `
        <a class="prop-row" href="/biz/${esc(b.slug)}">
          <span class="prop-name">${esc(b.name)}</span>
          <span class="prop-cat">${esc(b.category || 'salon')}</span>
        </a>
      `).join('')}
    </div>
  </section>
`).join('')}

<footer class="footer">
  <span>The Proprietors · LA Salon Directory · ${new Date().getFullYear()}</span>
  <span>${businesses.length.toLocaleString()} sole-prop chairs · sourced from CA BBCB</span>
  <span><a href="/">← Home</a></span>
</footer>
</body>
</html>`;
}