← 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{'&':'&','<':'<','>':'>','"':'"',"'":'''}[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)+' — 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"> · '+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(' · ')+'</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 →