···
let export_merged_feed state ~title ~format ?limit () =
let all_posts = get_all_posts state ?limit () in
576
-
(* Rewrite author metadata from Sortal user info *)
577
-
let rewrite_entry_author username (entry : Syndic.Atom.entry) =
578
-
match Storage.get_user state username with
576
+
(* Rewrite author metadata from Sortal user info and replace tags with River categories *)
577
+
let rewrite_entry_author_and_categories username (entry : Syndic.Atom.entry) =
578
+
let entry = match Storage.get_user state username with
(* Get user's full name and email from Sortal *)
···
(* Update entry with new author, keeping existing contributors *)
let _, other_authors = entry.authors in
{ entry with authors = (new_author, other_authors) }
600
+
(* Replace original blog tags with River categories *)
601
+
let post_id = Uri.to_string entry.id in
602
+
let river_category_ids = get_post_categories state ~post_id in
603
+
(* Deduplicate category IDs and create Atom categories *)
604
+
let unique_category_ids = List.sort_uniq String.compare river_category_ids in
605
+
let river_categories = List.filter_map (fun cat_id ->
606
+
match get_category state ~id:cat_id with
607
+
| Some cat -> Some (Syndic.Atom.category ~label:(Category.name cat) cat_id)
609
+
) unique_category_ids in
611
+
{ entry with categories = river_categories }
let entries = List.map (fun (username, entry) ->
601
-
rewrite_entry_author username entry
615
+
rewrite_entry_author_and_categories username entry
···
String.concat "" (List.map Syndic.XML.to_string nodes)
| Some (Syndic.Atom.Mime _) | Some (Syndic.Atom.Src _) | None -> ""
696
-
let author, _ = entry.authors in
697
-
let tags = List.map (fun (c : Syndic.Atom.category) -> c.term) entry.categories in
710
+
(* Get author name from Sortal, fallback to entry author *)
711
+
let author_name = match Sortal.lookup state.sortal username with
712
+
| Some contact -> Sortal.Contact.name contact
714
+
let author, _ = entry.authors in
717
+
(* Don't use original blog tags - River categories will be fetched separately *)
let post_id = Uri.to_string entry.id in
699
-
(username, title, author.name, entry.updated, link_uri, content_html, tags, post_id)
719
+
(username, title, author_name, entry.updated, link_uri, content_html, [], post_id)
···
i >= start_idx && i < start_idx + posts_per_page
725
-
let post_htmls = List.map (fun (username, title, _feed_author, date, link, content, tags, post_id) ->
745
+
let post_htmls = List.map (fun (username, title, _feed_author, date, link, content, _tags, post_id) ->
Log.debug (fun m -> m " Processing post: %s by @%s" title username);
(* Get author name from Sortal, fallback to username *)
···
let excerpt = Format.Html.post_excerpt_from_html content ~max_length:300 in
let full_content = Format.Html.full_content_from_html content in
746
-
(* Get custom categories for this post *)
747
-
let custom_category_ids = get_post_categories state ~post_id in
748
-
let custom_categories = List.filter_map (fun cat_id ->
766
+
(* Get River categories for this post *)
767
+
let river_category_ids = get_post_categories state ~post_id in
768
+
let river_categories = List.filter_map (fun cat_id ->
match get_category state ~id:cat_id with
| Some cat -> Some (Category.id cat, Category.name cat)
752
-
) custom_category_ids in
772
+
) river_category_ids in
754
-
(* Combine feed tags and custom categories *)
755
-
let all_tags = tags @ List.map fst custom_categories in
774
+
(* Display only River categories *)
757
-
match all_tags with
776
+
match river_categories with
760
-
(* Display feed tags *)
761
-
let tag_links = List.map (fun tag ->
762
-
Printf.sprintf {|<a href="categories/%s.html" class="tag-feed">%s</a>|}
763
-
(Format.Html.html_escape (sanitize_filename tag)) (Format.Html.html_escape tag)
765
-
(* Display custom categories with different styling *)
let category_links = List.map (fun (cat_id, cat_name) ->
767
-
Printf.sprintf {|<a href="categories/%s.html" class="tag-custom">%s</a>|}
780
+
Printf.sprintf {|<a href="categories/%s.html">%s</a>|}
(Format.Html.html_escape (sanitize_filename cat_id)) (Format.Html.html_escape cat_name)
769
-
) custom_categories in
770
-
Printf.sprintf {|<div class="post-tags">%s%s</div>|}
771
-
(String.concat "" tag_links)
782
+
) river_categories in
783
+
Printf.sprintf {|<div class="post-tags">%s</div>|}
(String.concat "" category_links)
786
+
let tags_and_actions =
787
+
if tags_html = "" then
788
+
{|<a href="#" class="read-more">Read more</a>|}
790
+
Printf.sprintf {|<div class="post-tags-and-actions"><a href="#" class="read-more">Read more</a>%s</div>|}
let thumbnail_html = match get_author_thumbnail username with
Printf.sprintf {|<a href="authors/%s.html"><img src="%s" alt="%s" class="author-thumbnail"></a>|}
···
<div class="post-full-content">
795
-
<a href="#" class="read-more">Read more</a>
···
···
i >= start_idx && i < start_idx + posts_per_page
1054
-
let post_htmls = List.map (fun (_username, title, author, date, link, content, tags, post_id) ->
1072
+
let post_htmls = List.map (fun (_username, title, author, date, link, content, _tags, post_id) ->
let date_str = Format.Html.format_date date in
let link_html = match link with
···
let excerpt = Format.Html.post_excerpt_from_html content ~max_length:300 in
let full_content = Format.Html.full_content_from_html content in
1066
-
(* Get custom categories for this post *)
1067
-
let custom_category_ids = get_post_categories state ~post_id in
1068
-
let custom_categories = List.filter_map (fun cat_id ->
1084
+
(* Get River categories for this post *)
1085
+
let river_category_ids = get_post_categories state ~post_id in
1086
+
let river_categories = List.filter_map (fun cat_id ->
match get_category state ~id:cat_id with
| Some cat -> Some (Category.id cat, Category.name cat)
1072
-
) custom_category_ids in
1090
+
) river_category_ids in
1092
+
(* Display only River categories *)
1075
-
let all_tags_exist = tags <> [] || custom_categories <> [] in
1076
-
if not all_tags_exist then ""
1094
+
match river_categories with
1097
+
let category_links = List.map (fun (cat_id, cat_name) ->
1098
+
Printf.sprintf {|<a href="../categories/%s.html">%s</a>|}
1099
+
(Format.Html.html_escape (sanitize_filename cat_id)) (Format.Html.html_escape cat_name)
1100
+
) river_categories in
1101
+
Printf.sprintf {|<div class="post-tags">%s</div>|}
1102
+
(String.concat "" category_links)
1104
+
let tags_and_actions =
1105
+
if tags_html = "" then
1106
+
{|<a href="#" class="read-more">Read more</a>|}
1078
-
(* Display feed tags *)
1079
-
let tag_links = List.map (fun tag ->
1080
-
Printf.sprintf {|<a href="../categories/%s.html" class="tag-feed">%s</a>|}
1081
-
(Format.Html.html_escape (sanitize_filename tag)) (Format.Html.html_escape tag)
1083
-
(* Display custom categories with different styling *)
1084
-
let category_links = List.map (fun (cat_id, cat_name) ->
1085
-
Printf.sprintf {|<a href="../categories/%s.html" class="tag-custom">%s</a>|}
1086
-
(Format.Html.html_escape (sanitize_filename cat_id)) (Format.Html.html_escape cat_name)
1087
-
) custom_categories in
1088
-
Printf.sprintf {|<div class="post-tags">%s%s</div>|}
1089
-
(String.concat "" tag_links)
1090
-
(String.concat "" category_links)
1108
+
Printf.sprintf {|<div class="post-tags-and-actions"><a href="#" class="read-more">Read more</a>%s</div>|}
Printf.sprintf {|<article class="post">
<h2 class="post-title">%s</h2>
···
<div class="post-full-content">
1103
-
<a href="#" class="read-more">Read more</a>
···
let posts_with_header = author_header ^ "\n" ^ String.concat "\n" post_htmls in
···
i >= start_idx && i < start_idx + posts_per_page
1207
-
let post_htmls = List.map (fun (username, title, author, date, link, content, tags, post_id) ->
1225
+
let post_htmls = List.map (fun (username, title, author, date, link, content, _tags, post_id) ->
let date_str = Format.Html.format_date date in
let link_html = match link with
···
let excerpt = Format.Html.post_excerpt_from_html content ~max_length:300 in
let full_content = Format.Html.full_content_from_html content in
1219
-
(* Get custom categories for this post *)
1220
-
let custom_category_ids = get_post_categories state ~post_id in
1221
-
let custom_categories = List.filter_map (fun cat_id ->
1237
+
(* Get River categories for this post *)
1238
+
let river_category_ids = get_post_categories state ~post_id in
1239
+
let river_categories = List.filter_map (fun cat_id ->
match get_category state ~id:cat_id with
| Some cat -> Some (Category.id cat, Category.name cat)
1225
-
) custom_category_ids in
1243
+
) river_category_ids in
1245
+
(* Display only River categories *)
1228
-
let all_tags_exist = tags <> [] || custom_categories <> [] in
1229
-
if not all_tags_exist then ""
1247
+
match river_categories with
1250
+
let category_links = List.map (fun (cat_id, cat_name) ->
1251
+
Printf.sprintf {|<a href="%s.html">%s</a>|}
1252
+
(Format.Html.html_escape (sanitize_filename cat_id)) (Format.Html.html_escape cat_name)
1253
+
) river_categories in
1254
+
Printf.sprintf {|<div class="post-tags">%s</div>|}
1255
+
(String.concat "" category_links)
1257
+
let tags_and_actions =
1258
+
if tags_html = "" then
1259
+
{|<a href="#" class="read-more">Read more</a>|}
1231
-
(* Display feed tags *)
1232
-
let tag_links = List.map (fun t ->
1233
-
Printf.sprintf {|<a href="%s.html" class="tag-feed">%s</a>|}
1234
-
(Format.Html.html_escape (sanitize_filename t)) (Format.Html.html_escape t)
1236
-
(* Display custom categories with different styling *)
1237
-
let category_links = List.map (fun (cat_id, cat_name) ->
1238
-
Printf.sprintf {|<a href="%s.html" class="tag-custom">%s</a>|}
1239
-
(Format.Html.html_escape (sanitize_filename cat_id)) (Format.Html.html_escape cat_name)
1240
-
) custom_categories in
1241
-
Printf.sprintf {|<div class="post-tags">%s%s</div>|}
1242
-
(String.concat "" tag_links)
1243
-
(String.concat "" category_links)
1261
+
Printf.sprintf {|<div class="post-tags-and-actions"><a href="#" class="read-more">Read more</a>%s</div>|}
1264
+
(* Get thumbnail *)
1265
+
let thumbnail_html = match get_author_thumbnail username with
1266
+
| Some thumb_path ->
1267
+
Printf.sprintf {|<a href="../authors/%s.html"><img src="../%s" alt="%s" class="author-thumbnail"></a>|}
1268
+
(Format.Html.html_escape (sanitize_filename username))
1269
+
(Format.Html.html_escape thumb_path)
1270
+
(Format.Html.html_escape author)
1272
+
Printf.sprintf {|<a href="../authors/%s.html"><div class="author-thumbnail" style="background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%); color: white; display: flex; align-items: center; justify-content: center; font-size: 20px; font-weight: 700;">%s</div></a>|}
1273
+
(Format.Html.html_escape (sanitize_filename username))
1274
+
(String.uppercase_ascii (String.sub author 0 (min 1 (String.length author))))
Printf.sprintf {|<article class="post">
<h2 class="post-title">%s</h2>
1247
-
<div class="post-meta">
1248
-
By <a href="../authors/%s.html">%s</a> on %s
1279
+
<div class="post-meta-line">By <a href="../authors/%s.html">%s</a> · %s</div>
<div class="post-excerpt">
<div class="post-full-content">
1256
-
<a href="#" class="read-more">Read more</a>
(Format.Html.html_escape (sanitize_filename username))
(Format.Html.html_escape author)
let page_html = Format.Html.render_posts_page
···
Log.info (fun m -> m " Deduplicated to %d unique links" (List.length sorted_links));
1324
-
let items = List.map (fun (href, (link_text, username, author, post_title, post_link, date), all_entries) ->
1325
-
let date_str = Format.Html.format_date date in
1326
-
let display_text = if link_text = "" || link_text = href then href else link_text in
1327
-
let post_link_html = match post_link with
1329
-
Printf.sprintf {|<a href="%s">%s</a>|}
1330
-
(Format.Html.html_escape (Uri.to_string uri))
1331
-
(Format.Html.html_escape post_title)
1332
-
| None -> Format.Html.html_escape post_title
1354
+
let items = List.map (fun (href, (_link_text, _username, _author, _post_title, _post_link, _date), all_entries) ->
1355
+
(* Parse URL to extract domain and path *)
1356
+
let uri = Uri.of_string href in
1357
+
let domain = match Uri.host uri with
1359
+
| None -> "unknown"
1361
+
let path = Uri.path uri in
1362
+
let fragment = Uri.fragment uri in
1364
+
(* Shorten path if too long *)
1365
+
let shortened_path =
1366
+
let full_path = path ^ (match fragment with Some f -> "#" ^ f | None -> "") in
1367
+
if String.length full_path > 40 then
1368
+
let start = String.sub full_path 0 20 in
1369
+
let ending = String.sub full_path (String.length full_path - 17) 17 in
1370
+
start ^ "..." ^ ending
1334
-
let count_str = if List.length all_entries > 1 then
1335
-
Printf.sprintf " (mentioned in %d posts)" (List.length all_entries)
1375
+
let display_text =
1376
+
if shortened_path = "" || shortened_path = "/" then
1377
+
Printf.sprintf {|<span class="link-domain">%s</span>|}
1378
+
(Format.Html.html_escape domain)
1380
+
Printf.sprintf {|<span class="link-domain">%s</span><span class="link-path">%s</span>|}
1381
+
(Format.Html.html_escape domain)
1382
+
(Format.Html.html_escape shortened_path)
1385
+
(* Group all backlinks *)
1386
+
let backlinks_html = List.map (fun (_, _username, author, post_title, post_link, date) ->
1387
+
let date_str = Format.Html.format_date date in
1388
+
let post_link_html = match post_link with
1390
+
Printf.sprintf {|<a href="%s" title="%s by %s on %s">%s</a>|}
1391
+
(Format.Html.html_escape (Uri.to_string uri))
1392
+
(Format.Html.html_escape post_title)
1393
+
(Format.Html.html_escape author)
1395
+
(Format.Html.html_escape post_title)
1396
+
| None -> Format.Html.html_escape post_title
1398
+
Printf.sprintf {|<span class="link-backlink"><span class="link-backlink-icon">↩</span>%s</span>|}
1400
+
) all_entries |> String.concat "" in
Printf.sprintf {|<div class="link-item">
<div class="link-url"><a href="%s">%s</a></div>
1340
-
<div class="link-meta">From %s by <a href="authors/%s.html">%s</a> on %s%s</div>
1404
+
<div class="link-backlinks">%s</div>
(Format.Html.html_escape href)
1343
-
(Format.Html.html_escape display_text)
1345
-
(Format.Html.html_escape (sanitize_filename username))
1346
-
(Format.Html.html_escape author)