← back to Ventura Corridor
feat(news): news_count badge on /chains.html bar rows
4cada626b3a380cff7fc15c1f5ae04b27beea831 · 2026-05-07 14:25:04 -0700 · SteveStudio2
Chain-store roster now flags any chain whose locations have scraped
news/blog posts on file. Aggregates news_count across every business_id
that maps to the chain's normalized name (so Coffee Bean across 2
locations = 2, not 1).
API change:
/api/chains +news_count INT per chain (sum across all chain locations)
via with_news CTE joining grouped.ids[] against news_items
UI change:
public/chains.html bar-row new gold-border .news-badge "narrow N"
appended to chain name when news_count > 0
clicks through to /news.html
Cache: 5-minute server-side cache means new news lands on the chains
view within 5 minutes of summarizer/scraper completion.
3 of 60 chains currently flag (Coffee Bean Tea Leaf, Leading Tax Group,
300 Data Recovery). Grows automatically with the nightly 03:15 scrape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Files touched
M public/chains.htmlM src/server/index.ts
Diff
commit 4cada626b3a380cff7fc15c1f5ae04b27beea831
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Thu May 7 14:25:04 2026 -0700
feat(news): news_count badge on /chains.html bar rows
Chain-store roster now flags any chain whose locations have scraped
news/blog posts on file. Aggregates news_count across every business_id
that maps to the chain's normalized name (so Coffee Bean across 2
locations = 2, not 1).
API change:
/api/chains +news_count INT per chain (sum across all chain locations)
via with_news CTE joining grouped.ids[] against news_items
UI change:
public/chains.html bar-row new gold-border .news-badge "narrow N"
appended to chain name when news_count > 0
clicks through to /news.html
Cache: 5-minute server-side cache means new news lands on the chains
view within 5 minutes of summarizer/scraper completion.
3 of 60 chains currently flag (Coffee Bean Tea Leaf, Leading Tax Group,
300 Data Recovery). Grows automatically with the nightly 03:15 scrape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
public/chains.html | 7 ++++++-
src/server/index.ts | 12 ++++++++++--
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/public/chains.html b/public/chains.html
index 3ee4c63..62d79fe 100644
--- a/public/chains.html
+++ b/public/chains.html
@@ -58,6 +58,8 @@
.bar-row .ct { font-family: var(--serif); font-style: italic; font-size: 18px; color: var(--metal); text-align: right; font-variant-numeric: tabular-nums; }
.bar-row.parking .nm { color: var(--metal-glow); }
.bar-row .cities { font-family: var(--mono); font-size: 8px; letter-spacing: .12em; color: var(--ink-mute); display: block; margin-top: 2px; }
+ .bar-row .news-badge { display: inline-block; padding: 1px 6px; border: 1px solid var(--metal-glow); color: var(--metal-glow); font-size: 9px; letter-spacing: .12em; text-decoration: none; vertical-align: middle; margin-left: 4px; }
+ .bar-row .news-badge:hover { background: var(--metal-glow); color: var(--noir); }
footer {
padding: 24px 40px 40px; border-top: 1px solid var(--rule);
font-family: var(--serif); font-style: italic; color: var(--ink-mute); font-size: 12px; text-align: center;
@@ -126,9 +128,12 @@ fetch('/api/chains').then(r => r.json()).then(d => {
const isPark = PARK_RE.test(r.name);
const w = (Number(r.n_locations) / max * 100).toFixed(1);
const cities = (r.cities || []).slice(0, 4).map(c => c.toLowerCase().replace(/\b\w/g, m => m.toUpperCase())).join(' · ');
+ const newsBadge = (r.news_count > 0)
+ ? `<a href="/news.html" class="news-badge" title="${r.news_count} scraped news/blog post${r.news_count === 1 ? '' : 's'} across this chain's locations">📰 ${r.news_count}</a>`
+ : '';
return `<div class="bar-row${isPark ? ' parking' : ''}">
<div>
- <div class="nm">${escapeHtml(r.name)}</div>
+ <div class="nm">${escapeHtml(r.name)} ${newsBadge}</div>
<span class="cities">${escapeHtml(cities)}</span>
</div>
<div class="meter"><i style="width:${w}%"></i></div>
diff --git a/src/server/index.ts b/src/server/index.ts
index 07a7e1c..e4e38ff 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -4901,9 +4901,17 @@ app.get('/api/chains', async (_req, res) => {
FROM normed
GROUP BY nname
HAVING count(*) >= 2
+ ),
+ with_news AS (
+ SELECT g.*,
+ COALESCE(
+ (SELECT COUNT(*)::int FROM news_items WHERE business_id = ANY(g.ids)),
+ 0
+ ) AS news_count
+ FROM grouped g
)
- SELECT nname AS name, n_locations, variants, cities, sample_category
- FROM grouped
+ SELECT nname AS name, n_locations, variants, cities, sample_category, news_count
+ FROM with_news
ORDER BY n_locations DESC, nname ASC
LIMIT 60
`);
← d444435 fix(generate_features): tighten banned-phrase regex to catch
·
back to Ventura Corridor
·
feat(news): RSS feed at /news/feed.xml + auto-discovery on / b329ecf →