this repo has no description
0
fork

Configure Feed

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

river

+545 -29
+354 -22
stack/river/lib/format.ml
··· 152 152 line-height: 1.5; 153 153 color: #333; 154 154 background: #fff; 155 - max-width: 800px; 155 + display: flex; 156 + flex-direction: column; 157 + } 158 + 159 + .site-container { 160 + display: flex; 161 + max-width: 1200px; 156 162 margin: 0 auto; 163 + width: 100%; 164 + } 165 + 166 + .date-tracker { 167 + position: sticky; 168 + top: 20px; 169 + width: 120px; 170 + padding: 20px 15px; 171 + height: fit-content; 172 + flex-shrink: 0; 173 + } 174 + 175 + .date-tracker-content { 176 + text-align: right; 177 + border-right: 3px solid #e1e4e8; 178 + padding-right: 15px; 179 + } 180 + 181 + .date-year { 182 + font-size: 24px; 183 + font-weight: 700; 184 + color: #0366d6; 185 + margin-bottom: 4px; 186 + } 187 + 188 + .date-month { 189 + font-size: 14px; 190 + font-weight: 600; 191 + color: #586069; 192 + margin-bottom: 2px; 193 + } 194 + 195 + .date-day { 196 + font-size: 13px; 197 + color: #959da5; 198 + } 199 + 200 + .main-content { 201 + flex: 1; 202 + min-width: 0; 157 203 padding: 15px; 158 204 } 159 205 ··· 188 234 color: #0366d6; 189 235 } 190 236 237 + @media (max-width: 768px) { 238 + .site-container { 239 + flex-direction: column; 240 + } 241 + 242 + .date-tracker { 243 + position: relative; 244 + top: 0; 245 + width: 100%; 246 + padding: 10px 15px; 247 + border-bottom: 1px solid #e1e4e8; 248 + } 249 + 250 + .date-tracker-content { 251 + text-align: left; 252 + border-right: none; 253 + border-bottom: 2px solid #e1e4e8; 254 + padding-right: 0; 255 + padding-bottom: 10px; 256 + } 257 + 258 + .main-content { 259 + padding: 15px; 260 + } 261 + } 262 + 191 263 .post { 192 264 margin-bottom: 25px; 193 265 padding-bottom: 20px; ··· 233 305 } 234 306 235 307 .author-thumbnail { 236 - width: 24px; 237 - height: 24px; 308 + width: 48px; 309 + height: 48px; 238 310 border-radius: 50%; 239 311 object-fit: cover; 312 + flex-shrink: 0; 240 313 } 241 314 242 315 .post-meta-text { 243 316 flex: 1; 317 + display: flex; 318 + flex-direction: column; 319 + justify-content: center; 320 + } 321 + 322 + .author-name { 323 + font-size: 14px; 324 + font-weight: 600; 325 + color: #24292e; 326 + margin-bottom: 2px; 327 + } 328 + 329 + .post-date { 330 + font-size: 12px; 331 + color: #586069; 244 332 } 245 333 246 334 .post-excerpt { ··· 505 593 color: #0366d6; 506 594 } 507 595 508 - .author-list, .category-list { 596 + .author-list { 509 597 list-style: none; 510 598 } 511 599 512 - .author-list li, .category-list li { 600 + .author-item { 601 + display: flex; 602 + align-items: center; 603 + gap: 12px; 604 + padding: 12px 0; 605 + border-bottom: 1px solid #e1e4e8; 606 + transition: background 0.15s; 607 + } 608 + 609 + .author-item:hover { 610 + background: #f6f8fa; 611 + margin: 0 -8px; 612 + padding: 12px 8px; 613 + } 614 + 615 + .author-item-thumbnail { 616 + width: 40px; 617 + height: 40px; 618 + border-radius: 50%; 619 + object-fit: cover; 620 + flex-shrink: 0; 621 + border: 1px solid #e1e4e8; 622 + } 623 + 624 + .author-item-main { 625 + flex: 1; 626 + min-width: 0; 627 + } 628 + 629 + .author-item-name { 630 + font-size: 15px; 631 + font-weight: 600; 632 + margin-bottom: 2px; 633 + } 634 + 635 + .author-item-name a { 636 + color: #24292e; 637 + text-decoration: none; 638 + } 639 + 640 + .author-item-name a:hover { 641 + color: #0366d6; 642 + } 643 + 644 + .author-item-meta { 645 + display: flex; 646 + align-items: center; 647 + gap: 12px; 648 + font-size: 13px; 649 + color: #586069; 650 + flex-wrap: wrap; 651 + } 652 + 653 + .author-item-username { 654 + color: #586069; 655 + } 656 + 657 + .author-item-stat { 658 + display: inline-flex; 659 + align-items: center; 660 + gap: 4px; 661 + color: #586069; 662 + } 663 + 664 + .author-item-links { 665 + display: flex; 666 + align-items: center; 667 + gap: 6px; 668 + } 669 + 670 + .author-item-link { 671 + display: inline-flex; 672 + align-items: center; 673 + color: #586069; 674 + text-decoration: none; 675 + transition: color 0.2s; 676 + } 677 + 678 + .author-item-link:hover { 679 + color: #0366d6; 680 + } 681 + 682 + .author-item-link svg { 683 + width: 16px; 684 + height: 16px; 685 + } 686 + 687 + .author-header { 688 + background: #f6f8fa; 689 + border: 1px solid #e1e4e8; 690 + border-radius: 6px; 691 + padding: 24px; 692 + margin-bottom: 24px; 693 + } 694 + 695 + .author-header-main { 696 + display: flex; 697 + align-items: start; 698 + gap: 20px; 699 + margin-bottom: 20px; 700 + } 701 + 702 + .author-header-thumbnail { 703 + width: 96px; 704 + height: 96px; 705 + border-radius: 50%; 706 + object-fit: cover; 707 + border: 3px solid #fff; 708 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 709 + flex-shrink: 0; 710 + } 711 + 712 + .author-header-info { 713 + flex: 1; 714 + } 715 + 716 + .author-header-name { 717 + font-size: 28px; 718 + font-weight: 700; 719 + color: #24292e; 720 + margin-bottom: 6px; 721 + } 722 + 723 + .author-header-username { 724 + font-size: 16px; 725 + color: #586069; 726 + margin-bottom: 12px; 727 + } 728 + 729 + .author-header-bio { 730 + font-size: 14px; 731 + color: #586069; 732 + line-height: 1.5; 733 + margin-bottom: 12px; 734 + } 735 + 736 + .author-header-links { 737 + display: flex; 738 + flex-wrap: wrap; 739 + gap: 10px; 740 + } 741 + 742 + .author-header-link { 743 + display: inline-flex; 744 + align-items: center; 745 + padding: 6px 12px; 746 + background: #fff; 747 + border: 1px solid #e1e4e8; 748 + border-radius: 4px; 749 + font-size: 13px; 750 + color: #586069; 751 + text-decoration: none; 752 + transition: all 0.2s; 753 + } 754 + 755 + .author-header-link:hover { 756 + border-color: #0366d6; 757 + color: #0366d6; 758 + } 759 + 760 + .author-header-stats { 761 + display: flex; 762 + gap: 24px; 763 + padding-top: 16px; 764 + border-top: 1px solid #e1e4e8; 765 + } 766 + 767 + .author-header-stat { 768 + display: flex; 769 + flex-direction: column; 770 + } 771 + 772 + .author-header-stat-value { 773 + font-size: 24px; 774 + font-weight: 700; 775 + color: #0366d6; 776 + } 777 + 778 + .author-header-stat-label { 779 + font-size: 12px; 780 + color: #586069; 781 + text-transform: uppercase; 782 + letter-spacing: 0.5px; 783 + } 784 + 785 + .category-list { 786 + list-style: none; 787 + } 788 + 789 + .category-list li { 513 790 margin-bottom: 12px; 514 791 padding-bottom: 12px; 515 792 border-bottom: 1px solid #e1e4e8; 516 793 } 517 794 518 - .author-list li:last-child, .category-list li:last-child { 795 + .category-list li:last-child { 519 796 border-bottom: none; 520 797 } 521 798 522 - .author-list a, .category-list a { 799 + .category-list a { 523 800 color: #0366d6; 524 801 text-decoration: none; 525 802 font-size: 15px; 526 803 } 527 804 528 - .author-list a:hover, .category-list a:hover { 805 + .category-list a:hover { 529 806 text-decoration: underline; 530 807 } 531 808 ··· 574 851 <style>%s</style> 575 852 </head> 576 853 <body> 577 - <header> 578 - <h1><a href="index.html">River Feed</a></h1> 579 - <nav> 580 - <a href="index.html"%s>Posts</a> 581 - <a href="authors/index.html"%s>Authors</a> 582 - <a href="categories/index.html"%s>Categories</a> 583 - <a href="links.html"%s>Links</a> 584 - </nav> 585 - </header> 586 - <main> 854 + <div class="site-container"> 855 + <aside class="date-tracker" id="date-tracker"> 856 + <div class="date-tracker-content"> 857 + <div class="date-year" id="current-year">2025</div> 858 + <div class="date-month" id="current-month">January</div> 859 + <div class="date-day" id="current-day">1</div> 860 + </div> 861 + </aside> 862 + <div class="main-content"> 863 + <header> 864 + <h1><a href="index.html">River Feed</a></h1> 865 + <nav> 866 + <a href="index.html"%s>Posts</a> 867 + <a href="authors/index.html"%s>Authors</a> 868 + <a href="categories/index.html"%s>Categories</a> 869 + <a href="links.html"%s>Links</a> 870 + </nav> 871 + </header> 872 + <main> 587 873 %s 588 - </main> 589 - <footer> 590 - Generated by River Feed Aggregator 591 - </footer> 874 + </main> 875 + <footer> 876 + Generated by River Feed Aggregator 877 + </footer> 878 + </div> 879 + </div> 592 880 <div class="lightbox" id="lightbox"> 593 881 <img id="lightbox-img" src="" alt=""> 594 882 </div> ··· 642 930 } 643 931 } 644 932 }); 933 + 934 + // Date tracker scroll update 935 + const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 936 + 'July', 'August', 'September', 'October', 'November', 'December']; 937 + 938 + function updateDateTracker() { 939 + const posts = document.querySelectorAll('.post[data-date]'); 940 + if (posts.length === 0) return; 941 + 942 + // Find the post currently in the middle of the viewport 943 + const viewportMiddle = window.scrollY + (window.innerHeight / 2); 944 + let closestPost = posts[0]; 945 + let closestDistance = Math.abs(posts[0].offsetTop - viewportMiddle); 946 + 947 + posts.forEach(post => { 948 + const distance = Math.abs(post.offsetTop - viewportMiddle); 949 + if (distance < closestDistance) { 950 + closestDistance = distance; 951 + closestPost = post; 952 + } 953 + }); 954 + 955 + const dateStr = closestPost.getAttribute('data-date'); 956 + if (dateStr) { 957 + const date = new Date(dateStr); 958 + const yearEl = document.getElementById('current-year'); 959 + const monthEl = document.getElementById('current-month'); 960 + const dayEl = document.getElementById('current-day'); 961 + 962 + if (yearEl) yearEl.textContent = date.getFullYear(); 963 + if (monthEl) monthEl.textContent = monthNames[date.getMonth()]; 964 + if (dayEl) dayEl.textContent = date.getDate(); 965 + } 966 + } 967 + 968 + // Update on scroll and initial load 969 + let scrollTimeout; 970 + window.addEventListener('scroll', function() { 971 + clearTimeout(scrollTimeout); 972 + scrollTimeout = setTimeout(updateDateTracker, 50); 973 + }); 974 + 975 + // Initial update 976 + updateDateTracker(); 645 977 })(); 646 978 </script> 647 979 </body>
+191 -7
stack/river/lib/state.ml
··· 569 569 (* We'll need to adapt this since we're working with Atom entries *) 570 570 let post_html = 571 571 let date_str = Format.Html.format_date date in 572 + (* Format date for data attribute (ISO 8601) *) 573 + let date_iso = Ptime.to_rfc3339 date in 572 574 let link_html = match link with 573 575 | Some uri -> 574 576 Printf.sprintf {|<a href="%s">%s</a>|} ··· 596 598 (Format.Html.html_escape author) 597 599 | None -> "" 598 600 in 599 - Printf.sprintf {|<article class="post"> 601 + Printf.sprintf {|<article class="post" data-date="%s"> 600 602 <h2 class="post-title">%s</h2> 601 603 <div class="post-meta"> 602 - %s<div class="post-meta-text">By <a href="authors/%s.html">%s</a> on %s</div> 604 + %s<div class="post-meta-text"> 605 + <div class="author-name"><a href="authors/%s.html">%s</a></div> 606 + <div class="post-date">%s</div> 607 + </div> 603 608 </div> 604 609 <div class="post-excerpt"> 605 610 %s ··· 610 615 <a href="#" class="read-more">Read more</a> 611 616 %s 612 617 </article>|} 618 + date_iso 613 619 link_html 614 620 thumbnail_html 615 621 (Format.Html.html_escape (sanitize_filename username)) ··· 656 662 Log.info (fun m -> m "Found %d authors" (List.length authors_list)); 657 663 658 664 let authors_index_content = 659 - let items = List.map (fun (username, author, count) -> 660 - Printf.sprintf {|<li><a href="%s.html">%s</a><span class="count">%d post%s</span></li>|} 665 + (* SVG icon definitions *) 666 + let icon_github = {|<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>|} in 667 + let icon_email = {|<svg viewBox="0 0 16 16" fill="currentColor"><path d="M0 4a2 2 0 012-2h12a2 2 0 012 2v8a2 2 0 01-2 2H2a2 2 0 01-2-2V4zm2-1a1 1 0 00-1 1v.217l7 4.2 7-4.2V4a1 1 0 00-1-1H2zm13 2.383l-4.758 2.855L15 11.114v-5.73zm-.034 6.878L9.271 8.82 8 9.583 6.728 8.82l-5.694 3.44A1 1 0 002 13h12a1 1 0 00.966-.739zM1 11.114l4.758-2.876L1 5.383v5.73z"/></svg>|} in 668 + let icon_link = {|<svg viewBox="0 0 16 16" fill="currentColor"><path d="M4.715 6.542L3.343 7.914a3 3 0 104.243 4.243l1.828-1.829A3 3 0 008.586 5.5L8 6.086a1.001 1.001 0 00-.154.199 2 2 0 01.861 3.337L6.88 11.45a2 2 0 11-2.83-2.83l.793-.792a4.018 4.018 0 01-.128-1.287z"/><path d="M6.586 4.672A3 3 0 007.414 9.5l.775-.776a2 2 0 01-.896-3.346L9.12 3.55a2 2 0 112.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 10-4.243-4.243L6.586 4.672z"/></svg>|} in 669 + let icon_rss = {|<svg viewBox="0 0 16 16" fill="currentColor"><path d="M2 0a2 2 0 00-2 2v12a2 2 0 002 2h12a2 2 0 002-2V2a2 2 0 00-2-2H2zm1.5 2.5c5.523 0 10 4.477 10 10a1 1 0 11-2 0 8 8 0 00-8-8 1 1 0 010-2zm0 4a6 6 0 016 6 1 1 0 11-2 0 4 4 0 00-4-4 1 1 0 010-2zm.5 7a1.5 1.5 0 110-3 1.5 1.5 0 010 3z"/></svg>|} in 670 + 671 + let items = List.map (fun (username, _author, count) -> 672 + (* Get Sortal contact data *) 673 + let contact_opt = Sortal.lookup state.sortal username in 674 + 675 + (* Get the proper display name from Sortal, fallback to username *) 676 + let display_name = match contact_opt with 677 + | Some contact -> Sortal.Contact.name contact 678 + | None -> username 679 + in 680 + 681 + let thumbnail_html = match contact_opt with 682 + | Some _contact -> 683 + (match get_author_thumbnail username with 684 + | Some thumb_path -> 685 + Printf.sprintf {|<img src="../%s" alt="%s" class="author-item-thumbnail">|} 686 + (Format.Html.html_escape thumb_path) 687 + (Format.Html.html_escape display_name) 688 + | None -> 689 + Printf.sprintf {|<div class="author-item-thumbnail" style="background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%); color: white; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700;">%s</div>|} 690 + (String.uppercase_ascii (String.sub display_name 0 1))) 691 + | None -> 692 + Printf.sprintf {|<div class="author-item-thumbnail" style="background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%); color: white; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700;">%s</div>|} 693 + (String.uppercase_ascii (String.sub display_name 0 1)) 694 + in 695 + 696 + let links_html = match contact_opt with 697 + | Some contact -> 698 + let links = [] in 699 + let links = match Sortal.Contact.github contact with 700 + | Some gh -> (Printf.sprintf {|<a href="https://github.com/%s" class="author-item-link" target="_blank" title="GitHub">%s</a>|} gh icon_github) :: links 701 + | None -> links 702 + in 703 + let links = match Sortal.Contact.url contact with 704 + | Some url -> (Printf.sprintf {|<a href="%s" class="author-item-link" target="_blank" title="Website">%s</a>|} url icon_link) :: links 705 + | None -> links 706 + in 707 + let links = match Sortal.Contact.email contact with 708 + | Some email -> (Printf.sprintf {|<a href="mailto:%s" class="author-item-link" title="Email">%s</a>|} email icon_email) :: links 709 + | None -> links 710 + in 711 + if links = [] then "" else 712 + Printf.sprintf {|<div class="author-item-links">%s</div>|} (String.concat "" (List.rev links)) 713 + | None -> "" 714 + in 715 + 716 + let feed_count = match contact_opt with 717 + | Some contact -> 718 + (match Sortal.Contact.feeds contact with 719 + | Some feeds -> List.length feeds 720 + | None -> 0) 721 + | None -> 0 722 + in 723 + 724 + Printf.sprintf {|<div class="author-item"> 725 + %s 726 + <div class="author-item-main"> 727 + <div class="author-item-name"><a href="%s.html">%s</a></div> 728 + <div class="author-item-meta"> 729 + <span class="author-item-username">@%s</span> 730 + <span class="author-item-stat">%d post%s</span> 731 + %s 732 + %s 733 + </div> 734 + </div> 735 + </div>|} 736 + thumbnail_html 661 737 (Format.Html.html_escape (sanitize_filename username)) 662 - (Format.Html.html_escape author) 738 + (Format.Html.html_escape display_name) 739 + (Format.Html.html_escape username) 663 740 count 664 741 (if count = 1 then "" else "s") 742 + (if feed_count > 0 then Printf.sprintf {|<span class="author-item-stat">%s %d feed%s</span>|} icon_rss feed_count (if feed_count = 1 then "" else "s") else "") 743 + links_html 665 744 ) authors_list in 666 - Printf.sprintf "<ul class=\"author-list\">\n%s\n</ul>" 745 + Printf.sprintf "<div class=\"author-list\">\n%s\n</div>" 667 746 (String.concat "\n" items) 668 747 in 669 748 ··· 683 762 let author_pages = (author_total + posts_per_page - 1) / posts_per_page in 684 763 Log.info (fun m -> m " Author: %s (@%s) - %d posts, %d pages" author username author_total author_pages); 685 764 765 + (* Generate author header with Sortal data *) 766 + let author_header = 767 + let contact_opt = Sortal.lookup state.sortal username in 768 + 769 + (* Get proper display name from Sortal *) 770 + let display_name = match contact_opt with 771 + | Some contact -> Sortal.Contact.name contact 772 + | None -> author 773 + in 774 + 775 + (* SVG icons for author header *) 776 + let icon_github = {|<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>|} in 777 + let icon_email = {|<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M0 4a2 2 0 012-2h12a2 2 0 012 2v8a2 2 0 01-2 2H2a2 2 0 01-2-2V4zm2-1a1 1 0 00-1 1v.217l7 4.2 7-4.2V4a1 1 0 00-1-1H2zm13 2.383l-4.758 2.855L15 11.114v-5.73zm-.034 6.878L9.271 8.82 8 9.583 6.728 8.82l-5.694 3.44A1 1 0 002 13h12a1 1 0 00.966-.739zM1 11.114l4.758-2.876L1 5.383v5.73z"/></svg>|} in 778 + let icon_link = {|<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M4.715 6.542L3.343 7.914a3 3 0 104.243 4.243l1.828-1.829A3 3 0 008.586 5.5L8 6.086a1.001 1.001 0 00-.154.199 2 2 0 01.861 3.337L6.88 11.45a2 2 0 11-2.83-2.83l.793-.792a4.018 4.018 0 01-.128-1.287z"/><path d="M6.586 4.672A3 3 0 007.414 9.5l.775-.776a2 2 0 01-.896-3.346L9.12 3.55a2 2 0 112.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 10-4.243-4.243L6.586 4.672z"/></svg>|} in 779 + 780 + match contact_opt with 781 + | Some contact -> 782 + let thumbnail_html = match get_author_thumbnail username with 783 + | Some thumb_path -> 784 + Printf.sprintf {|<img src="../%s" alt="%s" class="author-header-thumbnail">|} 785 + (Format.Html.html_escape thumb_path) 786 + (Format.Html.html_escape display_name) 787 + | None -> 788 + Printf.sprintf {|<div class="author-header-thumbnail" style="background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%); color: white; display: flex; align-items: center; justify-content: center; font-size: 36px; font-weight: 700;">%s</div>|} 789 + (String.uppercase_ascii (String.sub display_name 0 1)) 790 + in 791 + 792 + let links = [] in 793 + let links = match Sortal.Contact.github contact with 794 + | Some gh -> (Printf.sprintf {|<a href="https://github.com/%s" class="author-header-link" target="_blank">%s GitHub</a>|} gh icon_github) :: links 795 + | None -> links 796 + in 797 + let links = match Sortal.Contact.twitter contact with 798 + | Some tw -> (Printf.sprintf {|<a href="https://twitter.com/%s" class="author-header-link" target="_blank">%s Twitter</a>|} tw icon_link) :: links 799 + | None -> links 800 + in 801 + let links = match Sortal.Contact.mastodon contact with 802 + | Some m -> (Printf.sprintf {|<a href="%s" class="author-header-link" target="_blank">%s Mastodon</a>|} m icon_link) :: links 803 + | None -> links 804 + in 805 + let links = match Sortal.Contact.url contact with 806 + | Some url -> (Printf.sprintf {|<a href="%s" class="author-header-link" target="_blank">%s Website</a>|} url icon_link) :: links 807 + | None -> links 808 + in 809 + let links = match Sortal.Contact.email contact with 810 + | Some email -> (Printf.sprintf {|<a href="mailto:%s" class="author-header-link">%s Email</a>|} email icon_email) :: links 811 + | None -> links 812 + in 813 + 814 + let links_html = if links = [] then "" else 815 + Printf.sprintf {|<div class="author-header-links">%s</div>|} (String.concat "" (List.rev links)) 816 + in 817 + 818 + let feed_count = match Sortal.Contact.feeds contact with 819 + | Some feeds -> List.length feeds 820 + | None -> 0 821 + in 822 + 823 + Printf.sprintf {|<div class="author-header"> 824 + <div class="author-header-main"> 825 + %s 826 + <div class="author-header-info"> 827 + <div class="author-header-name">%s</div> 828 + <div class="author-header-username">@%s</div> 829 + %s 830 + </div> 831 + </div> 832 + <div class="author-header-stats"> 833 + <div class="author-header-stat"> 834 + <div class="author-header-stat-value">%d</div> 835 + <div class="author-header-stat-label">Posts</div> 836 + </div> 837 + <div class="author-header-stat"> 838 + <div class="author-header-stat-value">%d</div> 839 + <div class="author-header-stat-label">Feeds</div> 840 + </div> 841 + </div> 842 + </div>|} 843 + thumbnail_html 844 + (Format.Html.html_escape display_name) 845 + (Format.Html.html_escape username) 846 + links_html 847 + author_total 848 + feed_count 849 + | None -> 850 + Printf.sprintf {|<div class="author-header"> 851 + <div class="author-header-main"> 852 + <div class="author-header-info"> 853 + <div class="author-header-name">%s</div> 854 + <div class="author-header-username">@%s</div> 855 + </div> 856 + </div> 857 + <div class="author-header-stats"> 858 + <div class="author-header-stat"> 859 + <div class="author-header-stat-value">%d</div> 860 + <div class="author-header-stat-label">Posts</div> 861 + </div> 862 + </div> 863 + </div>|} 864 + (Format.Html.html_escape display_name) 865 + (Format.Html.html_escape username) 866 + author_total 867 + in 868 + 686 869 for page = 1 to author_pages do 687 870 let start_idx = (page - 1) * posts_per_page in 688 871 let page_posts = List.filteri (fun i _ -> ··· 733 916 tags_html 734 917 ) page_posts in 735 918 919 + let posts_with_header = author_header ^ "\n" ^ String.concat "\n" post_htmls in 736 920 let page_html = Format.Html.render_posts_page 737 921 ~title:(author ^ " - " ^ title) 738 - ~posts:post_htmls 922 + ~posts:[posts_with_header] 739 923 ~current_page:page 740 924 ~total_pages:author_pages 741 925 ~base_path:(sanitize_filename username ^ "-")