···
let export_merged_feed state ~title ~format ?limit () =
let all_posts = get_all_posts state ?limit () in
+
(* Rewrite author metadata from Sortal user info and replace tags with River categories *)
+
let rewrite_entry_author_and_categories username (entry : Syndic.Atom.entry) =
+
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) }
+
(* Replace original blog tags with River categories *)
+
let post_id = Uri.to_string entry.id in
+
let river_category_ids = get_post_categories state ~post_id in
+
(* Deduplicate category IDs and create Atom categories *)
+
let unique_category_ids = List.sort_uniq String.compare river_category_ids in
+
let river_categories = List.filter_map (fun cat_id ->
+
match get_category state ~id:cat_id with
+
| Some cat -> Some (Syndic.Atom.category ~label:(Category.name cat) cat_id)
+
) unique_category_ids in
+
{ entry with categories = river_categories }
let entries = List.map (fun (username, entry) ->
+
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 -> ""
+
(* Get author name from Sortal, fallback to entry author *)
+
let author_name = match Sortal.lookup state.sortal username with
+
| Some contact -> Sortal.Contact.name contact
+
let author, _ = entry.authors in
+
(* Don't use original blog tags - River categories will be fetched separately *)
let post_id = Uri.to_string entry.id in
+
(username, title, author_name, entry.updated, link_uri, content_html, [], post_id)
···
i >= start_idx && i < start_idx + posts_per_page
+
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
+
(* Get River categories for this post *)
+
let river_category_ids = get_post_categories state ~post_id in
+
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)
+
) river_category_ids in
+
(* Display only River categories *)
+
match river_categories with
let category_links = List.map (fun (cat_id, cat_name) ->
+
Printf.sprintf {|<a href="categories/%s.html">%s</a>|}
(Format.Html.html_escape (sanitize_filename cat_id)) (Format.Html.html_escape cat_name)
+
Printf.sprintf {|<div class="post-tags">%s</div>|}
(String.concat "" category_links)
+
{|<a href="#" class="read-more">Read more</a>|}
+
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">
···
···
i >= start_idx && i < start_idx + posts_per_page
+
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
+
(* Get River categories for this post *)
+
let river_category_ids = get_post_categories state ~post_id in
+
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)
+
) river_category_ids in
+
(* Display only River categories *)
+
match river_categories with
+
let category_links = List.map (fun (cat_id, cat_name) ->
+
Printf.sprintf {|<a href="../categories/%s.html">%s</a>|}
+
(Format.Html.html_escape (sanitize_filename cat_id)) (Format.Html.html_escape cat_name)
+
Printf.sprintf {|<div class="post-tags">%s</div>|}
+
(String.concat "" category_links)
+
{|<a href="#" class="read-more">Read more</a>|}
+
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">
···
let posts_with_header = author_header ^ "\n" ^ String.concat "\n" post_htmls in
···
i >= start_idx && i < start_idx + posts_per_page
+
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
+
(* Get River categories for this post *)
+
let river_category_ids = get_post_categories state ~post_id in
+
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)
+
) river_category_ids in
+
(* Display only River categories *)
+
match river_categories with
+
let category_links = List.map (fun (cat_id, cat_name) ->
+
Printf.sprintf {|<a href="%s.html">%s</a>|}
+
(Format.Html.html_escape (sanitize_filename cat_id)) (Format.Html.html_escape cat_name)
+
Printf.sprintf {|<div class="post-tags">%s</div>|}
+
(String.concat "" category_links)
+
{|<a href="#" class="read-more">Read more</a>|}
+
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>|}
+
(Format.Html.html_escape (sanitize_filename username))
+
(Format.Html.html_escape thumb_path)
+
(Format.Html.html_escape author)
+
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>|}
+
(Format.Html.html_escape (sanitize_filename username))
+
(String.uppercase_ascii (String.sub author 0 (min 1 (String.length author))))
Printf.sprintf {|<article class="post">
<h2 class="post-title">%s</h2>
+
<div class="post-meta-line">By <a href="../authors/%s.html">%s</a> · %s</div>
<div class="post-excerpt">
<div class="post-full-content">
(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));
+
let items = List.map (fun (href, (_link_text, _username, _author, _post_title, _post_link, _date), all_entries) ->
+
(* Parse URL to extract domain and path *)
+
let uri = Uri.of_string href in
+
let domain = match Uri.host uri with
+
let path = Uri.path uri in
+
let fragment = Uri.fragment uri in
+
(* Shorten path if too long *)
+
let full_path = path ^ (match fragment with Some f -> "#" ^ f | None -> "") in
+
if String.length full_path > 40 then
+
let start = String.sub full_path 0 20 in
+
let ending = String.sub full_path (String.length full_path - 17) 17 in
+
if shortened_path = "" || shortened_path = "/" then
+
Printf.sprintf {|<span class="link-domain">%s</span>|}
+
(Format.Html.html_escape domain)
+
Printf.sprintf {|<span class="link-domain">%s</span><span class="link-path">%s</span>|}
+
(Format.Html.html_escape domain)
+
(Format.Html.html_escape shortened_path)
+
(* Group all backlinks *)
+
let backlinks_html = List.map (fun (_, _username, author, post_title, post_link, date) ->
+
let date_str = Format.Html.format_date date in
+
let post_link_html = match post_link with
+
Printf.sprintf {|<a href="%s" title="%s by %s on %s">%s</a>|}
+
(Format.Html.html_escape (Uri.to_string uri))
+
(Format.Html.html_escape post_title)
+
(Format.Html.html_escape author)
+
(Format.Html.html_escape post_title)
+
| None -> Format.Html.html_escape post_title
+
Printf.sprintf {|<span class="link-backlink"><span class="link-backlink-icon">↩</span>%s</span>|}
+
) all_entries |> String.concat "" in
Printf.sprintf {|<div class="link-item">
<div class="link-url"><a href="%s">%s</a></div>
+
<div class="link-backlinks">%s</div>
(Format.Html.html_escape href)