← back to Ventura Corridor
feat(news): per-business RSS via /news/feed.xml?business=N
3b4d52317c2007de5ce185628daea147eb33fca0 · 2026-05-07 18:28:07 -0700 · SteveStudio2
Same RSS handler now optionally filters to a single business. Combines
with vertical: ?business=49&vertical=shop returns the AND.
Routes (all on the existing /news/feed.xml):
/news/feed.xml firehose, 50 most recent
/news/feed.xml?vertical=amenity top-cat filter
/news/feed.xml?vertical=shop:bakery exact subcategory filter
/news/feed.xml?business=N single-business feed
/news/feed.xml?business=N&vertical=X intersection (AND)
UI:
/business/:id/news <head> <link rel=alternate type=rss>
so feed readers auto-subscribe
when the user opens the page
/business/:id/news header "📡 RSS subscribe" anchor next
to the website link
Channel <title> dynamically suffixes the business_name when filtered
to one biz, so feed-reader panes label correctly.
Smoke: biz 49 (Solar Unlimited Encino, 4 articles) feed validates
with xmllint, channel title contains the business name, /business/49/news
includes the subscribe button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 3b4d52317c2007de5ce185628daea147eb33fca0
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Thu May 7 18:28:07 2026 -0700
feat(news): per-business RSS via /news/feed.xml?business=N
Same RSS handler now optionally filters to a single business. Combines
with vertical: ?business=49&vertical=shop returns the AND.
Routes (all on the existing /news/feed.xml):
/news/feed.xml firehose, 50 most recent
/news/feed.xml?vertical=amenity top-cat filter
/news/feed.xml?vertical=shop:bakery exact subcategory filter
/news/feed.xml?business=N single-business feed
/news/feed.xml?business=N&vertical=X intersection (AND)
UI:
/business/:id/news <head> <link rel=alternate type=rss>
so feed readers auto-subscribe
when the user opens the page
/business/:id/news header "📡 RSS subscribe" anchor next
to the website link
Channel <title> dynamically suffixes the business_name when filtered
to one biz, so feed-reader panes label correctly.
Smoke: biz 49 (Solar Unlimited Encino, 4 articles) feed validates
with xmllint, channel title contains the business name, /business/49/news
includes the subscribe button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
src/server/index.ts | 25 ++++++++++++++++++++-----
1 file changed, 20 insertions(+), 5 deletions(-)
diff --git a/src/server/index.ts b/src/server/index.ts
index 075420b..0890155 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -5149,6 +5149,7 @@ app.get('/business/:id/news', async (req, res) => {
<meta charset="utf-8">
<title>${esc(b.name)} — News · Ventura Corridor</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
+<link rel="alternate" type="application/rss+xml" title="${esc(b.name)} — Ventura Corridor news" href="/news/feed.xml?business=${id}">
<style>
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;1,400;1,500&family=Inter:wght@300;400;500&family=JetBrains+Mono:wght@200;300&display=swap');
:root{--noir:#0a0a0c;--noir-rise:#131316;--ink:#f0ece2;--ink-mute:#888475;--metal:#b89968;--metal-glow:#d4b683;--rule:#2a2622}
@@ -5181,6 +5182,7 @@ main{max-width:920px;margin:0 auto;padding:32px}
<h1>News from <em>${esc(b.name)}</em></h1>
<p class="meta">${items.rowCount} article${items.rowCount === 1 ? '' : 's'} scraped from
${b.website ? `<a href="${safeUrl(b.website)}" target="_blank" rel="noopener noreferrer">${esc(host(b.website) || b.website)}</a>` : 'their website'}
+ · <a href="/news/feed.xml?business=${id}" style="color:var(--metal-glow);text-decoration:none;border-bottom:1px dotted var(--metal-glow)">📡 RSS subscribe</a>
</p>
</section>
<main>
@@ -5209,19 +5211,25 @@ app.get('/news/feed.xml', async (req, res) => {
// that matches either "amenity: restaurant" exactly or any "amenity: %"
// child when only the top-cat is given.
let vertical = String(req.query.vertical || '').trim().slice(0, 60).toLowerCase();
+ const businessId = parseInt(String(req.query.business || ''), 10);
const params: any[] = [];
- let where = '';
+ const conds: string[] = [];
if (vertical) {
const tokens = vertical.split(/[:\-_\s]+/).filter(Boolean);
if (tokens.length === 1) {
params.push(tokens[0] + ': %');
- where = `WHERE b.category ILIKE $1`;
+ conds.push(`b.category ILIKE $${params.length}`);
} else if (tokens.length >= 2) {
const exact = tokens[0] + ': ' + tokens.slice(1).join(' ');
params.push(exact);
- where = `WHERE b.category ILIKE $1`;
+ conds.push(`b.category ILIKE $${params.length}`);
}
}
+ if (Number.isFinite(businessId) && businessId > 0) {
+ params.push(businessId);
+ conds.push(`b.id = $${params.length}`);
+ }
+ const where = conds.length ? `WHERE ${conds.join(' AND ')}` : '';
const r = await query(`
SELECT n.id, n.business_id, n.source_url, n.title, n.excerpt, n.summary,
n.published_guess, n.fetched_at,
@@ -5257,8 +5265,15 @@ app.get('/news/feed.xml', async (req, res) => {
}).join('');
res.setHeader('Content-Type', 'application/rss+xml; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=300');
- const titleSuffix = vertical ? ` (${_esc0(vertical)})` : '';
- const descExtra = vertical ? ` Filtered to ${_esc0(vertical)}.` : '';
+ let titleSuffix = '';
+ let descExtra = '';
+ if (Number.isFinite(businessId) && businessId > 0 && r.rowCount > 0) {
+ titleSuffix = ` — ${_esc0(r.rows[0].business_name || `business ${businessId}`)}`;
+ descExtra = ` Single business: ${_esc0(r.rows[0].business_name || '')}.`;
+ } else if (vertical) {
+ titleSuffix = ` (${_esc0(vertical)})`;
+ descExtra = ` Filtered to ${_esc0(vertical)}.`;
+ }
res.send(`<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
← 52178df feat(news): per-vertical RSS feeds via /news/feed.xml?vertic
·
back to Ventura Corridor
·
feat(news): recency badge (Hot/Fresh/Stale) on map detail pa 67f7514 →