+35
devlog/001-self-publishing-docs.md
+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
+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", .{});