My agentic slop goes here. Not intended for anyone else!

river

Changed files
+237 -114
stack
+19 -2
stack/river/cmd/river_cmd.ml
···
) links;
Fmt.pr "@.");
-
(* Categories/Tags if verbose *)
+
(* River Categories - always show if any assigned *)
+
let river_category_ids = River.State.get_post_categories state ~post_id in
+
(match river_category_ids with
+
| [] -> ()
+
| cat_ids ->
+
Fmt.pr "%a@." Fmt.(styled `Cyan string) "River Categories:";
+
List.iter (fun cat_id ->
+
match River.State.get_category state ~id:cat_id with
+
| Some cat ->
+
Fmt.pr " - %a (%a)@."
+
Fmt.(styled (`Fg `Green) string) (River.Category.name cat)
+
Fmt.(styled `Faint string) cat_id
+
| None ->
+
Fmt.pr " - %a@." Fmt.(styled `Faint string) cat_id
+
) cat_ids;
+
Fmt.pr "@.");
+
+
(* Original blog tags if verbose *)
if verbose then begin
match entry.categories with
| [] -> ()
| categories ->
-
Fmt.pr "%a@." Fmt.(styled `Cyan string) "Tags:";
+
Fmt.pr "%a@." Fmt.(styled `Cyan string) "Original Blog Tags:";
List.iter (fun cat ->
Fmt.pr " - %s@." cat.Syndic.Atom.term
) categories;
+65 -19
stack/river/lib/format.ml
···
.author-thumbnail {
float: right;
-
width: 48px;
-
height: 48px;
+
width: 64px;
+
height: 64px;
border-radius: 50%;
object-fit: cover;
margin-left: 15px;
···
.read-more {
display: inline-block;
color: #0366d6;
-
font-size: 13px;
+
font-size: 11px;
cursor: pointer;
text-decoration: none;
-
margin-top: 8px;
-
padding: 4px 8px;
+
padding: 2px 8px;
border: 1px solid #e1e4e8;
border-radius: 3px;
background: #f6f8fa;
transition: background 0.2s;
+
margin-right: 6px;
+
vertical-align: middle;
}
.read-more:hover {
···
.read-more::after {
content: ' ▼';
-
font-size: 10px;
+
font-size: 9px;
}
.read-more.active::after {
···
margin-top: 8px;
font-size: 11px;
clear: both;
+
display: inline-block;
}
.post-tags a {
···
text-decoration: none;
margin-right: 4px;
margin-bottom: 4px;
+
vertical-align: middle;
+
font-size: 11px;
}
.post-tags a:hover {
background: #dbedff;
}
+
.post-tags-and-actions {
+
margin-top: 8px;
+
display: flex;
+
align-items: center;
+
clear: both;
+
}
+
.pagination {
margin-top: 30px;
padding-top: 15px;
···
}
.link-item {
-
margin-bottom: 15px;
-
padding-bottom: 12px;
-
border-bottom: 1px solid #e1e4e8;
+
margin-bottom: 6px;
+
padding: 4px 0;
+
border-bottom: 1px solid #f0f0f0;
+
display: flex;
+
align-items: baseline;
+
font-size: 13px;
+
line-height: 1.4;
}
.link-item:last-child {
···
}
.link-url {
-
font-size: 14px;
-
margin-bottom: 3px;
+
flex: 0 0 auto;
+
margin-right: 8px;
}
.link-url a {
color: #0366d6;
text-decoration: none;
-
word-break: break-all;
+
font-weight: 500;
}
.link-url a:hover {
text-decoration: underline;
}
-
.link-meta {
+
.link-domain {
+
color: #24292e;
+
font-weight: 500;
+
}
+
+
.link-path {
+
color: #586069;
+
font-weight: 400;
+
}
+
+
.link-backlinks {
+
flex: 1 1 auto;
font-size: 11px;
color: #586069;
+
display: flex;
+
flex-wrap: wrap;
+
gap: 6px;
}
-
.link-meta a {
+
.link-backlink {
+
display: inline-flex;
+
align-items: center;
+
gap: 3px;
+
}
+
+
.link-backlink a {
color: #586069;
text-decoration: none;
}
-
.link-meta a:hover {
+
.link-backlink a:hover {
color: #0366d6;
+
}
+
+
.link-backlink-icon {
+
color: #959da5;
+
font-size: 10px;
}
.author-list {
···
(* Convert markdown back to HTML using Cmarkit with custom renderer *)
let doc = Cmarkit.Doc.of_string excerpt_md in
-
(* Custom renderer that makes headings smaller and inline *)
-
let inline_headings =
+
(* Custom renderer that makes headings smaller and strips images *)
+
let excerpt_customizations =
let block c = function
| Cmarkit.Block.Heading (h, _) ->
let level = Cmarkit.Block.Heading.level h in
···
true
| _ -> false
in
-
Cmarkit_renderer.make ~block ()
+
let inline _c = function
+
| Cmarkit.Inline.Image _ ->
+
(* Skip images in excerpts *)
+
true
+
| _ -> false
+
in
+
Cmarkit_renderer.make ~block ~inline ()
in
-
let renderer = Cmarkit_renderer.compose (Cmarkit_html.renderer ~safe:true ()) inline_headings in
+
let renderer = Cmarkit_renderer.compose (Cmarkit_html.renderer ~safe:true ()) excerpt_customizations in
Cmarkit_renderer.doc_to_string renderer doc
let render_post_html ~post ~author_username =
+153 -93
stack/river/lib/state.ml
···
let export_merged_feed state ~title ~format ?limit () =
let all_posts = get_all_posts state ?limit () in
-
(* Rewrite author metadata from Sortal user info *)
-
let rewrite_entry_author username (entry : Syndic.Atom.entry) =
-
match Storage.get_user state username with
+
(* 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
| None -> entry
| Some user ->
(* 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) }
+
in
+
+
(* 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)
+
| None -> None
+
) unique_category_ids in
+
+
{ entry with categories = river_categories }
in
let entries = List.map (fun (username, entry) ->
-
rewrite_entry_author username entry
+
rewrite_entry_author_and_categories username entry
) all_posts in
match format with
···
String.concat "" (List.map Syndic.XML.to_string nodes)
| Some (Syndic.Atom.Mime _) | Some (Syndic.Atom.Src _) | None -> ""
in
-
let author, _ = entry.authors in
-
let tags = List.map (fun (c : Syndic.Atom.category) -> c.term) entry.categories in
+
(* 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
+
| None ->
+
let author, _ = entry.authors in
+
author.name
+
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, tags, post_id)
+
(username, title, author_name, entry.updated, link_uri, content_html, [], post_id)
in
(* Get all posts *)
···
i >= start_idx && i < start_idx + posts_per_page
) html_data in
-
let post_htmls = List.map (fun (username, title, _feed_author, date, link, content, tags, post_id) ->
+
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 custom categories for this post *)
-
let custom_category_ids = get_post_categories state ~post_id in
-
let custom_categories = List.filter_map (fun cat_id ->
+
(* 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)
| None -> None
-
) custom_category_ids in
+
) river_category_ids in
-
(* Combine feed tags and custom categories *)
-
let all_tags = tags @ List.map fst custom_categories in
+
(* Display only River categories *)
let tags_html =
-
match all_tags with
+
match river_categories with
| [] -> ""
| _ ->
-
(* Display feed tags *)
-
let tag_links = List.map (fun tag ->
-
Printf.sprintf {|<a href="categories/%s.html" class="tag-feed">%s</a>|}
-
(Format.Html.html_escape (sanitize_filename tag)) (Format.Html.html_escape tag)
-
) tags in
-
(* Display custom categories with different styling *)
let category_links = List.map (fun (cat_id, cat_name) ->
-
Printf.sprintf {|<a href="categories/%s.html" class="tag-custom">%s</a>|}
+
Printf.sprintf {|<a href="categories/%s.html">%s</a>|}
(Format.Html.html_escape (sanitize_filename cat_id)) (Format.Html.html_escape cat_name)
-
) custom_categories in
-
Printf.sprintf {|<div class="post-tags">%s%s</div>|}
-
(String.concat "" tag_links)
+
) river_categories in
+
Printf.sprintf {|<div class="post-tags">%s</div>|}
(String.concat "" category_links)
in
+
let tags_and_actions =
+
if tags_html = "" then
+
{|<a href="#" class="read-more">Read more</a>|}
+
else
+
Printf.sprintf {|<div class="post-tags-and-actions"><a href="#" class="read-more">Read more</a>%s</div>|}
+
tags_html
+
in
let thumbnail_html = match get_author_thumbnail username with
| Some thumb_path ->
Printf.sprintf {|<a href="authors/%s.html"><img src="%s" alt="%s" class="author-thumbnail"></a>|}
···
<div class="post-full-content">
%s
</div>
-
<a href="#" class="read-more">Read more</a>
%s
</article>|}
thumbnail_html
···
date_str
excerpt
full_content
-
tags_html
+
tags_and_actions
in
post_html
) page_posts in
···
i >= start_idx && i < start_idx + posts_per_page
) author_posts in
-
let post_htmls = List.map (fun (_username, title, author, date, link, content, tags, post_id) ->
+
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
| Some uri ->
···
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 custom categories for this post *)
-
let custom_category_ids = get_post_categories state ~post_id in
-
let custom_categories = List.filter_map (fun cat_id ->
+
(* 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)
| None -> None
-
) custom_category_ids in
+
) river_category_ids in
+
(* Display only River categories *)
let tags_html =
-
let all_tags_exist = tags <> [] || custom_categories <> [] in
-
if not all_tags_exist then ""
+
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)
+
) river_categories in
+
Printf.sprintf {|<div class="post-tags">%s</div>|}
+
(String.concat "" category_links)
+
in
+
let tags_and_actions =
+
if tags_html = "" then
+
{|<a href="#" class="read-more">Read more</a>|}
else
-
(* Display feed tags *)
-
let tag_links = List.map (fun tag ->
-
Printf.sprintf {|<a href="../categories/%s.html" class="tag-feed">%s</a>|}
-
(Format.Html.html_escape (sanitize_filename tag)) (Format.Html.html_escape tag)
-
) tags in
-
(* Display custom categories with different styling *)
-
let category_links = List.map (fun (cat_id, cat_name) ->
-
Printf.sprintf {|<a href="../categories/%s.html" class="tag-custom">%s</a>|}
-
(Format.Html.html_escape (sanitize_filename cat_id)) (Format.Html.html_escape cat_name)
-
) custom_categories in
-
Printf.sprintf {|<div class="post-tags">%s%s</div>|}
-
(String.concat "" tag_links)
-
(String.concat "" category_links)
+
Printf.sprintf {|<div class="post-tags-and-actions"><a href="#" class="read-more">Read more</a>%s</div>|}
+
tags_html
in
Printf.sprintf {|<article class="post">
<h2 class="post-title">%s</h2>
···
<div class="post-full-content">
%s
</div>
-
<a href="#" class="read-more">Read more</a>
%s
</article>|}
link_html
···
date_str
excerpt
full_content
-
tags_html
+
tags_and_actions
) page_posts in
let posts_with_header = author_header ^ "\n" ^ String.concat "\n" post_htmls in
···
i >= start_idx && i < start_idx + posts_per_page
) tag_posts in
-
let post_htmls = List.map (fun (username, title, author, date, link, content, tags, post_id) ->
+
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
| Some uri ->
···
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 custom categories for this post *)
-
let custom_category_ids = get_post_categories state ~post_id in
-
let custom_categories = List.filter_map (fun cat_id ->
+
(* 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)
| None -> None
-
) custom_category_ids in
+
) river_category_ids in
+
(* Display only River categories *)
let tags_html =
-
let all_tags_exist = tags <> [] || custom_categories <> [] in
-
if not all_tags_exist then ""
+
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)
+
) river_categories in
+
Printf.sprintf {|<div class="post-tags">%s</div>|}
+
(String.concat "" category_links)
+
in
+
let tags_and_actions =
+
if tags_html = "" then
+
{|<a href="#" class="read-more">Read more</a>|}
else
-
(* Display feed tags *)
-
let tag_links = List.map (fun t ->
-
Printf.sprintf {|<a href="%s.html" class="tag-feed">%s</a>|}
-
(Format.Html.html_escape (sanitize_filename t)) (Format.Html.html_escape t)
-
) tags in
-
(* Display custom categories with different styling *)
-
let category_links = List.map (fun (cat_id, cat_name) ->
-
Printf.sprintf {|<a href="%s.html" class="tag-custom">%s</a>|}
-
(Format.Html.html_escape (sanitize_filename cat_id)) (Format.Html.html_escape cat_name)
-
) custom_categories in
-
Printf.sprintf {|<div class="post-tags">%s%s</div>|}
-
(String.concat "" tag_links)
-
(String.concat "" category_links)
+
Printf.sprintf {|<div class="post-tags-and-actions"><a href="#" class="read-more">Read more</a>%s</div>|}
+
tags_html
+
in
+
(* Get thumbnail *)
+
let thumbnail_html = match get_author_thumbnail username with
+
| Some thumb_path ->
+
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)
+
| None ->
+
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))))
in
Printf.sprintf {|<article class="post">
+
%s
<h2 class="post-title">%s</h2>
-
<div class="post-meta">
-
By <a href="../authors/%s.html">%s</a> on %s
-
</div>
+
<div class="post-meta-line">By <a href="../authors/%s.html">%s</a> · %s</div>
<div class="post-excerpt">
%s
</div>
<div class="post-full-content">
%s
</div>
-
<a href="#" class="read-more">Read more</a>
%s
</article>|}
+
thumbnail_html
link_html
(Format.Html.html_escape (sanitize_filename username))
(Format.Html.html_escape author)
date_str
excerpt
full_content
-
tags_html
+
tags_and_actions
) page_posts in
let page_html = Format.Html.render_posts_page
···
Log.info (fun m -> m " Deduplicated to %d unique links" (List.length sorted_links));
let links_content =
-
let items = List.map (fun (href, (link_text, username, author, post_title, post_link, date), all_entries) ->
-
let date_str = Format.Html.format_date date in
-
let display_text = if link_text = "" || link_text = href then href else link_text in
-
let post_link_html = match post_link with
-
| Some uri ->
-
Printf.sprintf {|<a href="%s">%s</a>|}
-
(Format.Html.html_escape (Uri.to_string uri))
-
(Format.Html.html_escape post_title)
-
| None -> Format.Html.html_escape post_title
+
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
+
| Some h -> h
+
| None -> "unknown"
+
in
+
let path = Uri.path uri in
+
let fragment = Uri.fragment uri in
+
+
(* Shorten path if too long *)
+
let shortened_path =
+
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
+
start ^ "..." ^ ending
+
else
+
full_path
in
-
let count_str = if List.length all_entries > 1 then
-
Printf.sprintf " (mentioned in %d posts)" (List.length all_entries)
-
else ""
+
+
let display_text =
+
if shortened_path = "" || shortened_path = "/" then
+
Printf.sprintf {|<span class="link-domain">%s</span>|}
+
(Format.Html.html_escape domain)
+
else
+
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)
in
+
+
(* 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
+
| Some uri ->
+
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)
+
date_str
+
(Format.Html.html_escape post_title)
+
| None -> Format.Html.html_escape post_title
+
in
+
Printf.sprintf {|<span class="link-backlink"><span class="link-backlink-icon">↩</span>%s</span>|}
+
post_link_html
+
) all_entries |> String.concat "" in
+
Printf.sprintf {|<div class="link-item">
<div class="link-url"><a href="%s">%s</a></div>
-
<div class="link-meta">From %s by <a href="authors/%s.html">%s</a> on %s%s</div>
+
<div class="link-backlinks">%s</div>
</div>|}
(Format.Html.html_escape href)
-
(Format.Html.html_escape display_text)
-
post_link_html
-
(Format.Html.html_escape (sanitize_filename username))
-
(Format.Html.html_escape author)
-
date_str
-
count_str
+
display_text
+
backlinks_html
) sorted_links in
String.concat "\n" items
in