My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 254 lines 7.3 kB view raw
1(* Generate blog index.mld files and recent entries list *) 2 3let month_name = function 4 | 1 -> "January" | 2 -> "February" | 3 -> "March" | 4 -> "April" 5 | 5 -> "May" | 6 -> "June" | 7 -> "July" | 8 -> "August" 6 | 9 -> "September" | 10 -> "October" | 11 -> "November" | 12 -> "December" 7 | _ -> "Unknown" 8 9type post = { 10 year : int; 11 month : int; 12 slug : string; (* e.g., "claude-and-dune" *) 13 title : string; 14 published : string; (* e.g., "2025-12-18" *) 15 tags : string list; 16} 17 18let parse_post path = 19 let ic = open_in path in 20 let content = really_input_string ic (in_channel_length ic) in 21 close_in ic; 22 23 (* Extract title from {0 ...} *) 24 let title = 25 let re = Str.regexp "{0 \\([^}]+\\)}" in 26 if Str.string_match re content 0 then 27 Str.matched_group 1 content 28 else 29 Filename.basename path |> Filename.remove_extension 30 in 31 32 (* Extract published date from @published ... *) 33 let published = 34 let re = Str.regexp "@published \\([0-9-]+\\)" in 35 try 36 ignore (Str.search_forward re content 0); 37 Str.matched_group 1 content 38 with Not_found -> "" 39 in 40 41 (* Extract tags from @page-tags ... *) 42 let tags = 43 let re = Str.regexp "@page-tags \\([^\n]+\\)" in 44 try 45 ignore (Str.search_forward re content 0); 46 Str.matched_group 1 content 47 |> String.split_on_char ' ' 48 |> List.filter (fun s -> s <> "") 49 |> List.map String.lowercase_ascii 50 with Not_found -> [] 51 in 52 53 (* Parse path to get year/month/slug *) 54 let parts = String.split_on_char '/' path in 55 match parts with 56 | _ :: _ :: year_s :: month_s :: filename :: [] | _ :: year_s :: month_s :: filename :: [] -> 57 let year = int_of_string year_s in 58 let month = int_of_string month_s in 59 let slug = Filename.remove_extension filename in 60 Some { year; month; slug; title; published; tags } 61 | _ -> None 62 63let find_posts () = 64 let posts = ref [] in 65 let rec scan_dir dir = 66 let entries = Sys.readdir dir in 67 Array.iter (fun entry -> 68 let path = Filename.concat dir entry in 69 if entry.[0] <> '.' && Sys.is_directory path then 70 scan_dir path 71 else if Filename.extension entry = ".mld" && entry <> "index.mld" 72 && entry.[0] <> '.' then 73 match parse_post path with 74 | Some post when post.published <> "" -> posts := post :: !posts 75 | _ -> () 76 ) entries 77 in 78 scan_dir "blog"; 79 (* Sort by published date descending *) 80 List.sort (fun a b -> String.compare b.published a.published) !posts 81 82let post_link post = 83 let slug_needs_quotes = 84 String.exists (fun c -> c = '-' || c = '_') post.slug 85 in 86 let slug_fmt = 87 if slug_needs_quotes then 88 Printf.sprintf "page-\"%s\"" post.slug 89 else 90 Printf.sprintf "page-%s" post.slug 91 in 92 Printf.sprintf "{{!//blog/%04d/%02d/%s}%s}" 93 post.year post.month slug_fmt post.title 94 95let generate_month_index year month posts = 96 let month_posts = List.filter (fun p -> p.year = year && p.month = month) posts in 97 if month_posts = [] then None 98 else 99 let children = 100 month_posts 101 |> List.map (fun p -> p.slug) 102 |> String.concat " " 103 in 104 let post_links = 105 month_posts 106 |> List.map (fun p -> "- " ^ post_link p) 107 |> String.concat "\n" 108 in 109 let content = Printf.sprintf "@children_order %s\n\n{0 %s}\n\n%s\n" 110 children (month_name month) post_links 111 in 112 Some content 113 114let generate_year_index year posts = 115 let year_posts = List.filter (fun p -> p.year = year) posts in 116 if year_posts = [] then None 117 else 118 (* Get unique months, sorted descending *) 119 let months = 120 year_posts 121 |> List.map (fun p -> p.month) 122 |> List.sort_uniq (fun a b -> compare b a) 123 in 124 let children_order = 125 months 126 |> List.map (Printf.sprintf "%02d/") 127 |> String.concat " " 128 in 129 let post_links = 130 year_posts 131 |> List.map (fun p -> "- " ^ post_link p) 132 |> String.concat "\n" 133 in 134 let content = Printf.sprintf "@children_order %s\n\n{0 %d}\n\n%s\n" 135 children_order year post_links 136 in 137 Some content 138 139let generate_blog_index posts = 140 (* Get unique years, sorted descending *) 141 let years = 142 posts 143 |> List.map (fun p -> p.year) 144 |> List.sort_uniq (fun a b -> compare b a) 145 in 146 let children_order = 147 years 148 |> List.map (fun y -> Printf.sprintf "%d/" y) 149 |> String.concat " " 150 in 151 let post_items = 152 posts 153 |> List.map (fun p -> 154 Printf.sprintf "{- %s %s}" (post_link p) p.published) 155 |> String.concat "\n" 156 in 157 Printf.sprintf "@children_order %s\n\n{0 Blog}\n\n@recent-posts\n{ul\n%s\n}\n" 158 children_order post_items 159 160let ensure_dir path = 161 let dir = Filename.dirname path in 162 let rec mkdir_p d = 163 if d <> "." && d <> "/" && not (Sys.file_exists d) then begin 164 mkdir_p (Filename.dirname d); 165 Sys.mkdir d 0o755 166 end 167 in 168 mkdir_p dir 169 170let write_file path content = 171 let gen_path = Filename.concat "_blog_gen" path in 172 ensure_dir gen_path; 173 let oc = open_out gen_path in 174 output_string oc content; 175 close_out oc; 176 Printf.eprintf "Wrote %s\n" gen_path 177 178let () = 179 let posts = find_posts () in 180 Printf.eprintf "Found %d posts\n\n" (List.length posts); 181 182 (* Get unique year/month combinations *) 183 let year_months = 184 posts 185 |> List.map (fun p -> (p.year, p.month)) 186 |> List.sort_uniq compare 187 in 188 189 (* Generate month indexes *) 190 List.iter (fun (year, month) -> 191 match generate_month_index year month posts with 192 | Some content -> 193 let dir = Printf.sprintf "blog/%04d/%02d" year month in 194 let path = Filename.concat dir "index.mld" in 195 write_file path content 196 | None -> () 197 ) year_months; 198 199 (* Get unique years *) 200 let years = 201 posts 202 |> List.map (fun p -> p.year) 203 |> List.sort_uniq (fun a b -> compare b a) 204 in 205 206 (* Generate year indexes *) 207 List.iter (fun year -> 208 match generate_year_index year posts with 209 | Some content -> 210 let path = Printf.sprintf "blog/%04d/index.mld" year in 211 write_file path content 212 | None -> () 213 ) years; 214 215 (* Generate main blog index *) 216 let blog_index = generate_blog_index posts in 217 write_file "blog/index.mld" blog_index; 218 219 (* Generate tag pages *) 220 let all_tags = 221 posts 222 |> List.concat_map (fun p -> p.tags) 223 |> List.sort_uniq String.compare 224 in 225 List.iter (fun tag -> 226 let tagged = List.filter (fun p -> List.mem tag p.tags) posts in 227 (* Already sorted newest-first from find_posts *) 228 let post_items = 229 tagged 230 |> List.map (fun p -> Printf.sprintf "{- %s %s}" (post_link p) p.published) 231 |> String.concat "\n" 232 in 233 let content = Printf.sprintf 234 "{0 Tag: %s}\n\n@tagged-pages %s\n\n{ul\n%s\n}\n" tag tag post_items 235 in 236 write_file (Printf.sprintf "tags/%s.mld" tag) content 237 ) all_tags; 238 239 (* Generate tag index *) 240 let tag_links = 241 all_tags 242 |> List.map (fun tag -> 243 let needs_quotes = String.exists (fun c -> c = '-' || c = '_') tag in 244 let ref_fmt = 245 if needs_quotes then Printf.sprintf "page-\"%s\"" tag 246 else Printf.sprintf "page-%s" tag 247 in 248 Printf.sprintf "{- {{!%s}%s}}" ref_fmt tag) 249 |> String.concat "\n" 250 in 251 let tag_index = Printf.sprintf "{0 Tags}\n\nPages grouped by tag.\n\n{ul\n%s\n}\n" 252 tag_links 253 in 254 write_file "tags/index.mld" tag_index