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