[object Object]

← back to Ventura Corridor

feat(news): "Latest from their site" section on /magazine/:id

33b09e9ad15db9f15da19ef986865d10459e5ed7 · 2026-05-07 16:34:13 -0700 · SteveStudio2

Closes the loop between editorial features and live scraped news.
The magazine article view (/magazine/:id) now includes a 5-item
news rail above the building-mates and category-related sections,
showing what the business has been actually publishing on their
own website.

Implementation:
  - new newsBiz query in the /magazine/:id handler returns last 5
    news_items for f.business_id
  - rendered between the existing "Pipeline events" details and the
    "In the same building" mates section
  - safeUrl scheme-validation pattern carried forward from tick 43:
    href is escaped + http(s)-only, falling back to "#"
  - "All news from <biz> -->" deep-link goes to /business/:id/news
  - Hidden when newsBiz.rows is empty so unpublished biz features
    don't grow a blank section

Real reach: 3 features currently on news-having biz (Manu Farrarons,
Coffee Bean & Tea Leaf, $300 Data Recovery). Grows automatically
with the nightly scrape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 33b09e9ad15db9f15da19ef986865d10459e5ed7
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Thu May 7 16:34:13 2026 -0700

    feat(news): "Latest from their site" section on /magazine/:id
    
    Closes the loop between editorial features and live scraped news.
    The magazine article view (/magazine/:id) now includes a 5-item
    news rail above the building-mates and category-related sections,
    showing what the business has been actually publishing on their
    own website.
    
    Implementation:
      - new newsBiz query in the /magazine/:id handler returns last 5
        news_items for f.business_id
      - rendered between the existing "Pipeline events" details and the
        "In the same building" mates section
      - safeUrl scheme-validation pattern carried forward from tick 43:
        href is escaped + http(s)-only, falling back to "#"
      - "All news from <biz> -->" deep-link goes to /business/:id/news
      - Hidden when newsBiz.rows is empty so unpublished biz features
        don't grow a blank section
    
    Real reach: 3 features currently on news-having biz (Manu Farrarons,
    Coffee Bean & Tea Leaf, $300 Data Recovery). Grows automatically
    with the nightly scrape.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 src/server/index.ts | 39 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 39 insertions(+)

diff --git a/src/server/index.ts b/src/server/index.ts
index 75e6b45..c205699 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -4384,6 +4384,17 @@ app.get('/magazine/:id', async (req, res) => {
      LIMIT 3`,
     [f.category_tag, id]
   );
+  // Latest 5 scraped news/blog/press articles from this business's website.
+  // Closes the loop between editorial features and live news so readers see
+  // both "what we said about them" + "what they're saying right now".
+  const newsBiz = await query(
+    `SELECT id, source_url, title, excerpt, summary, published_guess, fetched_at
+       FROM news_items WHERE business_id = $1
+      ORDER BY COALESCE(published_guess, fetched_at::date) DESC,
+               fetched_at DESC
+      LIMIT 5`,
+    [f.business_id]
+  );
   // Pull last 10 pipeline events for the underlying business (status changes, walks, replies, etc)
   const events = await query(
     `SELECT pel.event_type, pel.old_value, pel.new_value, pel.occurred_at
@@ -4511,6 +4522,34 @@ footer{margin-top:48px;padding:24px 0;text-align:center;font-size:10px;letter-sp
     </div>
     <div style="margin-top:8px;font-size:10px;color:var(--ink-mute);font-style:italic">From the DW outreach pipeline that produces the magazine — every walk, every reply, every status change.</div>
   </details>` : ''}
+  ${newsBiz.rows.length > 0 ? `<section style="margin-top:48px;padding-top:24px;border-top:1px solid var(--rule)">
+    <div style="display:flex;justify-content:space-between;align-items:baseline;font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:14px">
+      <span>📰 Latest from their site · ${newsBiz.rowCount}</span>
+      <a href="/business/${f.business_id}/news" style="color:var(--metal);text-decoration:none">All news from ${esc(f.biz_name)} ↗</a>
+    </div>
+    <div style="display:flex;flex-direction:column;gap:0">
+      ${newsBiz.rows.map((n: any) => {
+        const dt = n.published_guess || n.fetched_at;
+        const dateStr = dt ? new Date(dt).toLocaleDateString('en-US', { month:'short', day:'numeric', year:'numeric' }) : '';
+        const text = n.summary || n.excerpt || '';
+        let host = '';
+        try { host = new URL(n.source_url).hostname.replace(/^www\./, ''); } catch {}
+        const safeHref = (() => {
+          try {
+            const u = new URL(String(n.source_url || ''));
+            return (u.protocol === 'http:' || u.protocol === 'https:') ? esc(u.toString()) : '#';
+          } catch { return '#'; }
+        })();
+        return `<a href="${safeHref}" target="_blank" rel="noopener noreferrer" style="display:block;padding:12px 0;border-bottom:1px dotted var(--rule);color:var(--ink);text-decoration:none">
+          <h4 style="font-family:'Cormorant Garamond',serif;font-style:italic;font-weight:500;font-size:18px;line-height:1.2;margin:0 0 4px">${esc((n.title || '(untitled)').slice(0, 140))}</h4>
+          <p style="font-size:13px;line-height:1.5;color:var(--ink-mute);margin:0 0 6px">${esc(text.slice(0, 220))}${text.length > 220 ? '…' : ''}</p>
+          <div style="font-family:'Inter',sans-serif;font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-mute);display:flex;gap:10px">
+            <span>${esc(dateStr)}</span><span>·</span><span style="color:var(--metal)">${esc(host)}</span>
+          </div>
+        </a>`;
+      }).join('')}
+    </div>
+  </section>` : ''}
   ${mates.rows.length > 0 ? `<section style="margin-top:48px;padding-top:24px;border-top:1px solid var(--rule)">
     <div style="display:flex;justify-content:space-between;align-items:baseline;font-size:9px;letter-spacing:.32em;text-transform:uppercase;color:var(--ink-mute);margin-bottom:14px">
       <span>In the same building · ${esc(bldg)}</span>

← ec89429 feat(news): /api/news/by-vertical + heatmap on /coverage.htm  ·  back to Ventura Corridor  ·  feat(seo): include news pages in /sitemap.xml 9027832 →