← back to Ventura Claw Leads
Sitemap covers paginated pages; head supports rel=prev/next + filtered canonicals; map-CTA on home + find
5cc49d8833053a1376f95aa8f431faf094eb4edc · 2026-05-07 10:35:53 -0700 · Steve Abrams
Three SEO improvements stacked together since they're a single user
journey:
1. sitemap.xml now lists /find?vertical=X&page=2..N for each paginated
vertical (beauty 4p, food 3p, retail 2p, creative 2p) — 7 new URLs
so Google crawls every business via the listing pages, not just the
1,819 direct /business/<slug>.
2. head.ejs gains rel='prev'/rel='next' link tags + a canonicalPath
override. The /find route now passes the full filter+page query
string as canonical — /find?vertical=beauty&page=2 self-canonicalizes
instead of folding into /find.
3. Home + /find both get a 'browse the map →' CTA. Map already accepted
?vertical= filter; the /find→/map link preserves it so a user
filtering for fitness on the list view sees only fitness pins on the
map. 1,719 markers were unreachable from the home hero before this.
Files touched
M routes/public.jsM views/partials/head.ejsM views/public/find.ejsM views/public/home.ejs
Diff
commit 5cc49d8833053a1376f95aa8f431faf094eb4edc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 7 10:35:53 2026 -0700
Sitemap covers paginated pages; head supports rel=prev/next + filtered canonicals; map-CTA on home + find
Three SEO improvements stacked together since they're a single user
journey:
1. sitemap.xml now lists /find?vertical=X&page=2..N for each paginated
vertical (beauty 4p, food 3p, retail 2p, creative 2p) — 7 new URLs
so Google crawls every business via the listing pages, not just the
1,819 direct /business/<slug>.
2. head.ejs gains rel='prev'/rel='next' link tags + a canonicalPath
override. The /find route now passes the full filter+page query
string as canonical — /find?vertical=beauty&page=2 self-canonicalizes
instead of folding into /find.
3. Home + /find both get a 'browse the map →' CTA. Map already accepted
?vertical= filter; the /find→/map link preserves it so a user
filtering for fitness on the list view sees only fitness pins on the
map. 1,719 markers were unreachable from the home hero before this.
---
routes/public.js | 45 +++++++++++++++++++++++++++++++++++++++++----
views/partials/head.ejs | 15 ++++++++++++++-
views/public/find.ejs | 3 +++
views/public/home.ejs | 3 +++
4 files changed, 61 insertions(+), 5 deletions(-)
diff --git a/routes/public.js b/routes/public.js
index 9e6eea5..c6a82a7 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -145,13 +145,35 @@ router.get('/find', async (req, res, next) => {
: 'Search Ventura Blvd';
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
+
+ // Build absolute prev/next URLs for the head <link rel> tags. SEO signal
+ // for paginated content — Google uses these to understand the series.
+ const baseUrl = (process.env.PUBLIC_URL || 'https://leads.venturaclaw.com').replace(/\/+$/, '');
+ const buildPageUrl = (n) => {
+ const qsParts = [];
+ if (q) qsParts.push(`q=${encodeURIComponent(q)}`);
+ if (v) qsParts.push(`vertical=${encodeURIComponent(v)}`);
+ if (city) qsParts.push(`city=${encodeURIComponent(city)}`);
+ if (sortKey && orderClauses[sortKey] && sortKey !== 'featured') qsParts.push(`sort=${encodeURIComponent(sortKey)}`);
+ if (n > 1) qsParts.push(`page=${n}`);
+ return baseUrl + '/find' + (qsParts.length ? '?' + qsParts.join('&') : '');
+ };
+ const prevUrl = page > 1 ? buildPageUrl(page - 1) : null;
+ const nextUrl = page < totalPages ? buildPageUrl(page + 1) : null;
+
+ // Canonical for /find: include the meaningful filter params so Google
+ // indexes /find?vertical=beauty&page=2 as its own page, not as a dup of
+ // /find. Use the same buildPageUrl helper to ensure consistent ordering.
+ const canonicalPath = buildPageUrl(page).replace(baseUrl, '');
+
res.render('public/find', {
title: customTitle,
metaDescription: customMeta,
businesses, totalCount, q, v, city, verticals: VERTICALS,
sort: orderClauses[sortKey] ? sortKey : 'featured',
customH1, verticalLabel,
- page, totalPages, pageSize: PAGE_SIZE
+ page, totalPages, pageSize: PAGE_SIZE,
+ prevUrl, nextUrl, canonicalPath
});
} catch (err) { next(err); }
});
@@ -529,9 +551,24 @@ router.get('/sitemap.xml', async (req, res, next) => {
{ loc: '/terms', changefreq: 'yearly', priority: '0.3' }
];
// One <url> per vertical filter — useful long-tail landing pages.
- const verticalPages = VERTICALS.map(v => ({
- loc: `/find?vertical=${v.key}`, changefreq: 'daily', priority: '0.7'
- }));
+ // Plus page=2..N for any vertical whose total >200 (mirrors the /find
+ // pagination), so Google can crawl every business through the listing
+ // pages and not rely solely on direct /business/<slug> URLs.
+ const verticalCounts = await db.many(`
+ SELECT vertical, COUNT(*)::int AS n
+ FROM businesses WHERE status = 'active'
+ GROUP BY vertical
+ `);
+ const PAGE_SIZE = 200;
+ const verticalCountMap = Object.fromEntries(verticalCounts.map(r => [r.vertical, r.n]));
+ const verticalPages = [];
+ for (const v of VERTICALS) {
+ verticalPages.push({ loc: `/find?vertical=${v.key}`, changefreq: 'daily', priority: '0.7' });
+ const totalPages = Math.ceil((verticalCountMap[v.key] || 0) / PAGE_SIZE);
+ for (let p = 2; p <= totalPages; p++) {
+ verticalPages.push({ loc: `/find?vertical=${v.key}&page=${p}`, changefreq: 'daily', priority: '0.5' });
+ }
+ }
// One <url> per neighborhood landing page — strong local-SEO signal.
const neighborhoodPages = NEIGHBORHOODS.map(n => ({
loc: `/neighborhood/${n.slug}`, changefreq: 'daily', priority: '0.7'
diff --git a/views/partials/head.ejs b/views/partials/head.ejs
index d9bf589..5b52894 100644
--- a/views/partials/head.ejs
+++ b/views/partials/head.ejs
@@ -9,10 +9,23 @@
? metaDescription
: 'Ventura Claw — directory and lead-routing service for small businesses on Ventura Blvd.';
var _canonicalBase = (typeof publicUrl !== 'undefined' && publicUrl) ? publicUrl.replace(/\/+$/, '') : 'https://venturaclaw.com';
- var _canonicalUrl = _canonicalBase + (typeof path === 'string' ? path : '/');
+ // Canonical priority: explicit canonicalPath (route override) > path (req.path).
+ // Routes with meaningful query params (filters, pagination) should pass
+ // canonicalPath so Google indexes each filter combination separately
+ // instead of folding them all under the bare /find canonical.
+ var _canonicalRel = (typeof canonicalPath === 'string' && canonicalPath)
+ ? canonicalPath
+ : (typeof path === 'string' ? path : '/');
+ var _canonicalUrl = _canonicalBase + _canonicalRel;
%>
<meta name="description" content="<%= _metaDesc %>">
<link rel="canonical" href="<%= _canonicalUrl %>">
+ <% if (typeof prevUrl !== 'undefined' && prevUrl) { %>
+ <link rel="prev" href="<%= prevUrl %>">
+ <% } %>
+ <% if (typeof nextUrl !== 'undefined' && nextUrl) { %>
+ <link rel="next" href="<%= nextUrl %>">
+ <% } %>
<meta property="og:type" content="website">
<meta property="og:url" content="<%= _canonicalUrl %>">
<meta property="og:site_name" content="Ventura Claw">
diff --git a/views/public/find.ejs b/views/public/find.ejs
index 2abfd8e..c5e1a49 100644
--- a/views/public/find.ejs
+++ b/views/public/find.ejs
@@ -27,6 +27,9 @@
<section class="find-page">
<p class="kicker">Find a business</p>
<h1 class="display-sm"><%= customH1 %></h1>
+ <p class="muted" style="margin:-8px 0 16px;font-size:14px">
+ Prefer the map? <a href="/map<%= v ? '?vertical=' + v : '' %>" style="font-weight:600">View these on a map →</a>
+ </p>
<form action="/find" method="get" class="find-filters" role="search" autocomplete="off">
<div class="filter-row">
diff --git a/views/public/home.ejs b/views/public/home.ejs
index bc1f3f1..a830514 100644
--- a/views/public/home.ejs
+++ b/views/public/home.ejs
@@ -9,6 +9,9 @@
<input type="search" name="q" placeholder="Try 'salon Sherman Oaks' or 'cafe Studio City'" autocomplete="off">
<button type="submit" class="btn btn-primary btn-lg">Search →</button>
</form>
+ <p class="hero-secondary" style="margin:8px 0 0;font-size:14px">
+ or <a href="/map" style="font-weight:600">browse the map →</a> · <%= stats.total %> pins on Ventura Blvd
+ </p>
<div class="hero-meta">
<span><strong><%= stats.total %></strong> businesses</span>
<span><strong><%= stats.vertical_count %></strong> categories</span>
← 3f90e3f Add pagination to /find — page=N param + Prev/Next nav
·
back to Ventura Claw Leads
·
Add per-vertical sample rails to home page (food/beauty/reta 05ae6c7 →