2 // AGPL-3.0 License (see LICENSE)
4 use chrono::{Datelike, Local};
5 use sailfish::Template;
7 use std::collections::HashMap;
8 use std::sync::{Arc, Mutex};
10 use crate::site::blog_post::BlogPost;
11 use crate::site::blog_post::parse_markdown_file;
12 use crate::site::helper::Helper;
14 pub struct BlogShared {
15 state: Mutex<BlogState>,
19 pub fn new(base_dir: String, num_previews_per_page: usize) -> Self {
21 state: Mutex::new(BlogState::new(base_dir, num_previews_per_page)),
25 pub fn shared(self) -> Arc<Self> {
31 #[template(path = "blog_overview.stpl")]
32 pub struct BlogState {
34 num_previews_per_page: usize,
35 blog_posts: Vec<BlogPost>,
36 topic_blog_indices: HashMap<String, Vec<usize>>,
38 topics_sanitized: Vec<String>,
39 year_blog_indices: HashMap<String, Vec<usize>>,
41 overview_page_url: String,
42 overview_current_page: usize,
43 overview_num_pages: usize,
44 overview_offset: usize,
45 overview_num_posts: usize,
46 overview_keywords: String,
47 overview_title: String,
48 overview_topic: String,
49 overview_topic_sanitized: String,
50 overview_year: String,
55 pub fn new(base_dir: String, num_previews_per_page: usize) -> Self {
58 num_previews_per_page: num_previews_per_page,
59 blog_posts: Vec::new(),
60 topic_blog_indices: HashMap::new(),
62 topics_sanitized: Vec::new(),
63 year_blog_indices: HashMap::new(),
65 overview_page_url: String::new(),
66 overview_current_page: 0,
67 overview_num_pages: 0,
69 overview_num_posts: 0,
70 overview_keywords: String::new(),
71 overview_title: String::new(),
72 overview_topic: String::new(),
73 overview_topic_sanitized: String::new(),
74 overview_year: String::new(),
80 pub async fn generate_blog(shared: Arc<BlogShared>, base_dir: String) {
81 // create output dirs for topic, year and feeds:
82 create_output_dirs(shared.clone());
84 // parse all the markdown files in 'blog-posts' folder:
85 parse_markdown_files(shared.clone(), base_dir).await;
87 // generate all individual blog posts:
88 generate_blog_posts(shared.clone()).await;
90 // generate blog overview:
91 generate_overview_posts(shared.clone()).await;
93 // generate blog overview by topic:
94 generate_overview_topic(shared.clone()).await;
96 // generate blog overview by year:
97 generate_overview_year(shared.clone()).await;
99 // generate blog atom feed:
100 generate_atom_feed(shared.clone()).await;
103 pub fn get_latest_blog_posts(shared: Arc<BlogShared>, num_posts: usize) -> Vec<BlogPost> {
104 let lock = shared.state.lock().unwrap();
105 let mut blog_posts = Vec::new();
107 for i in 0..num_posts {
108 if let Some(value) = lock.blog_posts.get(i) {
109 blog_posts.push(value.clone());
116 fn create_output_dirs(shared: Arc<BlogShared>) {
117 let lock = shared.state.lock().unwrap();
119 Helper::create_dir_all(&Helper::get_output_dir().join(&lock.base_dir).join("topic"));
120 Helper::create_dir_all(&Helper::get_output_dir().join(&lock.base_dir).join("year"));
121 Helper::create_dir_all(&Helper::get_output_dir().join("feeds"));
124 async fn parse_markdown_files(shared: Arc<BlogShared>, base_dir: String) {
125 let mut reader = tokio::fs::read_dir("blog-posts").await.unwrap();
126 let mut tasks = vec![];
128 if let Some(f) = reader.next_entry().await.unwrap() {
129 tasks.push(tokio::spawn(parse_markdown_file(
138 // await all created blog_posts:
139 let mut blog_posts = Vec::with_capacity(tasks.len());
141 blog_posts.push(task.await.unwrap());
144 // sort so that latest is first:
145 blog_posts.sort_by(|a, b| b.get_published_date().cmp(a.get_published_date()));
147 let mut topic_blog_indices: HashMap<String, Vec<usize>> = HashMap::new();
148 let mut year_blog_indices: HashMap<String, Vec<usize>> = HashMap::new();
149 for (i, post) in blog_posts.iter().enumerate() {
150 // add blog_id for each blog topic for later lookup:
151 for topic in post.get_topics() {
152 if let Some(indices) = topic_blog_indices.get_mut(topic) {
155 topic_blog_indices.insert(String::from(topic), vec![i]);
159 // add blog_id for each blog year for later lookup:
160 let year_str = post.get_year();
161 if let Some(indices) = year_blog_indices.get_mut(&year_str) {
164 year_blog_indices.insert(String::from(year_str), vec![i]);
169 let mut lock = shared.state.lock().unwrap();
170 lock.blog_posts = blog_posts;
172 for (key, _value) in &topic_blog_indices {
173 lock.topics.push(String::from(key));
174 lock.topics_sanitized
175 .push(String::from(Helper::sanitize_string(key)));
177 // sort by topic name:
179 .sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
180 lock.topics_sanitized.sort_by(|a, b| a.cmp(b));
182 for (key, _value) in &year_blog_indices {
183 lock.years.push(String::from(key));
186 lock.years.sort_by(|a, b| a.cmp(b));
188 // move created hashmaps:
189 lock.topic_blog_indices = topic_blog_indices;
190 lock.year_blog_indices = year_blog_indices;
193 async fn generate_blog_posts(shared: Arc<BlogShared>) {
194 let lock = shared.state.lock().unwrap();
196 for post in &lock.blog_posts {
197 post.create_output_dir();
202 async fn generate_overview_posts(shared: Arc<BlogShared>) {
203 let mut lock = shared.state.lock().unwrap();
205 lock.overview_offset = 0;
206 let num = lock.blog_posts.len() as f32 / lock.num_previews_per_page as f32;
207 lock.overview_num_pages = num.ceil() as usize;
208 lock.overview_num_posts = lock.num_previews_per_page;
209 lock.overview_keywords = String::from("overview");
210 lock.overview_title = String::from("Blog Overview");
211 lock.overview_type = 0;
213 for i in 1..lock.overview_num_pages + 1 {
214 lock.overview_current_page = i;
215 lock.overview_page_url = format!("{}/page/{}", lock.base_dir, i);
217 if i == lock.overview_num_pages {
218 lock.overview_num_posts = cmp::min(
219 lock.blog_posts.len() - lock.num_previews_per_page * (i - 1),
220 lock.num_previews_per_page,
224 if lock.overview_current_page == 1 {
225 // write page to disk:
226 Helper::write_file_sync(
227 &Helper::get_output_dir()
228 .join(&lock.base_dir)
230 &lock.render().unwrap().as_bytes(),
235 // create dir recursively:
236 Helper::create_dir_all(&Helper::get_output_dir().join(&lock.overview_page_url));
238 // write page to disk:
239 Helper::write_file_sync(
240 &Helper::get_output_dir()
241 .join(&lock.overview_page_url)
243 &lock.render().unwrap().as_bytes(),
247 lock.overview_offset += lock.overview_num_posts;
251 async fn generate_overview_topic(shared: Arc<BlogShared>) {
252 let mut lock = shared.state.lock().unwrap();
254 // TODO: How to solve this without a clone??
255 for (key, indices) in &lock.topic_blog_indices.clone() {
256 lock.overview_offset = 0;
257 let num = indices.len() as f32 / lock.num_previews_per_page as f32;
258 lock.overview_num_pages = num.ceil() as usize;
259 lock.overview_num_posts = lock.num_previews_per_page;
260 lock.overview_keywords = format!("topic, {}", key);
261 lock.overview_topic = String::from(key);
262 lock.overview_topic_sanitized = Helper::sanitize_string(key);
263 lock.overview_type = 1;
265 for i in 1..lock.overview_num_pages + 1 {
266 lock.overview_current_page = i;
267 lock.overview_page_url = format!(
268 "{}/topic/{}/page/{}",
270 Helper::sanitize_string(key),
273 lock.overview_title = format!("Blog posts by topic: {}", key);
275 if i == lock.overview_num_pages {
276 lock.overview_num_posts = cmp::min(
277 indices.len() - lock.num_previews_per_page * (i - 1),
278 lock.num_previews_per_page,
282 if lock.overview_current_page == 1 {
283 // create dir recursively:
284 Helper::create_dir_all(
285 &Helper::get_output_dir()
286 .join(&lock.base_dir)
288 .join(Helper::sanitize_string(key)),
291 // write page to disk:
292 Helper::write_file_sync(
293 &Helper::get_output_dir()
294 .join(&lock.base_dir)
296 .join(Helper::sanitize_string(key))
298 &lock.render().unwrap().as_bytes(),
303 // create dir recursively:
304 Helper::create_dir_all(&Helper::get_output_dir().join(&lock.overview_page_url));
306 // write page to disk:
307 Helper::write_file_sync(
308 &Helper::get_output_dir()
309 .join(&lock.overview_page_url)
311 &lock.render().unwrap().as_bytes(),
315 lock.overview_offset += lock.overview_num_posts;
320 async fn generate_overview_year(shared: Arc<BlogShared>) {
321 let mut lock = shared.state.lock().unwrap();
323 // TODO: How to solve this without a clone??
324 for (key, indices) in &lock.year_blog_indices.clone() {
325 lock.overview_offset = 0;
326 let num = indices.len() as f32 / lock.num_previews_per_page as f32;
327 lock.overview_num_pages = num.ceil() as usize;
328 lock.overview_num_posts = lock.num_previews_per_page;
329 lock.overview_keywords = format!("year, {}", key);
330 lock.overview_year = String::from(key);
331 lock.overview_type = 2;
333 for i in 1..lock.overview_num_pages + 1 {
334 lock.overview_current_page = i;
335 lock.overview_page_url = format!(
336 "{}/year/{}/page/{}",
338 Helper::sanitize_string(key),
341 lock.overview_title = format!("Blog posts by year: {}", key);
343 if i == lock.overview_num_pages {
344 lock.overview_num_posts = cmp::min(
345 indices.len() - lock.num_previews_per_page * (i - 1),
346 lock.num_previews_per_page,
350 if lock.overview_current_page == 1 {
351 // create dir recursively:
352 Helper::create_dir_all(
353 &Helper::get_output_dir()
354 .join(&lock.base_dir)
356 .join(Helper::sanitize_string(key)),
359 // write page to disk:
360 Helper::write_file_sync(
361 &Helper::get_output_dir()
362 .join(&lock.base_dir)
364 .join(Helper::sanitize_string(key))
366 &lock.render().unwrap().as_bytes(),
371 // create dir recursively:
372 Helper::create_dir_all(&Helper::get_output_dir().join(&lock.overview_page_url));
374 // write page to disk:
375 Helper::write_file_sync(
376 &Helper::get_output_dir()
377 .join(&lock.overview_page_url)
379 &lock.render().unwrap().as_bytes(),
383 lock.overview_offset += lock.overview_num_posts;
388 async fn generate_atom_feed(shared: Arc<BlogShared>) {
389 let lock = shared.state.lock().unwrap();
391 let mut _feed_data = String::new();
392 let date_now = get_date_now_for_feed();
395 _feed_data = String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
396 _feed_data += "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n";
397 _feed_data += " <id>https:/www.luflow.net/feeds/blog.atom</id>\n";
398 _feed_data += " <title>luflow.net Blog</title>\n";
399 _feed_data += " <updated>";
400 _feed_data += &date_now;
401 _feed_data += "</updated>\n";
402 _feed_data += " <generator>https://codeberg.org/hfsoulz/flow-web.git</generator>\n";
403 _feed_data += " <author>\n";
404 _feed_data += " <name>luflow.net</name>\n";
405 _feed_data += " <uri>https://www.luflow.net/</uri>\n";
406 _feed_data += " </author>\n";
407 _feed_data += " <link rel=\"alternate\" href=\"https:/www.luflow.net/blog/\"/>\n";
408 _feed_data += " <link rel=\"self\" href=\"https:/www.luflow.net/feeds/blog.atom\"/>\n";
409 _feed_data += " <subtitle>This blog is dedicated to free software in general.</subtitle>\n";
410 _feed_data += " <logo>https:/www.luflow.net/static/img/icon.png</logo>\n";
411 _feed_data += " <icon>https:/www.luflow.net/favicon.ico</icon>\n";
414 for blog_post in &lock.blog_posts {
415 _feed_data += " <entry>\n";
418 _feed_data += " <author>\n";
419 _feed_data += " <name>Andreas</name>\n";
420 _feed_data += " </author>\n";
423 _feed_data += " <title type=\"html\"><![CDATA[";
424 _feed_data += &blog_post.title;
425 _feed_data += "]]></title>\n";
428 _feed_data += " <link href=\"https:/www.luflow.net/";
429 _feed_data += &lock.base_dir;
431 _feed_data += &blog_post.url;
432 _feed_data += "/\"/>\n";
435 _feed_data += " <id>https:/www.luflow.net/";
436 _feed_data += &lock.base_dir;
438 _feed_data += &blog_post.url;
439 _feed_data += "/</id>\n";
442 _feed_data += " <updated>";
443 _feed_data += &blog_post.updated_for_feed;
444 _feed_data += "</updated>\n";
447 _feed_data += " <published>";
448 _feed_data += &blog_post.published_for_feed;
449 _feed_data += "</published>\n";
452 for topic in &blog_post.topics {
453 _feed_data += " <category term=\"";
454 _feed_data += &topic;
455 _feed_data += "\"/>\n";
459 _feed_data += " <summary type=\"html\"><![CDATA[";
460 _feed_data += &blog_post.snippet;
461 _feed_data += "]]></summary>\n";
464 _feed_data += " <content type=\"html\"><![CDATA[";
465 _feed_data += &blog_post.html;
466 _feed_data += "]]></content>\n";
468 _feed_data += " </entry>\n";
472 _feed_data += "</feed>";
475 Helper::write_file_sync(
476 &Helper::get_output_dir().join("feeds").join("blog.atom"),
477 _feed_data.as_bytes(),
482 fn get_date_now_for_feed() -> String {
483 let date = Local::now();
485 let month = date.month();
486 let day = date.day();
488 let mut month_str = month.to_string();
489 let mut day_str = day.to_string();
492 month_str = format!("0{}", month);
496 day_str = format!("0{}", day);
499 return format!("{}-{}-{}T{}Z", date.year(), month_str, day_str, date.time());