[object Object]

← back to Ventura Corridor

iter 115+116+117: og/twitter/JSON-LD meta + /api/magazine.json + tap counter — reader page emits og:type/title/description/url/image (1080×1920 from /share/:id) + twitter:card summary_large_image + JSON-LD NewsArticle schema (author=The Corridor desk, publisher logo, articleSection, LocalBusiness about); /api/magazine.json renders JSON Feed 1.1 with id/url/title/summary/content_text/date_published/tags/image/_corridor for external rotators; magazine_features.taps + taps_breakdown JSONB columns track outbound clicks; /api/magazine/:id/tap?to=&dest= increments taps + redirects 302 to dest if provided; /magazine/:id reader toolbar 📱 share + ⭐ Sponsor links now route through tap counter for analytics

cfd5c5a6715f2dfb9c02e745b7b7f66a01386a5a · 2026-05-06 18:02:37 -0700 · SteveStudio2

Files touched

Diff

commit cfd5c5a6715f2dfb9c02e745b7b7f66a01386a5a
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Wed May 6 18:02:37 2026 -0700

    iter 115+116+117: og/twitter/JSON-LD meta + /api/magazine.json + tap counter — reader page emits og:type/title/description/url/image (1080×1920 from /share/:id) + twitter:card summary_large_image + JSON-LD NewsArticle schema (author=The Corridor desk, publisher logo, articleSection, LocalBusiness about); /api/magazine.json renders JSON Feed 1.1 with id/url/title/summary/content_text/date_published/tags/image/_corridor for external rotators; magazine_features.taps + taps_breakdown JSONB columns track outbound clicks; /api/magazine/:id/tap?to=&dest= increments taps + redirects 302 to dest if provided; /magazine/:id reader toolbar 📱 share + ⭐ Sponsor links now route through tap counter for analytics
---
 src/server/index.ts | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 96 insertions(+), 2 deletions(-)

diff --git a/src/server/index.ts b/src/server/index.ts
index 32526a0..7f075e8 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1838,6 +1838,71 @@ app.get('/api/magazine/random', async (req, res) => {
   }
 });
 
