implement blog
This commit is contained in:
		
							parent
							
								
									a508c3a7b4
								
							
						
					
					
						commit
						5f2a489056
					
				| 
						 | 
				
			
			@ -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),
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										32
									
								
								src/main.rs
								
								
								
								
							
							
						
						
									
										32
									
								
								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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -124,6 +124,12 @@ main {
 | 
			
		|||
  align-items: flex-start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reverse {
 | 
			
		||||
  flex-direction: row-reverse;
 | 
			
		||||
  flex-wrap: wrap-reverse;
 | 
			
		||||
  align-items: flex-end;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main-content {
 | 
			
		||||
  flex: 12 1 0;
 | 
			
		||||
  min-width: 50%;
 | 
			
		||||
| 
						 | 
				
			
			@ -316,6 +322,63 @@ iframe {
 | 
			
		|||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* blog */
 | 
			
		||||
 | 
			
		||||
.blogpost {
 | 
			
		||||
  font-family: Sligoil;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tags {
 | 
			
		||||
  display: flexbox;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  gap: 0.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tags a {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blogpost .tag {
 | 
			
		||||
  font-family: 'Go Mono';
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blogpost .title {
 | 
			
		||||
  font-family: 'kaeru kaeru';
 | 
			
		||||
  font-size: 4em;
 | 
			
		||||
  margin: 0.5em 0 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blogpost .created-at {
 | 
			
		||||
  font-family: 'Steps Mono';
 | 
			
		||||
  font-size: 1em;
 | 
			
		||||
  margin: 0 0 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blogpost .created-at a,
 | 
			
		||||
.blogpost-content a {
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin: 0 0.5em;
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
  color: #b52f6a;
 | 
			
		||||
  text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.blogpost-content ul {
 | 
			
		||||
  list-style-type: initial;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* filter-tags */
 | 
			
		||||
#filter-tags {
 | 
			
		||||
  font-family: 'Go Mono';
 | 
			
		||||
  z-index: -1;
 | 
			
		||||
  background-color: #afd7ff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#filter-tags h2 {
 | 
			
		||||
  font-family: 'Go Mono';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* branches */
 | 
			
		||||
 | 
			
		||||
.branch {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -37,7 +37,7 @@
 | 
			
		|||
        <ul id="nav">
 | 
			
		||||
          <li><a class="{% block nav_home %}{% endblock %}" href="/">home</a></li>
 | 
			
		||||
          <li><a class="{% block nav_contact %}{% endblock %}" style="font-family: 'Compagnon Roman';" href="/contact">kontakt</a></li>
 | 
			
		||||
          <li><a class="{% block nav_girlblog %}{% endblock %}" style="font-family: Sligoil" href="/blog">girlblog</a></li>
 | 
			
		||||
          <li><a class="{% block nav_blog %}{% endblock %}" style="font-family: Sligoil" href="/blog">girlblog</a></li>
 | 
			
		||||
          <li><a class="{% block nav_projects %}{% endblock %}" style="font-family: 'DeGerm LoCase';" href="/projects">projetos</a></li>
 | 
			
		||||
          <li><a class="{% block nav_sound %}{% endblock %}" style="font-family: 'kirieji'" href="/sound">音</a></li>
 | 
			
		||||
          <li><a class="{% block nav_listens %}{% endblock %}" style="font-family: 'Almendra Display'; font-weight: 900;" href="https://listenbrainz.org/celblossom">écoute</a></li>
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +56,7 @@
 | 
			
		|||
        </ul>
 | 
			
		||||
      </nav>
 | 
			
		||||
 | 
			
		||||
      <main>
 | 
			
		||||
      <main class="{% if reverse %}{{ reverse }}{% endif %}">
 | 
			
		||||
        <div class="main-content">
 | 
			
		||||
 | 
			
		||||
          {% block content %}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
{% extends "base" %}
 | 
			
		||||
 | 
			
		||||
{% block nav_blog %}active{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
{% for blogpost in blogposts %}
 | 
			
		||||
{% include "blogpost-panel" %}
 | 
			
		||||
{% endfor %}
 | 
			
		||||
 | 
			
		||||
{% endblock content %}
 | 
			
		||||
 | 
			
		||||
{% block aside %}
 | 
			
		||||
 | 
			
		||||
<aside>
 | 
			
		||||
 | 
			
		||||
  {% include "latestposts" %}
 | 
			
		||||
  {% include "filtertags" %}
 | 
			
		||||
 | 
			
		||||
</aside>
 | 
			
		||||
 | 
			
		||||
{% endblock aside %}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
<div class="panel content blogpost">
 | 
			
		||||
  <h1 class="title">{{ blogpost.subject }}</h1>
 | 
			
		||||
  <h2 class="created-at">{{ blogpost.created_at }}<a href="/blog/{{ blogpost.data.name }}">permalink</a></h2>
 | 
			
		||||
  <div class="tags">{% for tag in blogpost.tags %}<a class="tag" href="/tag/{{ tag }}">{{ tag }}</a>{% endfor %}</div>
 | 
			
		||||
  <div class="blogpost-content">
 | 
			
		||||
  {{ blogpost.render }}
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
{% extends "base" %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
{% include "blogpost-panel" %}
 | 
			
		||||
 | 
			
		||||
{% endblock content %}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
<div class="panel" id="filter-tags">
 | 
			
		||||
<h2>filter tag</h2>
 | 
			
		||||
<div class="tags">
 | 
			
		||||
  {% for tag in tags %}<a href="/blog/tag/{{ tag }}">{{ tag }}</a>{% endfor %}
 | 
			
		||||
</div>
 | 
			
		||||
<br>
 | 
			
		||||
</div>
 | 
			
		||||
		Loading…
	
		Reference in New Issue