From 734ac8d3102867e4cf0b042c5fdd7de6e7b4c3f8 Mon Sep 17 00:00:00 2001 From: zzstoatzz Date: Tue, 6 Jan 2026 02:44:00 -0600 Subject: [PATCH] feat: add devlog publication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- devlog/001-self-publishing-docs.md | 35 ++++++++++++++++++++++ scripts/publish-docs.zig | 48 +++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 devlog/001-self-publishing-docs.md diff --git a/devlog/001-self-publishing-docs.md b/devlog/001-self-publishing-docs.md new file mode 100644 index 0000000..af31425 --- /dev/null +++ b/devlog/001-self-publishing-docs.md @@ -0,0 +1,35 @@ +# zat publishes its own docs to ATProto + +zat uses itself to publish these docs as `site.standard.document` records. here's how. + +## the idea + +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). + +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. + +## what we built + +a zig script ([`scripts/publish-docs.zig`](https://tangled.sh/zat.dev/zat/tree/main/scripts/publish-docs.zig)) that: + +1. authenticates with the PDS via `com.atproto.server.createSession` +2. creates a `site.standard.publication` record +3. publishes each doc as a `site.standard.document` pointing to that publication +4. uses deterministic TIDs so records get the same rkey every time (idempotent updates) + +## the mechanics + +### TIDs + +timestamp identifiers. base32-sortable. we use a fixed base timestamp with incrementing clock_id so each doc gets a stable rkey: + +```zig +const pub_tid = zat.Tid.fromTimestamp(1704067200000000, 0); // publication +const doc_tid = zat.Tid.fromTimestamp(1704067200000000, i + 1); // docs get 1, 2, 3... +``` + +### CI + +[`.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. + +`putRecord` with the same rkey overwrites, so the CI job overwrites `standard.site` records when you cut a tag. \ No newline at end of file diff --git a/scripts/publish-docs.zig b/scripts/publish-docs.zig index c433fbc..9f6c67a 100644 --- a/scripts/publish-docs.zig +++ b/scripts/publish-docs.zig @@ -3,13 +3,20 @@ const zat = @import("zat"); const Allocator = std.mem.Allocator; +const DocEntry = struct { path: []const u8, file: []const u8 }; + /// docs to publish as site.standard.document records -const docs = [_]struct { path: []const u8, file: []const u8 }{ +const docs = [_]DocEntry{ .{ .path = "/", .file = "README.md" }, .{ .path = "/roadmap", .file = "docs/roadmap.md" }, .{ .path = "/changelog", .file = "CHANGELOG.md" }, }; +/// devlog entries +const devlog = [_]DocEntry{ + .{ .path = "/001", .file = "devlog/001-self-publishing-docs.md" }, +}; + pub fn main() !void { // use page_allocator for CLI tool - OS reclaims on exit const allocator = std.heap.page_allocator; @@ -77,6 +84,45 @@ pub fn main() !void { std.debug.print("published: {s} -> at://{s}/site.standard.document/{s}\n", .{ doc.file, session.did, tid.str() }); } + // devlog publication (clock_id 100 to separate from docs) + const devlog_tid = zat.Tid.fromTimestamp(1704067200000000, 100); + const devlog_pub = Publication{ + .url = "https://zat.dev/devlog", + .name = "zat devlog", + .description = "building zat in public", + }; + + try putRecord(&client, allocator, session.did, "site.standard.publication", devlog_tid.str(), devlog_pub); + std.debug.print("created publication: at://{s}/site.standard.publication/{s}\n", .{ session.did, devlog_tid.str() }); + + var devlog_uri_buf: std.ArrayList(u8) = .empty; + defer devlog_uri_buf.deinit(allocator); + try devlog_uri_buf.print(allocator, "at://{s}/site.standard.publication/{s}", .{ session.did, devlog_tid.str() }); + const devlog_uri = devlog_uri_buf.items; + + // publish devlog entries (clock_id 101, 102, ...) + for (devlog, 0..) |entry, i| { + const content = std.fs.cwd().readFileAlloc(allocator, entry.file, 1024 * 1024) catch |err| { + std.debug.print("warning: could not read {s}: {}\n", .{ entry.file, err }); + continue; + }; + defer allocator.free(content); + + const title = extractTitle(content) orelse entry.file; + const tid = zat.Tid.fromTimestamp(1704067200000000, @intCast(101 + i)); + + const doc_record = Document{ + .site = devlog_uri, + .title = title, + .path = entry.path, + .textContent = content, + .publishedAt = &now, + }; + + try putRecord(&client, allocator, session.did, "site.standard.document", tid.str(), doc_record); + std.debug.print("published: {s} -> at://{s}/site.standard.document/{s}\n", .{ entry.file, session.did, tid.str() }); + } + std.debug.print("done\n", .{}); } -- 2.51.2