+// Outbound tap counter — /api/magazine/:id/tap?to=sponsor|share|website|other
+app.get('/api/magazine/:id/tap', async (req, res) => {
+  try {
+    const id = parseInt(req.params.id, 10);
+    const to = String(req.query.to || 'other').slice(0, 32).replace(/[^a-z0-9_-]/gi, '');
+    if (!Number.isFinite(id)) return res.status(400).end();
+    await query(
+      `UPDATE magazine_features
+       SET taps = taps + 1,
+           taps_breakdown = jsonb_set(
+             COALESCE(taps_breakdown, '{}'::jsonb),
+             ARRAY[$1::text],
+             to_jsonb(COALESCE((taps_breakdown->>$1)::int, 0) + 1),
+             true
+           )
+       WHERE id = $2`,
+      [to, id]
+    );
+    // Redirect to the dest if provided as ?dest=
+    const dest = String(req.query.dest || '');
+    if (dest && /^https?:\/\//.test(dest)) return res.redirect(302, dest);
+    res.json({ ok: true, to });
+  } catch (e: any) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
+// JSON Feed 1.1 — full feed of published features (https://jsonfeed.org/)
+app.get('/api/magazine.json', async (_req, res) => {
+  try {
+    const baseUrl = process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780';
+    const r = await query(
+      `SELECT mf.id, mf.headline, mf.subhead, mf.editorial, mf.pull_quote, mf.category_tag,
+              mf.generated_at, mf.published_at,
+              b.name AS biz_name, b.address AS biz_address, b.city
+       FROM magazine_features mf JOIN businesses b ON b.id = mf.business_id
+       WHERE mf.status = 'published'
+       ORDER BY mf.published_at DESC NULLS LAST, mf.generated_at DESC
+       LIMIT 200`
+    );
+    res.json({
+      version: 'https://jsonfeed.org/version/1.1',
+      title: 'The Corridor',
+      home_page_url: `${baseUrl}/issue`,
+      feed_url: `${baseUrl}/api/magazine.json`,
+      description: "A magazine of who's here on Ventura Boulevard.",
+      authors: [{ name: 'The Corridor desk' }],
+      language: 'en',
+      items: r.rows.map((f: any) => ({
+        id: `${baseUrl}/magazine/${f.id}`,
+        url: `${baseUrl}/magazine/${f.id}`,
+        title: f.headline || f.biz_name,
+        summary: f.subhead || '',
+        content_text: f.editorial || '',
+        date_published: f.published_at ? new Date(f.published_at).toISOString() : new Date(f.generated_at).toISOString(),
+        tags: [f.category_tag].filter(Boolean),
+        image: `${baseUrl}/share/${f.id}`,
+        _corridor: { biz_name: f.biz_name, biz_address: f.biz_address, city: f.city, pull_quote: f.pull_quote }
+      }))
+    });
+  } catch (e: any) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
 // Magazine CSV export — every published feature for accountant/CRM/spreadsheet hand-off
 app.get('/api/magazine.csv', async (_req, res) => {
   try {
@@ -2737,9 +2802,38 @@ app.get('/magazine/:id', async (req, res) => {
   ) : { rows: [] as any[] };
   const esc = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));
   res.setHeader('Content-Type', 'text/html; charset=utf-8');
+  const baseUrl = process.env.PUBLIC_BASE_URL || 'http://127.0.0.1:9780';
+  const desc = (f.subhead || (f.editorial || '').slice(0, 160) || '').replace(/\s+/g, ' ').trim();
   res.send(`<!doctype html><html lang="en"><head><meta charset="utf-8">
 <title>${esc(f.headline || f.biz_name)} — The Corridor</title>
 <meta name="viewport" content="width=device-width,initial-scale=1">
+<meta name="description" content="${esc(desc).slice(0, 200)}">
+<meta property="og:type" content="article">
+<meta property="og:title" content="${esc(f.headline || f.biz_name)}">
+<meta property="og:description" content="${esc(desc).slice(0, 200)}">
+<meta property="og:url" content="${baseUrl}/magazine/${id}">
+<meta property="og:site_name" content="The Corridor">
+<meta property="og:image" content="${baseUrl}/share/${id}">
+<meta property="og:image:width" content="1080">
+<meta property="og:image:height" content="1920">
+<meta property="article:section" content="${esc(f.category_tag || 'feature')}">
+<meta property="article:published_time" content="${f.published_at ? new Date(f.published_at).toISOString() : new Date(f.generated_at).toISOString()}">
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="${esc(f.headline || f.biz_name)}">
+<meta name="twitter:description" content="${esc(desc).slice(0, 200)}">
+<meta name="twitter:image" content="${baseUrl}/share/${id}">
+<script type="application/ld+json">${JSON.stringify({
+  '@context': 'https://schema.org',
+  '@type': 'NewsArticle',
+  headline: f.headline || f.biz_name,
+  description: desc,
+  datePublished: f.published_at ? new Date(f.published_at).toISOString() : new Date(f.generated_at).toISOString(),
+  author: { '@type': 'Organization', name: 'The Corridor desk' },
+  publisher: { '@type': 'Organization', name: 'The Corridor', logo: { '@type': 'ImageObject', url: `${baseUrl}/favicon.svg` } },
+  mainEntityOfPage: `${baseUrl}/magazine/${id}`,
+  articleSection: f.category_tag || 'feature',
+  about: { '@type': 'LocalBusiness', name: f.biz_name, address: f.biz_address || '' }
+})}</script>
 <style>
 @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,400;1,500&family=Inter:wght@300;400;500&family=JetBrains+Mono:wght@200;300&display=swap');
 :root { --paper:#faf6ee; --ink:#1a1815; --ink-mute:#6e6356; --metal:#8a6d3b; --accent:#6a3a1a; --rule:#d8cdb8; }
@@ -2770,8 +2864,8 @@ footer{margin-top:48px;padding:24px 0;text-align:center;font-size:10px;letter-sp
 <div class="toolbar">
   <div class="breadcrumb"><a href="/magazine.html">← The Corridor</a> · ${esc(f.category_tag || 'feature')}</div>
   <div>
-    <a href="/share/${id}" target="_blank">📱 IG share-card</a>
-    <a href="/sponsor/${id}" style="color:var(--accent,#6a3a1a);border-color:var(--accent,#6a3a1a)">⭐ Sponsor</a>
+    <a href="/api/magazine/${id}/tap?to=share&dest=${encodeURIComponent(`${baseUrl}/share/${id}`)}" target="_blank">📱 IG share-card</a>
+    <a href="/api/magazine/${id}/tap?to=sponsor&dest=${encodeURIComponent(`${baseUrl}/sponsor/${id}`)}" style="color:var(--accent,#6a3a1a);border-color:var(--accent,#6a3a1a)">⭐ Sponsor</a>
     <a href="javascript:window.print()">🖨 Print</a>
     <a href="/pitches.html?id=${f.business_id}" target="_blank">Pipeline ↗</a>
   </div>

← 0bc6fd6 iter 113+114: pitch_event_log audit panel on /magazine/:id +  ·  back to Ventura Corridor  ·  iter 118+119: /magazine.html admin meta strip + /buildings/: e2338c1 →