[object Object]

← 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 →