implement blog
This commit is contained in:
parent
a508c3a7b4
commit
5f2a489056
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
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")]
|
#[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()
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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