atproto pds in zig pds.zat.dev
pds atproto
26

Configure Feed

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

Polish landing residents and record links

zzstoatzz (May 24, 2026, 10:03 AM -0500) e39ea3ba f406bf13

+79 -55
+15 -20
src/http/landing/assets.zig
··· 41 41 \\ width: 28px; height: 28px; display: inline-grid; place-items: center; border: 1px solid var(--line); 42 42 \\ border-radius: 8px; background: var(--panel); 43 43 \\} 44 - \\.settings span { 45 - \\ width: 14px; height: 14px; display: block; border: 2px solid currentColor; border-radius: 999px; 46 - \\ position: relative; 47 - \\} 48 - \\.settings span::before, .settings span::after { 49 - \\ content: ""; position: absolute; inset: -5px 4px; border-top: 2px solid currentColor; border-bottom: 2px solid currentColor; 50 - \\} 51 - \\.settings span::after { transform: rotate(90deg); } 44 + \\.settings svg { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; } 52 45 \\.hero { margin: 52px 0 24px; } 53 46 \\h1 { margin: 0; color: var(--text); font-size: clamp(36px, 11vw, 74px); line-height: 0.95; letter-spacing: 0; } 54 47 \\.lede { margin: 14px 0 0; max-width: 47ch; color: var(--muted); font-size: 15px; } 55 48 \\.lede a, .lede code { color: var(--green); } 56 49 \\.pulse { 57 - \\ display: grid; grid-template-columns: 1fr; gap: 10px; margin: 0 0 30px; padding: 14px; 50 + \\ margin: 0 0 30px; padding: 14px; 58 51 \\ border: 1px solid var(--line); border-radius: 8px; background: 59 52 \\ linear-gradient(135deg, color-mix(in srgb, var(--green) 12%, transparent), transparent 46%), 60 53 \\ linear-gradient(315deg, color-mix(in srgb, var(--pink) 10%, transparent), transparent 44%), ··· 67 60 \\.pulse-path { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); } 68 61 \\.pulse-path code { color: var(--accent); } 69 62 \\.pulse-link { 70 - \\ display: block; max-width: 100%; color: var(--text); text-decoration: none; 63 + \\ display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px 12px; 64 + \\ max-width: 100%; margin-top: 3px; color: var(--text); text-decoration: none; 71 65 \\ overflow-wrap: anywhere; word-break: break-word; line-height: 1.35; 72 66 \\} 67 + \\.pulse-link span { min-width: 0; } 73 68 \\.pulse-link:hover code { color: var(--green); } 74 69 \\.pulse-link code { color: var(--accent); } 70 + \\.pulse-link em { justify-self: end; padding: 2px 7px; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); font-size: 10px; font-style: normal; white-space: nowrap; background: color-mix(in srgb, var(--panel) 78%, transparent); } 71 + \\.pulse-link:hover em { color: var(--text); border-color: var(--green); } 75 72 \\.pulse-cid { color: var(--dim); font-size: 11px; overflow-wrap: anywhere; word-break: break-word; line-height: 1.35; } 76 - \\.pulse-spark { display: grid; grid-template-columns: repeat(18, minmax(0, 1fr)); gap: 2px; min-height: 20px; align-items: end; min-width: 0; } 77 - \\.pulse-spark i { display: block; min-height: 3px; border-radius: 999px 999px 0 0; background: linear-gradient(180deg, var(--accent), var(--green)); opacity: 0.32; } 78 - \\.pulse-spark i.hot { opacity: 1; background: linear-gradient(180deg, var(--pink), var(--accent)); } 79 73 \\.grid { display: grid; gap: 28px; } 80 74 \\section { margin-top: 0; } 81 75 \\.section-title { margin: 0 0 10px; color: var(--dim); font-size: 12px; font-weight: 400; text-transform: lowercase; } 82 76 \\.residents { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line); } 83 - \\.resident a { display: grid; grid-template-columns: 42px minmax(0, 1fr); gap: 12px; padding: 14px 0; border-bottom: 1px solid var(--line); text-decoration: none; } 77 + \\.resident a { display: grid; grid-template-columns: 42px minmax(0, 1fr); gap: 11px 12px; padding: 14px 0; border-bottom: 1px solid var(--line); text-decoration: none; } 84 78 \\.resident a:hover .handle { color: var(--accent); } 85 79 \\.avatar { 86 80 \\ width: 42px; height: 42px; display: grid; place-items: center; border: 1px solid var(--line); 87 81 \\ border-radius: 8px; background: linear-gradient(135deg, var(--panel-2), color-mix(in srgb, var(--green) 18%, var(--panel))); 88 - \\ color: var(--text); text-transform: lowercase; 82 + \\ color: var(--text); text-transform: lowercase; overflow: hidden; 89 83 \\} 84 + \\.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; } 90 85 \\.who { min-width: 0; } 91 86 \\.handle, .did { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 92 87 \\.handle { color: var(--text); font-size: 14px; } 93 88 \\.did { color: var(--dim); font-size: 11px; } 94 - \\.meta { grid-column: 2; display: flex; flex-wrap: wrap; gap: 8px; color: var(--muted); font-size: 11px; } 95 - \\.meta span { padding: 2px 6px; border: 1px solid var(--line); border-radius: 999px; background: var(--panel); } 89 + \\.meta { grid-column: 2; display: flex; align-items: center; gap: 9px; color: var(--dim); font-size: 11px; line-height: 1; } 90 + \\.meta span { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; } 91 + \\.meta span + span::before { content: ""; width: 4px; height: 4px; border-radius: 999px; background: var(--line); } 96 92 \\.collections { display: grid; gap: 9px; list-style: none; margin: 0; padding: 0; } 97 93 \\.collection { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 10px 0 12px; border-bottom: 1px solid var(--line); overflow: hidden; } 98 94 \\.collection strong { display: block; color: var(--text); font-size: 13px; font-weight: 650; } ··· 119 115 \\.client img { width: 26px; height: 26px; border-radius: 6px; object-fit: cover; } 120 116 \\@media (min-width: 680px) { 121 117 \\ .shell { padding-top: 24px; } 122 - \\ .pulse { grid-template-columns: minmax(0, 1fr) 152px; align-items: end; } 123 118 \\ .pulse-link, .pulse-cid { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; word-break: normal; } 124 119 \\ .grid { grid-template-columns: minmax(0, 1.05fr) minmax(260px, 0.95fr); align-items: start; } 125 - \\ .resident a { grid-template-columns: 42px minmax(0, 1fr); align-items: center; } 120 + \\ .resident a { grid-template-columns: 42px minmax(0, 1fr) auto; align-items: center; } 126 121 \\ .meta { grid-column: auto; justify-content: flex-end; } 127 122 \\ .sheet { align-items: center; } 128 123 \\} ··· 136 131 \\ { value: 'blacksky', label: 'Blacksky', icon: 'https://blacksky.community/static/apple-touch-icon.png', url: function(id) { return 'https://blacksky.community/profile/' + encodeURIComponent(id); } }, 137 132 \\ { value: 'witchsky', label: 'Witchsky', icon: 'https://witchsky.app/favicon.ico', url: function(id) { return 'https://witchsky.app/profile/' + encodeURIComponent(id); } }, 138 133 \\ { value: 'reddwarf', label: 'Red Dwarf', icon: 'https://reddwarf.app/redstar.png', url: function(id) { return 'https://reddwarf.app/profile/' + encodeURIComponent(id); } }, 139 - \\ { value: 'pdsls', label: 'PDSls', icon: 'https://pdsls.dev/favicon.ico', url: function(id) { return 'https://pdsls.dev/at/' + encodeURIComponent(id); } } 134 + \\ { value: 'pdsls', label: 'PDSls', icon: 'https://pdsls.dev/favicon.ico', url: function(id) { return 'https://pdsls.dev/at/' + id; } } 140 135 \\ ]; 141 136 \\ function read() { try { return localStorage.getItem(key); } catch (_) { return null; } } 142 137 \\ function write(value) { try { localStorage.setItem(key, value); } catch (_) {} }
+38 -35
src/http/landing/mod.zig
··· 1 1 const std = @import("std"); 2 2 const assets = @import("assets.zig"); 3 + const config = @import("../../core/config.zig"); 3 4 const store = @import("../../storage/store.zig"); 4 5 5 6 const http = std.http; ··· 41 42 for (residents) |resident| { 42 43 const handle = try escapeHtml(allocator, resident.handle); 43 44 const did = try escapeHtml(allocator, resident.did); 45 + const avatar = try renderAvatar(allocator, resident); 44 46 const status = if (resident.active) "active" else "paused"; 45 47 try accounts.writer.print( 46 48 \\<li class="resident"> 47 49 \\ <a href="https://bsky.app/profile/{s}" data-profile={f} rel="me"> 48 - \\ <span class="avatar" aria-hidden="true">{c}</span> 50 + \\ {s} 49 51 \\ <span class="who"> 50 52 \\ <span class="handle">{s}</span> 51 53 \\ <span class="did">{s}</span> ··· 53 55 \\ <span class="meta"><span>{s}</span><span>{d} records</span></span> 54 56 \\ </a> 55 57 \\</li> 56 - , .{ resident.did, std.json.fmt(resident.did, .{}), initialFor(resident.handle), handle, did, status, resident.record_count }); 58 + , .{ resident.did, std.json.fmt(resident.did, .{}), avatar, handle, did, status, resident.record_count }); 57 59 } 58 60 } 59 61 ··· 80 82 81 83 const recent_records = try store.listLandingRecentRecords(allocator, 8); 82 84 const pulse = try renderPulse(allocator, recent_records); 85 + const public_url = config.publicUrl(); 86 + const public_host = try escapeHtml(allocator, displayHost(public_url)); 83 87 84 88 return std.fmt.allocPrint(allocator, 85 89 \\<!doctype html> ··· 97 101 \\ <main class="shell"> 98 102 \\ <header> 99 103 \\ <a class="brand" href="/" aria-label="zds home">zds</a> 100 - \\ <nav aria-label="page controls"><button class="settings" type="button" id="settings" aria-label="settings" title="settings"><span aria-hidden="true"></span></button><a href="/xrpc/_health">health</a><a href="/xrpc/com.atproto.server.describeServer">describe</a></nav> 104 + \\ <nav aria-label="page controls"><button class="settings" type="button" id="settings" aria-label="settings" title="settings"><svg aria-hidden="true" viewBox="0 0 24 24"><path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="17" r="2"/></svg></button><a href="/xrpc/_health">health</a><a href="/xrpc/com.atproto.server.describeServer">describe</a></nav> 101 105 \\ </header> 102 106 \\ <section class="hero" aria-labelledby="title"> 103 - \\ <h1 id="title">pds.zat.dev</h1> 104 - \\ <p class="lede">an <a href="https://atproto.com">atproto</a> personal data server for <code>zat.dev</code>.</p> 107 + \\ <h1 id="title">{s}</h1> 108 + \\ <p class="lede">an <a href="https://atproto.com">atproto</a> personal data server, written in zig.</p> 105 109 \\ </section> 106 110 \\ {s} 107 111 \\ <div class="grid"> ··· 122 126 \\ </main> 123 127 \\ <div class="sheet" id="client-sheet" role="dialog" aria-modal="true" aria-labelledby="client-title"> 124 128 \\ <div class="sheet-card"> 125 - \\ <div class="sheet-head"><h2 id="client-title">preferred profile app</h2><button class="close" type="button" aria-label="close" id="close-client">x</button></div> 129 + \\ <div class="sheet-head"><h2 id="client-title">open profiles with</h2><button class="close" type="button" aria-label="close" id="close-client">x</button></div> 126 130 \\ <div class="client-body"> 127 - \\ <p class="hint">resident profile links open in the app you choose here.</p> 131 + \\ <p class="hint">choose where resident links open.</p> 128 132 \\ <div class="client-grid" id="client-grid"></div> 129 133 \\ </div> 130 134 \\ </div> ··· 132 136 \\ <script>{s}</script> 133 137 \\</body> 134 138 \\</html> 135 - , .{ assets.css, pulse, accounts.written(), collection_items.written(), assets.script }); 139 + , .{ assets.css, public_host, pulse, accounts.written(), collection_items.written(), assets.script }); 136 140 } 137 141 138 142 fn renderPulse(allocator: std.mem.Allocator, records: []const store.RecentRecord) ![]const u8 { ··· 140 144 return allocator.dupe(u8, 141 145 \\<section class="pulse" aria-label="repo signal"> 142 146 \\ <div><div class="pulse-kicker">repo signal</div><div class="pulse-row"><span class="pulse-path">waiting for first write.</span></div></div> 143 - \\ <div class="pulse-spark" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i><i></i></div> 144 147 \\</section> 145 148 ); 146 149 } ··· 149 152 const collection = try escapeHtml(allocator, newest.collection); 150 153 const rkey = try escapeHtml(allocator, newest.rkey); 151 154 const cid = try escapeHtml(allocator, newest.cid); 152 - const at_uri = try std.fmt.allocPrint(allocator, "at://{s}/{s}/{s}", .{ newest.did, newest.collection, newest.rkey }); 153 - const at_uri_url = try percentEncode(allocator, at_uri); 154 - var spark: std.Io.Writer.Allocating = .init(allocator); 155 - defer spark.deinit(); 156 - const bars = 18; 157 - var i: usize = 0; 158 - while (i < bars) : (i += 1) { 159 - const active = i < records.len; 160 - const height = 4 + ((newest.seq + i * 7) % 17); 161 - try spark.writer.print("<i{s} style=\"height:{d}px\"></i>", .{ if (active) " class=\"hot\"" else "", height }); 162 - } 155 + const did = try escapeHtml(allocator, newest.did); 163 156 164 157 return std.fmt.allocPrint(allocator, 165 158 \\<section class="pulse" aria-label="repo signal"> 166 159 \\ <div> 167 - \\ <div class="pulse-kicker">latest signed write</div> 168 - \\ <a class="pulse-link" href="https://pdsls.dev/at/{s}"><code>{s}</code>/{s}</a> 160 + \\ <div class="pulse-kicker">latest write</div> 161 + \\ <a class="pulse-link" href="https://pdsls.dev/at/{s}/{s}/{s}"> 162 + \\ <span><code>{s}</code>/{s}</span> 163 + \\ <em>open in PDSls</em> 164 + \\ </a> 169 165 \\ <div class="pulse-cid">{s}</div> 170 166 \\ </div> 171 - \\ <div class="pulse-spark" aria-hidden="true">{s}</div> 172 167 \\</section> 173 - , .{ at_uri_url, collection, rkey, cid, spark.written() }); 168 + , .{ did, collection, rkey, collection, rkey, cid }); 169 + } 170 + 171 + fn renderAvatar(allocator: std.mem.Allocator, resident: store.Resident) ![]const u8 { 172 + const avatar_cid = try store.profileAvatarCid(allocator, resident.did) orelse { 173 + return std.fmt.allocPrint(allocator, "<span class=\"avatar\" aria-hidden=\"true\">{c}</span>", .{initialFor(resident.handle)}); 174 + }; 175 + const did = try escapeHtml(allocator, resident.did); 176 + const cid = try escapeHtml(allocator, avatar_cid); 177 + return std.fmt.allocPrint( 178 + allocator, 179 + "<span class=\"avatar\"><img src=\"/xrpc/com.atproto.sync.getBlob?did={s}&cid={s}\" alt=\"\"></span>", 180 + .{ did, cid }, 181 + ); 174 182 } 175 183 176 184 fn collectionLabel(collection: []const u8) []const u8 { ··· 204 212 return out.toOwnedSlice(); 205 213 } 206 214 207 - fn percentEncode(allocator: std.mem.Allocator, text: []const u8) ![]const u8 { 208 - var out: std.Io.Writer.Allocating = .init(allocator); 209 - defer out.deinit(); 210 - for (text) |c| { 211 - if (std.ascii.isAlphanumeric(c) or c == '-' or c == '_' or c == '.' or c == '~') { 212 - try out.writer.writeByte(c); 213 - } else { 214 - try out.writer.print("%{X:0>2}", .{c}); 215 - } 216 - } 217 - return out.toOwnedSlice(); 215 + fn displayHost(public_url: []const u8) []const u8 { 216 + var value = public_url; 217 + if (std.mem.startsWith(u8, value, "https://")) value = value["https://".len..]; 218 + if (std.mem.startsWith(u8, value, "http://")) value = value["http://".len..]; 219 + if (std.mem.indexOfScalar(u8, value, '/')) |idx| value = value[0..idx]; 220 + return value; 218 221 }
+26
src/storage/store.zig
··· 288 288 return residents.toOwnedSlice(allocator); 289 289 } 290 290 291 + pub fn profileAvatarCid(allocator: std.mem.Allocator, did: []const u8) !?[]const u8 { 292 + mutex.lockUncancelable(store_io); 293 + defer mutex.unlock(store_io); 294 + try requireInitialized(); 295 + 296 + const row = try conn.row( 297 + \\SELECT value_json 298 + \\FROM records 299 + \\WHERE did = ? AND collection = 'app.bsky.actor.profile' AND rkey = 'self' 300 + \\LIMIT 1 301 + , .{did}); 302 + if (row == null) return null; 303 + defer row.?.deinit(); 304 + 305 + var parsed = std.json.parseFromSlice(std.json.Value, allocator, row.?.text(0), .{}) catch return null; 306 + defer parsed.deinit(); 307 + if (parsed.value != .object) return null; 308 + const avatar = parsed.value.object.get("avatar") orelse return null; 309 + if (avatar != .object) return null; 310 + const ref = avatar.object.get("ref") orelse return null; 311 + if (ref != .object) return null; 312 + const link = ref.object.get("$link") orelse return null; 313 + if (link != .string) return null; 314 + return try allocator.dupe(u8, link.string); 315 + } 316 + 291 317 pub fn listCollectionSummaries(allocator: std.mem.Allocator, limit: usize) ![]CollectionSummary { 292 318 mutex.lockUncancelable(store_io); 293 319 defer mutex.unlock(store_io);