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.

coherence: consistent naming, better error handling, local dev support

- struct fields now match JSON output (basePath, createdAt, hasPublication)
- /tags returns error object instead of empty array on failure
- API_URL auto-detects localhost for local development
- UI shows "(documents only)" hint when filtering by tag
- added doc comments explaining type derivation logic

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

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

zzstoatzz df1ede2e e72a2238

+27 -17
+17 -14
backend/src/db/mod.zig
··· 89 89 c.exec("DELETE FROM publications_fts WHERE uri = ?", &.{uri}) catch {}; 90 90 } 91 91 92 + /// Document search result. 93 + /// Type derivation: has_publication=true → "article", false → "looseleaf" 92 94 const Doc = struct { 93 95 uri: []const u8, 94 96 did: []const u8, 95 97 title: []const u8, 96 98 snippet: []const u8, 97 - created_at: []const u8, 99 + createdAt: []const u8, 98 100 rkey: []const u8, 99 - base_path: []const u8, 100 - has_publication: bool, 101 + basePath: []const u8, 102 + hasPublication: bool, 101 103 102 104 fn fromRow(row: Row) Doc { 103 105 return .{ ··· 105 107 .did = row.text(1), 106 108 .title = row.text(2), 107 109 .snippet = row.text(3), 108 - .created_at = row.text(4), 110 + .createdAt = row.text(4), 109 111 .rkey = row.text(5), 110 - .base_path = row.text(6), 111 - .has_publication = row.int(7) != 0, 112 + .basePath = row.text(6), 113 + .hasPublication = row.int(7) != 0, 112 114 }; 113 115 } 114 116 }; ··· 149 151 \\ORDER BY rank LIMIT 40 150 152 ); 151 153 154 + /// Publication search result. Type is always "publication". 152 155 const Pub = struct { 153 156 uri: []const u8, 154 157 did: []const u8, 155 158 name: []const u8, 156 159 snippet: []const u8, 157 160 rkey: []const u8, 158 - base_path: []const u8, 161 + basePath: []const u8, 159 162 160 163 fn fromRow(row: Row) Pub { 161 164 return .{ ··· 164 167 .name = row.text(2), 165 168 .snippet = row.text(3), 166 169 .rkey = row.text(4), 167 - .base_path = row.text(5), 170 + .basePath = row.text(5), 168 171 }; 169 172 } 170 173 }; ··· 221 224 const doc = Doc.fromRow(row); 222 225 try jw.beginObject(); 223 226 try jw.objectField("type"); 224 - try jw.write(if (doc.has_publication) "article" else "looseleaf"); 227 + try jw.write(if (doc.hasPublication) "article" else "looseleaf"); 225 228 try jw.objectField("uri"); 226 229 try jw.write(doc.uri); 227 230 try jw.objectField("did"); ··· 231 234 try jw.objectField("snippet"); 232 235 try jw.write(doc.snippet); 233 236 try jw.objectField("createdAt"); 234 - try jw.write(doc.created_at); 237 + try jw.write(doc.createdAt); 235 238 try jw.objectField("rkey"); 236 239 try jw.write(doc.rkey); 237 240 try jw.objectField("basePath"); 238 - try jw.write(doc.base_path); 241 + try jw.write(doc.basePath); 239 242 try jw.endObject(); 240 243 } 241 244 } 242 245 243 - // search publications (only if no tag filter) 246 + // publications are excluded when filtering by tag (tags only apply to documents) 244 247 if (tag_filter == null) { 245 248 var pub_result = c.query( 246 249 PubSearch.positional, ··· 265 268 try jw.objectField("rkey"); 266 269 try jw.write(p.rkey); 267 270 try jw.objectField("basePath"); 268 - try jw.write(p.base_path); 271 + try jw.write(p.basePath); 269 272 try jw.endObject(); 270 273 } 271 274 } ··· 282 285 errdefer output.deinit(); 283 286 284 287 var res = c.query(TagsQuery.positional, &.{}) catch { 285 - try output.writer.writeAll("[]"); 288 + try output.writer.writeAll("{\"error\":\"failed to fetch tags\"}"); 286 289 return try output.toOwnedSlice(); 287 290 }; 288 291 defer res.deinit();
+10 -3
site/index.html
··· 263 263 </div> 264 264 265 265 <script> 266 - const API_URL = 'https://leaflet-search-backend.fly.dev'; 266 + const API_URL = location.hostname === 'localhost' || location.hostname === '127.0.0.1' 267 + ? 'http://localhost:8080' 268 + : 'https://leaflet-search-backend.fly.dev'; 267 269 268 270 const queryInput = document.getElementById('query'); 269 271 const searchBtn = document.getElementById('search-btn'); ··· 408 410 } 409 411 activeFilterDiv.innerHTML = ` 410 412 <div class="active-filter"> 411 - <span>filtering by tag: <strong>#${escapeHtml(currentTag)}</strong></span> 413 + <span>filtering by tag: <strong>#${escapeHtml(currentTag)}</strong> <span style="color:#666;font-size:10px">(documents only)</span></span> 412 414 <span class="clear" onclick="clearTag()">× clear</span> 413 415 </div> 414 416 `; ··· 428 430 async function loadTags() { 429 431 try { 430 432 const res = await fetch(`${API_URL}/tags`); 431 - allTags = await res.json(); 433 + const data = await res.json(); 434 + if (data.error) { 435 + console.error('failed to load tags:', data.error); 436 + return; 437 + } 438 + allTags = data; 432 439 renderTags(); 433 440 } catch (e) { 434 441 console.error('failed to load tags', e);