@@ -6,17 +6,20 @@ use std::time::{Duration, Instant};
66use rayon:: prelude:: * ;
77
88use super :: media_library:: { MediaLibrary , MediaLibraryError } ;
9- use super :: website_info:: { SITE_TOML , WebsiteInfo , WebsiteInfoError } ;
9+ use super :: website_info:: { WebsiteInfo , WebsiteInfoError , SITE_TOML } ;
1010use super :: website_item:: { GenerationResult , WebsiteItem } ;
1111
1212const DEFAULT_BUILD_DIR : & str = "build" ;
1313const DEFAULT_MEDIA_DIR : & str = "media" ;
1414const DEFAULT_PUBLIC_DIR : & str = "public" ;
1515
16+ const DEFAULT_FEED_FILE : & str = "feed.xml" ;
17+
1618const TEMPLATE_INDEX : & str = include_str ! ( "../../template/index.html" ) ;
1719const TEMPLATE_STYLE : & str = include_str ! ( "../../template/public/style.css" ) ;
1820const TEMPLATE_JS : & str = include_str ! ( "../../template/public/clutterlog.js" ) ;
1921const TEMPLATE_GITHUB_ACTION : & str = include_str ! ( "../../template/github_action.yaml" ) ;
22+ const TEMPLATE_RSS : & str = include_str ! ( "../../template/rss.xml" ) ;
2023
2124pub struct BuildReport {
2225 pub items_processed : usize ,
@@ -171,7 +174,7 @@ impl Website {
171174 library. update_metadata ( & source_media_path) ?;
172175
173176 // Scan source media directory, copy files, generate thumbnails, and collect data entries
174- let ( clutterlog_data, generation_results) =
177+ let ( clutterlog_data, rss_items , generation_results) =
175178 self . scan_and_copy_media ( & source_media_path, & build_media_path, & library) ?;
176179
177180 // Render index.html from template
@@ -184,6 +187,23 @@ impl Website {
184187 let index_path = build_path. join ( "index.html" ) ;
185188 fs:: write ( & index_path, & rendered) . map_err ( |e| WebsiteError :: Io ( index_path, e) ) ?;
186189
190+ // Render and write feed.xml
191+ let rss_items_str = if rss_items. is_empty ( ) {
192+ String :: new ( )
193+ } else {
194+ format ! ( "{}\n " , rss_items. join( "\n " ) )
195+ } ;
196+ let base_url = self . info . url . trim_end_matches ( '/' ) ;
197+ let feed_url = format ! ( "{}/" , base_url) ;
198+ let rss_rendered = TEMPLATE_RSS
199+ . replace ( "{{title}}" , & escape_html ( & self . info . title ) )
200+ . replace ( "{{url}}" , & escape_html ( & feed_url) )
201+ . replace ( "{{description}}" , & escape_html ( & self . info . description ) )
202+ . replace ( "{{items}}" , & rss_items_str) ;
203+
204+ let rss_path = build_path. join ( DEFAULT_FEED_FILE ) ;
205+ fs:: write ( & rss_path, & rss_rendered) . map_err ( |e| WebsiteError :: Io ( rss_path, e) ) ?;
206+
187207 // Write static assets
188208 let style_path = public_path. join ( "style.css" ) ;
189209 fs:: write ( & style_path, TEMPLATE_STYLE ) . map_err ( |e| WebsiteError :: Io ( style_path, e) ) ?;
@@ -202,11 +222,11 @@ impl Website {
202222 source_path : & Path ,
203223 dest_path : & Path ,
204224 library : & MediaLibrary ,
205- ) -> Result < ( String , Vec < GenerationResult > ) , WebsiteError > {
225+ ) -> Result < ( String , Vec < String > , Vec < GenerationResult > ) , WebsiteError > {
206226 let base_url = self . info . url . trim_end_matches ( '/' ) ;
207227
208228 if !source_path. exists ( ) {
209- return Ok ( ( "[]" . to_string ( ) , Vec :: new ( ) ) ) ;
229+ return Ok ( ( "[]" . to_string ( ) , Vec :: new ( ) , Vec :: new ( ) ) ) ;
210230 }
211231
212232 let dir_entries = fs:: read_dir ( source_path)
@@ -224,23 +244,30 @@ impl Website {
224244 . collect ( ) ;
225245
226246 // Process items in parallel: copy files and generate thumbnails
227- let processed: Vec < Result < ( GenerationResult , String ) , WebsiteError > > = items
247+ let processed: Vec < Result < ( GenerationResult , String , String ) , WebsiteError > > = items
228248 . par_iter ( )
229249 . filter_map ( |( path, datetime) | {
230250 let item = WebsiteItem :: from_path ( path, datetime. as_deref ( ) ) ?;
231251 let result = item. copy_and_generate_thumb ( dest_path) ;
232252 let entry = item. to_json_entry ( base_url, DEFAULT_MEDIA_DIR ) ;
233- Some ( result. map ( |r| ( r, entry) ) )
253+ let rss_item = item. to_rss_item ( base_url, DEFAULT_MEDIA_DIR ) ;
254+ let image_url = item. image_url ( base_url, DEFAULT_MEDIA_DIR ) ;
255+ Some ( result. map ( |mut r| {
256+ r. image_url = image_url;
257+ ( r, entry, rss_item)
258+ } ) )
234259 } )
235260 . collect ( ) ;
236261
237262 // Collect results, propagating any errors
238263 let mut results: Vec < GenerationResult > = Vec :: new ( ) ;
239264 let mut entries: Vec < String > = Vec :: new ( ) ;
265+ let mut rss_items: Vec < String > = Vec :: new ( ) ;
240266 for item_result in processed {
241- let ( gen_result, entry) = item_result?;
267+ let ( gen_result, entry, rss_item ) = item_result?;
242268 results. push ( gen_result) ;
243269 entries. push ( entry) ;
270+ rss_items. push ( rss_item) ;
244271 }
245272
246273 let json = if entries. is_empty ( ) {
@@ -249,7 +276,7 @@ impl Website {
249276 format ! ( "[\n {}\n ]" , entries. join( ",\n " ) )
250277 } ;
251278
252- Ok ( ( json, results) )
279+ Ok ( ( json, rss_items , results) )
253280 }
254281}
255282
0 commit comments