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.

fix: load user's site.standard.graph.subscription records

previously /api/my-publications loaded the user's OWN
site.standard.publication records — backwards. the canonical signal
for "publications this user cares about" is the user's
site.standard.graph.subscription records (the same collection a
standard.site reader writes when you click "subscribe" on a publication).

now the toggle list shows publications you follow, enriched with
name + url from pub-search's local publications mirror when indexed.
if a followed publication isn't in the index, we still return the
at-uri so the UI can show it.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>

+44 -22
+41 -19
backend/src/server/subscriptions.zig
··· 16 16 const oauth = @import("../oauth.zig"); 17 17 const store = @import("../state.zig"); 18 18 const notifications = @import("../notifications.zig"); 19 + const db = @import("../db.zig"); 19 20 20 21 const SUBSCRIPTION_COLLECTION = notifications.SUBSCRIPTION_COLLECTION; 21 22 ··· 170 171 try sendJson(request, out); 171 172 } 172 173 173 - /// GET /api/my-publications — list the authenticated user's 174 - /// site.standard.publication records by hitting their PDS directly 175 - /// (records are public; no DPoP needed). This is the source of truth 176 - /// for the toggle list in the subscriptions UI. 174 + /// GET /api/my-publications — list the publications the authenticated 175 + /// user is *subscribed to*, via their `site.standard.graph.subscription` 176 + /// records. That's standard.site's canonical "user follows publication" 177 + /// record — the signal we hook into so "turn on notifications" maps 178 + /// cleanly onto publications the user already cares about. 179 + /// 180 + /// For each subscription record we pluck the `publication` at-uri and 181 + /// try to resolve the publication's metadata (name/url) from pub-search's 182 + /// local mirror. If it's not in the index yet, we still return the at-uri 183 + /// so the UI can show it. 177 184 pub fn handleMyPublications(request: *http.Server.Request, io: Io) !void { 178 185 _ = io; 179 186 var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); ··· 189 196 return; 190 197 }; 191 198 192 - const url = try std.fmt.allocPrint(alloc, "{s}/xrpc/com.atproto.repo.listRecords?repo={s}&collection=site.standard.publication&limit=100", .{ 199 + const url = try std.fmt.allocPrint(alloc, "{s}/xrpc/com.atproto.repo.listRecords?repo={s}&collection=site.standard.graph.subscription&limit=100", .{ 193 200 session.pds_url, session.did, 194 201 }); 195 202 196 203 const body = oauth.httpGet(alloc, url) catch |err| { 197 204 std.log.warn("handleMyPublications: listRecords failed: {}", .{err}); 198 - try sendJsonStatus(request, .bad_gateway, "{\"error\":\"failed to fetch publications from PDS\"}"); 205 + try sendJsonStatus(request, .bad_gateway, "{\"error\":\"failed to fetch subscriptions from PDS\"}"); 199 206 return; 200 207 }; 201 208 ··· 214 221 return; 215 222 } 216 223 224 + const local = db.getLocalDbRaw(); 225 + 217 226 var out: std.Io.Writer.Allocating = .init(alloc); 218 227 var jw: json.Stringify = .{ .writer = &out.writer }; 219 228 try jw.beginArray(); 220 229 for (records.array.items) |rec| { 221 230 if (rec != .object) continue; 222 - const uri_v = rec.object.get("uri") orelse continue; 223 - if (uri_v != .string) continue; 224 231 const val = rec.object.get("value") orelse continue; 225 232 if (val != .object) continue; 233 + const pub_v = val.object.get("publication") orelse continue; 234 + if (pub_v != .string) continue; 235 + const pub_uri = pub_v.string; 236 + 237 + // enrich from local publications mirror — name + base_path 238 + var name: []const u8 = ""; 239 + var base_path: []const u8 = ""; 240 + if (local) |l| { 241 + if (l.query("SELECT name, base_path FROM publications WHERE uri = ?", .{pub_uri})) |q| { 242 + var rows = q; 243 + defer rows.deinit(); 244 + if (rows.next()) |row| { 245 + // dupe into arena — underlying slice dies with rows.deinit 246 + name = alloc.dupe(u8, row.text(0)) catch ""; 247 + base_path = alloc.dupe(u8, row.text(1)) catch ""; 248 + } 249 + } else |_| {} 250 + } 226 251 227 252 try jw.beginObject(); 228 253 try jw.objectField("uri"); 229 - try jw.write(uri_v.string); 230 - if (val.object.get("name")) |n| if (n == .string) { 254 + try jw.write(pub_uri); 255 + if (name.len > 0) { 231 256 try jw.objectField("name"); 232 - try jw.write(n.string); 233 - }; 234 - if (val.object.get("url")) |u| if (u == .string) { 257 + try jw.write(name); 258 + } 259 + if (base_path.len > 0) { 260 + const pub_url = try std.fmt.allocPrint(alloc, "https://{s}", .{base_path}); 235 261 try jw.objectField("url"); 236 - try jw.write(u.string); 237 - }; 238 - if (val.object.get("description")) |d| if (d == .string) { 239 - try jw.objectField("description"); 240 - try jw.write(d.string); 241 - }; 262 + try jw.write(pub_url); 263 + } 242 264 try jw.endObject(); 243 265 } 244 266 try jw.endArray();
+3 -3
site/subscriptions.html
··· 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 6 <link rel="icon" type="image/svg+xml" href="/favicon.svg"> 7 7 <title>subscriptions - pub search</title> 8 - <meta name="description" content="get a bsky DM when pub-search indexes a new document under one of your standard.site publications"> 8 + <meta name="description" content="get a bsky DM when pub-search indexes a new document under a standard.site publication you follow"> 9 9 <meta name="robots" content="noindex"> 10 10 <script> 11 11 (function() { ··· 275 275 <span class="me" id="me"></span> 276 276 </h1> 277 277 278 - <p>get a bsky DM whenever pub-search indexes a new document under one of your <code>site.standard.publication</code> records.</p> 278 + <p>get a bsky DM whenever pub-search indexes a new document under a publication you follow. follows come from your <code>site.standard.graph.subscription</code> records — the same ones your standard.site reader writes when you click "subscribe".</p> 279 279 280 280 <section id="login" class="card hidden"> 281 281 <p class="hint">sign in with your atproto handle:</p> ··· 385 385 function renderPubs(pubs, subIndex) { 386 386 const el = $('pubs'); 387 387 if (!pubs.length) { 388 - el.innerHTML = '<div class="empty">you don\'t have any <code>site.standard.publication</code> records on your PDS yet.</div>'; 388 + el.innerHTML = '<div class="empty">you aren\'t subscribed to any standard.site publications yet. head to any standard.site reader (e.g. a <code>pckt.blog</code> / <code>greengale.app</code> / <code>offprint.app</code> / <code>leaflet.pub</code> page) and hit subscribe.</div>'; 389 389 return; 390 390 } 391 391 el.innerHTML = '';