[object Object]

← back to Rentv

TK-10561: inline contractor widget + CA-market derivation into article.html

d4718a5f4f9314855faa20aa5199d35e977bbde0 · 2026-08-15 04:20:11 -0700 · steve@designerwallcoverings.com

Widget CSS+script inlined from contrib/contractors/widget.html. CA-market
derivation uses MutationObserver to watch for the h1 render, then pattern-
matches 30+ CA cities/counties from the story headline. Fail-soft: no CA
match leaves #deal-contractors hidden, never breaks the story.
Deploy (flock guard) and CONTRACTORS_API_AUTH/.env on Kamatera remain gated.

Files touched

Diff

commit d4718a5f4f9314855faa20aa5199d35e977bbde0
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date:   Sat Aug 15 04:20:11 2026 -0700

    TK-10561: inline contractor widget + CA-market derivation into article.html
    
    Widget CSS+script inlined from contrib/contractors/widget.html. CA-market
    derivation uses MutationObserver to watch for the h1 render, then pattern-
    matches 30+ CA cities/counties from the story headline. Fail-soft: no CA
    match leaves #deal-contractors hidden, never breaks the story.
    Deploy (flock guard) and CONTRACTORS_API_AUTH/.env on Kamatera remain gated.
---
 public/article.html | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 148 insertions(+)

