size: 6 KiB

1const std = @import("std");
2const fs = std.fs;
3const os = std.os;
4const mem = std.mem;
5const t = std.testing;
6
7const println = @import("utils.zig").println;
8const Djot = @import("Djot.zig");
9const Post = @import("Post.zig");
10const templates = @import("templates.zig");
11const html = @import("html.zig");
12const Meta = @import("Meta.zig");
13const feed = @import("feed.zig");
14const datetime = @import("datetime.zig");
15
16pub fn main() !u8 {
17 var target_dir: [*:0]const u8 = undefined;
18 switch (os.argv.len) {
19 1 => target_dir = ".",
20 2 => target_dir = os.argv[1],
21 else => {
22 std.debug.print("Usage: loa [target_dir]\n", .{});
23 return 1;
24 },
25 }
26
27 return try entrypoint(target_dir, null);
28}
29
30pub fn entrypoint(target_dir: [*:0]const u8, custom_now: ?[]const u8) !u8 {
31 var timer = try std.time.Timer.start();
32 defer println("Took {d}ms", .{timer.read() / 1_000_000});
33
34 var gpa_state = std.heap.DebugAllocator(.{}){};
35 const gpa = gpa_state.allocator();
36 defer _ = gpa_state.deinit();
37
38 var global_arena_state = std.heap.ArenaAllocator.init(gpa);
39 defer global_arena_state.deinit();
40 const global_arena = global_arena_state.allocator();
41
42 const now =
43 if (custom_now) |cnow|
44 cnow
45 else
46 try datetime.now(global_arena);
47
48 var dir = try fs.cwd().openDirZ(target_dir, .{ .iterate = true });
49 defer dir.close();
50
51 const meta = Meta.load(global_arena, dir) catch |err| {
52 switch (err) {
53 error.FileNotFound => {
54 println(
55 "Error: {s} not found. Please create it first.",
56 .{Meta.file_path},
57 );
58 return 1;
59 },
60 else => return err,
61 }
62 };
63
64 // Write static file(s)
65 {
66 var static_dir = try dir.makeOpenPath("_loa", .{});
67 defer static_dir.close();
68
69 try static_dir.writeFile(.{
70 .sub_path = "style.css",
71 .data = @embedFile("static/style.css"),
72 });
73 try static_dir.writeFile(.{
74 .sub_path = "script.js",
75 .data = @embedFile("static/script.js"),
76 });
77 }
78
79 const djot = try Djot.init(gpa);
80 defer djot.deinit();
81
82 var walker = try dir.walk(gpa);
83 defer walker.deinit();
84
85 var posts: std.ArrayList(Post) = try .initCapacity(gpa, 16);
86 defer {
87 for (posts.items) |p| {
88 p.deinit(gpa);
89 }
90 posts.deinit(gpa);
91 }
92
93 var post_arena_state = std.heap.ArenaAllocator.init(gpa);
94 defer post_arena_state.deinit();
95 const post_arena = post_arena_state.allocator();
96
97 // Collect posts and generate their respective pages one after another.
98 var djot_buf: [1024 * 16]u8 = undefined;
99 var writer_buf: [1024 * 4]u8 = undefined;
100 while (try walker.next()) |entry| {
101 defer _ = post_arena_state.reset(.retain_capacity);
102
103 if (entry.kind == .file and mem.eql(u8, entry.basename, "index.dj")) {
104 const post_text = try entry.dir.readFile(entry.basename, &djot_buf);
105
106 const p = try Post.init(gpa, post_text, entry.path);
107 try posts.append(gpa, p);
108
109 var html_file = try entry.dir.createFile("index.html", .{});
110 defer html_file.close();
111
112 var html_writer = html_file.writer(&writer_buf);
113 const writer = &html_writer.interface;
114
115 try templates.base_start(post_arena, writer, .{
116 .title = p.title,
117 .opengraph = .{
118 .title = p.title,
119 .description = p.djot_text[0..@min(p.djot_text.len, 256)],
120 },
121 });
122
123 try writer.writeAll(
124 \\<a href="/">« back to home</a>
125 \\
126 );
127 try writer.print(
128 \\<span class="post--published-at">
129 \\ published
130 \\ <time class="relative" datetime="{0s}" title="{0s}">{0s}</time>
131 \\</span>
132 \\
133 , .{
134 try html.escapeAlloc(post_arena, p.published_at),
135 });
136
137 try writer.writeAll("<h1 class=\"post--title\">");
138 try html.escape(writer, p.title);
139 try writer.writeAll("</h1>");
140
141 try djot.writeHtml(&html_writer.interface, p.djot_text);
142
143 try templates.base_end(writer);
144 try writer.flush();
145 }
146 }
147 println("Found {d} posts", .{posts.items.len});
148
149 // Sort posts by latest first
150 mem.sortUnstable(Post, posts.items, {}, Post.newestFirst);
151
152 // Generate atom feed
153 {
154 defer _ = post_arena_state.reset(.retain_capacity);
155
156 var feed_file = try dir.createFile(feed.feed_path, .{});
157 defer feed_file.close();
158
159 var buf: [4096]u8 = undefined;
160 var feed_writer = feed_file.writer(&buf);
161 const writer = &feed_writer.interface;
162
163 try feed.atom(.{
164 .arena = post_arena,
165 .writer = writer,
166 .meta = meta,
167 .posts = posts.items,
168 .now = now,
169 });
170
171 try writer.flush();
172 }
173
174 // Generate home page
175 {
176 defer _ = post_arena_state.reset(.retain_capacity);
177
178 var home_file = try dir.createFile("index.html", .{});
179 defer home_file.close();
180
181 var home_buf: [4096]u8 = undefined;
182 var file_writer = home_file.writer(&home_buf);
183 const writer = &file_writer.interface;
184
185 try templates.base_start(post_arena, writer, .{
186 .opengraph = .{
187 .title = "loa",
188 .description = "shouting into the void",
189 },
190 });
191
192 try writer.writeAll(
193 \\<h1 style="margin-bottom:0">
194 \\
195 );
196 try html.escape(writer, meta.site_name);
197 try writer.writeAll(
198 \\</h1>
199 \\
200 );
201
202 try writer.writeAll("<div class=\"tagline\">");
203 try html.escape(writer, meta.tagline);
204 try writer.writeAll("</div>\n");
205
206 try writer.writeAll(
207 \\<ul class="home--post-list">
208 \\
209 );
210 for (posts.items) |p| {
211 try writer.print(
212 \\<li>
213 \\ <a href="/{0s}/">{1s}</a>
214 \\ <time class="home--post-published-at relative" datetime="{2s}" title="{2s}">{2s}</time>
215 \\</li>
216 \\
217 , .{
218 try html.escapeAlloc(post_arena, p.slug),
219 try html.escapeAlloc(post_arena, p.title),
220 try html.escapeAlloc(post_arena, p.published_at),
221 });
222 }
223 try writer.writeAll(
224 \\</ul>
225 \\
226 );
227
228 try templates.base_end(writer);
229 try writer.flush();
230 }
231 return 0;
232}
233
234test {
235 std.testing.refAllDeclsRecursive(@This());
236}
237
238test entrypoint {
239 const status = try entrypoint("sample", "2025-11-12T03:04:05+06:00");
240 try t.expectEqual(0, status);
241}