implement blog

This commit is contained in:
cel 🌸 2023-06-21 23:46:20 +01:00
parent a508c3a7b4
commit 5f2a489056
Signed by: cel
GPG Key ID: 48E29AF13B5F1349
10 changed files with 202 additions and 15 deletions

View File

@ -8,6 +8,7 @@ pub enum BlossomError {
Chrono(Status, #[response(ignore)] chrono::ParseError), Chrono(Status, #[response(ignore)] chrono::ParseError),
Io(Status, #[response(ignore)] std::io::Error), Io(Status, #[response(ignore)] std::io::Error),
Deserialization(Status, #[response(ignore)] toml::de::Error), Deserialization(Status, #[response(ignore)] toml::de::Error),
NotFound(Status),
NoMetadata(Status), NoMetadata(Status),
Unimplemented(Status), Unimplemented(Status),
} }

View File

@ -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")] #[get("/contact")]
async fn contact() -> Template { async fn contact() -> Template {
Template::render("contact", context! {}) Template::render("contact", context! {})
@ -90,7 +120,7 @@ async fn main() -> std::result::Result<(), rocket::Error> {
.attach(Template::custom(|engines| { .attach(Template::custom(|engines| {
engines.tera.autoescape_on(vec![]); engines.tera.autoescape_on(vec![]);
})) }))
.mount("/", routes![home, contact, plants]) .mount("/", routes![home, contact, blog, blogpost, plants])
.register("/", catchers![catcher]) .register("/", catchers![catcher])
.mount("/", FileServer::from(relative!("static"))) .mount("/", FileServer::from(relative!("static")))
.launch() .launch()

View File

@ -1,6 +1,8 @@
use std::collections::HashSet;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use markdown::{mdast::Node, Constructs, ParseOptions}; use markdown::{mdast::Node, Constructs, Options, ParseOptions};
use rocket::http::Status; use rocket::http::Status;
use serde::{ser::SerializeStruct, Deserialize, Serialize}; use serde::{ser::SerializeStruct, Deserialize, Serialize};
use tokio::fs; use tokio::fs;
@ -28,15 +30,23 @@ pub struct Post<D: Content> {
updated_at: Option<DateTime<Utc>>, updated_at: Option<DateTime<Utc>>,
tags: Vec<String>, tags: Vec<String>,
post_type: PostType, post_type: PostType,
render: Option<String>,
data: D, 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> fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where where
S: serde::Serializer, 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("subject", &self.subject)?;
state.serialize_field("created_at", &self.created_at.to_string())?; state.serialize_field("created_at", &self.created_at.to_string())?;
state.serialize_field( state.serialize_field(
@ -45,6 +55,8 @@ impl<D: Content> Serialize for Post<D> {
)?; )?;
state.serialize_field("tags", &self.tags)?; state.serialize_field("tags", &self.tags)?;
state.serialize_field("post_type", &self.post_type)?; state.serialize_field("post_type", &self.post_type)?;
state.serialize_field("render", &self.render)?;
state.serialize_field("data", &self.data)?;
state.end() state.end()
} }
} }
@ -52,29 +64,55 @@ impl<D: Content> Serialize for Post<D> {
pub async fn get_blogposts() -> Result<Vec<Post<Article>>> { pub async fn get_blogposts() -> Result<Vec<Post<Article>>> {
let mut blogposts: Vec<Post<Article>> = Vec::new(); let mut blogposts: Vec<Post<Article>> = Vec::new();
let mut articles_dir = fs::read_dir("./articles").await?; let mut articles_dir = fs::read_dir("./articles").await?;
while let Some(dir) = articles_dir.next_entry().await? { while let Some(file) = articles_dir.next_entry().await? {
let mut file_path = dir.path(); let name = file.file_name();
file_path.push(dir.file_name()); let name = name.to_str().unwrap_or_default()[..name.len() - 3].to_owned();
let file_path = file_path.with_extension("md");
println!("{:?}", file_path);
let blogpost: Article = Article { 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(); let blogpost = Post::try_from(blogpost).await.unwrap();
println!("{:?}", blogpost);
blogposts.push(blogpost); blogposts.push(blogpost);
} }
Ok(blogposts) 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] #[async_trait]
pub trait Content { pub trait Content {
async fn render(&self) -> Result<String>; async fn render(&self) -> Result<String>;
} }
#[derive(Debug)] #[derive(Serialize, Debug)]
pub struct Article { pub struct Article {
path: String, path: String,
name: String,
} }
impl Article { impl Article {
@ -116,7 +154,17 @@ impl Content for Article {
let mut file = fs::File::open(&self.path).await?; let mut file = fs::File::open(&self.path).await?;
let mut buf = String::new(); let mut buf = String::new();
file.read_to_string(&mut buf).await?; 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, updated_at,
tags: metadata.tags, tags: metadata.tags,
post_type: PostType::Article, post_type: PostType::Article,
render: None,
data: article, data: article,
}) })
} }

View File

@ -124,6 +124,12 @@ main {
align-items: flex-start; align-items: flex-start;
} }
.reverse {
flex-direction: row-reverse;
flex-wrap: wrap-reverse;
align-items: flex-end;
}
.main-content { .main-content {
flex: 12 1 0; flex: 12 1 0;
min-width: 50%; min-width: 50%;
@ -316,6 +322,63 @@ iframe {
display: none; 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 */ /* branches */
.branch { .branch {

View File

@ -37,7 +37,7 @@
<ul id="nav"> <ul id="nav">
<li><a class="{% block nav_home %}{% endblock %}" href="/">home</a></li> <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_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_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_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> <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> </ul>
</nav> </nav>
<main> <main class="{% if reverse %}{{ reverse }}{% endif %}">
<div class="main-content"> <div class="main-content">
{% block content %} {% block content %}

22
templates/blog.html.tera Normal file
View File

@ -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 %}

View File

@ -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>

View File

@ -0,0 +1,7 @@
{% extends "base" %}
{% block content %}
{% include "blogpost-panel" %}
{% endblock content %}

View File

@ -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>