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

Configure Feed

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

feat: add platform filter to search UI and backend

- add platform filter UI (leaflet/pckt toggle buttons)
- fix platform filter to work in SQL query (not post-filter)
- previously platform filter was applied after LIMIT 40, missing results
- now uses DocsByFtsAndPlatform query with platform in WHERE clause
- exclude publications from results when platform filter is active
- bump input font-size to 16px to prevent iOS auto-zoom

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

zzstoatzz 70832172 713b0b15

+122 -27
+21 -14
backend/src/search.zig
··· 105 105 \\ORDER BY rank LIMIT 40 106 106 ); 107 107 108 + const DocsByFtsAndPlatform = zql.Query( 109 + \\SELECT f.uri, d.did, d.title, 110 + \\ snippet(documents_fts, 2, '', '', '...', 32) as snippet, 111 + \\ d.created_at, d.rkey, 112 + \\ COALESCE(p.base_path, (SELECT base_path FROM publications WHERE did = d.did LIMIT 1), '') as base_path, 113 + \\ CASE WHEN d.publication_uri != '' THEN 1 ELSE 0 END as has_publication, 114 + \\ d.platform, COALESCE(d.path, '') as path 115 + \\FROM documents_fts f 116 + \\JOIN documents d ON f.uri = d.uri 117 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 118 + \\WHERE documents_fts MATCH :query AND d.platform = :platform 119 + \\ORDER BY rank LIMIT 40 120 + ); 121 + 108 122 /// Publication search result (internal) 109 123 const Pub = struct { 110 124 uri: []const u8, ··· 167 181 c.query(DocsByTag.positional, DocsByTag.bind(.{ .tag = tag_filter.? })) catch null 168 182 else if (tag_filter) |tag| 169 183 c.query(DocsByFtsAndTag.positional, DocsByFtsAndTag.bind(.{ .query = fts_query, .tag = tag })) catch null 184 + else if (platform_filter) |pf| 185 + c.query(DocsByFtsAndPlatform.positional, DocsByFtsAndPlatform.bind(.{ .query = fts_query, .platform = pf })) catch null 170 186 else 171 187 c.query(DocsByFts.positional, DocsByFts.bind(.{ .query = fts_query })) catch null; 172 188 173 189 if (doc_result) |*res| { 174 190 defer res.deinit(); 175 191 for (res.rows) |row| { 176 - const doc = Doc.fromRow(row); 177 - // filter by platform if specified 178 - if (platform_filter) |pf| { 179 - if (!std.mem.eql(u8, doc.platform, pf)) continue; 180 - } 181 - try jw.write(doc.toJson()); 192 + try jw.write(Doc.fromRow(row).toJson()); 182 193 } 183 194 } 184 195 185 - // publications are excluded when filtering by tag 186 - if (tag_filter == null) { 196 + // publications are excluded when filtering by tag or platform 197 + // (platform filter is for documents only - publications don't have meaningful platform distinction) 198 + if (tag_filter == null and platform_filter == null) { 187 199 var pub_result = c.query( 188 200 PubSearch.positional, 189 201 PubSearch.bind(.{ .query = fts_query }), ··· 192 204 if (pub_result) |*res| { 193 205 defer res.deinit(); 194 206 for (res.rows) |row| { 195 - const publication = Pub.fromRow(row); 196 - // filter by platform if specified 197 - if (platform_filter) |pf| { 198 - if (!std.mem.eql(u8, publication.platform, pf)) continue; 199 - } 200 - try jw.write(publication.toJson()); 207 + try jw.write(Pub.fromRow(row).toJson()); 201 208 } 202 209 } 203 210 }
+101 -13
site/index.html
··· 75 75 flex: 1; 76 76 padding: 0.5rem; 77 77 font-family: monospace; 78 - font-size: 14px; 78 + font-size: 16px; /* prevents iOS auto-zoom on focus */ 79 79 background: #111; 80 80 border: 1px solid #333; 81 81 color: #ccc; ··· 325 325 margin-left: 4px; 326 326 } 327 327 328 + .platform-filter { 329 + margin-bottom: 1rem; 330 + } 331 + 332 + .platform-filter-label { 333 + font-size: 11px; 334 + color: #444; 335 + margin-bottom: 0.5rem; 336 + } 337 + 338 + .platform-filter-list { 339 + display: flex; 340 + gap: 0.5rem; 341 + } 342 + 343 + .platform-option { 344 + font-size: 11px; 345 + padding: 3px 8px; 346 + background: #151515; 347 + border: 1px solid #252525; 348 + border-radius: 3px; 349 + cursor: pointer; 350 + color: #777; 351 + } 352 + 353 + .platform-option:hover { 354 + background: #1a1a1a; 355 + border-color: #333; 356 + color: #aaa; 357 + } 358 + 359 + .platform-option.active { 360 + background: rgba(180, 100, 64, 0.2); 361 + border-color: #d4956a; 362 + color: #d4956a; 363 + } 364 + 328 365 .active-filter { 329 366 display: flex; 330 367 align-items: center; ··· 363 400 364 401 <div id="tags" class="tags"></div> 365 402 403 + <div id="platform-filter" class="platform-filter"></div> 404 + 366 405 <div id="results" class="results"> 367 406 <div class="empty-state"> 368 407 <p>search atproto publishing platforms</p> ··· 385 424 const tagsDiv = document.getElementById('tags'); 386 425 const activeFilterDiv = document.getElementById('active-filter'); 387 426 const suggestionsDiv = document.getElementById('suggestions'); 427 + const platformFilterDiv = document.getElementById('platform-filter'); 388 428 389 429 let currentTag = null; 430 + let currentPlatform = null; 390 431 let allTags = []; 391 432 let popularSearches = []; 392 433 393 - async function search(query, tag = null) { 394 - if (!query.trim() && !tag) return; 434 + async function search(query, tag = null, platform = null) { 435 + if (!query.trim() && !tag && !platform) return; 395 436 396 437 searchBtn.disabled = true; 397 438 let searchUrl = `${API_URL}/search?q=${encodeURIComponent(query || '')}`; 398 439 if (tag) searchUrl += `&tag=${encodeURIComponent(tag)}`; 440 + if (platform) searchUrl += `&platform=${encodeURIComponent(platform)}`; 399 441 resultsDiv.innerHTML = `<div class="status">searching...</div>`; 400 442 401 443 try { ··· 418 460 if (results.length === 0) { 419 461 resultsDiv.innerHTML = ` 420 462 <div class="empty-state"> 421 - <p>no results${query ? ` for "${escapeHtml(query)}"` : ''}${tag ? ` in #${escapeHtml(tag)}` : ''}</p> 463 + <p>no results${query ? ` for "${escapeHtml(query)}"` : ''}${tag ? ` in #${escapeHtml(tag)}` : ''}${platform ? ` on ${escapeHtml(platform)}` : ''}</p> 422 464 <p>try different keywords</p> 423 465 </div> 424 466 `; ··· 567 609 const q = queryInput.value.trim(); 568 610 if (q) params.set('q', q); 569 611 if (currentTag) params.set('tag', currentTag); 612 + if (currentPlatform) params.set('platform', currentPlatform); 570 613 const url = params.toString() ? `?${params}` : '/'; 571 614 history.pushState(null, '', url); 572 615 } 573 616 574 617 function doSearch() { 575 618 updateUrl(); 576 - search(queryInput.value, currentTag); 619 + search(queryInput.value, currentTag, currentPlatform); 577 620 } 578 621 579 622 function setTag(tag) { ··· 592 635 renderActiveFilter(); 593 636 renderTags(); 594 637 updateUrl(); 595 - if (queryInput.value.trim()) { 596 - search(queryInput.value, null); 638 + if (queryInput.value.trim() || currentPlatform) { 639 + search(queryInput.value, null, currentPlatform); 597 640 } else { 598 641 renderEmptyState(); 599 642 } 600 643 } 601 644 645 + function setPlatform(platform) { 646 + if (currentPlatform === platform) { 647 + clearPlatform(); 648 + return; 649 + } 650 + currentPlatform = platform; 651 + renderActiveFilter(); 652 + renderPlatformFilter(); 653 + doSearch(); 654 + } 655 + 656 + function clearPlatform() { 657 + currentPlatform = null; 658 + renderActiveFilter(); 659 + renderPlatformFilter(); 660 + updateUrl(); 661 + if (queryInput.value.trim() || currentTag) { 662 + search(queryInput.value, currentTag, null); 663 + } else { 664 + renderEmptyState(); 665 + } 666 + } 667 + 668 + function renderPlatformFilter() { 669 + const platforms = [ 670 + { id: 'leaflet', label: 'leaflet' }, 671 + { id: 'pckt', label: 'pckt' }, 672 + ]; 673 + const html = platforms.map(p => ` 674 + <span class="platform-option${currentPlatform === p.id ? ' active' : ''}" onclick="setPlatform('${p.id}')">${p.label}</span> 675 + `).join(''); 676 + platformFilterDiv.innerHTML = `<div class="platform-filter-label">filter by platform:</div><div class="platform-filter-list">${html}</div>`; 677 + } 678 + 602 679 function renderActiveFilter() { 603 - if (!currentTag) { 680 + if (!currentTag && !currentPlatform) { 604 681 activeFilterDiv.innerHTML = ''; 605 682 return; 606 683 } 684 + let parts = []; 685 + if (currentTag) parts.push(`tag: <strong>#${escapeHtml(currentTag)}</strong>`); 686 + if (currentPlatform) parts.push(`platform: <strong>${escapeHtml(currentPlatform)}</strong>`); 687 + const clearActions = []; 688 + if (currentTag) clearActions.push(`<span class="clear" onclick="clearTag()">× tag</span>`); 689 + if (currentPlatform) clearActions.push(`<span class="clear" onclick="clearPlatform()">× platform</span>`); 607 690 activeFilterDiv.innerHTML = ` 608 691 <div class="active-filter"> 609 - <span>filtering by tag: <strong>#${escapeHtml(currentTag)}</strong> <span style="color:#666;font-size:10px">(documents only)</span></span> 610 - <span class="clear" onclick="clearTag()">× clear</span> 692 + <span>filtering by ${parts.join(', ')} <span style="color:#666;font-size:10px">(documents only)</span></span> 693 + ${clearActions.join(' ')} 611 694 </div> 612 695 `; 613 696 } ··· 689 772 const params = new URLSearchParams(location.search); 690 773 queryInput.value = params.get('q') || ''; 691 774 currentTag = params.get('tag') || null; 775 + currentPlatform = params.get('platform') || null; 692 776 renderActiveFilter(); 693 777 renderTags(); 694 - if (queryInput.value || currentTag) search(queryInput.value, currentTag); 778 + renderPlatformFilter(); 779 + if (queryInput.value || currentTag || currentPlatform) search(queryInput.value, currentTag, currentPlatform); 695 780 }); 696 781 697 782 // init 698 783 const initialParams = new URLSearchParams(location.search); 699 784 const initialQuery = initialParams.get('q'); 700 785 const initialTag = initialParams.get('tag'); 786 + const initialPlatform = initialParams.get('platform'); 701 787 if (initialQuery) queryInput.value = initialQuery; 702 788 if (initialTag) currentTag = initialTag; 789 + if (initialPlatform) currentPlatform = initialPlatform; 703 790 renderActiveFilter(); 791 + renderPlatformFilter(); 704 792 705 - if (initialQuery || initialTag) { 706 - search(initialQuery || '', initialTag); 793 + if (initialQuery || initialTag || initialPlatform) { 794 + search(initialQuery || '', initialTag, initialPlatform); 707 795 } 708 796 709 797 async function loadRelated(topResult) {