search for standard sites pub-search.waow.tech
search zig blog atproto
0

Configure Feed

Select the types of activity you want to include in your feed.

fix(wrapped): unblock CI (import shadowing) + handle typeahead + X-Client reporting

- server.zig: rename `wrapped` import → `wrapped_ep`; the bare name shadowed two
local `const wrapped` vars and ReleaseSafe (CI) rejects shadowing. fixes the
failed deploy so /wrapped actually ships.
- wrapped.html: real handle typeahead on the entry box (bare handle, no '@'),
reusing the shared .lf-typeahead-* dropdown + typeahead.waow.tech. pick →
load. reports via X-Client: pub-search.waow.tech/wrapped.
- ui.js / identity.js: send X-Client: pub-search.waow.tech on the homepage
search typeahead + shared profile resolution so we show up by name in the
service's traffic stats instead of as "unknown".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

+97 -12
+2 -2
backend/src/server.zig
··· 17 17 const recommenders = @import("server/recommenders.zig"); 18 18 const subscribed = @import("server/subscribed.zig"); 19 19 const subscribers = @import("server/subscribers.zig"); 20 - const wrapped = @import("server/wrapped.zig"); 20 + const wrapped_ep = @import("server/wrapped.zig"); 21 21 22 22 pub const initRecommendedCache = recommended.init; 23 23 pub const initCuratorsCache = curators.init; ··· 635 635 } 636 636 span.setAttribute("did", did.?); 637 637 638 - const body = try wrapped.fetch(alloc, did.?); 638 + const body = try wrapped_ep.fetch(alloc, did.?); 639 639 try sendJson(request, body); 640 640 } 641 641
+2 -1
site/identity.js
··· 42 42 43 43 return Promise.all(chunks.map(function (chunk) { 44 44 var qs = chunk.map(function (a) { return 'actors=' + encodeURIComponent(a); }).join('&'); 45 - return fetch(BASE + '/xrpc/app.bsky.actor.getProfiles?' + qs) 45 + return fetch(BASE + '/xrpc/app.bsky.actor.getProfiles?' + qs, 46 + { headers: { 'X-Client': 'pub-search.waow.tech' } }) 46 47 .then(function (r) { return r.ok ? r.json() : null; }) 47 48 .catch(function () { return null; }); 48 49 })).then(function (results) {
+2 -1
site/ui.js
··· 421 421 lastFetched = info.partial; 422 422 var query = info.partial; 423 423 fetch(TYPEAHEAD_BASE + '/xrpc/app.bsky.actor.searchActorsTypeahead?q=' + 424 - encodeURIComponent(query) + '&limit=' + LIMIT) 424 + encodeURIComponent(query) + '&limit=' + LIMIT, 425 + { headers: { 'X-Client': 'pub-search.waow.tech' } }) 425 426 .then(function(r) { return r.ok ? r.json() : null; }) 426 427 .then(function(d) { 427 428 if (!d || !matchInfo || matchInfo.partial !== query) return;
+91 -8
site/wrapped.html
··· 107 107 } 108 108 .entry .sub { color: var(--text-secondary); margin-bottom: 1.25rem; } 109 109 .entry form { display: flex; gap: 0.5rem; } 110 + .entry .ta-wrap { position: relative; flex: 1; min-width: 0; } 110 111 .entry input { 111 - flex: 1; min-width: 0; font: inherit; padding: 0.7rem 0.8rem; 112 + width: 100%; min-width: 0; font: inherit; padding: 0.7rem 0.8rem; 112 113 background: var(--bg-subtle); border: 1px solid var(--border); 113 114 border-radius: 8px; color: var(--text-bright); 114 115 } ··· 293 294 } 294 295 295 296 // ----- entry screen ------------------------------------------------- 297 + function go(handle) { 298 + var v = String(handle || '').trim().replace(/^@/, ''); 299 + if (!v) return; 300 + var key = v.indexOf('did:') === 0 ? 'did' : 'handle'; 301 + history.pushState({}, '', '/wrapped?' + key + '=' + encodeURIComponent(v)); 302 + load(); 303 + } 304 + 296 305 function renderEntry() { 297 306 root.innerHTML = 298 307 '<div class="entry">' + ··· 300 309 '<div class="sub">a snapshot of how you show up across the network — ' + 301 310 'what you publish, what you recommend, what you read.</div>' + 302 311 '<form id="entry-form">' + 303 - '<input id="entry-input" type="text" autocapitalize="off" autocorrect="off" ' + 304 - 'spellcheck="false" placeholder="enter a handle (e.g. alice.bsky.social)">' + 312 + '<div class="ta-wrap">' + 313 + '<input id="entry-input" type="text" autocapitalize="off" autocorrect="off" ' + 314 + 'spellcheck="false" autocomplete="off" placeholder="enter a handle (e.g. alice.bsky.social)">' + 315 + '</div>' + 305 316 '<button type="submit">go</button>' + 306 317 '</form>' + 307 318 '</div>'; 308 319 var form = document.getElementById('entry-form'); 309 320 var input = document.getElementById('entry-input'); 310 321 input.focus(); 322 + setupHandleTypeahead(input, function(actor) { 323 + input.value = actor.handle; 324 + go(actor.did || actor.handle); 325 + }); 311 326 form.addEventListener('submit', function(e) { 312 327 e.preventDefault(); 313 - var v = input.value.trim().replace(/^@/, ''); 314 - if (!v) return; 315 - var key = v.indexOf('did:') === 0 ? 'did' : 'handle'; 316 - history.pushState({}, '', '/wrapped?' + key + '=' + encodeURIComponent(v)); 317 - load(); 328 + go(input.value); 318 329 }); 330 + } 331 + 332 + // Handle typeahead against nate's community service (typeahead.waow.tech), 333 + // reusing the shared .lf-typeahead-* dropdown styles from components.css. 334 + // Unlike the homepage search box this triggers on the bare handle (no '@' 335 + // prefix) since the whole field IS the handle. We report ourselves via the 336 + // X-Client header so this shows up by name in the service's traffic stats. 337 + var TYPEAHEAD_BASE = 'https://typeahead.waow.tech'; 338 + var TYPEAHEAD_CLIENT = 'pub-search.waow.tech/wrapped'; 339 + 340 + function setupHandleTypeahead(input, onPick) { 341 + var DEBOUNCE_MS = 150, LIMIT = 8; 342 + var dd = document.createElement('div'); 343 + dd.className = 'lf-typeahead-dropdown'; 344 + dd.setAttribute('role', 'listbox'); 345 + input.parentNode.appendChild(dd); 346 + 347 + var timer = null, items = [], sel = -1, last = ''; 348 + 349 + function hide() { dd.classList.remove('open'); dd.innerHTML = ''; items = []; sel = -1; } 350 + function render() { 351 + dd.innerHTML = items.map(function(a, i) { 352 + var dn = a.displayName ? '<span class="lf-typeahead-name">' + esc(a.displayName) + '</span>' : ''; 353 + var av = a.avatar 354 + ? '<img class="lf-typeahead-avatar" src="' + esc(a.avatar) + '" loading="lazy" alt="">' 355 + : '<span class="lf-typeahead-avatar lf-typeahead-avatar-ph"></span>'; 356 + return '<div class="lf-typeahead-item' + (i === sel ? ' active' : '') + 357 + '" data-i="' + i + '" role="option">' + av + 358 + '<div class="lf-typeahead-text"><span class="lf-typeahead-handle">@' + esc(a.handle) + '</span>' + dn + '</div></div>'; 359 + }).join(''); 360 + dd.classList.toggle('open', items.length > 0); 361 + } 362 + function curVal() { return input.value.trim().replace(/^@/, ''); } 363 + function fetchNow() { 364 + var q = curVal(); 365 + if (q.length < 1) { hide(); return; } 366 + if (q === last) return; 367 + last = q; 368 + fetch(TYPEAHEAD_BASE + '/xrpc/app.bsky.actor.searchActorsTypeahead?q=' + 369 + encodeURIComponent(q) + '&limit=' + LIMIT, { headers: { 'X-Client': TYPEAHEAD_CLIENT } }) 370 + .then(function(r) { return r.ok ? r.json() : null; }) 371 + .then(function(d) { 372 + if (!d || curVal() !== q) return; 373 + items = (d.actors || []).slice(0, LIMIT); 374 + sel = items.length ? 0 : -1; 375 + render(); 376 + }) 377 + .catch(function() {}); 378 + } 379 + function choose(i) { var a = items[i]; if (!a) return; hide(); onPick(a); } 380 + 381 + input.addEventListener('input', function() { 382 + if (timer) clearTimeout(timer); 383 + timer = setTimeout(fetchNow, DEBOUNCE_MS); 384 + }); 385 + input.addEventListener('keydown', function(e) { 386 + if (!dd.classList.contains('open') || !items.length) return; 387 + if (e.key === 'ArrowDown') { e.preventDefault(); sel = (sel + 1) % items.length; render(); } 388 + else if (e.key === 'ArrowUp') { e.preventDefault(); sel = (sel - 1 + items.length) % items.length; render(); } 389 + else if (e.key === 'Enter') { e.preventDefault(); e.stopImmediatePropagation(); choose(sel); } 390 + else if (e.key === 'Escape') { e.preventDefault(); hide(); } 391 + }); 392 + dd.addEventListener('mousedown', function(e) { 393 + var t = e.target.closest('.lf-typeahead-item'); 394 + if (!t) return; 395 + e.preventDefault(); 396 + choose(parseInt(t.getAttribute('data-i'), 10)); 397 + }); 398 + document.addEventListener('click', function(e) { 399 + if (!input.contains(e.target) && !dd.contains(e.target)) hide(); 400 + }); 401 + input.addEventListener('blur', function() { setTimeout(hide, 150); }); 319 402 } 320 403 321 404 // ----- rank badge ---------------------------------------------------