0
TOOLS#b842586a
Local SEO Lens
@Owner·deposited 2w ago·updated 2w ago·57 views
TOOLS#b842586a
Local SEO Lens
OW
@Owner
57Views
0Comments
0Forks
0Saves
SHARE · REMIX
Local SEO Lens — a HTML tools widget by @Owner.
CONTROLS
local SEOschemaauditNAPSEO
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" by @Owner]
Source: https://itjustvibes.com/Owner/local-seo-lens
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>body{margin:0;background:radial-gradient(circle at top left,#28372f,#111814 40%,#070b09);font-family:system-ui;color:#18201c}#app{max-width:1180px;margin:auto;padding:14px}.hero,.sec{background:#f7efe2;border-radius:24px;box-shadow:0 18px 50px #0006;margin:0 0 14px;overflow:hidden}.hero{display:grid;grid-template-columns:1.2fr .8fr}.pad{padding:22px}.darkpad{padding:22px;background:#101512;color:#fff4df}.ttl{font-size:clamp(34px,5vw,58px);line-height:.92;letter-spacing:-.06em;font-weight:950}.sub,.mut{color:#58645d;line-height:1.5}.darkpad .sub{color:#c9d1cb}.url{display:grid;grid-template-columns:1fr auto auto;gap:10px;margin-top:18px}input{min-height:38px;border:1px solid #18201c22;border-radius:12px;padding:8px;font:inherit}button{border:1px solid #18201c22;border-radius:999px;background:#fffaf0;padding:10px 13px;font-weight:850}.p{background:#17201b;color:#fff4df}.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;padding:12px}.g2{display:grid;grid-template-columns:1fr 1fr;gap:12px;padding:12px}.m,.card,.issue{background:#fffaf0;border:1px solid #18201c22;border-radius:18px;padding:13px}.m{border-left:5px solid #497b9b}.good{border-left-color:#5b8f68}.bad{border-left-color:#a95151}.m b{display:block;font-size:24px}.head{padding:15px 18px;border-bottom:1px solid #18201c22;display:flex;justify-content:space-between}.issue{margin:0 12px 10px}.badge{font-size:11px;border-radius:999px;padding:4px 8px;font-weight:950;text-transform:uppercase}.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}.code{max-height:260px;overflow:auto;background:#151c18;color:#f8efdd;border-radius:16px;padding:12px;font:12px monospace;white-space:pre-wrap}.ring{width:118px;height:118px;border-radius:50%;display:grid;place-items:center;margin:0 auto 12px;background:conic-gradient(#5b8f68 0deg,#243028 0)}.ring div{width:90px;height:90px;border-radius:50%;background:#101512;display:grid;place-items:center;color:#fff4df;font-size:28px;font-weight:950}@media(max-width:760px){.hero,.g2{grid-template-columns:1fr}.grid{grid-template-columns:1fr 1fr}.url{grid-template-columns:1fr}}@media(max-width:520px){.grid{grid-template-columns:1fr}.ttl{font-size:38px}}</style></head><body><div id=app></div><script>(function(){var K='local_seo_lens_tiny_fixed';function V(){if(!window.vibes)window.vibes={save:function(){return Promise.resolve()},load:function(){return Promise.resolve(null)},fetch:function(u,o){return fetch(u,o)}};return window.vibes}function Q(s){return document.querySelector(s)}function E(x){return String(x||'').replace(/[&<>]/g,function(c){return {'&':'&','<':'<','>':'>'}[c]})}function U(a){var o=[],m={};(a||[]).forEach(function(x){x=String(x||'').trim();if(x&&!m[x]){m[x]=1;o.push(x)}});return o}function GU(u){u=(u||'').trim();if(u.slice(0,4).toLowerCase()!='http')u='https://'+u;var x=new URL(u);x.hash='';return x.href}async function F(u){try{var r=await fetch(u,{redirect:'follow'}),t=await r.text();return {ok:r.ok,st:r.status,url:r.url||u,txt:t,via:'direct'}}catch(e){var r2=await V().fetch(u,{method:'GET'}),t2=typeof r2=='string'?r2:await r2.text();return {ok:r2.ok!==false,st:r2.status||200,url:r2.url||u,txt:t2,via:'vibes.fetch'}}}function text(d){var c=d.body?d.body.cloneNode(1):d;[].slice.call(c.querySelectorAll('script,style,noscript,svg')).forEach(function(n){n.remove()});return (c.textContent||'').replace(/ +/g,' ').trim()}function phones(t){return U(t.match(/(?:[+]1[ .-]?)?(?:[(]?[0-9]{3}[)]?[ .-]?)?[0-9]{3}[ .-]?[0-9]{4}/g)||[]).slice(0,8)}function addrs(t){var r=/[0-9]{1,6} (?:[A-Za-z0-9.#-]+ ){1,6}(?:Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Drive|Dr|Lane|Ln|Way|Court|Ct|Place|Pl)(?: [A-Za-z .-]{2,35})?(?: [A-Z]{2})?(?: [0-9]{5})?/gi;return U((t.match(r)||[]).map(function(x){return x.replace(/ +/g,' ').replace(/[.,;:]+$/,'').trim()})).slice(0,8)}function typ(n){var t=n&&n['@type'];return Array.isArray(t)?t.map(String):(t?[String(t)]:[])}function walk(x,a){a=a||[];if(!x)return a;if(Array.isArray(x)){x.forEach(function(v){walk(v,a)});return a}if(typeof x=='object'){if(x['@type']||x['@graph'])a.push(x);if(Array.isArray(x['@graph']))x['@graph'].forEach(function(v){walk(v,a)});Object.keys(x).forEach(function(k){if(typeof x[k]=='object')walk(x[k],a)})}return a}function schema(d){var nodes=[],errs=[],blocks=[].slice.call(d.querySelectorAll('script[type*=ld][type*=json]'));blocks.forEach(function(s,i){var raw=(s.textContent||'').trim();if(raw.indexOf('<!--')==0)raw=raw.slice(4);if(raw.lastIndexOf('-->')==raw.length-3)raw=raw.slice(0,-3);raw=raw.replace(/"/g,'QMARK').replace(/&/g,'&').split(',}').join('}').split(',]').join(']').replace(/QMARK/g,String.fromCharCode(34));if(!raw)return;try{walk(JSON.parse(raw),nodes)}catch(e){errs.push('Block '+(i+1)+': '+e.message)}});var types=U([].concat.apply([],nodes.map(typ))),local=nodes.filter(function(n){return typ(n).some(function(t){return t=='LocalBusiness'||/Business$/.test(t)||['Restaurant','Dentist','Store','ProfessionalService','Attorney','Plumber','Electrician'].indexOf(t)>-1})}),org=nodes.filter(function(n){return typ(n).indexOf('Organization')>-1});return {blocks:blocks.length,nodes:nodes,errs:errs,types:types,local:local,org:org}}function val(v){if(Array.isArray(v))return v.map(val).filter(Boolean).join(', ');return v&&typeof v=='object'?(v.name||v.url||v['@id']||''):(v||'')}function primary(j){var n=j.local[0]||j.org[0]||{},a=typeof n.address=='string'?{streetAddress:n.address}:n.address||{};return {name:val(n.name),phone:val(n.telephone||n.phone),url:val(n.url),street:val(a.streetAddress),city:val(a.addressLocality),region:val(a.addressRegion),postal:val(a.postalCode),hours:n.openingHoursSpecification||n.openingHours,same:n.sameAs||[],map:val(n.hasMap)}}function links(d){var L={contact:0,map:0,social:0,cta:0};[].slice.call(d.querySelectorAll('a[href]')).forEach(function(a){var z=(a.href+' '+a.textContent).toLowerCase();if(/contact|location|directions|visit/.test(z))L.contact++;if(z.indexOf('google.com/maps')>-1||z.indexOf('maps.app.goo')>-1||z.indexOf('directions')>-1)L.map++;if(/facebook|instagram|linkedin|youtube|yelp|bbb/.test(z))L.social++;if(/call|quote|schedule|book|appointment|request/.test(z))L.cta++});return L}function add(A,se,p,t,e,f){A.push({se:se,p:p,t:t,e:e,f:f})}async function root(u){var o=new URL(u).origin,r={block:0,robots:'unchecked',sitemap:'unchecked'};try{var a=await F(o+'/robots.txt');r.robots=a.st;var low=a.txt.toLowerCase();r.block=low.indexOf('user-agent: *')>-1&&low.indexOf('disallow: /')>-1}catch(e){}try{var b=await F(o+'/sitemap.xml');r.sitemap=b.st}catch(e){}return r}function starter(url,D){return JSON.stringify({'@context':'https://schema.org','@type':'LocalBusiness',name:D.name||'REPLACE_WITH_BUSINESS_NAME',url:D.url||url,telephone:D.phone||'REPLACE_WITH_PHONE',address:{'@type':'PostalAddress',streetAddress:D.street||'REPLACE_WITH_STREET',addressLocality:D.city||'REPLACE_WITH_CITY',addressRegion:D.region||'REPLACE_WITH_STATE',postalCode:D.postal||'REPLACE_WITH_POSTAL',addressCountry:'US'}},null,2)}function score(R){var d=R.d,T=R.T,J=R.J,D=R.D,L=R.L,ph=R.ph,ad=R.ad,rt=R.rt,title=(d.title||'').trim(),desc=(d.querySelector('meta[name=description]')||{}).content||'',h1=[].slice.call(d.querySelectorAll('h1')).map(function(x){return x.textContent.trim()}).filter(Boolean),I=[],s={nap:0,schema:0,index:0,page:0,trust:0};if(D.name||h1[0]||title)s.nap+=5;else add(I,'critical','NAP','Business name missing','No visible or schema name found.','Add the business name visibly and in schema.');if(ph.length||D.phone)s.nap+=4;else add(I,'medium','NAP','Phone missing','No phone detected.','Add primary phone visibly and in schema.');if(ad.length||D.street)s.nap+=5;else add(I,'high','NAP','Address missing','No address detected.','Add address or service-area wording.');if(/near me|serving|service area|locally owned|directions/i.test(T)||D.city||D.region)s.nap+=4;else add(I,'medium','NAP','Local language weak','Few city/service-area terms found.','Mention served city or service area.');if(L.contact)s.nap+=7;else add(I,'medium','NAP','Contact/location link weak','No obvious contact/location link.','Add Contact, Location, or Directions link.');if(J.blocks&&!J.errs.length)s.schema+=4;else add(I,J.errs.length?'critical':'high','Schema',J.errs.length?'JSON-LD parse error':'No JSON-LD found',J.errs.join(' | ')||'No JSON-LD detected.','Add valid LocalBusiness JSON-LD.');if(J.local.length)s.schema+=6;else add(I,'high','Schema','LocalBusiness schema missing','Detected types: '+(J.types.join(', ')||'none'),'Add LocalBusiness or a specific subtype.');if(D.name)s.schema+=4;else add(I,'high','Schema','Schema name missing','No schema name.','Add exact business name.');if(D.street&&D.city&&D.region)s.schema+=6;else add(I,'high','Schema','Schema address incomplete','PostalAddress fields incomplete.','Add street, city, region, postal, and country.');if(D.phone)s.schema+=3;else add(I,'medium','Schema','Schema telephone missing','No telephone property.','Add telephone.');if(D.hours||/hours|mon|tue|wed|thu|fri|sat|sun|open/i.test(T))s.schema+=2;else add(I,'medium','Schema','Hours missing','No hours found.','Add visible hours and openingHoursSpecification.');if(D.same.length||D.map||L.map)s.schema+=5;else add(I,'opportunity','Schema','Entity enrichment weak','No sameAs or map signal.','Add verified profiles and map/directions.');s.index+=10;if(!/noindex/i.test(R.html))s.index+=4;else add(I,'critical','Index','Noindex detected','The page includes noindex.','Remove noindex if this page should rank.');if(rt.block)add(I,'critical','Index','robots.txt may block crawl','robots.txt appears to disallow all.','Review robots.txt.');else s.index+=6;if(title.length>18)s.page+=4;else add(I,'medium','Page','Title weak','Title: '+(title||'missing'),'Write a concise service/location title.');if(desc.length>50)s.page+=3;else add(I,'medium','Page','Description thin','Meta description length '+desc.length+'.','Add a service/location description.');if(h1.length)s.page+=3;else add(I,'medium','Page','H1 missing','No H1 found.','Add one clear H1.');if(/service|services|products|menu|repair|install|consult|estimate/i.test(T))s.page+=2;if(/near me|serving|service area|city|county|region/i.test(T))s.page+=3;else add(I,'opportunity','Page','Local terms can improve','Few local/service-area terms found.','Add accurate city/service-area copy.');if(L.map)s.trust+=2;if(L.social||D.same.length)s.trust+=2;else add(I,'low','Trust','Identity links missing','No profile or sameAs links found.','Add verified profile URLs.');if(/review|testimonial|rating|stars/i.test(T))s.trust+=2;else add(I,'opportunity','Trust','Review signal missing','No review/testimonial language found.','Add honest review links or testimonials.');if(L.cta)s.trust+=2;else add(I,'opportunity','Trust','CTA weak','No call/quote/book action detected.','Add a strong local CTA.');s.trust+=2;I.sort(function(a,b){return ['critical','high','medium','low','opportunity'].indexOf(a.se)-['critical','high','medium','low','opportunity'].indexOf(b.se)});s.overall=Math.max(0,Math.min(100,Math.round(s.nap+s.schema+s.index+s.page+s.trust)));return {s:s,I:I,meta:{title:title}}}async function run(u){u=GU(u);var f=await F(u),d=new DOMParser().parseFromString(f.txt,'text/html'),T=text(d),J=schema(d),D=primary(J),L=links(d),ph=phones(T),ad=addrs(T),rt=await root(f.url),Q=score({d:d,T:T,html:f.txt,J:J,D:D,L:L,ph:ph,ad:ad,rt:rt});return {url:f.url,via:f.via,score:Q.s,issues:Q.I,j:{types:J.types,local:J.local.length,nodes:J.nodes},D:D,ph:ph,ad:ad,gen:starter(f.url,D)}}function shell(r){var sc=r?r.score.overall:0,c=sc>=80?'#5b8f68':sc>=60?'#b78a34':'#a95151';Q('#app').innerHTML='<div class=hero><div class=pad><div class=mut style=font-weight:950;letter-spacing:.14em;text-transform:uppercase>Local SEO Lens</div><div class=ttl>Local search diagnostics with evidence, not vibes.</div><div class=sub>Audit public website signals for LocalBusiness schema, NAP consistency, indexability basics, and local page signals.</div><div class=url><input id=url placeholder=https://example.com/location-page><button class=p id=scan>Run Local SEO Audit</button><button id=demo>Demo</button></div></div><div class=darkpad><div class=ring style=background:conic-gradient('+c+' '+(sc*3.6)+'deg,#243028 0)><div>'+(r?sc:'—')+'</div></div><h2>'+(r?(sc>=85?'Strong':sc>=70?'Usable':sc>=50?'Needs work':'High risk'):'Ready to scan')+'</h2><p class=sub>'+(r?E(r.url)+' · '+r.issues.length+' issues. Top fix: '+E((r.issues[0]||{}).t||'none'):'Enter a public URL and run the audit.')+'</p></div></div><div id=out>'+(r?report(r):'<section class=sec><div class=card style=margin:12px><b>What this checks:</b> JSON-LD LocalBusiness schema, NAP visibility, noindex/robots hints, title/meta/H1, contact/map/social/review/CTA signals, validator links, and starter JSON-LD.</div></section>')+'</div>';bind()}function report(r){var html='<section class=sec><div class=head><h2>Executive scorecard</h2><button class=mini id=copy>Copy summary</button></div><div class=grid>';[['Readiness',r.score.overall,100],['Local entity / NAP',r.score.nap,25],['Schema quality',r.score.schema,30],['Indexability',r.score.index,20]].forEach(function(x){html+='<div class=m><span>'+x[0]+'</span><b>'+x[1]+'/'+x[2]+'</b></div>'});html+='</div></section><section class=sec><div class=head><h2>Prioritized issues</h2></div>';r.issues.forEach(function(i){html+='<div class=issue><h3><span class=badge>'+i.se+'</span> '+E(i.t)+'</h3><p><b>Evidence:</b> '+E(i.e)+'</p><p><b>Fix:</b> '+E(i.f)+'</p><p class=mut>Pillar: '+E(i.p)+'</p></div>'});html+='</section><section class=sec><div class=head><h2>Schema, NAP, and validators</h2><div><a target=_blank href=https://search.google.com/test/rich-results?url='+encodeURIComponent(r.url)+'><button class=mini>Rich Results</button></a> <a target=_blank href=https://validator.schema.org/#url='+encodeURIComponent(r.url)+'><button class=mini>Schema Validator</button></a></div></div><div class=g2><div class=card><p><b>Types:</b> '+E((r.j.types.length?r.j.types:['none']).join(', '))+'</p><p><b>LocalBusiness:</b> '+r.j.local+'</p><p><b>Phones:</b> '+E(r.ph.join(', ')||'none')+'</p><p><b>Addresses:</b> '+E(r.ad.join(' | ')||'none')+'</p><pre class=code>'+E(JSON.stringify(r.j.nodes.slice(0,3),null,2))+'</pre></div><div class=card><h3>Starter LocalBusiness JSON-LD</h3><button class=mini id=copyjson>Copy JSON-LD</button><pre id=json class=code>'+E(r.gen)+'</pre></div></div></section>';return html}function bind(){Q('#scan').onclick=async function(){var b=this;b.textContent='Scanning…';try{var r=await run(Q('#url').value);shell(r);V().save(K,r).catch(function(){})}catch(e){shell({url:Q('#url').value||'failed',via:'failed',score:{overall:0,nap:0,schema:0,index:0,page:0,trust:0},issues:[{se:'critical',p:'Fetch',t:'Audit failed',e:e.message,f:'Confirm URL is public and proxy-fetchable.'}],j:{types:[],local:0,nodes:[]},ph:[],ad:[],gen:''})}b.textContent='Run Local SEO Audit'};Q('#demo').onclick=function(){shell({url:'https://example-local-business.test/',via:'demo',score:{overall:68,nap:18,schema:17,index:16,page:11,trust:6},issues:[{se:'high',p:'Schema',t:'LocalBusiness schema missing',e:'Only Organization schema detected.',f:'Add LocalBusiness JSON-LD.'},{se:'medium',p:'NAP',t:'Phone visible but not in schema',e:'Detected phone on page only.',f:'Add telephone to schema.'}],j:{types:['Organization'],local:0,nodes:[{'@context':'https://schema.org','@type':'Organization',name:'Example Local Co'}]},ph:['(919) 555-0188'],ad:['123 Main Street Raleigh NC 27601'],gen:starter('https://example-local-business.test/',{name:'Example Local Co',phone:'(919) 555-0188',street:'123 Main Street',city:'Raleigh',region:'NC',postal:'27601'})})};var a=Q('#copy');if(a)a.onclick=function(){navigator.clipboard&&navigator.clipboard.writeText('Local SEO Lens Audit')};var j=Q('#copyjson');if(j)j.onclick=function(){navigator.clipboard&&navigator.clipboard.writeText(Q('#json').textContent)}}V().load(K).then(function(r){shell(r)}).catch(function(){shell()})})();</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 */