← back to Ventura Claw Leads
feat(seo): image sitemap support for Google Image search
92a37d618a9e0f5d112c516f81dcb848d99742e8 · 2026-05-07 09:04:45 -0700 · Steve Abrams
- sitemap.xml now declares xmlns:image and emits <image:image> entries
inside each /business/:slug URL block when the business has a photo
- image entries include <image:loc> (absolute URL) and <image:title>
(business name); falls back to no image entry when photo absent
- skips og.png as image source — those are share cards, not real photos,
and would dilute Image-search relevance
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 92a37d618a9e0f5d112c516f81dcb848d99742e8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 7 09:04:45 2026 -0700
feat(seo): image sitemap support for Google Image search
- sitemap.xml now declares xmlns:image and emits <image:image> entries
inside each /business/:slug URL block when the business has a photo
- image entries include <image:loc> (absolute URL) and <image:title>
(business name); falls back to no image entry when photo absent
- skips og.png as image source — those are share cards, not real photos,
and would dilute Image-search relevance
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
routes/public.js | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/routes/public.js b/routes/public.js
index dd5ca3d..6a46fb2 100644
--- a/routes/public.js
+++ b/routes/public.js
@@ -469,7 +469,7 @@ router.get('/sitemap.xml', async (req, res, next) => {
try {
const baseUrl = (process.env.PUBLIC_URL || 'https://leads.venturaclaw.com').replace(/\/+$/, '');
const businesses = await db.many(`
- SELECT slug, updated_at, vertical
+ SELECT slug, updated_at, vertical, business_name, photo_path
FROM businesses
WHERE status = 'active'
ORDER BY updated_at DESC
@@ -495,13 +495,26 @@ router.get('/sitemap.xml', async (req, res, next) => {
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';
+ // image: namespace declared on <urlset> so Google Image search can ingest
+ // <image:image> children for businesses that have a photo on file. Unphotographed
+ // listings still appear (just without an <image:image> child) — falling back
+ // to the dynamic OG card here would produce thin Image-search results since
+ // those aren't real photos of the business.
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">\n';
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) {
const lastmod = (b.updated_at instanceof Date ? b.updated_at : new Date(b.updated_at)).toISOString().slice(0, 10);
- xml += ` <url><loc>${baseUrl}/business/${escape(b.slug)}</loc><lastmod>${lastmod}</lastmod><changefreq>weekly</changefreq><priority>0.6</priority></url>\n`;
+ const loc = `${baseUrl}/business/${escape(b.slug)}`;
+ let imageBlock = '';
+ if (b.photo_path) {
+ const imgUrl = b.photo_path.startsWith('http')
+ ? b.photo_path
+ : baseUrl + b.photo_path;
+ imageBlock = `<image:image><image:loc>${escape(imgUrl)}</image:loc><image:title>${escape(b.business_name || '')}</image:title></image:image>`;
+ }
+ xml += ` <url><loc>${loc}</loc><lastmod>${lastmod}</lastmod><changefreq>weekly</changefreq><priority>0.6</priority>${imageBlock}</url>\n`;
}
xml += '</urlset>\n';
res.set('Cache-Control', 'public, max-age=3600').type('application/xml').send(xml);
← d818054 feat(seo): per-business dynamic Open Graph images
·
back to Ventura Claw Leads
·
Seed food vertical from ventura_corridor public data (OSM + 50d32a1 →