aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/error.rs1
-rw-r--r--src/main.rs32
-rw-r--r--src/posts/mod.rs73
3 files changed, 93 insertions, 13 deletions
diff --git a/src/error.rs b/src/error.rs
index 81cadd1..eb6f6e6 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -8,6 +8,7 @@ pub enum BlossomError {
Chrono(Status, #[response(ignore)] chrono::ParseError),
Io(Status, #[response(ignore)] std::io::Error),
Deserialization(Status, #[response(ignore)] toml::de::Error),
+ NotFound(Status),
NoMetadata(Status),
Unimplemented(Status),
}
diff --git a/src/main.rs b/src/main.rs
index dd2bd34..c08c2e1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -47,6 +47,36 @@ async fn home(clients: &State<Clients>) -> Template {
)
}
+#[get("/blog")]
+async fn blog() -> Template {
+ let mut blogposts = posts::get_blogposts().await.unwrap_or_default();
+ let tags = posts::get_tags(&blogposts);
+ for blogpost in &mut blogposts {
+ blogpost.render().await;
+ }
+ let reverse = "reverse".to_owned();
+ Template::render(
+ "blog",
+ context! {
+ reverse,
+ blogposts,
+ tags,
+ },
+ )
+}
+
+#[get("/blog/<blogpost>")]
+async fn blogpost(blogpost: &str) -> Result<Template> {
+ let mut blogpost = posts::get_blogpost(blogpost).await?;
+ blogpost.render().await?;
+ Ok(Template::render(
+ "blogpost",
+ context! {
+ blogpost,
+ },
+ ))
+}
+
#[get("/contact")]
async fn contact() -> Template {
Template::render("contact", context! {})
@@ -90,7 +120,7 @@ async fn main() -> std::result::Result<(), rocket::Error> {
.attach(Template::custom(|engines| {
engines.tera.autoescape_on(vec![]);
}))
- .mount("/", routes![home, contact, plants])
+ .mount("/", routes![home, contact, blog, blogpost, plants])
.register("/", catchers![catcher])
.mount("/", FileServer::from(relative!("static")))
.launch()
diff --git a/src/posts/mod.rs b/src/posts/mod.rs
index b188ee0..be1e1a9 100644
--- a/src/posts/mod.rs
+++ b/src/posts/mod.rs
@@ -1,6 +1,8 @@
+use std::collections::HashSet;
+
use async_trait::async_trait;
use chrono::{DateTime, Utc};
-use markdown::{mdast::Node, Constructs, ParseOptions};
+use markdown::{mdast::Node, Constructs, Options, ParseOptions};
use rocket::http::Status;
use serde::{ser::SerializeStruct, Deserialize, Serialize};
use tokio::fs;
@@ -28,15 +30,23 @@ pub struct Post<D: Content> {
updated_at: Option<DateTime<Utc>>,
tags: Vec<String>,
post_type: PostType,
+ render: Option<String>,
data: D,
}
-impl<D: Content> Serialize for Post<D> {
+impl<D: Content + Serialize> Post<D> {
+ pub async fn render(&mut self) -> Result<()> {
+ self.render = Some(self.data.render().await?);
+ Ok(())
+ }
+}
+
+impl<D: Content + Serialize> Serialize for Post<D> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
- let mut state = serializer.serialize_struct("Post", 5)?;
+ let mut state = serializer.serialize_struct("Post", 7)?;
state.serialize_field("subject", &self.subject)?;
state.serialize_field("created_at", &self.created_at.to_string())?;
state.serialize_field(
@@ -45,6 +55,8 @@ impl<D: Content> Serialize for Post<D> {
)?;
state.serialize_field("tags", &self.tags)?;
state.serialize_field("post_type", &self.post_type)?;
+ state.serialize_field("render", &self.render)?;
+ state.serialize_field("data", &self.data)?;
state.end()
}
}
@@ -52,29 +64,55 @@ impl<D: Content> Serialize for Post<D> {
pub async fn get_blogposts() -> Result<Vec<Post<Article>>> {
let mut blogposts: Vec<Post<Article>> = Vec::new();
let mut articles_dir = fs::read_dir("./articles").await?;
- while let Some(dir) = articles_dir.next_entry().await? {
- let mut file_path = dir.path();
- file_path.push(dir.file_name());
- let file_path = file_path.with_extension("md");
- println!("{:?}", file_path);
+ while let Some(file) = articles_dir.next_entry().await? {
+ let name = file.file_name();
+ let name = name.to_str().unwrap_or_default()[..name.len() - 3].to_owned();
let blogpost: Article = Article {
- path: file_path.to_str().unwrap_or_default().to_owned(),
+ path: file.path().to_str().unwrap_or_default().to_owned(),
+ name,
};
let blogpost = Post::try_from(blogpost).await.unwrap();
- println!("{:?}", blogpost);
blogposts.push(blogpost);
}
Ok(blogposts)
}
+pub async fn get_blogpost(post_name: &str) -> Result<Post<Article>> {
+ let mut articles_dir = fs::read_dir("./articles").await?;
+ while let Some(file) = articles_dir.next_entry().await? {
+ let name = file.file_name();
+ let name = &name.to_str().unwrap_or_default()[..name.len() - 3];
+ if name == post_name {
+ let blogpost = Article {
+ path: file.path().to_str().unwrap_or_default().to_owned(),
+ name: name.to_owned(),
+ };
+ let blogpost = Post::try_from(blogpost).await.unwrap();
+ return Ok(blogpost);
+ }
+ }
+ Err(BlossomError::NotFound(Status::new(404)))
+}
+
+pub fn get_tags<D: Content>(posts: &Vec<Post<D>>) -> HashSet<String> {
+ posts.into_iter().fold(HashSet::new(), |mut acc, post| {
+ let tags = &post.tags;
+ for tag in tags {
+ acc.insert(tag.to_owned());
+ }
+ acc
+ })
+}
+
#[async_trait]
pub trait Content {
async fn render(&self) -> Result<String>;
}
-#[derive(Debug)]
+#[derive(Serialize, Debug)]
pub struct Article {
path: String,
+ name: String,
}
impl Article {
@@ -116,7 +154,17 @@ impl Content for Article {
let mut file = fs::File::open(&self.path).await?;
let mut buf = String::new();
file.read_to_string(&mut buf).await?;
- Ok(markdown::to_html(&buf))
+ let options = Options {
+ parse: ParseOptions {
+ constructs: Constructs {
+ frontmatter: true,
+ ..Constructs::default()
+ },
+ ..ParseOptions::default()
+ },
+ ..Options::default()
+ };
+ Ok(markdown::to_html_with_options(&buf, &options).unwrap())
}
}
@@ -144,6 +192,7 @@ impl Post<Article> {
updated_at,
tags: metadata.tags,
post_type: PostType::Article,
+ render: None,
data: article,
})
}