0
TOOLS#581cf8ba
Local SEO Lens v2
@Owner·deposited 3w ago·updated 3w ago·31 views
TOOLS#581cf8ba
Local SEO Lens v2
OW
@Owner
31Views
0Comments
0Forks
0Saves
SHARE · REMIX
Local SEO Lens v2 — a HTML tools widget by @Owner.
CONTROLS
No comments yet. Be the first!
✦ Remix with AI
SDK in this widget
- raw fetch(): Raw fetch() detected. Prefer vibes.fetch for proxied requests.
Generated prompt
You are helping me modify a vibe-coded widget from itjustvibes.com.
[VIBE CODE: "Local SEO Lens v2" by @Owner]
Source: https://itjustvibes.com/Owner/local-seo-lens-v2
Type: HTML
--- SOURCE CODE ---
```html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Local SEO Lens</title>
<style>
:root{--bg:#111814;--panel:#f7efe2;--paper:#fffaf0;--ink:#18201c;--mut:#56635b;--line:#18201c22;--dark:#101512;--cream:#fff4df;--green:#5b8f68;--blue:#497b9b;--amber:#b78a34;--red:#a95151;--violet:#6f668d}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 15% 0,#26342c 0,#111814 36%,#0b100d 100%);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,Segoe UI,sans-serif}#app{min-height:100vh;padding:14px}.w{max-width:1360px;margin:auto}.hero,.sec{background:var(--panel);border:1px solid #ffffff44;border-radius:24px;box-shadow:0 18px 50px #0006;margin:0 0 14px;overflow:hidden}.hero{display:grid;grid-template-columns:1.15fr .85fr}.pad{padding:22px}.dark{background:var(--dark);color:#f8efdd}.ey{font-size:12px;letter-spacing:.14em;text-transform:uppercase;font-weight:950;color:#5b675f}.dark .ey{color:#b8c5bc}.ttl{font-size:clamp(34px,5.4vw,58px);line-height:.92;letter-spacing:-.06em;font-weight:950;margin:9px 0}.sub{color:var(--mut);line-height:1.5;max-width:820px}.dark .sub{color:#c9d1cb}.row{display:flex;gap:9px;align-items:center;flex-wrap:wrap}.urlbar{display:grid;grid-template-columns:1fr auto auto;gap:10px;margin-top:18px}.k{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:13px}.k div{background:#ffffff12;border:1px solid #ffffff1f;border-radius:16px;padding:12px}.k span,.mut{font-size:12px;color:#6b766f}.dark span{color:#c3cec7}.k b{display:block;font-size:23px;margin-top:5px;letter-spacing:-.04em}.head{padding:15px 18px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;gap:10px;align-items:center}.head h2{margin:0;font-size:18px}.g2{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:12px}.g3{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.g4{display:grid;grid-template-columns:repeat(4,1fr);gap:9px;padding:12px}.g5{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;padding:12px}.card{background:var(--paper);border:1px solid var(--line);border-radius:18px;padding:13px}.card.darkcard{background:#151c18;color:#f8efdd;border-color:#ffffff1f}label{display:block;font-size:12px;font-weight:850;color:#4e5a53;margin:0 0 5px}input,select,textarea{width:100%;min-height:38px;border:1px solid var(--line);border-radius:11px;background:#fffdf8;padding:8px 9px;font:inherit;color:var(--ink)}textarea{min-height:72px;resize:vertical}button{border:1px solid #1a241d22;background:#fffaf0;border-radius:999px;padding:10px 13px;font-weight:850;cursor:pointer;color:var(--ink);transition:.18s transform,.18s background,.18s box-shadow}button:hover{transform:translateY(-1px);box-shadow:0 8px 18px #0002}button.p{background:#17201b;color:#fff4df}.mini{font-size:12px;padding:7px 10px}.ghost{background:#ffffff66}.metrics{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;padding:12px}.m{background:var(--paper);border:1px solid var(--line);border-radius:18px;padding:13px;border-left:5px solid var(--blue);min-height:86px}.m.g{border-left-color:var(--green)}.m.a{border-left-color:var(--amber)}.m.r{border-left-color:var(--red)}.m.v{border-left-color:var(--violet)}.m b{display:block;font-size:24px;letter-spacing:-.04em;margin-top:6px}.m small{display:block;color:var(--mut);margin-top:3px}.pillars{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;padding:12px}.pillar{background:var(--paper);border:1px solid var(--line);border-radius:18px;padding:13px}.bar{height:9px;background:#e2d8c8;border-radius:99px;overflow:hidden;margin-top:9px}.bar i{display:block;height:100%;background:linear-gradient(90deg,var(--red),var(--amber),var(--green));border-radius:99px;width:0;transition:width .6s ease}.tabs{display:flex;padding:0 12px 12px;gap:8px;flex-wrap:wrap}.tabs button{flex:1;min-width:110px}.on{background:#17201b!important;color:#fff4df!important}.issue{background:var(--paper);border:1px solid var(--line);border-radius:18px;padding:14px;margin-bottom:10px}.issue h3{margin:0 0 8px;font-size:16px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.badge{font-size:11px;text-transform:uppercase;letter-spacing:.08em;border-radius:999px;padding:4px 8px;font-weight:950;border:1px solid #0001}.critical{background:#f3d6d0;color:#842a24}.high{background:#f3dfbd;color:#7b4a00}.medium{background:#f5edc9;color:#665600}.low{background:#dce8f0;color:#28536e}.opportunity{background:#dfdced;color:#514974}.pass{background:#deebdf;color:#2f653b}.issue p{margin:6px 0;color:#4d594f;line-height:1.45}.schemaGrid{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:12px}.typeList{display:flex;flex-wrap:wrap;gap:7px}.typeList span{background:#e9dfcd;border:1px solid var(--line);border-radius:999px;padding:6px 9px;font-size:12px;font-weight:800}.tree{max-height:320px;overflow:auto;background:#151c18;color:#f8efdd;border-radius:16px;padding:12px;font-size:12px;white-space:pre-wrap}.codebox{max-height:360px;overflow:auto;background:#151c18;color:#f8efdd;border-radius:16px;padding:12px;font:12px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap}.status{display:grid;grid-template-columns:repeat(6,1fr);gap:8px;padding:12px}.step{background:#fffaf0;border:1px solid var(--line);border-radius:16px;padding:10px;font-size:12px;font-weight:850;color:#657067}.step b{display:block;color:#17201b;margin-bottom:5px}.step.active{outline:2px solid #497b9b55}.step.done{background:#e9f0e5;color:#315c39}.scanline{height:4px;background:#e2d8c8;overflow:hidden;border-radius:99px;margin-top:7px}.active .scanline i{display:block;width:40%;height:100%;background:#497b9b;border-radius:99px;animation:sweep 1.1s infinite ease-in-out}@keyframes sweep{0%{margin-left:-45%}100%{margin-left:105%}}.napTable{width:100%;border-collapse:collapse;background:var(--paper);border-radius:14px;overflow:hidden}.napTable th,.napTable td{padding:9px;border-bottom:1px solid #18201c18;text-align:left;vertical-align:top}.napTable th{background:#eee4d4;font-size:12px}.history{display:grid;gap:8px}.hist{display:flex;justify-content:space-between;gap:10px;align-items:center;background:#fffaf0;border:1px solid var(--line);border-radius:16px;padding:10px}.hist b{font-size:13px}.hist small{display:block;color:var(--mut)}.foot{color:#c8d0ca;font-size:12px;padding:5px 4px 22px;line-height:1.45}.hidden{display:none!important}.split{display:grid;grid-template-columns:1.2fr .8fr;gap:12px;padding:12px}.empty{padding:20px;color:var(--mut);text-align:center}.copyok{color:#5b8f68;font-size:12px;font-weight:900}.switch{display:flex;gap:8px;align-items:center;background:#fffaf0;border:1px solid var(--line);border-radius:999px;padding:7px 10px;font-weight:850;font-size:12px}.switch input{width:auto;min-height:auto}.linklist a{display:block;color:#294f6e;font-weight:850;margin:6px 0;word-break:break-word}.progressRing{width:120px;height:120px;border-radius:50%;display:grid;place-items:center;margin:0 auto 12px;background:conic-gradient(var(--green) 0deg,#243028 0deg)}.progressRing div{width:92px;height:92px;border-radius:50%;background:#101512;display:grid;place-items:center;color:#fff4df;font-size:28px;font-weight:950}.inspectorTitle{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}h3{margin:0 0 9px}.notice{background:#e8dfce;border:1px solid var(--line);padding:10px 12px;border-radius:14px;color:#4a554d;font-size:13px;line-height:1.45}.source{font-size:11px;color:#68746c;margin-top:8px}@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}@media(max-width:1000px){.hero,.split,.schemaGrid{grid-template-columns:1fr}.metrics,.pillars,.g5{grid-template-columns:1fr 1fr}.status{grid-template-columns:repeat(3,1fr)}.urlbar{grid-template-columns:1fr}}@media(max-width:640px){#app{padding:10px}.metrics,.pillars,.g3,.g4,.g5,.g2,.status{grid-template-columns:1fr}.ttl{font-size:38px}.hero,.sec{border-radius:20px}.pad{padding:17px}.sticky{position:sticky;bottom:8px;z-index:5;box-shadow:0 12px 30px #0005}.tabs button{min-width:95px}.k{grid-template-columns:1fr}.hist{align-items:flex-start;flex-direction:column}.m b{font-size:22px}}
</style>
</head>
<body><div id="app">Loading...</div><script>
(()=>{
'use strict';
const KEY='local_seo_lens_v1',MAX_HIST=10;
const $=(q,r=document)=>r.querySelector(q),$$=(q,r=document)=>Array.from(r.querySelectorAll(q));
const esc=v=>String(v==null?'':v).replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]));
const attr=esc, pct=n=>Math.max(0,Math.min(100,Math.round(+n||0))), uniq=a=>Array.from(new Set((a||[]).filter(Boolean))), now=()=>new Date().toISOString();
const sevOrder=['critical','high','medium','low','opportunity'];
const localTypes=new Set('AccountingService Attorney AutoRepair Bakery BarOrPub BeautySalon ChildCare Dentist DryCleaningOrLaundry Electrician EmergencyService Florist FoodEstablishment FurnitureStore GardenStore GeneralContractor HealthAndBeautyBusiness HomeAndConstructionBusiness HVACBusiness LegalService LocalBusiness Locksmith MedicalBusiness MovingCompany Plumber ProfessionalService RealEstateAgent Restaurant RoofingContractor Store AutomotiveBusiness MedicalClinic Optician Pharmacy Physician AnimalShelter TouristInformationCenter FinancialService InsuranceAgency EmploymentAgency TravelAgency'.split(' '));
let S={tab:'critical',busy:false,copied:'',status:'Ready',step:'idle',request:{url:'',scanDepth:'basic',includePageSpeed:false,expected:{name:'',telephone:'',streetAddress:'',addressLocality:'',addressRegion:'',postalCode:'',addressCountry:'US',serviceArea:'',primaryCategory:'',googleBusinessProfileUrl:'',sameAs:''}},result:null,history:[]},timer;
function V(){if(!window.vibes)window.vibes={onReady:f=>document.readyState==='loading'?document.addEventListener('DOMContentLoaded',f):f(),save:async()=>{},load:async()=>null,fetch:(u,o)=>fetch(u,o)};return window.vibes}
function setPath(path,val){let a=S,ks=path.split('.');for(let i=0;i<ks.length-1;i++)a=a[ks[i]];a[ks.at(-1)]=val;save();}
function save(){clearTimeout(timer);timer=setTimeout(()=>V().save(KEY,{request:S.request,history:S.history}).catch(()=>{}),180)}
function normalizedUrl(u){u=(u||'').trim();if(!u)throw Error('Enter a website URL.');if(!/^https?:\/\//i.test(u))u='https://'+u;let x=new URL(u);x.hash='';return x.href}
async function fetchText(url,opts={}){let err,ctrl,t=setTimeout(()=>ctrl&&ctrl.abort(),opts.timeout||12000);try{ctrl=new AbortController();let r=await fetch(url,{method:'GET',redirect:'follow',signal:ctrl.signal,headers:{'Accept':'text/html,application/xhtml+xml,text/plain;q=.9,*/*;q=.8'}});let text=await r.text();clearTimeout(t);return{ok:r.ok,status:r.status,url:r.url||url,text,headers:headersObj(r.headers),via:'direct'}}catch(e){err=e;clearTimeout(t)}try{let r=await V().fetch(url,{method:'GET',headers:{'Accept':'text/html,application/xhtml+xml,text/plain;q=.9,*/*;q=.8'}});let text=typeof r==='string'?r:await r.text();return{ok:r.ok!==false,status:r.status||200,url:r.url||url,text,headers:r.headers?headersObj(r.headers):{},via:'vibes.fetch'}}catch(e2){throw Error('Fetch failed: '+(e2.message||err?.message||'blocked by CORS/proxy'))}}
function headersObj(h){let o={};try{h.forEach((v,k)=>o[k.toLowerCase()]=v)}catch(e){}return o}
function parseDoc(html){return new DOMParser().parseFromString(html||'','text/html')}
function textOf(doc){let c=doc.body?doc.body.cloneNode(true):doc.cloneNode(true);$$('script,style,noscript,svg,canvas',c).forEach(n=>n.remove());return (c.textContent||'').replace(/\s+/g,' ').trim()}
function meta(doc,n){let m=$(`meta[name="${n}"]`,doc)||$(`meta[property="${n}"]`,doc);return m?m.getAttribute('content')||'':''}
function absUrl(h,base){try{return new URL(h,base).href}catch(e){return''}}
function discoverLinks(doc,base){let links=$$('a[href]',doc).map(a=>({href:absUrl(a.getAttribute('href'),base),text:(a.textContent||'').trim().slice(0,80),rel:a.getAttribute('rel')||''})).filter(x=>x.href);let contact=[],location=[],map=[],social=[],profile=[],cta=[];let soc=/facebook|instagram|linkedin|youtube|twitter|x\.com|tiktok|yelp|bbb\.org|angi|houzz|avvo|healthgrades|zocdoc|tripadvisor/i;links.forEach(l=>{let h=l.href,t=(l.text+' '+h).toLowerCase();if(/contact|get-in-touch|request|quote|appointment|schedule|book|call|email/.test(t))contact.push(l);if(/location|directions|visit|office|showroom|find-us|where-we-are|contact/.test(t))location.push(l);if(/google\.com\/maps|maps\.app\.goo|goo\.gl\/maps|apple\.com\/maps|bing\.com\/maps|directions/.test(t))map.push(l);if(soc.test(h))social.push(l);if(/business\.google\.com|g\.page|google\.com\/search\?/.test(h))profile.push(l);if(/call|quote|appointment|schedule|book|contact|get started|request/.test(t))cta.push(l)});return{all:links,contact:uniqObj(contact,'href'),location:uniqObj(location,'href'),map:uniqObj(map,'href'),social:uniqObj(social,'href'),profile:uniqObj(profile,'href'),cta:uniqObj(cta,'href')}}
function uniqObj(a,k){let s=new Set;return a.filter(x=>!s.has(x[k])&&s.add(x[k])).slice(0,12)}
function extractPhones(txt){let out=[],re=/(?:\+?1[\s.-]?)?(?:\(?\d{3}\)?[\s.-]?)\d{3}[\s.-]?\d{4}/g,m;while((m=re.exec(txt)))out.push(m[0].trim());return uniq(out).slice(0,8)}
function extractAddresses(txt){let suf='Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Drive|Dr|Lane|Ln|Way|Court|Ct|Circle|Cir|Highway|Hwy|Parkway|Pkwy|Place|Pl|Terrace|Ter|Suite|Ste';let re=new RegExp('\\b\\d{1,6}\\s+[A-Za-z0-9 .#\\-]+\\s(?:'+suf+')\\b(?:[,\\s]+(?:Suite|Ste|Unit|#)\\s?[A-Za-z0-9-]+)?(?:[,\\s]+[A-Za-z .-]+)?(?:[,\\s]+[A-Z]{2})?(?:[,\\s]+\\d{5}(?:-\\d{4})?)?','gi'),m,out=[];while((m=re.exec(txt)))out.push(m[0].replace(/\s+/g,' ').trim());return uniq(out).slice(0,8)}
function extractTerms(txt,exp){let t=txt.toLowerCase(),terms=[];['near me','serving','service area','locally owned','family owned','directions','appointment','estimate','emergency','licensed','insured','reviews','testimonials','hours'].forEach(x=>{if(t.includes(x))terms.push(x)});['addressLocality','addressRegion','postalCode','serviceArea','primaryCategory'].forEach(k=>{let v=(exp[k]||'').trim();if(v&&t.includes(v.toLowerCase()))terms.push(v)});return uniq(terms).slice(0,20)}
function cleanJson(raw){return raw.replace(/^\s*<!--|-->\s*$/g,'').replace(/"/g,'"').replace(/&/g,'&').trim()}
function parseJsonLd(doc){let blocks=$$('script[type="application/ld+json"]',doc),parsed=[],errors=[],nodes=[];blocks.forEach((s,i)=>{let raw=cleanJson(s.textContent||'');if(!raw)return;try{let j=JSON.parse(raw);parsed.push(j);collectNodes(j,nodes)}catch(e){try{let j=JSON.parse(raw.replace(/,\s*([}\]])/g,'$1'));parsed.push(j);collectNodes(j,nodes)}catch(e2){errors.push({block:i+1,message:e2.message,preview:raw.slice(0,180)})}}});let types=uniq(nodes.flatMap(typeOf));let lb=nodes.filter(n=>typeOf(n).some(t=>localTypes.has(t)||String(t).endsWith('Business')));let org=nodes.filter(n=>typeOf(n).includes('Organization'));return{blocks:blocks.length,parsed,errors,nodes,types,localBusinessNodes:lb,organizationNodes:org}}
function collectNodes(x,out){if(!x)return;if(Array.isArray(x)){x.forEach(v=>collectNodes(v,out));return}if(typeof x==='object'){if(x['@type']||x['@graph'])out.push(x);if(Array.isArray(x['@graph']))x['@graph'].forEach(v=>collectNodes(v,out));Object.keys(x).forEach(k=>{if(k!=='@graph'&&typeof x[k]==='object')collectNodes(x[k],out)})}}
function typeOf(n){let t=n&&n['@type'];return Array.isArray(t)?t.map(String):(t?[String(t)]:[])}
function val(v){if(Array.isArray(v))return v.map(val).filter(Boolean).join(', ');if(v&&typeof v==='object')return v.name||v['@id']||v.url||'';return v==null?'':String(v)}
function schemaData(sd){let n=sd.localBusinessNodes[0]||sd.organizationNodes[0]||{};let addr=n.address||{};let ad=typeof addr==='string'?{text:addr}:addr||{};return{node:n,type:typeOf(n)[0]||'',name:val(n.name),telephone:val(n.telephone||n.phone),url:val(n.url),streetAddress:val(ad.streetAddress||ad.text),addressLocality:val(ad.addressLocality),addressRegion:val(ad.addressRegion),postalCode:val(ad.postalCode),addressCountry:val(ad.addressCountry),geo:n.geo||null,hours:n.openingHoursSpecification||n.openingHours||null,sameAs:n.sameAs||[],logo:val(n.logo),image:val(n.image||n.photo),hasMap:val(n.hasMap),areaServed:val(n.areaServed),priceRange:val(n.priceRange),department:n.department,aggregateRating:n.aggregateRating,review:n.review}}
const normName=s=>String(s||'').toLowerCase().replace(/\b(llc|inc|co|corp|company|pllc|ltd)\b/g,'').replace(/[^a-z0-9]+/g,' ').trim();
const normPhone=s=>String(s||'').replace(/\D/g,'').replace(/^1(?=\d{10}$)/,'');
const normAddr=s=>String(s||'').toLowerCase().replace(/\bstreet\b/g,'st').replace(/\bavenue\b/g,'ave').replace(/\broad\b/g,'rd').replace(/\bboulevard\b/g,'blvd').replace(/\bdrive\b/g,'dr').replace(/[^a-z0-9]+/g,' ').trim();
function matchStr(a,b,kind='text'){if(!a||!b)return false;if(kind==='phone')return normPhone(a)&&normPhone(a)===normPhone(b);if(kind==='addr')return normAddr(a)&&normAddr(b)&&(normAddr(a).includes(normAddr(b))||normAddr(b).includes(normAddr(a)));let A=normName(a),B=normName(b);return A&&B&&(A.includes(B)||B.includes(A))}
function analyzeNap(exp,sch,visible){let fields=['name','telephone','streetAddress','addressLocality','addressRegion','postalCode'];let supplied=fields.filter(k=>(exp[k]||'').trim());let matches=0,mismatches=[];supplied.forEach(k=>{let kind=k==='telephone'?'phone':k==='streetAddress'?'addr':'text';let ok=matchStr(exp[k],sch[k],kind)||matchStr(exp[k],visible[k],kind);if(ok)matches++;else mismatches.push(k)});let conf='missing';if(supplied.length){let ratio=matches/supplied.length;if(ratio>.82)conf='verified';else if(ratio>.45)conf='mixed';else conf='weak'}else if((sch.name&&visible.name)||(sch.telephone&&visible.telephone)||(sch.streetAddress&&visible.streetAddress))conf='likely';else if(sch.name||visible.name||visible.telephone||visible.streetAddress)conf='weak';return{confidence:conf,matches,mismatches,supplied:supplied.length}}
function pageBasics(doc,finalUrl,headers,exp){let title=(doc.title||'').trim(),description=meta(doc,'description'),h1=$$('h1',doc).map(x=>(x.textContent||'').trim()).filter(Boolean).slice(0,6);let canonical=($('link[rel="canonical"]',doc)||{}).href||'';let robots=$$('meta[name="robots"],meta[name="googlebot"]',doc).map(x=>x.getAttribute('content')||'');let text=textOf(doc),links=discoverLinks(doc,finalUrl);let phones=extractPhones(text),addresses=extractAddresses(text);let visible={name:'',telephone:phones[0]||'',streetAddress:addresses[0]||'',addressLocality:'',addressRegion:'',postalCode:''};if(exp.name&&text.toLowerCase().includes(exp.name.toLowerCase()))visible.name=exp.name;else visible.name=(h1[0]||title||'').slice(0,90);['addressLocality','addressRegion','postalCode'].forEach(k=>{let v=exp[k];if(v&&text.toLowerCase().includes(v.toLowerCase()))visible[k]=v});return{title,description,h1,canonical,robotsMeta:robots,xRobots:headers['x-robots-tag']||'',https:/^https:/i.test(finalUrl),text,phones,addresses,terms:extractTerms(text,exp),links,visible}}
async function rootChecks(finalUrl,setStep){let u=new URL(finalUrl),origin=u.origin,robots={status:'not checked',available:false,blocking:false,sitemaps:[],text:''},sitemap={status:'not checked',available:false,url:''};try{setStep('robots');let r=await fetchText(origin+'/robots.txt',{timeout:8000});robots.status=r.status;robots.available=r.status<400&&!!r.text;robots.text=(r.text||'').slice(0,5000);robots.sitemaps=uniq((r.text.match(/^sitemap:\s*(.+)$/gim)||[]).map(x=>x.replace(/^sitemap:\s*/i,'').trim()));robots.blocking=/user-agent:\s*\*[^#]*disallow:\s*\/\s*(?:$|\n)/i.test(r.text)}catch(e){robots.status='unavailable'}try{setStep('sitemap');let sm=robots.sitemaps[0]||origin+'/sitemap.xml';let r=await fetchText(sm,{timeout:8000});sitemap={status:r.status,available:r.status<400&&/(<urlset|<sitemapindex|https?:\/\/)/i.test(r.text),url:sm}}catch(e){sitemap={status:'unavailable',available:false,url:(robots.sitemaps[0]||origin+'/sitemap.xml')}}return{robots,sitemap}}
function likelyInternalPages(links,finalUrl,depth){if(depth==='single')return[];let origin=new URL(finalUrl).origin,lim=depth==='deeper'?3:2,rank=['contact','location','locations','about','service','services','visit','directions'];let candidates=links.all.filter(l=>{try{return new URL(l.href).origin===origin}catch(e){return false}}).map(l=>({...l,score:rank.reduce((s,r,i)=>s+(((l.href+' '+l.text).toLowerCase().includes(r))?20-i:0),0)})).filter(l=>l.score>0&&l.href!==finalUrl).sort((a,b)=>b.score-a.score);return uniqObj(candidates,'href').slice(0,lim)}
async function crawlExtra(pages,setStep){let merged={phones:[],addresses:[],terms:[],texts:[],fetched:[]};for(let i=0;i<pages.length;i++){try{setStep('crawl');let r=await fetchText(pages[i].href,{timeout:9000}),d=parseDoc(r.text),t=textOf(d);merged.phones.push(...extractPhones(t));merged.addresses.push(...extractAddresses(t));merged.terms.push(...extractTerms(t,{}));merged.texts.push(t.slice(0,4000));merged.fetched.push({url:pages[i].href,status:r.status,title:(d.title||'').trim()})}catch(e){merged.fetched.push({url:pages[i].href,status:'failed',error:e.message})}}
merged.phones=uniq(merged.phones).slice(0,12);merged.addresses=uniq(merged.addresses).slice(0,12);merged.terms=uniq(merged.terms).slice(0,20);return merged}
function addIssue(issues,severity,pillar,title,evidence,why,fix,source='audit',confidence='medium'){issues.push({id:title.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,''),severity,pillar,title,evidence,whyItMatters:why,howToFix:fix,source,confidence})}
function scoreAndIssues(ctx){let {url,fetchRes,meta,sd,sch,nap,root,extra,ps}=ctx,issues=[],s={nap:0,schema:0,indexability:0,localRelevance:0,trust:0};let exp=S.request.expected,txt=meta.text.toLowerCase();
// NAP 25
if(meta.visible.name||sch.name)s.nap+=5;else addIssue(issues,'critical','nap','Business name was not found','No clear business name appeared in visible text, title/H1, or LocalBusiness schema.','Search engines and users need a stable entity name to understand the business.','Add the real business name visibly on the page and in LocalBusiness schema.','entity-extractor','high');
if(meta.phones.length||extra.phones?.length)s.nap+=4;else addIssue(issues,'medium','nap','Visible phone number was not found','No phone-like string was detected on the scanned pages.','A local page should make contact information easy for users and crawlers to find.','Add the primary local phone number in visible text and in schema telephone.');
if(meta.addresses.length||extra.addresses?.length||sch.streetAddress)s.nap+=5;else addIssue(issues,'high','nap','Visible address was not found','No address-like text was detected and schema address is weak or missing.','Physical location clarity is one of the strongest local entity signals.','Add a real address on the contact/location page and use PostalAddress schema, or clearly describe service-area-only status.');
if(meta.visible.addressLocality||meta.visible.addressRegion||meta.visible.postalCode||meta.terms.length)s.nap+=4;else addIssue(issues,'medium','nap','City, state, or service-area language is weak','The page did not clearly mention the expected city/region/postal code or service area.','Local pages need visible location context, not only generic service language.','Mention the served city/region naturally in the title, H1, body, footer, and/or service area section.');
if(meta.links.contact.length||meta.links.location.length)s.nap+=3;else addIssue(issues,'medium','nap','Contact or location page link was not obvious','The scan did not find a clear contact, location, directions, or visit link.','Users and crawlers often rely on contact/location pages to verify NAP details.','Add a prominent Contact, Location, Directions, or Visit Us link.');
if(nap.supplied)s.nap+=Math.round(4*nap.matches/nap.supplied);else s.nap+=(nap.confidence==='likely'?3:nap.confidence==='weak'?1:0);
if(nap.mismatches.length)addIssue(issues,'high','nap','Expected NAP does not fully match detected data','Mismatched fields: '+nap.mismatches.join(', ')+'.','Conflicting NAP data can weaken local entity confidence and confuse users.','Make the business name, phone, and address consistent across visible page content and LocalBusiness schema.','nap-compare','high');
// Schema 30
if(sd.blocks&&sd.errors.length<sd.blocks)s.schema+=4;else if(sd.blocks)addIssue(issues,'critical','schema','JSON-LD was found but did not parse','Parse errors: '+sd.errors.map(e=>`Block ${e.block}: ${e.message}`).join(' | '),'Invalid structured data may be ignored by search engines and validators.','Fix the JSON syntax and retest with Schema.org Validator and Google Rich Results Test.','jsonld-parser','high');
else addIssue(issues,'high','schema','No JSON-LD structured data was found','The MVP parser found no application/ld+json blocks.','JSON-LD is the recommended, maintainable way to communicate structured business data.','Add a valid JSON-LD LocalBusiness block to the page.','jsonld-parser','high');
let hasLB=sd.localBusinessNodes.length>0;if(hasLB)s.schema+=6;else if(sd.organizationNodes.length)addIssue(issues,'high','schema','Organization schema is present, but LocalBusiness schema was not found','Detected Organization schema but no LocalBusiness or local subtype.','Organization schema is useful but is not a substitute for location-level local business details.','Add LocalBusiness or a specific subtype such as Restaurant, Dentist, HVACBusiness, Store, or ProfessionalService.','schema-type','high');else addIssue(issues,'high','schema','LocalBusiness schema was not found','Detected types: '+(sd.types.join(', ')||'none')+'.','Search engines have less structured location information to interpret.','Add LocalBusiness JSON-LD describing the real business location or service area.','schema-type','high');
if(sch.name)s.schema+=4;else addIssue(issues,'high','schema','Schema name is missing','The LocalBusiness/Organization node does not include a name.','Google identifies name as required for LocalBusiness rich result eligibility.','Add the exact real-world business name to schema name.','schema-field','high');
if(sch.streetAddress&&sch.addressLocality&&sch.addressRegion)s.schema+=6;else if(sch.streetAddress||sch.addressLocality)s.schema+=3,addIssue(issues,'medium','schema','Schema address is incomplete','Detected partial address fields only.','A structured PostalAddress helps search engines understand the real location.','Use address.@type PostalAddress with streetAddress, addressLocality, addressRegion, postalCode, and addressCountry.','schema-field','medium');else addIssue(issues,'high','schema','Schema address is missing','No structured PostalAddress was detected.','Google identifies address as required for LocalBusiness rich result eligibility.','Add address as a PostalAddress object.','schema-field','high');
if(sch.telephone)s.schema+=3;else addIssue(issues,'medium','schema','Schema telephone is missing','No telephone property was detected in the local business node.','Telephone helps connect the site entity to public business contact data.','Add the primary business phone number to telephone.');
if(sch.url)s.schema+=2;else addIssue(issues,'low','schema','Schema URL is missing','No url property was detected.','The url property clarifies the canonical website for the business entity.','Add the audited page or canonical business homepage URL to schema url.');
if(sch.geo||sch.hasMap||meta.links.map.length)s.schema+=2;else addIssue(issues,'low','schema','Map or geo signal is missing','No geo, hasMap, or visible map/directions link was detected.','Map and geo signals can strengthen location clarity when accurate.','Add hasMap or a directions link; add geo only after verifying coordinates.');
if(sch.hours||/hours|open|closed|mon|tue|wed|thu|fri|sat|sun/i.test(meta.text))s.schema+=2;else addIssue(issues,'medium','schema','Hours are not clearly supplied','No openingHoursSpecification or obvious hours text was detected.','Hours help users and validators understand availability.','Add visible business hours and openingHoursSpecification when appropriate.');
if(sch.sameAs?.length||sch.logo||sch.image||sch.hasMap||sch.areaServed||sch.priceRange||sch.department)s.schema+=1;else addIssue(issues,'opportunity','schema','Schema enrichment fields can be improved','No sameAs, logo/image, hasMap, areaServed, department, or priceRange enrichment was detected.','Enrichment fields help complete the local entity graph when they are accurate.','Add only verified sameAs/profile links and relevant enrichment fields.');
if(sch.aggregateRating||sch.review)addIssue(issues,'opportunity','schema','Review/rating schema needs manual policy review','aggregateRating or review markup appears to be present.','Review markup has policy restrictions and should not be fabricated or used casually.','Verify against Google review snippet guidelines before relying on review/rating markup.','schema-policy','medium');
// Indexability 20
if(fetchRes.ok)s.indexability+=4;else addIssue(issues,'critical','indexability','Page fetch did not return a healthy response','Status: '+fetchRes.status+'.','If crawlers cannot fetch the page, they cannot evaluate its local signals.','Fix server availability, redirects, security blocks, or crawl restrictions.','fetch','high');
if(+fetchRes.status>=200&&+fetchRes.status<400)s.indexability+=3;else addIssue(issues,'critical','indexability','HTTP status appears unhealthy','Status: '+fetchRes.status+'.','Non-2xx/3xx responses can prevent indexing or cause crawlers to ignore the page.','Return a normal 200 status for the canonical public page.','fetch','high');
if(meta.https)s.indexability+=3;else addIssue(issues,'medium','indexability','URL is not HTTPS','The audited URL does not use HTTPS.','HTTPS is expected for modern sites and user trust.','Use HTTPS and redirect HTTP to HTTPS.');
let noindex=[...meta.robotsMeta,meta.xRobots].join(' ').toLowerCase().includes('noindex');if(!noindex)s.indexability+=4;else addIssue(issues,'critical','indexability','Noindex directive detected','Robots meta or X-Robots-Tag contains noindex.','A noindex directive can keep the page out of search results.','Remove noindex from public pages that should be eligible for search.','robots','high');
let canonOk=true;if(meta.canonical){try{canonOk=new URL(meta.canonical,fetchRes.url).origin===new URL(fetchRes.url).origin}catch(e){canonOk=false}}if(canonOk)s.indexability+=3;else addIssue(issues,'high','indexability','Canonical points to a different or invalid URL','Canonical: '+meta.canonical+'.','A conflicting canonical can shift signals away from the audited page.','Set canonical to the correct final page URL or a same-domain canonical target.','canonical','high');
if(root.robots.available&&!root.robots.blocking||root.sitemap.available)s.indexability+=3;else addIssue(issues,'low','indexability','Robots or sitemap signals are weak','robots.txt: '+root.robots.status+'; sitemap: '+root.sitemap.status+'.','Sitemaps and non-blocking robots rules help crawlers discover important pages.','Confirm robots.txt is not blocking important pages and publish a sitemap.xml.','robots-sitemap','medium');
if(root.robots.blocking)addIssue(issues,'critical','indexability','robots.txt may block the entire site','A User-agent: * group appears to disallow /.','Blocking crawl can prevent search engines from seeing pages and schema.','Review robots.txt and remove broad disallow rules unless intentional.','robots','high');
// Local relevance 15
let localHint=[exp.name,exp.addressLocality,exp.addressRegion,exp.serviceArea,exp.primaryCategory].filter(Boolean).some(v=>(meta.title+' '+meta.description+' '+meta.h1.join(' ')).toLowerCase().includes(v.toLowerCase()));if(localHint||meta.title.length>18)s.localRelevance+=localHint?4:2;if(!meta.title)addIssue(issues,'medium','local-relevance','Page title is missing or weak','Title: '+(meta.title||'missing')+'.','The title is a prominent search and browser signal.','Write a concise title with the business/service and location when appropriate.');
if(meta.description&&meta.description.length>50)s.localRelevance+=3;else addIssue(issues,'medium','local-relevance','Meta description is missing or thin','Meta description length: '+(meta.description||'').length+'.','A useful description can improve search-result clarity and click intent.','Add a natural description with service, location, and value proposition.');
if(meta.h1.length)s.localRelevance+=3;else addIssue(issues,'medium','local-relevance','H1 was not found','No H1 heading was detected.','A clear H1 helps users and crawlers understand page intent.','Add one clear H1 aligned with the service/location page.');
if(/service|services|products|menu|practice areas|solutions|repair|install|consult|estimate/i.test(meta.text))s.localRelevance+=2;else addIssue(issues,'opportunity','local-relevance','Service/product language is not obvious','The scan found limited service/product terms.','Local landing pages should make the offered services clear.','Add visible service or product descriptions that match the real business.');
if(meta.terms.length||extra.terms?.length)s.localRelevance+=3;else addIssue(issues,'opportunity','local-relevance','Local service-area language can be strengthened','Few local or service-area terms were detected.','Visible local context helps connect the page to the served market.','Add natural city, neighborhood, region, or service-area language where accurate.');
// Trust 10
if(meta.links.map.length||meta.links.profile.length||exp.googleBusinessProfileUrl)s.trust+=2;else addIssue(issues,'opportunity','trust','Map, directions, or GBP profile link was not found','No obvious map/profile/directions URL was detected.','Directions/profile links help users verify the real-world business.','Add a Google Maps/directions/profile link when appropriate.');
if(meta.links.social.length||sch.sameAs?.length)s.trust+=2;else addIssue(issues,'low','trust','Social/profile identity links are missing','No social/profile links or sameAs values were detected.','sameAs/profile links can help connect identity signals when accurate.','Add verified social/profile URLs to sameAs and visible footer/profile links.');
if(/review|reviews|testimonial|testimonials|rating|stars/i.test(meta.text))s.trust+=2;else addIssue(issues,'opportunity','trust','Review/testimonial signal was not detected','No obvious review/testimonial language was found.','Reviews and testimonials can support user trust, though they must follow platform policies.','Add honest testimonials or review links; do not fabricate review schema.');
if(meta.links.cta.length||/(call now|get a quote|schedule|book|appointment|contact us|request)/i.test(meta.text))s.trust+=2;else addIssue(issues,'opportunity','trust','Call-to-action is not obvious','No strong CTA/contact action was detected.','Local SEO pages should convert qualified visitors into calls, bookings, or inquiries.','Add a clear call, quote, schedule, or contact action.');
if(sch.logo||sch.image||/logo|gallery|photo|image|team/i.test(meta.text))s.trust+=2;else addIssue(issues,'low','trust','Business identity imagery is weak','No schema logo/image or obvious image identity signal was detected.','Logo and business imagery can help complete entity and conversion signals.','Add a logo/image in schema and visible brand imagery on the page.');
Object.keys(s).forEach(k=>s[k]=pct(s[k]));let overall=s.nap+s.schema+s.indexability+s.localRelevance+s.trust;return{scores:{...s,overall:pct(overall)},issues:issues.sort((a,b)=>sevOrder.indexOf(a.severity)-sevOrder.indexOf(b.severity))}}
function snippet(finalUrl,sch,exp){let type=(exp.primaryCategory&&localTypes.has(exp.primaryCategory.trim()))?exp.primaryCategory.trim():(sch.type&&localTypes.has(sch.type)?sch.type:'LocalBusiness');let obj={'@context':'https://schema.org','@type':type};obj.name=exp.name||sch.name||'REPLACE_WITH_BUSINESS_NAME';obj.url=sch.url||finalUrl;let phone=exp.telephone||sch.telephone;if(phone)obj.telephone=phone;let ad={};['streetAddress','addressLocality','addressRegion','postalCode','addressCountry'].forEach(k=>{let v=exp[k]||sch[k];if(v)ad[k]=v});if(Object.keys(ad).length){obj.address={'@type':'PostalAddress',...ad}}else obj.address={'@type':'PostalAddress','streetAddress':'REPLACE_WITH_STREET_ADDRESS','addressLocality':'REPLACE_WITH_CITY','addressRegion':'REPLACE_WITH_REGION','postalCode':'REPLACE_WITH_POSTAL_CODE','addressCountry':exp.addressCountry||'US'};if(exp.serviceArea)obj.areaServed=exp.serviceArea;if(exp.googleBusinessProfileUrl||sch.hasMap)obj.hasMap=exp.googleBusinessProfileUrl||sch.hasMap;let same=(exp.sameAs||'').split(/[\n,]+/).map(x=>x.trim()).filter(Boolean).concat(Array.isArray(sch.sameAs)?sch.sameAs:[]);if(same.length)obj.sameAs=uniq(same);return JSON.stringify(obj,null,2)}
async function pageSpeed(url,setStep){try{setStep('pagespeed');let api='https://www.googleapis.com/pagespeedonline/v5/runPagespeed?strategy=mobile&category=PERFORMANCE&category=ACCESSIBILITY&category=BEST_PRACTICES&category=SEO&url='+encodeURIComponent(url);let r=await fetchText(api,{timeout:18000});let j=JSON.parse(r.text),c=j.lighthouseResult?.categories||{};return{ok:true,performance:Math.round((c.performance?.score||0)*100),accessibility:Math.round((c.accessibility?.score||0)*100),bestPractices:Math.round((c['best-practices']?.score||0)*100),seo:Math.round((c.seo?.score||0)*100)}}catch(e){return{ok:false,error:e.message}}}
function setStep(x){S.step=x;S.status=({fetch:'Fetching page',parse:'Parsing metadata',schema:'Extracting schema',robots:'Checking robots.txt',sitemap:'Checking sitemap',crawl:'Scanning obvious internal pages',pagespeed:'Checking PageSpeed',score:'Building recommendations'}[x]||'Scanning');render(false)}
async function runAudit(){let input;try{input=normalizedUrl(S.request.url)}catch(e){alert(e.message);return}S.busy=true;S.result=null;S.tab='critical';setStep('fetch');try{let fetchRes=await fetchText(input);let finalUrl=fetchRes.url||input;setStep('parse');let doc=parseDoc(fetchRes.text),meta=pageBasics(doc,finalUrl,fetchRes.headers,S.request.expected);setStep('schema');let sd=parseJsonLd(doc),sch=schemaData(sd);let extraPages=likelyInternalPages(meta.links,finalUrl,S.request.scanDepth),extra=await crawlExtra(extraPages,setStep);if(extra.phones.length)meta.phones=uniq(meta.phones.concat(extra.phones)).slice(0,12);if(extra.addresses.length)meta.addresses=uniq(meta.addresses.concat(extra.addresses)).slice(0,12);let root=await rootChecks(finalUrl,setStep);let nap=analyzeNap(S.request.expected,sch,meta.visible);let ps=S.request.includePageSpeed?await pageSpeed(finalUrl,setStep):null;setStep('score');let out=scoreAndIssues({url:input,fetchRes,meta,sd,sch,nap,root,extra,ps});let res={id:'audit_'+Date.now(),createdAt:now(),inputUrl:input,finalUrl,status:'complete',fetch:{status:fetchRes.status,via:fetchRes.via},metadata:{title:meta.title,description:meta.description,h1:meta.h1,canonical:meta.canonical,robotsMeta:meta.robotsMeta,xRobots:meta.xRobots,https:meta.https},extracted:{phones:meta.phones,addresses:meta.addresses,localTerms:uniq(meta.terms.concat(extra.terms||[])),links:meta.links,extraPages:extra.fetched||[]},schema:{jsonLdBlocks:sd.blocks,parseErrors:sd.errors,types:sd.types,localBusinessCount:sd.localBusinessNodes.length,organizationCount:sd.organizationNodes.length,node:sch,rawNodes:sd.nodes.slice(0,8)},confidence:{entity:nap.confidence,schema:sd.localBusinessNodes.length?(out.scores.schema>23?'strong':'usable'):(sd.blocks?'weak':'missing')},scores:out.scores,issues:out.issues,pageSpeed:ps,robots:root.robots,sitemap:root.sitemap,generatedJsonLd:snippet(finalUrl,sch,S.request.expected)};if(ps&&!ps.ok)addIssue(res.issues,'low','trust','PageSpeed request did not complete',ps.error,'PageSpeed is optional and does not block the core Local SEO Lens audit.','Try again later or run PageSpeed Insights manually.','pagespeed','low');S.result=res;S.history=[{id:res.id,createdAt:res.createdAt,url:res.finalUrl,score:res.scores.overall,title:res.metadata.title,result:res},...S.history.filter(h=>h.url!==res.finalUrl)].slice(0,MAX_HIST);S.status='Complete';S.step='done';save()}catch(e){S.result={status:'failed',createdAt:now(),inputUrl:input||S.request.url,scores:{overall:0,nap:0,schema:0,indexability:0,localRelevance:0,trust:0},issues:[{severity:'critical',pillar:'fetch',title:'Audit failed before analysis',evidence:e.message,whyItMatters:'The tool needs readable public HTML to inspect local SEO signals.',howToFix:'Confirm the URL is public, not login-gated, and not blocking proxy fetches.',source:'fetch',confidence:'high'}],generatedJsonLd:''};S.status='Failed';S.step='done'}finally{S.busy=false;render()}}
function setDemo(){S.request.url='https://example-local-business.test/';S.result=demoResult();S.history=[{id:S.result.id,createdAt:S.result.createdAt,url:S.result.finalUrl,score:S.result.scores.overall,title:S.result.metadata.title,result:S.result},...S.history].slice(0,MAX_HIST);save();render()}
function demoResult(){let issues=[],scores={nap:18,schema:17,indexability:16,localRelevance:11,trust:6,overall:68};addIssue(issues,'high','schema','LocalBusiness schema is incomplete','Demo schema has name and URL but no PostalAddress.','Incomplete location schema gives search engines less precise local entity data.','Add PostalAddress, telephone, hours, and sameAs fields.');addIssue(issues,'medium','nap','Phone number is visible but not in schema','Detected phone on page only.','Schema and visible NAP should reinforce each other.','Add the same primary phone number to telephone.');addIssue(issues,'opportunity','trust','Add map and profile links','No map/profile links are present in the demo.','Verified profile links can strengthen identity clarity.','Add Google Maps/directions and verified sameAs URLs.');return{id:'demo_'+Date.now(),createdAt:now(),inputUrl:'https://example-local-business.test/',finalUrl:'https://example-local-business.test/',status:'demo',fetch:{status:200,via:'demo'},metadata:{title:'Example HVAC Repair in Raleigh | Example Local Co',description:'Example Local Co provides HVAC repair and installation in Raleigh, NC.',h1:['HVAC Repair in Raleigh'],canonical:'https://example-local-business.test/',robotsMeta:[],xRobots:'',https:true},extracted:{phones:['(919) 555-0188'],addresses:['123 Main Street Raleigh NC 27601'],localTerms:['Raleigh','NC','service area','estimate'],links:{contact:[{href:'#contact',text:'Contact'}],location:[],map:[],social:[],profile:[],cta:[{href:'#quote',text:'Request estimate'}]},extraPages:[]},schema:{jsonLdBlocks:1,parseErrors:[],types:['Organization','WebSite'],localBusinessCount:0,organizationCount:1,node:{type:'Organization',name:'Example Local Co',telephone:'',url:'https://example-local-business.test/',streetAddress:'',addressLocality:'',addressRegion:'',postalCode:'',sameAs:[]},rawNodes:[{'@context':'https://schema.org','@type':'Organization',name:'Example Local Co',url:'https://example-local-business.test/'}]},confidence:{entity:'likely',schema:'weak'},scores,issues,pageSpeed:null,robots:{available:true,blocking:false,status:200,sitemaps:[]},sitemap:{available:false,status:'unavailable',url:'/sitemap.xml'},generatedJsonLd:JSON.stringify({'@context':'https://schema.org','@type':'HVACBusiness',name:'Example Local Co',url:'https://example-local-business.test/',telephone:'(919) 555-0188',address:{'@type':'PostalAddress',streetAddress:'123 Main Street',addressLocality:'Raleigh',addressRegion:'NC',postalCode:'27601',addressCountry:'US'}},null,2)}}
function scoreLabel(n){return n>=85?'Strong':n>=70?'Usable':n>=50?'Needs work':'High risk'}
function severityCounts(r){let o={critical:0,high:0,medium:0,low:0,opportunity:0};(r?.issues||[]).forEach(i=>o[i.severity]=(o[i.severity]||0)+1);return o}
function statusHtml(){let steps=[['fetch','Fetch'],['parse','Metadata'],['schema','Schema'],['robots','Robots'],['crawl','NAP scan'],['score','Report']];return `<section class="sec"><div class="status">${steps.map(([id,label])=>`<div class="step ${S.step===id?'active':''} ${S.step==='done'||steps.findIndex(x=>x[0]===S.step)>steps.findIndex(x=>x[0]===id)?'done':''}"><b>${label}</b>${esc(S.step===id?S.status:'Ready')}<div class="scanline"><i></i></div></div>`).join('')}</div></section>`}
function inputHtml(){let e=S.request.expected;return `<section class="hero"><div class="pad"><div class="ey">Local SEO Lens</div><div class="ttl">Local search diagnostics with evidence, not vibes.</div><div class="sub">Audit a public page for LocalBusiness schema, NAP consistency, indexability blockers, and local on-page signals. Free scan uses public website signals only -- no Google Business Profile, Search Console, Maps, or paid citation database connection.</div><div class="urlbar sticky"><input data-p="request.url" placeholder="https://www.examplebusiness.com/location-page" value="${attr(S.request.url)}"><button class="p" data-a="run">${S.busy?'Scanning...':'Run Local SEO Audit'}</button><button data-a="demo">Demo</button></div><div class="row" style="margin-top:12px"><label class="switch"><input type="checkbox" data-check="request.includePageSpeed" ${S.request.includePageSpeed?'checked':''}> Optional PageSpeed</label><label class="switch">Scan depth <select data-p="request.scanDepth" style="width:auto;min-height:28px;padding:3px 8px"><option value="single" ${S.request.scanDepth==='single'?'selected':''}>single page</option><option value="basic" ${S.request.scanDepth==='basic'?'selected':''}>basic</option><option value="deeper" ${S.request.scanDepth==='deeper'?'selected':''}>deeper</option></select></label><button class="mini" data-a="toggleAdvanced">${S.showAdvanced?'Hide':'Expected business info'}</button></div></div><div class="pad dark"><div class="progressRing" style="background:conic-gradient(${S.result?colorFor(S.result.scores.overall):'#5b8f68'} ${(S.result?S.result.scores.overall:0)*3.6}deg,#243028 0deg)"><div>${S.result?S.result.scores.overall:'--'}</div></div><h2>${S.result?scoreLabel(S.result.scores.overall):'Ready to scan'}</h2><p class="sub">${S.result?topLine(S.result):'Enter a public URL. Add expected business data when you want NAP verification instead of detected-only confidence.'}</p><div class="k"><div><span>Schema</span><b>${S.result?esc(S.result.confidence.schema):'--'}</b></div><div><span>NAP</span><b>${S.result?esc(S.result.confidence.entity):'--'}</b></div><div><span>Fetch</span><b>${S.result?esc(S.result.fetch?.via||S.result.status):'--'}</b></div><div><span>Issues</span><b>${S.result?(S.result.issues||[]).length:'--'}</b></div></div></div></section>${S.showAdvanced?advancedHtml(e):''}`}
function advancedHtml(e){return `<section class="sec"><div class="head"><h2>Expected business information</h2><span class="mut">Optional, used for NAP verification</span></div><div class="g4">${inp('Business name','request.expected.name',e.name)}${inp('Phone','request.expected.telephone',e.telephone)}${inp('Primary category','request.expected.primaryCategory',e.primaryCategory,'Restaurant, Dentist, HVACBusiness...')}${inp('Country','request.expected.addressCountry',e.addressCountry)}</div><div class="g4">${inp('Street address','request.expected.streetAddress',e.streetAddress)}${inp('City','request.expected.addressLocality',e.addressLocality)}${inp('State/region','request.expected.addressRegion',e.addressRegion)}${inp('Postal code','request.expected.postalCode',e.postalCode)}</div><div class="g3" style="padding:0 12px 12px">${inp('Service area','request.expected.serviceArea',e.serviceArea)}${inp('Google Business Profile / map URL','request.expected.googleBusinessProfileUrl',e.googleBusinessProfileUrl)}<div><label>Social/profile URLs</label><textarea data-p="request.expected.sameAs" placeholder="One per line or comma separated">${esc(e.sameAs)}</textarea></div></div></section>`}
function inp(label,path,value,ph=''){return `<div><label>${label}</label><input data-p="${path}" value="${attr(value)}" placeholder="${attr(ph)}"></div>`}
function colorFor(n){return n>=80?'#5b8f68':n>=60?'#b78a34':'#a95151'}
function topLine(r){let c=severityCounts(r),top=(r.issues||[])[0];return `${r.finalUrl} scored ${r.scores.overall}/100. ${c.critical} critical, ${c.high} high, ${c.medium} medium. Top issue: ${top?top.title:'No major issue found'}.`}
function metricsHtml(r){return `<section class="sec"><div class="head"><h2>Executive scorecard</h2><span class="mut">${new Date(r.createdAt).toLocaleString()}</span></div><div class="metrics"><div class="m ${r.scores.overall>=75?'g':r.scores.overall<50?'r':'a'}"><span>Readiness</span><b>${r.scores.overall}/100</b><small>${scoreLabel(r.scores.overall)}</small></div><div class="m"><span>Schema status</span><b>${esc(r.confidence.schema)}</b><small>${r.schema.localBusinessCount} LocalBusiness node(s)</small></div><div class="m v"><span>NAP confidence</span><b>${esc(r.confidence.entity)}</b><small>${r.extracted.phones.length} phone · ${r.extracted.addresses.length} address</small></div><div class="m ${r.issues.some(i=>i.severity==='critical')?'r':'g'}"><span>Indexability</span><b>${r.scores.indexability}/20</b><small>${r.metadata.robotsMeta.join(', ')||'no noindex seen'}</small></div><div class="m a"><span>Top fix</span><b>${esc((r.issues[0]||{}).severity||'pass')}</b><small>${esc((r.issues[0]||{}).pillar||'No major issues')}</small></div></div></section>`}
function pillarsHtml(r){let ps=[['nap','Local entity / NAP',25],['schema','LocalBusiness schema',30],['indexability','Crawl / indexability',20],['localRelevance','On-page local',15],['trust','Trust / enrichment',10]];return `<section class="sec"><div class="head"><h2>Signal pillars</h2><span class="mut">Score is decomposed so the evidence stays visible</span></div><div class="pillars">${ps.map(([k,l,max])=>{let raw=r.scores[k],per=Math.round(raw/max*100);return `<div class="pillar"><b>${l}</b><div class="bar"><i style="width:${pct(per)}%"></i></div><p class="mut">${raw}/${max} points</p></div>`}).join('')}</div></section>`}
function issuesHtml(r){let counts=severityCounts(r),list=(r.issues||[]).filter(i=>i.severity===S.tab);if(!list.length&&S.tab!=='all')list=[];return `<section class="sec"><div class="head"><h2>Prioritized issue board</h2><div class="row"><button class="mini" data-a="summary">Copy summary</button><button class="mini" data-a="csv">Export issues CSV</button></div></div><div class="tabs">${sevOrder.map(s=>`<button data-tab="${s}" class="${S.tab===s?'on':''}">${label(s)} (${counts[s]||0})</button>`).join('')}</div><div style="padding:0 12px 12px">${list.length?list.map(issueCard).join(''):`<div class="empty">No ${label(S.tab).toLowerCase()} issues in this audit.</div>`}</div></section>`}
function label(s){return s.charAt(0).toUpperCase()+s.slice(1)}
function issueCard(i){return `<div class="issue"><h3><span class="badge ${i.severity}">${label(i.severity)}</span>${esc(i.title)}</h3><p><b>Evidence:</b> ${esc(i.evidence)}</p><p><b>Why it matters:</b> ${esc(i.whyItMatters)}</p><p><b>How to fix:</b> ${esc(i.howToFix)}</p><div class="source">Source: ${esc(i.source)} · Confidence: ${esc(i.confidence)} · Pillar: ${esc(i.pillar)}</div></div>`}
function schemaHtml(r){let n=r.schema.node||{};return `<section class="sec"><div class="head"><h2>Structured data inspector</h2><div class="row"><a target="_blank" rel="noopener" href="https://search.google.com/test/rich-results?url=${encodeURIComponent(r.finalUrl)}"><button class="mini">Rich Results Test</button></a><a target="_blank" rel="noopener" href="https://validator.schema.org/#url=${encodeURIComponent(r.finalUrl)}"><button class="mini">Schema Validator</button></a></div></div><div class="schemaGrid"><div class="card"><div class="inspectorTitle"><h3>Detected schema types</h3><span class="badge ${r.schema.localBusinessCount?'pass':'high'}">${r.schema.localBusinessCount?'Local schema':'Missing local'}</span></div><div class="typeList">${(r.schema.types.length?r.schema.types:['none']).map(t=>`<span>${esc(t)}</span>`).join('')}</div><hr><p><b>JSON-LD blocks:</b> ${r.schema.jsonLdBlocks} · <b>Parse errors:</b> ${r.schema.parseErrors.length}</p><p><b>Primary node:</b> ${esc(n.type||'none')} · ${esc(n.name||'No name')}</p>${r.schema.parseErrors.length?`<div class="notice">${r.schema.parseErrors.map(e=>`Block ${e.block}: ${esc(e.message)}`).join('<br>')}</div>`:''}<h3 style="margin-top:14px">Raw node preview</h3><pre class="tree">${esc(JSON.stringify(r.schema.rawNodes||[],null,2))}</pre></div><div class="card"><div class="inspectorTitle"><h3>Generated starter JSON-LD</h3><button class="mini" data-copy="jsonld">Copy</button></div><div class="notice">Verify every value before publishing. This snippet is a starting point, not an automatic CMS update.</div><pre class="codebox" id="jsonldBox">${esc(r.generatedJsonLd)}</pre>${S.copied==='jsonld'?'<div class="copyok">Copied JSON-LD starter snippet.</div>':''}</div></div></section>`}
function napHtml(r){let e=S.request.expected,n=r.schema.node||{},v={name:r.extracted?.visibleName||r.metadata.h1?.[0]||r.metadata.title,telephone:r.extracted.phones[0]||'',streetAddress:r.extracted.addresses[0]||'',addressLocality:e.addressLocality,addressRegion:e.addressRegion,postalCode:e.postalCode};let rows=['name','telephone','streetAddress','addressLocality','addressRegion','postalCode'];return `<section class="sec"><div class="head"><h2>NAP comparison</h2><span class="mut">Expected vs schema vs visible page</span></div><div class="split"><div class="card"><table class="napTable"><tr><th>Field</th><th>Expected</th><th>Schema</th><th>Visible</th><th>Status</th></tr>${rows.map(k=>{let kind=k==='telephone'?'phone':k==='streetAddress'?'addr':'text',ok=e[k]?matchStr(e[k],n[k],kind)||matchStr(e[k],v[k],kind):(n[k]||v[k]);return `<tr><td>${esc(k)}</td><td>${esc(e[k]||'--')}</td><td>${esc(n[k]||'--')}</td><td>${esc(v[k]||'--')}</td><td><span class="badge ${ok?'pass':'medium'}">${ok?'match/found':'missing'}</span></td></tr>`}).join('')}</table></div><div class="card"><h3>Detected local evidence</h3><p><b>Phones:</b> ${esc((r.extracted.phones||[]).join(', ')||'none')}</p><p><b>Addresses:</b> ${esc((r.extracted.addresses||[]).join(' | ')||'none')}</p><p><b>Local terms:</b> ${esc((r.extracted.localTerms||[]).join(', ')||'none')}</p><p><b>Extra pages:</b> ${esc((r.extracted.extraPages||[]).map(x=>x.title||x.url).join(' | ')||'none')}</p></div></div></section>`}
function validationHtml(r){let ps=r.pageSpeed;return `<section class="sec"><div class="head"><h2>Official validation and fetch details</h2><span class="mut">Manual verification is still required</span></div><div class="g3" style="padding:12px"><div class="card linklist"><h3>Validators</h3><a target="_blank" rel="noopener" href="https://search.google.com/test/rich-results?url=${encodeURIComponent(r.finalUrl)}">Open Google Rich Results Test</a><a target="_blank" rel="noopener" href="https://validator.schema.org/#url=${encodeURIComponent(r.finalUrl)}">Open Schema.org Validator</a><a target="_blank" rel="noopener" href="https://pagespeed.web.dev/analysis?url=${encodeURIComponent(r.finalUrl)}">Open PageSpeed Insights</a></div><div class="card"><h3>Robots / sitemap</h3><p><b>robots.txt:</b> ${esc(r.robots.status)} ${r.robots.blocking?'<span class="badge critical">blocking?</span>':''}</p><p><b>Sitemap:</b> ${esc(r.sitemap.status)} ${r.sitemap.available?'<span class="badge pass">found</span>':'<span class="badge low">weak</span>'}</p><p class="mut">${esc(r.sitemap.url||'')}</p></div><div class="card"><h3>PageSpeed</h3>${ps?ps.ok?`<p><b>Performance:</b> ${ps.performance} · <b>Accessibility:</b> ${ps.accessibility}</p><p><b>Best practices:</b> ${ps.bestPractices} · <b>SEO:</b> ${ps.seo}</p>`:`<p>${esc(ps.error)}</p>`:'<p class="mut">Optional PageSpeed check was off for this scan.</p>'}</div></div></section>`}
function historyHtml(){return `<section class="sec"><div class="head"><h2>Audit history</h2><span class="mut">Saved per user with Vibes SDK</span></div><div style="padding:12px" class="history">${S.history.length?S.history.map((h,i)=>`<div class="hist"><div><b>${esc(h.title||h.url)}</b><small>${esc(h.url)} · ${new Date(h.createdAt).toLocaleString()} · score ${h.score}</small></div><div class="row"><button class="mini" data-loadhist="${i}">Open</button><button class="mini" data-rescan="${i}">Rescan</button></div></div>`).join(''):'<div class="empty">No saved audits yet.</div>'}</div></section>`}
function summaryText(r){let counts=severityCounts(r);let top=(r.issues||[]).slice(0,5).map((i,idx)=>`${idx+1}. [${i.severity.toUpperCase()}] ${i.title} -- ${i.howToFix}`).join('\n');return `Local SEO Lens Audit\nURL: ${r.finalUrl}\nScore: ${r.scores.overall}/100 (${scoreLabel(r.scores.overall)})\nSchema: ${r.confidence.schema}\nNAP confidence: ${r.confidence.entity}\nIssues: ${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low, ${counts.opportunity} opportunities\n\nTop fixes:\n${top||'No major issues found.'}\n\nDisclaimer: Educational SEO diagnostics only. Verify with Google Search Console, Rich Results Test, Schema.org Validator, CMS, and a qualified SEO professional.`}
function exportCsv(r){let lines=['severity,pillar,title,evidence,how_to_fix'].concat((r.issues||[]).map(i=>[i.severity,i.pillar,i.title,i.evidence,i.howToFix].map(csv).join(',')));download('local-seo-lens-issues.csv',lines.join('\n'),'text/csv')}
function csv(v){return '"'+String(v||'').replace(/"/g,'""')+'"'}
function download(name,text,type='text/plain'){let u=URL.createObjectURL(new Blob([text],{type})),a=document.createElement('a');a.href=u;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(u),1000)}
async function copy(text,id){try{await navigator.clipboard.writeText(text);S.copied=id;render(false);setTimeout(()=>{S.copied='';render(false)},1800)}catch(e){download(id+'.txt',text)}}
function bind(){ $$('[data-p]').forEach(el=>{el.onchange=el.oninput?.name==='textarea'?null:undefined;el.addEventListener('change',()=>setPath(el.dataset.p,el.value))}); $$('textarea[data-p]').forEach(el=>el.addEventListener('input',()=>setPath(el.dataset.p,el.value))); $$('[data-check]').forEach(el=>el.addEventListener('change',()=>setPath(el.dataset.check,el.checked))); $$('[data-a]').forEach(b=>b.onclick=()=>act(b.dataset.a)); $$('[data-tab]').forEach(b=>b.onclick=()=>{S.tab=b.dataset.tab;render(false)}); $$('[data-copy]').forEach(b=>b.onclick=()=>{if(b.dataset.copy==='jsonld')copy($('#jsonldBox')?.textContent||'', 'jsonld')}); $$('[data-loadhist]').forEach(b=>b.onclick=()=>{let h=S.history[+b.dataset.loadhist];if(h?.result){S.result=h.result;render()}}); $$('[data-rescan]').forEach(b=>b.onclick=()=>{let h=S.history[+b.dataset.rescan];if(h){S.request.url=h.url;runAudit()}})}
function act(a){if(a==='run'&&!S.busy)runAudit();if(a==='demo')setDemo();if(a==='toggleAdvanced'){S.showAdvanced=!S.showAdvanced;render(false)}if(a==='summary'&&S.result)copy(summaryText(S.result),'summary');if(a==='csv'&&S.result)exportCsv(S.result)}
function render(doBind=true){let r=S.result;$('#app').innerHTML=`<div class="w">${inputHtml()}${(S.busy||S.step!=='idle')?statusHtml():''}${r?metricsHtml(r)+pillarsHtml(r)+issuesHtml(r)+schemaHtml(r)+napHtml(r)+validationHtml(r):`<section class="sec"><div class="empty"><b>What this checks</b><br>LocalBusiness JSON-LD, visible NAP, robots/noindex/canonical, local page basics, map/social/review/CTA signals, and a safe starter schema snippet.</div></section>`}${historyHtml()}<div class="foot">This tool is for educational SEO diagnostics only. It does not guarantee rankings, indexing, map-pack visibility, rich results, traffic, leads, or Google Business Profile changes. Search engines decide how and whether to use structured data. Users should verify recommendations with Google Search Console, Google's Rich Results Test, Schema.org Validator, their CMS, and a qualified SEO professional when needed.</div></div>`;if(doBind)bind();else bind()}
V().onReady(async()=>{try{let x=await V().load(KEY);if(x){S.request={...S.request,...(x.request||{})};S.history=x.history||[]}}catch(e){}render()});
})();
</script></body></html>
```
[REQUESTED CHANGES]
(no specific request — apply your best judgment)
--- HOW TO RESPOND (READ FIRST) ---
Before writing any code, follow this exact process:
1. **ANALYZE** the widget source code provided above and identify:
a. Which Vibes SDK features it already uses (vibes.save, vibes.load, vibes.shared.join, etc.)
b. Which SDK features would genuinely benefit THIS specific widget — tailored to what it does, not a dump of everything available.
2. **PRESENT A NUMBERED LIST** covering:
- SDK features currently active in this widget
- New SDK features that would concretely improve this widget (be specific: why this widget, what it enables)
3. **WAIT** — do not write any code yet. Reply with your analysis and numbered list, then stop and ask the user which numbered items they want.
4. **IMPLEMENT ONLY** the items the user confirms, plus any explicit change they requested. Do not add unrequested features.
**IMPORTANT — Shared state room names:**
If you add vibes.shared.join(), do NOT use a hardcoded string literal as the room name (e.g. vibes.shared.join("lobby")) unless the user explicitly wants ALL viewers to share one single global state. A hardcoded room name means every person who visits this widget reads and writes the same shared state — it is a global room. For per-user or per-session isolation, derive the room name from a variable (e.g. a user ID, session token, or random value). When in doubt, use vibes.save/vibes.load for per-user persistence instead.
--- VIBES SDK CONTEXT ---
## Vibes SDK Reference
You are building an HTML widget for It Just Vibes (itjustvibes.com). The Vibes SDK is auto-injected — do NOT add a script tag. Just use `window.vibes` (or just `vibes`).
### Setup
Wrap your startup code in `vibes.onReady`:
```js
vibes.onReady(async () => {
const saved = await vibes.load("myKey");
// your widget logic here
});
```
### State (Per-User Persistence)
Every user gets their own isolated state per widget. All methods return Promises.
| Method | Description |
|--------|-------------|
| `await vibes.save(key, value)` | Save JSON-serializable data |
| `await vibes.load(key)` | Load saved data (returns `null` if not found) |
| `await vibes.delete(key)` | Delete a saved key |
| `await vibes.listKeys()` | Get array of all saved key names |
**Key rules:**
- Keys are strings, max 64 characters, alphanumeric + dashes/underscores
- Values must be JSON-serializable (objects, arrays, strings, numbers, booleans)
- Max 100KB per value, 500KB per widget per user, 5MB per user total
- Max 100 keys per widget per user
### Fetch Proxy
Use direct `fetch()` for APIs with permissive CORS headers (`Access-Control-Allow-Origin: *`). Use `vibes.fetch` for APIs without CORS headers (the proxy handles cross-origin requests):
```js
const resp = await vibes.fetch("https://api.example.com/data", {
method: "GET", // GET, POST, PUT, DELETE
headers: {}, // optional headers
body: null, // optional body (string)
timeout: null // optional timeout in ms
});
const data = await resp.json(); // or resp.text()
console.log(resp.status, resp.ok);
```
### Multiplayer (Shared State)
Real-time shared state across all users viewing the same widget.
```js
// Join a room (call once at startup)
await vibes.shared.join("lobby", { persistent: true });
// Set shared state (broadcasts to all users)
await vibes.shared.set("score", { player1: 10, player2: 7 });
// Read shared state (synchronous, returns last known value)
const score = vibes.shared.get("score");
// Listen for changes to a specific key
vibes.shared.onChange("score", (newValue) => {
console.log("Score updated:", newValue);
});
// Listen for any shared state change
vibes.shared.onAny((key, value) => {
console.log(key, "changed to", value);
});
// Get number of connected users
const count = await vibes.shared.getUserCount();
// Leave room
vibes.shared.leave();
// Clear all shared state for this room
await vibes.shared.clear();
```
**Shared state options:**
- `{ persistent: true }` — state survives page reloads (stored server-side)
- Default room name is "__default__" if omitted
### Agent-Accessible State (vibes.ai.*)
State written with the standard `vibes.save` / `vibes.load` methods is private to each user and is **NOT readable by AI agents**. To share state with an AI agent (via the MCP connector), use the `vibes.ai` sub-namespace:
| Method | Description |
|--------|-------------|
| `await vibes.ai.setState(key, value)` | Write agent-readable state. Key is stored internally as `ai/<key>`. |
| `await vibes.ai.getState(key)` | Read agent-readable state. Returns `null` if key absent. |
| `await vibes.ai.listKeys()` | List all agent-readable keys (without the `ai/` prefix). |
**Important rules:**
- Regular `vibes.save()` / `vibes.load()` is **NOT agent-accessible** — use `vibes.ai.*` for state you want agents to read.
- The widget **owner** must enable agent access in **Manage → Agent tab** before any agent can read or write `vibes.ai.*` state.
- Keys are auto-prefixed to `ai/` internally; you supply just the short key (e.g. `'context'`).
```js
vibes.onReady(async () => {
// Write state an AI agent can later read
await vibes.ai.setState('context', { currentLevel: 3, score: 1500 });
// Read it back (same auto-prefix applies)
const ctx = await vibes.ai.getState('context');
// List all agent-accessible keys for this widget
const keys = await vibes.ai.listKeys(); // e.g. ['context']
});
```
### Rules
1. **Do NOT use localStorage, sessionStorage, or window.storage** — they are blocked or undefined in the sandbox. Use `vibes.save`/`vibes.load` instead.
2. **Do NOT add a script tag to import the SDK** — it is auto-injected.
3. **Wrap startup code in `vibes.onReady()`** — the SDK may not be ready immediately.
4. **Await all SDK calls** — every method (except `vibes.shared.get`) returns a Promise.
5. **External JS libraries are allowed and encouraged** — load them via a `<script src>` tag from a reputable CDN (jsDelivr, unpkg, or cdnjs) with a pinned version, or inline the library source. The only exception: do NOT add a script tag for the Vibes SDK itself — it is auto-injected (see Rule 2).
6. **Keep total code under 100KB** — that is the default widget size limit (your account limit may be higher).
### Rate Limits
| Operation | Limit |
|-----------|-------|
| Writes (save + delete combined) | 30/min |
| Reads (load) | 60/min |
| List keys | 30/min |
| Fetch proxy | Rate limited per widget |
### Error Handling
All SDK methods can reject. Wrap in try/catch:
```js
try {
await vibes.save("key", value);
} catch (err) {
console.error("Save failed:", err.message);
}
```
Fetch proxy errors include `err.code`: `"RATE_LIMITED"`, `"BLOCKED"`, `"FETCH_ERROR"`.
### Data Export
Export rows of data as CSV or Excel directly from your widget.
```js
// Register dataset for the platform's "Export data" button
// AND optionally trigger an immediate download
vibes.exportData(rows, { filename: 'results.csv' })
// rows: Array of arrays (each inner array is one row; first row = headers)
// options.filename: sets the download filename and format (csv or xlsx)
// options.directDownload: false to only register without downloading (default: true)
```
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `filename` | string | `'data.csv'` | Download filename; extension determines format (`.csv` or `.xlsx`) |
| `directDownload` | boolean | `true` | Trigger immediate browser download in addition to registering the dataset |
**Notes:**
- First call registers the dataset so the widget chrome shows an "Export data" button
- Use `.csv` extension for CSV, `.xlsx` for Excel
- CSV injection safety is automatic (formula-starting cells prefixed with `'`)
- Data stays in the browser — no server round-trip
--- FORK & RESUBMIT INSTRUCTION ---
Return the complete, self-contained HTML file with all changes applied.
It should be ready to paste into itjustvibes.com/submit to create a new fork.
Include this comment at the top of the output:
/* Forked from: https://itjustvibes.com/Owner/local-seo-lens-v2 */