[object Object]

← back to Rentv

Cody-gate: posts public-status now published|live in lockstep with /api/search (was published-only → a future 'live' post would 404 on its page while showing in search); cross-ref comment on the parallel search filter

1fd2ec0767eef3d4540c7b7ae6c7dfb3a8fbdfa8 · 2026-08-05 14:31:20 -0700 · Steve Abrams

Files touched

Diff

commit 1fd2ec0767eef3d4540c7b7ae6c7dfb3a8fbdfa8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Aug 5 14:31:20 2026 -0700

    Cody-gate: posts public-status now published|live in lockstep with /api/search (was published-only → a future 'live' post would 404 on its page while showing in search); cross-ref comment on the parallel search filter
---
 server.js | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/server.js b/server.js
index 093cc1c9..05ed5e2c 100644
--- a/server.js
+++ b/server.js
@@ -459,8 +459,10 @@ app.get('/api/posts', (req, res) => {
   let a = readPosts();
   // Non-admins ONLY ever see published posts — drafts (outside-wire, copyright-sensitive per
   // Option 1) must never be served to a user-tier caller, even one past the site Basic-auth.
+  // "Public" = published OR live — kept in lockstep with /api/search's posts filter.
+  const isPublic = (p) => p.status === 'published' || p.status === 'live';
   if (req.role === 'admin') { if (req.query.status) a = a.filter(p => p.status === req.query.status); }
-  else a = a.filter(p => p.status === 'published');
+  else a = a.filter(isPublic);
   a.sort((x, y) => (y.created_at || '').localeCompare(x.created_at || ''));
   res.json({ count: a.length, items: a });
 });
@@ -468,7 +470,8 @@ app.get('/api/posts/:id', (req, res) => {
   const p = readPosts().find(x => x.id === req.params.id);
   if (!p) return res.status(404).json({ error: 'not found' });
   // A draft is invisible to non-admins — 404 as if it doesn't exist (no guessing draft ids).
-  if (p.status !== 'published' && req.role !== 'admin') return res.status(404).json({ error: 'not found' });
+  const isPublic = p.status === 'published' || p.status === 'live';
+  if (!isPublic && req.role !== 'admin') return res.status(404).json({ error: 'not found' });
   res.json(p);
 });
 app.post('/api/posts', adminOnly, async (req, res) => {
@@ -1427,7 +1430,9 @@ app.get('/api/search', (req, res) => {
     const blob = [v.title, v.desc, v.cat].join(' ');
     if (hit(blob)) push('Videos', { title: v.title, sub: v.cat || 'Video', href: v.view_url || v.embed || '#', image: v.thumb || '', s: score(blob) });
   });
-  // Editorial posts
+  // Editorial posts — direct posts.json read (NOT via /api/posts). The status filter below
+  // MUST stay in sync with the user-tier gate in app.get('/api/posts'): only 'published'/'live'
+  // are ever exposed here, so drafts never leak into search results. Keep both in lockstep.
   asArr(readJSON('posts.json', [])).forEach((p) => {
     if (p.status && p.status !== 'published' && p.status !== 'live') return;
     const blob = [p.title, p.dek, p.author, p.cat].join(' ');

← f40b0bc9 PR intel: linkedin-enrich prioritizes media/PR contacts (org  ·  back to Rentv  ·  PR intel: CRM name/title hygiene tool (byline + trailing-pun 1663154b →