@@ -6,6 +6,8 @@ use anyhow::{Context, Result};
66use sha2:: { Digest , Sha256 } ;
77use walkdir:: WalkDir ;
88
9+ use crate :: util:: split_front_matter;
10+
911/// Resolve blog editorial configuration into a template-ready data file.
1012///
1113/// Reads `_data/blog.yml` (editorial config) and all post front-matter from
@@ -97,7 +99,10 @@ pub fn run(src_dir: &Path) -> Result<()> {
9799 resolved_slots. push ( ( * name, resolved) ) ;
98100 }
99101
100- // 6. Build river
102+ // 6. Build sorted authors list (by post count desc, then name asc)
103+ let authors_sorted = build_sorted_authors ( & all_posts, & data_dir) ?;
104+
105+ // 7. Build river
101106 let river: Vec < serde_yaml:: Value > = all_posts
102107 . iter ( )
103108 . filter ( |p| !used. contains ( & p. slug ) )
@@ -108,7 +113,7 @@ pub fn run(src_dir: &Path) -> Result<()> {
108113 } )
109114 . collect ( ) ;
110115
111- // 7 . Assemble output
116+ // 8 . Assemble output
112117 let mut out = serde_yaml:: Mapping :: new ( ) ;
113118
114119 for ( name, value) in & resolved_slots {
@@ -118,13 +123,14 @@ pub fn run(src_dir: &Path) -> Result<()> {
118123 }
119124
120125 out. insert ( ykey ( "river" ) , serde_yaml:: Value :: Sequence ( river) ) ;
126+ out. insert ( ykey ( "authors_sorted" ) , serde_yaml:: Value :: Sequence ( authors_sorted) ) ;
121127 out. insert ( ykey ( "default_cover_image" ) , ystr ( default_cover) ) ;
122128 out. insert ( ykey ( "default_cover_alt" ) , ystr ( default_alt) ) ;
123129
124130 let yaml_out = serde_yaml:: to_string ( & serde_yaml:: Value :: Mapping ( out) )
125131 . context ( "serializing blog_resolved.yml" ) ?;
126132
127- // 8 . Write (idempotent)
133+ // 9 . Write (idempotent)
128134 let resolved_path = data_dir. join ( "blog_resolved.yml" ) ;
129135 let needs_write = if resolved_path. exists ( ) {
130136 let existing = fs:: read_to_string ( & resolved_path) . unwrap_or_default ( ) ;
@@ -409,19 +415,72 @@ fn slug_from_path(path: &Path, posts_dir: &Path) -> Option<String> {
409415 }
410416}
411417
412- /// Split YAML front-matter (`---` delimited) from body content.
413- fn split_front_matter ( content : & str ) -> Option < ( String , & str ) > {
414- let trimmed = content. trim_start ( ) ;
415- let rest = trimmed. strip_prefix ( "---" ) ?;
416- let end = rest. find ( "\n ---" ) ?;
417- let fm = rest[ ..end] . to_string ( ) ;
418- let body_start = end + 4 ;
419- let body = if body_start < rest. len ( ) && rest. as_bytes ( ) [ body_start] == b'\n' {
420- & rest[ body_start + 1 ..]
421- } else {
422- & rest[ body_start..]
423- } ;
424- Some ( ( fm, body) )
418+
419+ /// Build a sorted list of authors with post counts for template use.
420+ ///
421+ /// Sorted by post count descending, then display name ascending for ties.
422+ fn build_sorted_authors ( posts : & [ PostMeta ] , data_dir : & Path ) -> Result < Vec < serde_yaml:: Value > > {
423+ let path = data_dir. join ( "authors.yml" ) ;
424+ if !path. exists ( ) {
425+ return Ok ( Vec :: new ( ) ) ;
426+ }
427+
428+ let content = fs:: read_to_string ( & path) . context ( "reading authors.yml" ) ?;
429+ let doc: serde_yaml:: Value =
430+ serde_yaml:: from_str ( & content) . context ( "parsing authors.yml" ) ?;
431+
432+ let mut post_counts: HashMap < String , usize > = HashMap :: new ( ) ;
433+ for post in posts {
434+ * post_counts. entry ( post. author_slug . clone ( ) ) . or_insert ( 0 ) += 1 ;
435+ }
436+
437+ let mut author_entries: Vec < ( String , String , Option < String > , Option < String > , usize ) > = Vec :: new ( ) ;
438+
439+ if let Some ( map) = doc. as_mapping ( ) {
440+ for ( key, value) in map {
441+ if let Some ( slug) = key. as_str ( ) {
442+ let name = value
443+ . get ( "name" )
444+ . and_then ( |v| v. as_str ( ) )
445+ . unwrap_or ( slug)
446+ . to_string ( ) ;
447+ let bio = value
448+ . get ( "bio" )
449+ . and_then ( |v| v. as_str ( ) )
450+ . map ( String :: from) ;
451+ let avatar = value
452+ . get ( "avatar" )
453+ . and_then ( |v| v. as_str ( ) )
454+ . map ( String :: from) ;
455+ let count = post_counts. get ( slug) . copied ( ) . unwrap_or ( 0 ) ;
456+ author_entries. push ( ( slug. to_string ( ) , name, bio, avatar, count) ) ;
457+ }
458+ }
459+ }
460+
461+ author_entries. sort_by ( |a, b| b. 4 . cmp ( & a. 4 ) . then_with ( || a. 1 . cmp ( & b. 1 ) ) ) ;
462+
463+ let sorted: Vec < serde_yaml:: Value > = author_entries
464+ . into_iter ( )
465+ . map ( |( slug, name, bio, avatar, count) | {
466+ let mut map = serde_yaml:: Mapping :: new ( ) ;
467+ map. insert ( ykey ( "slug" ) , ystr ( & slug) ) ;
468+ map. insert ( ykey ( "name" ) , ystr ( & name) ) ;
469+ if let Some ( b) = & bio {
470+ map. insert ( ykey ( "bio" ) , ystr ( b) ) ;
471+ }
472+ if let Some ( a) = & avatar {
473+ map. insert ( ykey ( "avatar" ) , ystr ( a) ) ;
474+ }
475+ map. insert (
476+ ykey ( "post_count" ) ,
477+ serde_yaml:: Value :: Number ( serde_yaml:: Number :: from ( count as u64 ) ) ,
478+ ) ;
479+ serde_yaml:: Value :: Mapping ( map)
480+ } )
481+ . collect ( ) ;
482+
483+ Ok ( sorted)
425484}
426485
427486/// Load author slug → display name mapping from `_data/authors.yml`.
0 commit comments