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

river

Changed files
+68 -196
stack
river
+47 -177
stack/river/lib/format.ml
···
line-height: 1.5;
color: #333;
background: #fff;
-
display: flex;
-
flex-direction: column;
-
}
-
-
.site-container {
-
display: flex;
-
max-width: 1200px;
margin: 0 auto;
-
width: 100%;
-
}
-
-
.date-tracker {
-
position: sticky;
-
top: 20px;
-
width: 120px;
-
padding: 20px 15px;
-
height: fit-content;
-
flex-shrink: 0;
-
}
-
-
.date-tracker-content {
-
text-align: right;
-
border-right: 3px solid #e1e4e8;
-
padding-right: 15px;
-
}
-
-
.date-year {
-
font-size: 24px;
-
font-weight: 700;
-
color: #0366d6;
-
margin-bottom: 4px;
-
}
-
-
.date-month {
-
font-size: 14px;
-
font-weight: 600;
-
color: #586069;
-
margin-bottom: 2px;
-
}
-
-
.date-day {
-
font-size: 13px;
-
color: #959da5;
-
}
-
-
.main-content {
-
flex: 1;
-
min-width: 0;
padding: 15px;
}
···
color: #0366d6;
}
-
@media (max-width: 768px) {
-
.site-container {
-
flex-direction: column;
-
}
-
-
.date-tracker {
-
position: relative;
-
top: 0;
-
width: 100%;
-
padding: 10px 15px;
-
border-bottom: 1px solid #e1e4e8;
-
}
-
-
.date-tracker-content {
-
text-align: left;
-
border-right: none;
-
border-bottom: 2px solid #e1e4e8;
-
padding-right: 0;
-
padding-bottom: 10px;
-
}
-
-
.main-content {
-
padding: 15px;
-
}
-
}
-
.post {
-
margin-bottom: 25px;
-
padding-bottom: 20px;
border-bottom: 1px solid #e1e4e8;
}
.post:last-child {
border-bottom: none;
}
.post-title {
font-size: 18px;
font-weight: 600;
-
margin-bottom: 5px;
line-height: 1.3;
}
···
text-decoration: underline;
}
-
.post-meta {
font-size: 12px;
color: #586069;
-
margin-bottom: 8px;
-
display: flex;
-
align-items: center;
-
gap: 8px;
}
-
.post-meta a {
color: #586069;
text-decoration: none;
}
-
.post-meta a:hover {
color: #0366d6;
}
-
.author-thumbnail {
-
width: 48px;
-
height: 48px;
-
border-radius: 50%;
-
object-fit: cover;
-
flex-shrink: 0;
-
}
-
-
.post-meta-text {
-
flex: 1;
-
display: flex;
-
flex-direction: column;
-
justify-content: center;
-
}
-
-
.author-name {
-
font-size: 14px;
-
font-weight: 600;
-
color: #24292e;
-
margin-bottom: 2px;
-
}
-
-
.post-date {
-
font-size: 12px;
-
color: #586069;
}
.post-excerpt {
···
<style>%s</style>
</head>
<body>
-
<div class="site-container">
-
<aside class="date-tracker" id="date-tracker">
-
<div class="date-tracker-content">
-
<div class="date-year" id="current-year">2025</div>
-
<div class="date-month" id="current-month">January</div>
-
<div class="date-day" id="current-day">1</div>
-
</div>
-
</aside>
-
<div class="main-content">
-
<header>
-
<h1><a href="index.html">River Feed</a></h1>
-
<nav>
-
<a href="index.html"%s>Posts</a>
-
<a href="authors/index.html"%s>Authors</a>
-
<a href="categories/index.html"%s>Categories</a>
-
<a href="links.html"%s>Links</a>
-
</nav>
-
</header>
-
<main>
%s
-
</main>
-
<footer>
-
Generated by River Feed Aggregator
-
</footer>
-
</div>
-
</div>
<div class="lightbox" id="lightbox">
<img id="lightbox-img" src="" alt="">
</div>
···
}
}
});
-
-
// Date tracker scroll update
-
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
-
'July', 'August', 'September', 'October', 'November', 'December'];
-
-
function updateDateTracker() {
-
const posts = document.querySelectorAll('.post[data-date]');
-
if (posts.length === 0) return;
-
-
// Find the post currently in the middle of the viewport
-
const viewportMiddle = window.scrollY + (window.innerHeight / 2);
-
let closestPost = posts[0];
-
let closestDistance = Math.abs(posts[0].offsetTop - viewportMiddle);
-
-
posts.forEach(post => {
-
const distance = Math.abs(post.offsetTop - viewportMiddle);
-
if (distance < closestDistance) {
-
closestDistance = distance;
-
closestPost = post;
-
}
-
});
-
-
const dateStr = closestPost.getAttribute('data-date');
-
if (dateStr) {
-
const date = new Date(dateStr);
-
const yearEl = document.getElementById('current-year');
-
const monthEl = document.getElementById('current-month');
-
const dayEl = document.getElementById('current-day');
-
-
if (yearEl) yearEl.textContent = date.getFullYear();
-
if (monthEl) monthEl.textContent = monthNames[date.getMonth()];
-
if (dayEl) dayEl.textContent = date.getDate();
-
}
-
}
-
-
// Update on scroll and initial load
-
let scrollTimeout;
-
window.addEventListener('scroll', function() {
-
clearTimeout(scrollTimeout);
-
scrollTimeout = setTimeout(updateDateTracker, 50);
-
});
-
-
// Initial update
-
updateDateTracker();
})();
</script>
</body>
···
line-height: 1.5;
color: #333;
background: #fff;
+
max-width: 900px;
margin: 0 auto;
padding: 15px;
}
···
color: #0366d6;
}
.post {
+
margin-bottom: 30px;
+
padding-bottom: 25px;
border-bottom: 1px solid #e1e4e8;
+
overflow: hidden;
}
.post:last-child {
border-bottom: none;
}
+
.author-thumbnail {
+
float: right;
+
width: 48px;
+
height: 48px;
+
border-radius: 50%;
+
object-fit: cover;
+
margin-left: 15px;
+
margin-bottom: 10px;
+
border: 2px solid #e1e4e8;
+
transition: transform 0.2s, box-shadow 0.2s;
+
}
+
+
.author-thumbnail:hover {
+
transform: scale(1.05);
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+
}
+
.post-title {
font-size: 18px;
font-weight: 600;
+
margin-bottom: 4px;
line-height: 1.3;
}
···
text-decoration: underline;
}
+
.post-meta-line {
font-size: 12px;
color: #586069;
+
margin-bottom: 10px;
}
+
.post-meta-line a {
color: #586069;
text-decoration: none;
+
font-weight: 600;
}
+
.post-meta-line a:hover {
color: #0366d6;
}
+
@media (max-width: 768px) {
+
.author-thumbnail {
+
float: none;
+
display: block;
+
margin: 0 auto 10px auto;
+
}
}
.post-excerpt {
···
<style>%s</style>
</head>
<body>
+
<header>
+
<h1><a href="index.html">River Feed</a></h1>
+
<nav>
+
<a href="index.html"%s>Posts</a>
+
<a href="authors/index.html"%s>Authors</a>
+
<a href="categories/index.html"%s>Categories</a>
+
<a href="links.html"%s>Links</a>
+
</nav>
+
</header>
+
<main>
%s
+
</main>
+
<footer>
+
Generated by River Feed Aggregator
+
</footer>
<div class="lightbox" id="lightbox">
<img id="lightbox-img" src="" alt="">
</div>
···
}
}
});
})();
</script>
</body>
+21 -19
stack/river/lib/state.ml
···
i >= start_idx && i < start_idx + posts_per_page
) html_data in
-
let post_htmls = List.map (fun (username, title, author, date, link, content, tags) ->
-
Log.debug (fun m -> m " Processing post: %s by %s (@%s)" title author username);
-
(* Create a temporary Post-like structure for rendering *)
-
(* We'll need to adapt this since we're working with Atom entries *)
let post_html =
let date_str = Format.Html.format_date date in
-
(* Format date for data attribute (ISO 8601) *)
-
let date_iso = Ptime.to_rfc3339 date in
let link_html = match link with
| Some uri ->
Printf.sprintf {|<a href="%s">%s</a>|}
···
in
let thumbnail_html = match get_author_thumbnail username with
| Some thumb_path ->
-
Printf.sprintf {|<img src="%s" alt="%s" class="author-thumbnail">|}
(Format.Html.html_escape thumb_path)
-
(Format.Html.html_escape author)
-
| None -> ""
in
-
Printf.sprintf {|<article class="post" data-date="%s">
<h2 class="post-title">%s</h2>
-
<div class="post-meta">
-
%s<div class="post-meta-text">
-
<div class="author-name"><a href="authors/%s.html">%s</a></div>
-
<div class="post-date">%s</div>
-
</div>
-
</div>
<div class="post-excerpt">
%s
</div>
···
<a href="#" class="read-more">Read more</a>
%s
</article>|}
-
date_iso
link_html
-
thumbnail_html
(Format.Html.html_escape (sanitize_filename username))
-
(Format.Html.html_escape author)
date_str
excerpt
full_content
···
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) ->
+
Log.debug (fun m -> m " Processing post: %s by @%s" title username);
+
+
(* Get author name from Sortal, fallback to username *)
+
let author_name = match Sortal.lookup state.sortal username with
+
| Some contact -> Sortal.Contact.name contact
+
| None -> username
+
in
+
let post_html =
let date_str = Format.Html.format_date date in
let link_html = match link with
| Some uri ->
Printf.sprintf {|<a href="%s">%s</a>|}
···
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>|}
+
(Format.Html.html_escape (sanitize_filename username))
(Format.Html.html_escape thumb_path)
+
(Format.Html.html_escape author_name)
+
| 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_name 0 1))
in
+
Printf.sprintf {|<article class="post">
+
%s
<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">
%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_name)
date_str
excerpt
full_content