atproto utils for zig zat.dev
atproto sdk zig

feat: add devlog publication

first entry: how zat publishes its own docs to ATProto

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

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

Changed files
+82 -1
devlog
scripts
+35
devlog/001-self-publishing-docs.md
··· 1 + # zat publishes its own docs to ATProto 2 + 3 + zat uses itself to publish these docs as `site.standard.document` records. here's how. 4 + 5 + ## the idea 6 + 7 + i'm working on [search for leaflet](https://leaflet-search.pages.dev/) and more generally, search for [standard.site](https://standard.site/) records. many are [currently thinking about how to facilitate better idea sharing on atproto right now](https://bsky.app/profile/eugenevinitsky.bsky.social/post/3mbpqpylv3s2e). 8 + 9 + this is me doing a rep of shipping a "standard.site", so i know what i'll be searching through, and to better understand why blogging platforms choose their schema extensions etc for i start indexing/searching their record types. 10 + 11 + ## what we built 12 + 13 + a zig script ([`scripts/publish-docs.zig`](https://tangled.sh/zat.dev/zat/tree/main/scripts/publish-docs.zig)) that: 14 + 15 + 1. authenticates with the PDS via `com.atproto.server.createSession` 16 + 2. creates a `site.standard.publication` record 17 + 3. publishes each doc as a `site.standard.document` pointing to that publication 18 + 4. uses deterministic TIDs so records get the same rkey every time (idempotent updates) 19 + 20 + ## the mechanics 21 + 22 + ### TIDs 23 + 24 + timestamp identifiers. base32-sortable. we use a fixed base timestamp with incrementing clock_id so each doc gets a stable rkey: 25 + 26 + ```zig 27 + const pub_tid = zat.Tid.fromTimestamp(1704067200000000, 0); // publication 28 + const doc_tid = zat.Tid.fromTimestamp(1704067200000000, i + 1); // docs get 1, 2, 3... 29 + ``` 30 + 31 + ### CI 32 + 33 + [`.tangled/workflows/publish-docs.yml`](https://tangled.sh/zat.dev/zat/tree/main/.tangled/workflows/publish-docs.yml) triggers on `v*` tags. tag a release, docs publish automatically. 34 + 35 + `putRecord` with the same rkey overwrites, so the CI job overwrites `standard.site` records when you cut a tag.
+47 -1
scripts/publish-docs.zig
··· 3 3 4 4 const Allocator = std.mem.Allocator; 5 5 6 + const DocEntry = struct { path: []const u8, file: []const u8 }; 7 + 6 8 /// docs to publish as site.standard.document records 7 - const docs = [_]struct { path: []const u8, file: []const u8 }{ 9 + const docs = [_]DocEntry{ 8 10 .{ .path = "/", .file = "README.md" }, 9 11 .{ .path = "/roadmap", .file = "docs/roadmap.md" }, 10 12 .{ .path = "/changelog", .file = "CHANGELOG.md" }, 13 + }; 14 + 15 + /// devlog entries 16 + const devlog = [_]DocEntry{ 17 + .{ .path = "/001", .file = "devlog/001-self-publishing-docs.md" }, 11 18 }; 12 19 13 20 pub fn main() !void { ··· 75 82 76 83 try putRecord(&client, allocator, session.did, "site.standard.document", tid.str(), doc_record); 77 84 std.debug.print("published: {s} -> at://{s}/site.standard.document/{s}\n", .{ doc.file, session.did, tid.str() }); 85 + } 86 + 87 + // devlog publication (clock_id 100 to separate from docs) 88 + const devlog_tid = zat.Tid.fromTimestamp(1704067200000000, 100); 89 + const devlog_pub = Publication{ 90 + .url = "https://zat.dev/devlog", 91 + .name = "zat devlog", 92 + .description = "building zat in public", 93 + }; 94 + 95 + try putRecord(&client, allocator, session.did, "site.standard.publication", devlog_tid.str(), devlog_pub); 96 + std.debug.print("created publication: at://{s}/site.standard.publication/{s}\n", .{ session.did, devlog_tid.str() }); 97 + 98 + var devlog_uri_buf: std.ArrayList(u8) = .empty; 99 + defer devlog_uri_buf.deinit(allocator); 100 + try devlog_uri_buf.print(allocator, "at://{s}/site.standard.publication/{s}", .{ session.did, devlog_tid.str() }); 101 + const devlog_uri = devlog_uri_buf.items; 102 + 103 + // publish devlog entries (clock_id 101, 102, ...) 104 + for (devlog, 0..) |entry, i| { 105 + const content = std.fs.cwd().readFileAlloc(allocator, entry.file, 1024 * 1024) catch |err| { 106 + std.debug.print("warning: could not read {s}: {}\n", .{ entry.file, err }); 107 + continue; 108 + }; 109 + defer allocator.free(content); 110 + 111 + const title = extractTitle(content) orelse entry.file; 112 + const tid = zat.Tid.fromTimestamp(1704067200000000, @intCast(101 + i)); 113 + 114 + const doc_record = Document{ 115 + .site = devlog_uri, 116 + .title = title, 117 + .path = entry.path, 118 + .textContent = content, 119 + .publishedAt = &now, 120 + }; 121 + 122 + try putRecord(&client, allocator, session.did, "site.standard.document", tid.str(), doc_record); 123 + std.debug.print("published: {s} -> at://{s}/site.standard.document/{s}\n", .{ entry.file, session.did, tid.str() }); 78 124 } 79 125 80 126 std.debug.print("done\n", .{});