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

feat: replace articles/looseleafs with platform breakdown

- dashboard shows documents by platform (leaflet, standardsite, etc.)
- fix tags link to use pub-search.waow.tech
- remove leaflet-specific terminology

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

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

Changed files
+43 -30
backend
site
+27 -18
backend/src/dashboard.zig
··· 7 7 const TagJson = struct { tag: []const u8, count: i64 }; 8 8 const TimelineJson = struct { date: []const u8, count: i64 }; 9 9 const PubJson = struct { name: []const u8, basePath: []const u8, count: i64 }; 10 + const PlatformJson = struct { platform: []const u8, count: i64 }; 10 11 11 12 /// All data needed to render the dashboard 12 13 pub const Data = struct { 13 14 started_at: i64, 14 15 searches: i64, 15 16 publications: i64, 16 - articles: i64, 17 - looseleafs: i64, 17 + documents: i64, 18 18 tags_json: []const u8, 19 19 timeline_json: []const u8, 20 20 top_pubs_json: []const u8, 21 + platforms_json: []const u8, 21 22 }; 22 23 23 24 // all dashboard queries batched into one request ··· 30 31 \\ (SELECT service_started_at FROM stats WHERE id = 1) as started_at 31 32 ; 32 33 33 - const DOC_TYPES_SQL = 34 - \\SELECT 35 - \\ SUM(CASE WHEN publication_uri != '' THEN 1 ELSE 0 END) as articles, 36 - \\ SUM(CASE WHEN publication_uri = '' OR publication_uri IS NULL THEN 1 ELSE 0 END) as looseleafs 34 + const PLATFORMS_SQL = 35 + \\SELECT platform, COUNT(*) as count 37 36 \\FROM documents 37 + \\GROUP BY platform 38 + \\ORDER BY count DESC 38 39 ; 39 40 40 41 const TAGS_SQL = ··· 69 70 // batch all 5 queries into one HTTP request 70 71 var batch = client.queryBatch(&.{ 71 72 .{ .sql = STATS_SQL }, 72 - .{ .sql = DOC_TYPES_SQL }, 73 + .{ .sql = PLATFORMS_SQL }, 73 74 .{ .sql = TAGS_SQL }, 74 75 .{ .sql = TIMELINE_SQL }, 75 76 .{ .sql = TOP_PUBS_SQL }, ··· 81 82 const started_at = if (stats_row) |r| r.int(4) else 0; 82 83 const searches = if (stats_row) |r| r.int(2) else 0; 83 84 const publications = if (stats_row) |r| r.int(1) else 0; 84 - 85 - // extract doc types (query 1) 86 - const doc_row = batch.getFirst(1); 87 - const articles = if (doc_row) |r| r.int(0) else 0; 88 - const looseleafs = if (doc_row) |r| r.int(1) else 0; 85 + const documents = if (stats_row) |r| r.int(0) else 0; 89 86 90 87 return .{ 91 88 .started_at = started_at, 92 89 .searches = searches, 93 90 .publications = publications, 94 - .articles = articles, 95 - .looseleafs = looseleafs, 91 + .documents = documents, 96 92 .tags_json = try formatTagsJson(alloc, batch.get(2)), 97 93 .timeline_json = try formatTimelineJson(alloc, batch.get(3)), 98 94 .top_pubs_json = try formatPubsJson(alloc, batch.get(4)), 95 + .platforms_json = try formatPlatformsJson(alloc, batch.get(1)), 99 96 }; 100 97 } 101 98 ··· 129 126 return try output.toOwnedSlice(); 130 127 } 131 128 129 + fn formatPlatformsJson(alloc: Allocator, rows: []const db.Row) ![]const u8 { 130 + var output: std.Io.Writer.Allocating = .init(alloc); 131 + errdefer output.deinit(); 132 + var jw: json.Stringify = .{ .writer = &output.writer }; 133 + try jw.beginArray(); 134 + for (rows) |row| try jw.write(PlatformJson{ .platform = row.text(0), .count = row.int(1) }); 135 + try jw.endArray(); 136 + return try output.toOwnedSlice(); 137 + } 138 + 132 139 /// Generate dashboard data as JSON for API endpoint 133 140 pub fn toJson(alloc: Allocator, data: Data) ![]const u8 { 134 141 var output: std.Io.Writer.Allocating = .init(alloc); ··· 146 153 try jw.objectField("publications"); 147 154 try jw.write(data.publications); 148 155 149 - try jw.objectField("articles"); 150 - try jw.write(data.articles); 156 + try jw.objectField("documents"); 157 + try jw.write(data.documents); 151 158 152 - try jw.objectField("looseleafs"); 153 - try jw.write(data.looseleafs); 159 + try jw.objectField("platforms"); 160 + try jw.beginWriteRaw(); 161 + try jw.writer.writeAll(data.platforms_json); 162 + jw.endWriteRaw(); 154 163 155 164 // use beginWriteRaw/endWriteRaw for pre-formatted JSON arrays 156 165 try jw.objectField("tags");
+2 -9
site/dashboard.html
··· 30 30 </section> 31 31 32 32 <section> 33 - <div class="section-title">documents</div> 33 + <div class="section-title">documents by platform</div> 34 34 <div class="chart-box"> 35 - <div class="doc-row"> 36 - <span class="doc-type">articles</span> 37 - <span class="doc-count" id="articles">--</span> 38 - </div> 39 - <div class="doc-row"> 40 - <span class="doc-type">looseleafs</span> 41 - <span class="doc-count" id="looseleafs">--</span> 42 - </div> 35 + <div id="platforms"></div> 43 36 </div> 44 37 </section> 45 38
+14 -3
site/dashboard.js
··· 57 57 if (!tags) return; 58 58 59 59 el.innerHTML = tags.slice(0, 20).map(t => 60 - '<a class="tag" href="https://leaflet-search.pages.dev/?tag=' + encodeURIComponent(t.tag) + '">' + 60 + '<a class="tag" href="https://pub-search.waow.tech/?tag=' + encodeURIComponent(t.tag) + '">' + 61 61 escapeHtml(t.tag) + '<span class="n">' + t.count + '</span></a>' 62 62 ).join(''); 63 + } 64 + 65 + function renderPlatforms(platforms) { 66 + const el = document.getElementById('platforms'); 67 + if (!platforms) return; 68 + 69 + platforms.forEach(p => { 70 + const row = document.createElement('div'); 71 + row.className = 'doc-row'; 72 + row.innerHTML = '<span class="doc-type">' + escapeHtml(p.platform) + '</span><span class="doc-count">' + p.count + '</span>'; 73 + el.appendChild(row); 74 + }); 63 75 } 64 76 65 77 function escapeHtml(str) { ··· 83 95 84 96 document.getElementById('searches').textContent = data.searches; 85 97 document.getElementById('publications').textContent = data.publications; 86 - document.getElementById('articles').textContent = data.articles; 87 - document.getElementById('looseleafs').textContent = data.looseleafs; 88 98 99 + renderPlatforms(data.platforms); 89 100 renderTimeline(data.timeline); 90 101 renderPubs(data.topPubs); 91 102 renderTags(data.tags);