← back to Small Business Builder
src/render/fresh.js
151 lines
// /fresh — newest state-licensed shops first. The BBCB Issue Date field is the
// "registered with the state" timestamp — a salon's grand-opening signal. This
// page lists the freshest 100 by issue date, perfect for a weekly-refresh
// page that pulls repeat traffic + ranks for "newly opened salon" / "new
// barbershop in {neighborhood}" long-tail queries.
import { esc, escJsonLd } from '../lib/escape.js';
function fmtDate(s) {
// BBCB stores either "MM-DD-YYYY" or "MM/DD/YYYY" — render as "Mar 14, 2025"
const m = String(s || '').match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})$/);
if (!m) return s || '';
const [, mm, dd, yyyy] = m;
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const idx = parseInt(mm, 10) - 1;
if (idx < 0 || idx > 11) return s;
return `${months[idx]} ${parseInt(dd, 10)}, ${yyyy}`;
}
function slugify(s) {
return String(s || '').toLowerCase().replace(/[^\w]+/g, '-').replace(/^-|-$/g, '');
}
export function renderFresh({ businesses = [] } = {}) {
const baseUrl = process.env.SITE_URL || 'https://localhost';
const collectionPage = {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
name: 'New & Newly-Licensed Salons in LA',
description: 'The 100 most-recently licensed salons, barbershops, and spas across LA + Orange + Riverside + San Bernardino + Ventura counties.',
url: `${baseUrl}/fresh`,
dateModified: new Date().toISOString(),
mainEntity: {
'@type': 'ItemList',
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>Just Opened · Newest LA Salons & Barbershops · LA Salon Directory</title>
<meta name="description" content="The freshest state-licensed salons, barbers, and spas across the LA region — sorted by Board of Barbering & Cosmetology issue date.">
<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(collectionPage))}</script>
<style>
:root{
--ink:#1a1a1a;--paper:#f5f5f5;--rule:#e6e3dc;--mute:#7a766f;
--accent:#a16207; /* warm gold-amber for "fresh / new" */
--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 48px;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,7vw,96px);line-height:.96;letter-spacing:-.02em;margin-bottom:20px;max-width:14ch}
.hero h1 em{font-style:italic;color:var(--accent)}
.hero-lede{font-size:18px;line-height:1.55;color:#3a3a3a;max-width:62ch;margin-bottom:28px}
.hero-meta{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--mute)}
.fresh-list{padding:48px 32px;max-width:1240px;margin:0 auto}
.fresh-row{display:grid;grid-template-columns:120px 1fr 200px 120px;gap:32px;align-items:baseline;padding:22px 0;border-bottom:1px solid var(--rule);transition:padding 180ms ease}
.fresh-row:hover{padding-left:8px;background:#fff}
.fresh-date{font-family:var(--mono);font-size:11px;letter-spacing:.06em;color:var(--accent);text-transform:uppercase;font-weight:500}
.fresh-name{font-family:var(--serif);font-size:24px;font-weight:500;letter-spacing:-.005em;line-height:1.1}
.fresh-name em{font-style:italic;color:var(--mute);font-size:13px;margin-left:8px}
.fresh-where{font-family:var(--mono);font-size:11px;letter-spacing:.06em;color:var(--mute);text-transform:uppercase;line-height:1.5}
.fresh-lic{font-family:var(--mono);font-size:9px;letter-spacing:.16em;color:var(--mute);text-align:right}
.fresh-lic a{padding:4px 8px;border:1px solid var(--rule);transition:all 150ms ease}
.fresh-lic a:hover{background:var(--accent);color:var(--paper);border-color:var(--accent)}
@media (max-width:880px){
.fresh-row{grid-template-columns:80px 1fr;gap:16px}
.fresh-where,.fresh-lic{grid-column:2;font-size:10px}
}
.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}
.fresh-list{padding:32px 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">/ just opened</span></div>
<nav class="topnav">
<a href="/neighborhoods">Neighborhoods</a>
<a href="/proprietors">Proprietors</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">Fresh · State-Licensed · This Quarter</div>
<h1>Just <em>opened.</em></h1>
<p class="hero-lede">The 100 most-recent state-licensed salons, barbers, and spas across the LA region — sorted by California Board of Barbering & Cosmetology issue date. New chairs, new lobbies, new colorists.</p>
<div class="hero-meta">${businesses.length} listings · sourced from CA DCA · refreshed ${new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</div>
</section>
<section class="fresh-list">
${businesses.map(b => {
const issued = b.source_data_json?.issue_date || '';
const lic = b.source_data_json?.license_number || '';
const licUrl = lic ? `https://search.dca.ca.gov/results?lic=${encodeURIComponent(lic)}` : '';
return `
<div class="fresh-row">
<span class="fresh-date">${esc(fmtDate(issued))}</span>
<a class="fresh-name" href="/biz/${esc(b.slug)}">${esc(b.name)}<em>${esc(b.category || '')}</em></a>
<span class="fresh-where">${esc(b.neighborhood || b.city || '')}</span>
<span class="fresh-lic">${licUrl ? `<a href="${esc(licUrl)}" target="_blank" rel="noopener">◆ ${esc(lic)}</a>` : ''}</span>
</div>`;
}).join('')}
</section>
<footer class="footer">
<span>LA Salon Directory · ${new Date().getFullYear()}</span>
<span>Source: CA Department of Consumer Affairs · BBCB</span>
<span><a href="/">← Home</a></span>
</footer>
</body>
</html>`;
}