← back to Ventura Claw Leads
yolo tick 26: per-neighborhood landing pages — /neighborhood/sherman-oaks etc.
f41ef50c993960f11342f15dc9a608d553ccf241 · 2026-05-07 02:07:29 -0700 · Steve Abrams
routes/public.js:
- GET /neighborhood/:slug — whitelist of 5 valid slugs (sherman-oaks,
studio-city, encino, tarzana, woodland-hills); anything else 404s.
Match in DB by slugified neighborhood OR slugified city, so /neighborhood/
sherman-oaks catches both city='Sherman Oaks' and neighborhood='Sherman
Oaks' rows. Same ORDER BY as /find: tier-priority + claimed-first +
photo-having + alpha. 200-row LIMIT (matches /find).
- Sitemap.xml: 5 new URLs added (one per neighborhood) at priority 0.7,
daily changefreq. Total now 50 URLs (was 45).
views/public/neighborhood.ejs: dedicated landing page —
- BreadcrumbList JSON-LD (Home › Find › <Neighborhood>) for rich-results
- <H1>Businesses in <Neighborhood></H1>
- 8 vertical-filter chips that pre-fill the neighborhood (clicking 'Food'
on /neighborhood/sherman-oaks goes to /find?vertical=food&city=Sherman+Oaks)
- Same business-card grid as /find (photo or gradient hero)
- Empty state for neighborhoods that haven't onboarded businesses yet
views/partials/footer.ejs: new 'Neighborhoods' column with 5 direct links
— internal-link signal that helps Google's crawler reach the new pages.
Verified live: 5 neighborhood URLs return 200, bogus slug 404s, sitemap
shows 5 /neighborhood/* entries, footer renders the column. Real
local-SEO landing-pages now exist for each segment of the corridor.
46/46 tests still pass.
Files touched
M routes/public.jsM views/partials/footer.ejsA views/public/neighborhood.ejs
Diff
commit f41ef50c993960f11342f15dc9a608d553ccf241
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 7 02:07:29 2026 -0700
yolo tick 26: per-neighborhood landing pages — /neighborhood/sherman-oaks etc.
routes/public.js:
- GET /neighborhood/:slug — whitelist of 5 valid slugs (sherman-oaks,
studio-city, encino, tarzana, woodland-hills); anything else 404s.
Match in DB by slugified neighborhood OR slugified city, so /neighborhood/
sherman-oaks catches both city='Sherman Oaks' and neighborhood='Sherman
Oaks' rows. Same ORDER BY as /find: tier-priority + claimed-first +
photo-having + alpha. 200-row LIMIT (matches /find).
- Sitemap.xml: 5 new URLs added (one per neighborhood) at priority 0.7,
daily changefreq. Total now 50 URLs (was 45).
views/public/neighborhood.ejs: dedicated landing page —
- BreadcrumbList JSON-LD (Home › Find › <Neighborhood>) for rich-results
- <H1>Businesses in <Neighborhood></H1>
- 8 vertical-filter chips that pre-fill the neighborhood (clicking 'Food'
on /neighborhood/sherman-oaks goes to /find?vertical=food&city=Sherman+Oaks)
- Same business-card grid as /find (photo or gradient hero)
- Empty state for neighborhoods that haven't onboarded businesses yet
views/partials/footer.ejs: new 'Neighborhoods' column with 5 direct links
— internal-link signal that helps Google's crawler reach the new pages.
Verified live: 5 neighborhood URLs return 200, bogus slug 404s, sitemap
shows 5 /neighborhood/* entries, footer renders the column. Real
local-SEO landing-pages now exist for each segment of the corridor.
46/46 tests still pass.
---
routes/public.js | 47 +++++++++++++++++++++++++-
views/partials/footer.ejs | 8 +++++
views/public/neighborhood.ejs | 76 +++++++++++++++++++++++++++++++++++++++++++
3 files changed, 130 insertions(+), 1 deletion(-)
diff --git a/routes/public.js b/routes/public.js
index cfa97f6..cb14044 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -107,6 +107,47 @@ router.get('/find', async (req, res, next) => {
} catch (err) { next(err); }
});
+// Per-neighborhood landing pages. Match by slugified neighborhood OR city
+// so /neighborhood/sherman-oaks catches both city='Sherman Oaks' and
+// neighborhood='Sherman Oaks' rows. SEO win: Google can rank the URL for
+// '<neighborhood> businesses'-style searches.
+const NEIGHBORHOODS = [
+ { slug: 'sherman-oaks', label: 'Sherman Oaks' },
+ { slug: 'studio-city', label: 'Studio City' },
+ { slug: 'encino', label: 'Encino' },
+ { slug: 'tarzana', label: 'Tarzana' },
+ { slug: 'woodland-hills', label: 'Woodland Hills' }
+];
+router.get('/neighborhood/:slug', async (req, res, next) => {
+ try {
+ const slug = String(req.params.slug || '').toLowerCase().trim();
+ const meta = NEIGHBORHOODS.find(n => n.slug === slug);
+ if (!meta) return res.status(404).render('public/404', { title: 'Not found' });
+
+ const businesses = await db.many(`
+ SELECT id, slug, business_name, vertical, headline, neighborhood, city, state,
+ tier, claim_status, verified, photo_path
+ FROM businesses
+ WHERE status = 'active'
+ AND (LOWER(REPLACE(neighborhood, ' ', '-')) = $1
+ OR LOWER(REPLACE(city, ' ', '-')) = $1)
+ ORDER BY tier = 'premier' DESC, tier = 'standard' DESC,
+ claim_status IN ('self','claimed') DESC,
+ (photo_path IS NOT NULL) DESC,
+ business_name ASC
+ LIMIT 200
+ `, [slug]);
+
+ res.render('public/neighborhood', {
+ title: `${meta.label} businesses on Ventura Blvd · Ventura Claw`,
+ metaDescription: `${businesses.length} small businesses on Ventura Blvd in ${meta.label} — restaurants, salons, retail, fitness, pet, auto detail, cleaning, creative pros. Search and message a business directly via Ventura Claw.`,
+ neighborhood: meta,
+ businesses,
+ verticals: VERTICALS
+ });
+ } catch (err) { next(err); }
+});
+
router.get('/business/:slug', async (req, res, next) => {
try {
const biz = await db.one(`
@@ -357,9 +398,13 @@ router.get('/sitemap.xml', async (req, res, next) => {
const verticalPages = VERTICALS.map(v => ({
loc: `/find?vertical=${v.key}`, changefreq: 'daily', priority: '0.7'
}));
+ // One <url> per neighborhood landing page — strong local-SEO signal.
+ const neighborhoodPages = NEIGHBORHOODS.map(n => ({
+ loc: `/neighborhood/${n.slug}`, changefreq: 'daily', priority: '0.7'
+ }));
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
- for (const p of [...staticPages, ...verticalPages]) {
+ for (const p of [...staticPages, ...verticalPages, ...neighborhoodPages]) {
xml += ` <url><loc>${baseUrl}${escape(p.loc)}</loc><lastmod>${today}</lastmod><changefreq>${p.changefreq}</changefreq><priority>${p.priority}</priority></url>\n`;
}
for (const b of businesses) {
diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs
index d1e353e..d9d041f 100644
--- a/views/partials/footer.ejs
+++ b/views/partials/footer.ejs
@@ -14,6 +14,14 @@
<a href="/find?vertical=pet_services">Pet services</a>
<a href="/find?vertical=creative">Creative</a>
</div>
+ <div class="footer-col">
+ <h4>Neighborhoods</h4>
+ <a href="/neighborhood/sherman-oaks">Sherman Oaks</a>
+ <a href="/neighborhood/studio-city">Studio City</a>
+ <a href="/neighborhood/encino">Encino</a>
+ <a href="/neighborhood/tarzana">Tarzana</a>
+ <a href="/neighborhood/woodland-hills">Woodland Hills</a>
+ </div>
<div class="footer-col">
<h4>Businesses</h4>
<a href="/for-businesses">List your business</a>
diff --git a/views/public/neighborhood.ejs b/views/public/neighborhood.ejs
new file mode 100644
index 0000000..d5bdf37
--- /dev/null
+++ b/views/public/neighborhood.ejs
@@ -0,0 +1,76 @@
+<%- include('../partials/head', { title }) %>
+<%- include('../partials/header') %>
+
+<%
+ // BreadcrumbList JSON-LD: Home › Find › Neighborhood
+ var _publicUrl = (typeof publicUrl !== 'undefined' && publicUrl) ? publicUrl.replace(/\/+$/, '') : 'https://leads.venturaclaw.com';
+ var _ldCrumbs = {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ itemListElement: [
+ { '@type': 'ListItem', position: 1, name: 'Home', item: _publicUrl + '/' },
+ { '@type': 'ListItem', position: 2, name: 'Find a business', item: _publicUrl + '/find' },
+ { '@type': 'ListItem', position: 3, name: neighborhood.label, item: _publicUrl + '/neighborhood/' + neighborhood.slug }
+ ]
+ };
+%>
+<script type="application/ld+json"><%- JSON.stringify(_ldCrumbs) %></script>
+
+<nav class="breadcrumb" aria-label="Breadcrumb" style="padding:16px 28px 0;max-width:1440px;margin:0 auto;font-size:12px;color:var(--fg-muted);letter-spacing:0.04em">
+ <a href="/" style="border:none;color:var(--fg-muted)">Home</a>
+ <span style="margin:0 6px;opacity:0.5">›</span>
+ <a href="/find" style="border:none;color:var(--fg-muted)">Find</a>
+ <span style="margin:0 6px;opacity:0.5">›</span>
+ <span style="color:var(--fg)"><%= neighborhood.label %></span>
+</nav>
+
+<section class="find-page">
+ <p class="kicker">On Ventura Blvd</p>
+ <h1 class="display-sm">Businesses in <%= neighborhood.label %></h1>
+ <p class="lede"><%= businesses.length %> business<%= businesses.length === 1 ? '' : 'es' %> in <%= neighborhood.label %>. Search by name, browse by category, or jump straight to a profile and send a message.</p>
+
+ <div class="grid-controls" role="region" aria-label="Filters" style="margin-top:24px">
+ <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
+ <% verticals.forEach(function(v){ %>
+ <a href="/find?vertical=<%= v.key %>&city=<%= encodeURIComponent(neighborhood.label) %>" class="btn btn-ghost btn-sm" style="font-size:12px"><%= v.icon %> <%= v.label %></a>
+ <% }); %>
+ </div>
+ </div>
+
+ <% if (businesses.length === 0) { %>
+ <div style="padding:40px 0;text-align:center">
+ <h3>No businesses listed in <%= neighborhood.label %> yet.</h3>
+ <p class="muted">We're adding new businesses every week. <a href="/find">Browse all listings</a>.</p>
+ </div>
+ <% } else { %>
+ <div class="business-grid" id="grid" style="margin-top:24px">
+ <% businesses.forEach(function(b){
+ var vMeta = verticals.find(function(vv){return vv.key===b.vertical});
+ %>
+ <a class="business-card" href="/business/<%= b.slug %>">
+ <% if (b.photo_path) { %>
+ <div class="biz-card-hero biz-card-hero-photo">
+ <img src="<%= b.photo_path %>" alt="<%= b.business_name %>" loading="lazy" decoding="async">
+ </div>
+ <% } else { %>
+ <div class="biz-card-hero vertical-hero-<%= b.vertical %>" aria-hidden="true">
+ <span class="biz-card-glyph"><%= vMeta ? vMeta.icon : '•' %></span>
+ </div>
+ <% } %>
+ <p class="biz-vertical"><%= vMeta ? vMeta.label : b.vertical %></p>
+ <p class="biz-name"><%= b.business_name %></p>
+ <p class="biz-loc"><%= [b.neighborhood, b.city, b.state].filter(Boolean).join(' · ') %></p>
+ <% if (b.headline) { %><p class="biz-headline"><%= b.headline %></p><% } %>
+ <div class="biz-badges">
+ <% if (b.tier === 'premier') { %><span class="biz-badge is-premier">Featured</span>
+ <% } else if (b.tier === 'standard') { %><span class="biz-badge is-standard">Standard</span><% } %>
+ <% if (b.claim_status === 'self' || b.claim_status === 'claimed') { %><span class="biz-badge is-claimed">Claimed</span><% } %>
+ <% if (b.verified) { %><span class="biz-badge">Verified</span><% } %>
+ </div>
+ </a>
+ <% }); %>
+ </div>
+ <% } %>
+</section>
+
+<%- include('../partials/footer') %>
← 870584a yolo tick 25: /find auto-suggest dropdown — debounced AJAX,
·
back to Ventura Claw Leads
·
yolo tick 27: 'More <vertical> nearby' rail at the bottom of f40cfda →