← back to Rentv 2026
rentv (main): href-to-deeper-data on the news feed — LOCAL only, deploy Steve-gated (TK-10126)
6fe9e4dac5da0cdbe566e433f11ebb75248c9211 · 2026-08-01 22:32:42 -0700 · Steve Abrams
Files touched
A public/drill.jsM public/index.html
Diff
commit 6fe9e4dac5da0cdbe566e433f11ebb75248c9211
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 1 22:32:42 2026 -0700
rentv (main): href-to-deeper-data on the news feed — LOCAL only, deploy Steve-gated (TK-10126)
---
public/drill.js | 36 ++++++++++++++++++++++++++++++++++++
public/index.html | 41 +++++++++++++++++++++++++++++++++++++----
2 files changed, 73 insertions(+), 4 deletions(-)
diff --git a/public/drill.js b/public/drill.js
new file mode 100644
index 00000000..e63cdd38
--- /dev/null
+++ b/public/drill.js
@@ -0,0 +1,36 @@
+// ── href-to-deeper-data primitives — SINGLE SOURCE OF TRUTH (HARD rule, 2026-07-31) ──
+// Every displayed data point must be an href to its deeper (filtered) data. These are the
+// reusable atoms; a viewer's index.html loads this file, then defines FIELDS, FIL, $, esc,
+// and applyFilters (which these reference at call time via the shared classic-script global
+// scope). Canonical copy: ~/Projects/_shared/web/drill.js → each viewer's public/drill.js.
+// Memory: all-data-points-href-to-deeper-data. Reference viewers: astek-landing, schumacher-internal.
+
+// Build the URL for a given filter set — any count/atom becomes a real, shareable link.
+function qstr(fil, q, pat) {
+ const u = new URLSearchParams();
+ for (const [k] of FIELDS) { if (fil[k]) u.set(k, fil[k]); }
+ if (q) u.set('q', q); if (pat) u.set('pat', pat);
+ const s = u.toString(); return s ? ('?' + s) : location.pathname;
+}
+// Read the URL's filter state back into FIL + the search inputs (deep-link / back-button).
+function readURL() {
+ const u = new URLSearchParams(location.search);
+ for (const [k] of FIELDS) FIL[k] = u.get(k) || '';
+ $('#q').value = u.get('q') || ''; $('#pattern').value = u.get('pat') || '';
+}
+// Reflect the current filter state into the URL (push = new history entry, else replace).
+function writeURL(push) {
+ const url = qstr(FIL, $('#q').value.trim(), $('#pattern').value.trim());
+ history[push ? 'pushState' : 'replaceState']({}, '', url);
+}
+// drill(field,value,label,cls) → an <a> whose href IS the filtered view for that value.
+// cmd/ctrl/middle-click opens it in a new tab; plain click filters in place (delegated listener).
+function drill(field, val, label, cls) {
+ if (val == null || val === '') return '';
+ const href = qstr({ ...FIL, [field]: String(val) }, $('#q').value.trim(), $('#pattern').value.trim());
+ return `<a class="drill ${cls || ''}" href="${href}" data-filter="${field}" data-v="${esc(val)}" title="Filter to ${esc(val)}">${label != null ? esc(label) : esc(val)}</a>`;
+}
+// Atom drill target: SET the filter to this value (drill in), vs pickF which TOGGLES.
+function setF(k, v) { if (FIL[k] === String(v)) return; FIL[k] = String(v); applyFilters('push'); }
+
+window.qstr = qstr; window.readURL = readURL; window.writeURL = writeURL; window.drill = drill; window.setF = setF;
diff --git a/public/index.html b/public/index.html
index a150a98c..c5d07dd7 100644
--- a/public/index.html
+++ b/public/index.html
@@ -95,6 +95,13 @@
.fine{max-width:1240px;margin:18px auto 0;padding:14px 22px 0;border-top:1px solid #1c232b;color:#6b747d}
@media(max-width:900px){.hero,.river,footer .cols{grid-template-columns:1fr}.lead{padding-right:0;border-right:0}.river{grid-template-columns:1fr 1fr}}
+ /* ---------- href-to-deeper-data: category drill chips (facet → ?cat=) ---------- */
+ /* In-card cat values are drill atoms, not nested <a> (cards are already article links). */
+ .drill{cursor:pointer;border:0;background:none;font:inherit;color:inherit}
+ .drill:hover{text-decoration:underline}
+ /* Ticker cat chip drills the feed; keep it visually a chip but clickable to filter. */
+ .ticker .it .c.drill:hover{background:var(--red);color:#fff;text-decoration:none}
+
/* ---------- CRE Market Pulse embed (dark rates terminal on the light homepage) ---------- */
.mp{background:linear-gradient(180deg,#0b0f16,#080b11);border:1px solid #1c2536;border-radius:14px;color:#e8edf5;padding:22px 22px 20px;margin:8px 0 6px;
--mp-mono:"SFMono-Regular",ui-monospace,"JetBrains Mono","Menlo",Consolas,monospace}
@@ -202,10 +209,36 @@
<div><h5>Company</h5><a href="/about">About</a><a href="/advertise">Advertise</a><a href="/subscribe">Subscribe</a></div>
<div><h5>Follow</h5><a href="https://www.instagram.com/therentv.com_/" target="_blank" rel="noopener noreferrer">Instagram</a><a href="https://www.facebook.com/Rentv.comInc/" target="_blank" rel="noopener noreferrer">Facebook</a><a href="https://www.linkedin.com/company/rentv.com-inc" target="_blank" rel="noopener noreferrer">LinkedIn</a></div>
</div><div class="fine">© 2026 RENTV.com · Headlines refreshed live from rentv.com · <span id="src"></span></div></footer>
+<script src="/drill.js"></script>
<script>
const $=s=>document.getElementById(s), esc=s=>(s||'').replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
const bg=o=>(o&&o.image)?(' style="background-image:url(\''+esc(o.image)+'\')"'):'';
const LK=x=>(x.orig?'/post/':'/news/')+encodeURIComponent(x.id);
+// ── href-to-deeper-data adapter (drill.js atom contract, adapted to this app's single-facet
+// ?cat= server model). The category is already a URL-addressable facet: the drawer links
+// /sales, /development … 301 to /?cat=Sales, and load() filters ALL by the ?cat= value.
+// So every DISPLAYED cat value becomes a real drill to that filtered feed. Cards are already
+// <a href="/news/…"> article links → the in-card cat chip is a <span class="drill"> (delegated,
+// stopPropagation), never a nested <a>. drill.js's qstr/readURL/writeURL are file-scope over a
+// multi-field FIELDS/FIL model; here the single facet is `cat`, so we keep the atom's
+// data-filter="cat"/data-v contract but drive it through the existing ?cat= URL. ──
+const catURL=v=>(v?('?cat='+encodeURIComponent(v)):location.pathname);
+// catChip(value) → a <span> drill atom (used INSIDE <a> cards); clicking filters the feed to that
+// category and reflects into the URL (deep-link/back-button). Falls back to '' for a blank cat.
+const catChip=(v,cls,label)=>{ if(v==null||v==='')return esc(label!=null?label:'');
+ return `<span class="drill ${cls||''}" data-filter="cat" data-v="${esc(v)}" role="link" tabindex="0" title="Filter to ${esc(v)}">${esc(label!=null?label:v)}</span>`; };
+// Delegated: a cat drill atom sets the ?cat= facet (full-page nav to the filtered feed). Because
+// the atom lives inside an <a> article card, stopPropagation + preventDefault keep the card link
+// from also firing. A cmd/ctrl click still opens the filtered view in this tab (no nested <a>).
+document.addEventListener('click',e=>{ const d=e.target.closest('.drill[data-filter="cat"]'); if(!d)return;
+ e.preventDefault(); e.stopPropagation(); const v=d.dataset.v||'';
+ const cur=(new URLSearchParams(location.search).get('cat')||'');
+ // toggle: clicking the currently-active category clears back to the full homepage
+ location.href=(cur.toLowerCase()===v.toLowerCase())?location.pathname:catURL(v);
+});
+document.addEventListener('keydown',e=>{ if(e.key!=='Enter'&&e.key!==' ')return;
+ const d=e.target.closest&&e.target.closest('.drill[data-filter="cat"]'); if(!d)return;
+ e.preventDefault(); e.stopPropagation(); location.href=catURL(d.dataset.v||''); });
$('burger').onclick=()=>{$('scrim').classList.add('open');$('drawer').classList.add('open');};
const closeDrawer=()=>{$('scrim').classList.remove('open');$('drawer').classList.remove('open');};
$('dclose').onclick=closeDrawer;$('scrim').onclick=closeDrawer;
@@ -247,17 +280,17 @@ async function load(){
const li=view.findIndex(x=>x&&x.image); const lead=li>=0?view[li]:view[0];
const rest=view.filter((_,i)=>i!==(li>=0?li:0));
const l=lead;
- $('lead').innerHTML=`<div class="im"${bg(l)}></div><span class="tag">${esc(l.cat||'RENTV')}${l.orig?'<span class="chipo">Original</span>':''}</span><h1>${esc(l.title)}</h1><div class="byl">${l.orig?'By '+esc(l.author||'RENTV Staff')+' · RENTV Original':'By RENTV Staff · live from rentv.com'}</div>`;
+ $('lead').innerHTML=`<div class="im"${bg(l)}></div><span class="tag">${l.cat?catChip(l.cat):'RENTV'}${l.orig?'<span class="chipo">Original</span>':''}</span><h1>${esc(l.title)}</h1><div class="byl">${l.orig?'By '+esc(l.author||'RENTV Staff')+' · RENTV Original':'By RENTV Staff · live from rentv.com'}</div>`;
$('lead').style.cursor='pointer';$('lead').onclick=()=>location.href=LK(l);
- $('aside').innerHTML=rest.slice(0,4).map(n=>`<a href="${LK(n)}"><div class="im"${bg(n)}></div><div><span class="tag" style="color:var(--red)">${esc(n.cat||'')}</span><h4>${esc(n.title)}</h4></div></a>`).join('');
- const items=view.slice(0,16), mk=n=>`<a class="it" data-id="${esc(n.id)}" data-cat="${esc(n.cat||'')}" data-title="${esc(n.title)}" data-href="${LK(n)}"><span class="c">${esc(n.cat||'')}</span>${esc(n.title)}</a>`;
+ $('aside').innerHTML=rest.slice(0,4).map(n=>`<a href="${LK(n)}"><div class="im"${bg(n)}></div><div><span class="tag" style="color:var(--red)">${catChip(n.cat,'','')||''}</span><h4>${esc(n.title)}</h4></div></a>`).join('');
+ const items=view.slice(0,16), mk=n=>`<a class="it" data-id="${esc(n.id)}" data-cat="${esc(n.cat||'')}" data-title="${esc(n.title)}" data-href="${LK(n)}"><span class="c ${n.cat?'drill':''}"${n.cat?` data-filter="cat" data-v="${esc(n.cat)}" role="link" tabindex="0" title="Filter to ${esc(n.cat)}"`:''}>${esc(n.cat||'')}</span>${esc(n.title)}</a>`;
$('track').innerHTML=items.map(mk).join('')+items.map(mk).join('');
const selbar=$('selbar');
$('track').querySelectorAll('.it').forEach(a=>a.addEventListener('click',e=>{e.preventDefault();
$('track').querySelectorAll('.it').forEach(x=>x.classList.remove('sel'));$('track').querySelectorAll('.it[data-id="'+a.dataset.id+'"]').forEach(x=>x.classList.add('sel'));
selbar.innerHTML=`<span class="c">${esc(a.dataset.cat)}</span><a href="${a.dataset.href}">${esc(a.dataset.title)}</a><button class="x" aria-label="close">✕</button>`;selbar.classList.add('on');
selbar.querySelector('.x').onclick=ev=>{ev.stopPropagation();selbar.classList.remove('on');$('track').querySelectorAll('.it').forEach(x=>x.classList.remove('sel'));};}));
- $('river').innerHTML=rest.slice(4,12).map(n=>`<a href="${LK(n)}"><div class="im"${bg(n)}></div><span class="tag" style="color:var(--red)">${esc(n.cat||'')}</span><h4>${esc(n.title)}</h4></a>`).join('');
+ $('river').innerHTML=rest.slice(4,12).map(n=>`<a href="${LK(n)}"><div class="im"${bg(n)}></div><span class="tag" style="color:var(--red)">${catChip(n.cat,'','')||''}</span><h4>${esc(n.title)}</h4></a>`).join('');
if(d&&d.fetched_at){$('src').textContent='last pull '+new Date(d.fetched_at).toLocaleTimeString();}
}
async function loadTicker(){
← 10699fc9 auto-save: 2026-08-01T22:09:48 (4 files) — data/markets.json
·
back to Rentv 2026
·
auto-save: 2026-08-01T22:40:01 (3 files) — data/markets.json 7ab66d57 →