From c344489275666cf4e6c06c0f7f8e7705b563dd68 Mon Sep 17 00:00:00 2001
From: tri <tri@thac.loan>
Date: Sat, 11 Oct 2025 22:52:23 +0700
Subject: [PATCH] bail early if metadata not found; add readme

---
 Makefile                             |  6 +++++
 build.zig                            |  1 -
 readme.md                            | 33 +++++++++++++++++++++++++++-
 sample/{meta.json => _loa_meta.json} |  0
 sample/second/meta.json              |  4 ----
 src/Meta.zig                         |  4 +++-
 src/main.zig                         | 26 +++++++++++++++++-----
 7 files changed, 61 insertions(+), 13 deletions(-)
 rename sample/{meta.json => _loa_meta.json} (100%)
 delete mode 100644 sample/second/meta.json

diff --git a/Makefile b/Makefile
index a359cf5..87612c5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,9 @@
+build:
+	zig build -Doptimize=ReleaseSafe
+
+watch:
+	zig build run --watch -- sample
+
 test:
 	zig build test --watch
 
diff --git a/build.zig b/build.zig
index 6d3b5df..c909164 100644
--- a/build.zig
+++ b/build.zig
@@ -17,7 +17,6 @@ pub fn build(b: *std.Build) void {
     const lua_dep = b.dependency("zlua", .{
         .target = target,
         .optimize = optimize,
-        .shared = true,
     });
     exe.root_module.addImport("zlua", lua_dep.module("zlua"));
 
diff --git a/readme.md b/readme.md
index 6972efc..ffa6e7e 100644
--- a/readme.md
+++ b/readme.md
@@ -1 +1,32 @@
-WIP: static blog generator
+loa (Vietnamese, noun): megaphone
+
+## What
+
+Loa is a dead simple static site generator that I use for my blog at <https://loa.thac.loan>.
+
+````sh
+mkdir mysite
+cd mysite
+
+# Write site metadata
+cat > _loa_meta.json << EOF
+{
+  "site_name": "Loa Sample Site",
+  "tagline": "Who samples the samplers?"
+}
+EOF
+
+# Create a post.
+# Loa assumes frontmatter followed by djot markup: https://djot.net/
+mkdir first
+cat > first/index.dj << 'EOF'
+```
+title: First
+published_at: 2025-10-09T21:13:03+07:00
+```
+
+## This is my very dope blog post
+
+A quick brown fox jumps over a lazy dog.
+EOF
+````
diff --git a/sample/meta.json b/sample/_loa_meta.json
similarity index 100%
rename from sample/meta.json
rename to sample/_loa_meta.json
diff --git a/sample/second/meta.json b/sample/second/meta.json
deleted file mode 100644
index 9261e08..0000000
--- a/sample/second/meta.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-  "title": "Secondly",
-  "published_at": "2025-10-09T23:51:45+07:00"
-}
diff --git a/src/Meta.zig b/src/Meta.zig
index 1bead20..d90b7fe 100644
--- a/src/Meta.zig
+++ b/src/Meta.zig
@@ -3,13 +3,15 @@ const fs = std.fs;
 const json = std.json;
 const mem = std.mem;
 
+pub const file_path = "_loa_meta.json";
+
 const Meta = @This();
 
 site_name: []const u8,
 tagline: []const u8,
 
 pub fn load(arena: mem.Allocator, dir: fs.Dir) !Meta {
-    const meta_text = try dir.readFileAlloc(arena, "meta.json", 1024 * 16);
+    const meta_text = try dir.readFileAlloc(arena, file_path, 1024 * 16);
 
     return try json.parseFromSliceLeaky(Meta, arena, meta_text, .{
         .allocate = .alloc_if_needed,
diff --git a/src/main.zig b/src/main.zig
index 9c6fdd3..1aa8461 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -21,19 +21,34 @@ pub fn main() !u8 {
         },
     }
 
-    try entrypoint(target_dir);
-
-    return 0;
+    return try entrypoint(target_dir);
 }
 
-pub fn entrypoint(target_dir: [*:0]const u8) !void {
+pub fn entrypoint(target_dir: [*:0]const u8) !u8 {
     var gpa_state = std.heap.DebugAllocator(.{}){};
     const gpa = gpa_state.allocator();
     defer _ = gpa_state.deinit();
 
+    var global_arena_state = std.heap.ArenaAllocator.init(gpa);
+    defer global_arena_state.deinit();
+    const global_arena = global_arena_state.allocator();
+
     var dir = try fs.cwd().openDirZ(target_dir, .{ .iterate = true });
     defer dir.close();
 
+    const meta = Meta.load(global_arena, dir) catch |err| {
+        switch (err) {
+            error.FileNotFound => {
+                println(
+                    "Error: {s} not found. Please create it first.",
+                    .{Meta.file_path},
+                );
+                return 1;
+            },
+            else => return err,
+        }
+    };
+
     // Write static file(s)
     {
         var static_dir = try dir.makeOpenPath("_loa", .{});
@@ -123,8 +138,6 @@ pub fn entrypoint(target_dir: [*:0]const u8) !void {
     {
         defer _ = post_arena_state.reset(.retain_capacity);
 
-        const meta = try Meta.load(post_arena, dir);
-
         var home_file = try dir.createFile("index.html", .{});
         defer home_file.close();
 
@@ -178,6 +191,7 @@ pub fn entrypoint(target_dir: [*:0]const u8) !void {
         try templates.base_end(writer);
         try writer.flush();
     }
+    return 0;
 }
 
 test {
-- 
2.47.3