diff --git a/public/article.html b/public/article.html
index 5ce06d67..e05ea324 100644
--- a/public/article.html
+++ b/public/article.html
@@ -135,5 +135,153 @@ hamBtn.addEventListener('click',()=>{const open=document.getElementById('nav').c
 load();
 </script>
 <script src="/theme.js"></script>
+<!-- TK-10488 / TK-10561 — Contractor widget + CA-market derivation (inlined) -->
+<style>
+  .rentv-contractors{font:14px/1.5 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a;margin:28px 0}
+  .rentv-contractors__title{font-size:15px;font-weight:700;margin:0 0 .5em;letter-spacing:.01em}
+  .rentv-contractors__list{list-style:none;margin:0;padding:0}
+  .rentv-contractors__item{padding:.55em 0;border-top:1px solid #e6e6e6}
+  .rentv-contractors__item:first-child{border-top:none}
+  .rentv-contractors__name{font-weight:600}
+  .rentv-contractors__trade{color:#555}
+  .rentv-contractors__phone{color:#333}
+  .rentv-contractors__meta{color:#888;font-size:12px}
+  .rentv-contractors__asof{color:#999;font-size:11px;margin-top:.2em;font-style:italic}
+  .rentv-contractors__attr{color:#999;font-size:11px;margin-top:.6em}
+  .rentv-contractors[hidden]{display:none}
+</style>
+<script>
+(function(){
+'use strict';
+if(window.RentvContractors)return;
+function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
+function marketLabel(m){if(!m)return'this market';return m.city||m.county||'this market';}
+function renderList(el,data){
+  var contractors=(data&&data.contractors)||[];
+  if(!contractors.length){el.hidden=true;return;}
+  var label=marketLabel(data.market);
+  var asOf=(data&&data.source_as_of)?String(data.source_as_of):'unknown';
+  var verify=(data&&data.verify_url)?String(data.verify_url):'cslb.ca.gov';
+  var asOfLine='CSLB data as of '+esc(asOf)+' &mdash; verify at '+esc(verify);
+  var rows=contractors.map(function(c){
+    var bits=['<span class="rentv-contractors__name">'+esc(c.name)+'</span>'];
+    if(c.trade)bits.push('<span class="rentv-contractors__trade"> &middot; '+esc(c.trade)+'</span>');
+    var phone=c.phone?'<div class="rentv-contractors__phone">'+esc(c.phone)+'</div>':'';
+    var meta=[];
+    if(c.city)meta.push(esc(c.city));
+    if(c.license_no)meta.push('Lic. '+esc(c.license_no));
+    if(c.license_status)meta.push(esc(c.license_status));
+    var metaLine=meta.length?'<div class="rentv-contractors__meta">'+meta.join(' &middot; ')+'</div>':'';
+    return'<li class="rentv-contractors__item">'+bits.join('')+phone+metaLine+'<div class="rentv-contractors__asof">'+asOfLine+'</div></li>';
+  }).join('');
+  el.innerHTML='<div class="rentv-contractors__title">Licensed contractors in '+esc(label)+'</div>'+
+    '<ul class="rentv-contractors__list">'+rows+'</ul>'+
+    '<div class="rentv-contractors__attr">'+esc((data&&data.attribution)||'CA CSLB licensed contractors')+'</div>';
+  el.hidden=false;
+}
+function render(el,opts){
+  if(!el)return;
+  opts=opts||{};
+  var endpoint=(opts.endpoint||el.getAttribute('data-endpoint')||'/contrib/contractors').replace(/\/+$/,'');
+  var county=opts.county||el.getAttribute('data-county')||'';
+  var city=opts.city||el.getAttribute('data-city')||'';
+  var limit=opts.limit||el.getAttribute('data-limit')||'';
+  var mode=opts.mode||el.getAttribute('data-mode')||'deal';
+  if(!county&&!city){el.hidden=true;return;}
+  var qs=new URLSearchParams({mode:mode});
+  if(county)qs.set('county',county);
+  if(city)qs.set('city',city);
+  if(limit)qs.set('limit',limit);
+  fetch(endpoint+'/api/contractors-for-market?'+qs.toString(),{headers:{accept:'application/json'},credentials:'same-origin'})
+    .then(function(r){return r.ok?r.json():{contractors:[]};})
+    .then(function(data){renderList(el,data);})
+    .catch(function(){el.hidden=true;});
+}
+window.RentvContractors={render:render};
+})();
+
+// CA-market derivation — parses the story headline for known CA city/county names.
+// Fail-soft: no match → widget stays hidden (never breaks the story).
+(function(){
+var CA_CITIES=[
+  ['Los Angeles','los angeles','Los Angeles County'],
+  ['Long Beach','long beach','Los Angeles County'],
+  ['Glendale','glendale','Los Angeles County'],
+  ['Burbank','burbank','Los Angeles County'],
+  ['Pasadena','pasadena','Los Angeles County'],
+  ['Torrance','torrance','Los Angeles County'],
+  ['El Segundo','el segundo','Los Angeles County'],
+  ['Culver City','culver city','Los Angeles County'],
+  ['Santa Monica','santa monica','Los Angeles County'],
+  ['Manhattan Beach','manhattan beach','Los Angeles County'],
+  ['Carson','carson','Los Angeles County'],
+  ['Compton','compton','Los Angeles County'],
+  ['Hawthorne','hawthorne','Los Angeles County'],
+  ['Gardena','gardena','Los Angeles County'],
+  ['Irvine','irvine','Orange County'],
+  ['Anaheim','anaheim','Orange County'],
+  ['Santa Ana','santa ana','Orange County'],
+  ['Huntington Beach','huntington beach','Orange County'],
+  ['Costa Mesa','costa mesa','Orange County'],
+  ['Newport Beach','newport beach','Orange County'],
+  ['San Diego','san diego','San Diego County'],
+  ['Chula Vista','chula vista','San Diego County'],
+  ['Riverside','riverside','Riverside County'],
+  ['Corona','corona','Riverside County'],
+  ['Moreno Valley','moreno valley','Riverside County'],
+  ['San Bernardino','san bernardino','San Bernardino County'],
+  ['Ontario','ontario, ca','San Bernardino County'],
+  ['Rancho Cucamonga','rancho cucamonga','San Bernardino County'],
+  ['Fontana','fontana','San Bernardino County'],
+  ['Sacramento','sacramento','Sacramento County'],
+  ['Oakland','oakland','Alameda County'],
+  ['San Jose','san jose','Santa Clara County'],
+  ['San Francisco','san francisco','San Francisco County'],
+  ['Fresno','fresno','Fresno County'],
+  ['Bakersfield','bakersfield','Kern County'],
+  ['Stockton','stockton','San Joaquin County'],
+  ['Ventura','ventura','Ventura County'],
+  ['Oxnard','oxnard','Ventura County'],
+];
+var CA_COUNTIES=[
+  ['Orange County','orange county'],
+  ['Los Angeles County','los angeles county'],
+  ['Los Angeles','los angeles'],
+  ['Riverside County','riverside county'],
+  ['San Diego County','san diego county'],
+  ['San Bernardino County','san bernardino county'],
+  ['Sacramento County','sacramento county'],
+  ['Ventura County','ventura county'],
+  ['Kern County','kern county'],
+  ['San Joaquin County','san joaquin county'],
+  ['Alameda County','alameda county'],
+  ['Santa Clara County','santa clara county'],
+];
+function deriveMarket(title){
+  var t=(title||'').toLowerCase();
+  for(var i=0;i<CA_CITIES.length;i++){if(t.indexOf(CA_CITIES[i][1])>=0)return{city:CA_CITIES[i][0],county:CA_CITIES[i][2]};}
+  for(var j=0;j<CA_COUNTIES.length;j++){if(t.indexOf(CA_COUNTIES[j][1])>=0)return{city:'',county:CA_COUNTIES[j][0]};}
+  return null;
+}
+// Hook into the article loader: after the article renders, derive market + fire the widget.
+var _origLoad=window._rentvArticleTitle;
+// The article loader sets document.title when done; we watch for that via a post-load hook
+// injected on the article's h1 via MutationObserver (title is set after innerHTML swap).
+var _tried=false;
+var obs=new MutationObserver(function(){
+  if(_tried)return;
+  var h1=document.querySelector('#art h1');
+  if(!h1||h1.textContent.indexOf('Loading')>=0)return;
+  _tried=true;
+  var title=h1.textContent;
+  var mkt=deriveMarket(title);
+  if(mkt&&window.RentvContractors){
+    var el=document.getElementById('deal-contractors');
+    if(el)window.RentvContractors.render(el,{county:mkt.county,city:mkt.city,limit:6});
+  }
+});
+obs.observe(document.getElementById('art')||document.body,{childList:true,subtree:true});
+})();
+</script>
 </body>
 </html>

← b6ac0ad7 auto-data-snapshot: 2026-08-15T04:08:27 (7 data files) — dat  ·  back to Rentv  ·  auto-data-snapshot: 2026-08-15T04:38:51 (7 data files) — dat 602ab4b4 →