Compare commits
2 commits
897e1cbf88
...
84932c0d1e
Author | SHA1 | Date | |
---|---|---|---|
84932c0d1e | |||
cf102126b3 |
12 changed files with 252 additions and 128 deletions
15
README.md
15
README.md
|
@ -22,7 +22,7 @@ blazingly fast markdown blog software written in rust memory safe
|
||||||
- [ ] ^ replace HashMap with HashCache once i implement [this](https://github.com/wvwwvwwv/scalable-concurrent-containers/issues/139)
|
- [ ] ^ replace HashMap with HashCache once i implement [this](https://github.com/wvwwvwwv/scalable-concurrent-containers/issues/139)
|
||||||
- [x] (de)compress cache with zstd on startup/shutdown
|
- [x] (de)compress cache with zstd on startup/shutdown
|
||||||
- [ ] make date parsing less strict
|
- [ ] make date parsing less strict
|
||||||
- [ ] make date formatting better
|
- [x] make date formatting better
|
||||||
- [ ] date formatting respects user timezone
|
- [ ] date formatting respects user timezone
|
||||||
- [x] clean up imports and require less features
|
- [x] clean up imports and require less features
|
||||||
- [ ] improve home page
|
- [ ] improve home page
|
||||||
|
@ -36,9 +36,16 @@ blazingly fast markdown blog software written in rust memory safe
|
||||||
the default configuration with comments looks like this
|
the default configuration with comments looks like this
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
title = "bingus-blog" # title of the website
|
title = "bingus-blog" # title of the blog
|
||||||
description = "blazingly fast markdown blog software written in rust memory safe" # description of the website
|
# description of the blog
|
||||||
raw_access = true # allow users to see the raw markdown of a post
|
description = "blazingly fast markdown blog software written in rust memory safe"
|
||||||
|
markdown_access = true # allow users to see the raw markdown of a post
|
||||||
|
# endpoint: /posts/<name>.md
|
||||||
|
date_format = "RFC3339" # format string used to format dates in the backend
|
||||||
|
# it's highly recommended to leave this as default,
|
||||||
|
# so the date can be formatted by the browser.
|
||||||
|
# format: https://docs.rs/chrono/latest/chrono/format/strftime/index.html#specifiers
|
||||||
|
js_enable = true # enable javascript (required for above)
|
||||||
|
|
||||||
[rss]
|
[rss]
|
||||||
enable = false # serve an rss field under /feed.xml
|
enable = false # serve an rss field under /feed.xml
|
||||||
|
|
46
src/app.rs
46
src/app.rs
|
@ -3,21 +3,21 @@ use std::time::Duration;
|
||||||
|
|
||||||
use askama_axum::Template;
|
use askama_axum::Template;
|
||||||
use axum::extract::{Path, Query, State};
|
use axum::extract::{Path, Query, State};
|
||||||
|
use axum::http::header::CONTENT_TYPE;
|
||||||
use axum::http::{header, Request};
|
use axum::http::{header, Request};
|
||||||
use axum::response::{IntoResponse, Redirect, Response};
|
use axum::response::{IntoResponse, Redirect, Response};
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::{Json, Router};
|
use axum::{Json, Router};
|
||||||
use rss::{Category, ChannelBuilder, ItemBuilder};
|
use rss::{Category, ChannelBuilder, ItemBuilder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tokio::io::AsyncReadExt;
|
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::{info, info_span, Span};
|
use tracing::{info, info_span, Span};
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::{Config, DateFormat};
|
||||||
use crate::error::{AppError, AppResult};
|
use crate::error::{AppError, AppResult};
|
||||||
use crate::filters;
|
use crate::filters;
|
||||||
use crate::post::{MarkdownPosts, PostManager, PostMetadata, RenderStats};
|
use crate::post::{MarkdownPosts, PostManager, PostMetadata, RenderStats, ReturnedPost};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
|
@ -31,6 +31,8 @@ struct IndexTemplate {
|
||||||
title: String,
|
title: String,
|
||||||
description: String,
|
description: String,
|
||||||
posts: Vec<PostMetadata>,
|
posts: Vec<PostMetadata>,
|
||||||
|
df: DateFormat,
|
||||||
|
js: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
|
@ -40,6 +42,8 @@ struct PostTemplate {
|
||||||
rendered: String,
|
rendered: String,
|
||||||
rendered_in: RenderStats,
|
rendered_in: RenderStats,
|
||||||
markdown_access: bool,
|
markdown_access: bool,
|
||||||
|
df: DateFormat,
|
||||||
|
js: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -61,6 +65,8 @@ async fn index(
|
||||||
title: config.title.clone(),
|
title: config.title.clone(),
|
||||||
description: config.description.clone(),
|
description: config.description.clone(),
|
||||||
posts,
|
posts,
|
||||||
|
df: config.date_format.clone(),
|
||||||
|
js: config.js_enable,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,26 +144,22 @@ async fn post(
|
||||||
State(AppState { config, posts }): State<AppState>,
|
State(AppState { config, posts }): State<AppState>,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
) -> AppResult<Response> {
|
) -> AppResult<Response> {
|
||||||
if name.ends_with(".md") && config.raw_access {
|
match posts.get_post(&name).await? {
|
||||||
let mut file = tokio::fs::OpenOptions::new()
|
ReturnedPost::Rendered(meta, rendered, rendered_in) => {
|
||||||
.read(true)
|
let page = PostTemplate {
|
||||||
.open(config.dirs.posts.join(&name))
|
meta,
|
||||||
.await?;
|
rendered,
|
||||||
|
rendered_in,
|
||||||
|
markdown_access: config.markdown_access,
|
||||||
|
df: config.date_format.clone(),
|
||||||
|
js: config.js_enable,
|
||||||
|
};
|
||||||
|
|
||||||
let mut buf = Vec::new();
|
Ok(page.into_response())
|
||||||
file.read_to_end(&mut buf).await?;
|
}
|
||||||
|
ReturnedPost::Raw(body, content_type) => {
|
||||||
Ok(([("content-type", "text/plain")], buf).into_response())
|
Ok(([(CONTENT_TYPE, content_type)], body).into_response())
|
||||||
} else {
|
}
|
||||||
let post = posts.get_post(&name).await?;
|
|
||||||
let page = PostTemplate {
|
|
||||||
meta: post.0,
|
|
||||||
rendered: post.1,
|
|
||||||
rendered_in: post.2,
|
|
||||||
markdown_access: config.raw_access,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(page.into_response())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv6Addr};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use color_eyre::eyre::{bail, Context, Result};
|
use color_eyre::eyre::{bail, Context, Result};
|
||||||
|
@ -59,13 +59,22 @@ pub struct RssConfig {
|
||||||
pub link: Url,
|
pub link: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
|
||||||
|
pub enum DateFormat {
|
||||||
|
#[default]
|
||||||
|
RFC3339,
|
||||||
|
#[serde(untagged)]
|
||||||
|
Strftime(String),
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub raw_access: bool,
|
pub markdown_access: bool,
|
||||||
pub num_posts: usize,
|
pub date_format: DateFormat,
|
||||||
|
pub js_enable: bool,
|
||||||
pub rss: RssConfig,
|
pub rss: RssConfig,
|
||||||
pub dirs: DirsConfig,
|
pub dirs: DirsConfig,
|
||||||
pub http: HttpConfig,
|
pub http: HttpConfig,
|
||||||
|
@ -78,8 +87,9 @@ impl Default for Config {
|
||||||
Self {
|
Self {
|
||||||
title: "bingus-blog".into(),
|
title: "bingus-blog".into(),
|
||||||
description: "blazingly fast markdown blog software written in rust memory safe".into(),
|
description: "blazingly fast markdown blog software written in rust memory safe".into(),
|
||||||
raw_access: true,
|
markdown_access: true,
|
||||||
num_posts: 5,
|
date_format: Default::default(),
|
||||||
|
js_enable: true,
|
||||||
// i have a love-hate relationship with serde
|
// i have a love-hate relationship with serde
|
||||||
// it was engimatic at first, but then i started actually using it
|
// it was engimatic at first, but then i started actually using it
|
||||||
// writing my own serialize and deserialize implementations.. spending
|
// writing my own serialize and deserialize implementations.. spending
|
||||||
|
@ -111,7 +121,7 @@ impl Default for DirsConfig {
|
||||||
impl Default for HttpConfig {
|
impl Default for HttpConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
host: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
host: IpAddr::V6(Ipv6Addr::UNSPECIFIED),
|
||||||
port: 3000,
|
port: 3000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,29 @@
|
||||||
use std::{collections::HashMap, time::Duration};
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Display;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use chrono::{DateTime, TimeZone};
|
use chrono::{DateTime, TimeZone};
|
||||||
|
|
||||||
|
use crate::config::DateFormat;
|
||||||
use crate::post::PostMetadata;
|
use crate::post::PostMetadata;
|
||||||
|
|
||||||
pub fn date<T: TimeZone>(date: &DateTime<T>) -> Result<String, askama::Error> {
|
fn format_date<T>(date: &DateTime<T>, date_format: &DateFormat) -> String
|
||||||
Ok(date.to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
|
where
|
||||||
|
T: TimeZone,
|
||||||
|
T::Offset: Display,
|
||||||
|
{
|
||||||
|
match date_format {
|
||||||
|
DateFormat::RFC3339 => date.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
|
||||||
|
DateFormat::Strftime(ref format_string) => date.format(format_string).to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn date<T>(date: &DateTime<T>, date_format: &DateFormat) -> Result<String, askama::Error>
|
||||||
|
where
|
||||||
|
T: TimeZone,
|
||||||
|
T::Offset: Display,
|
||||||
|
{
|
||||||
|
Ok(format_date(date, date_format))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn duration(duration: &&Duration) -> Result<String, askama::Error> {
|
pub fn duration(duration: &&Duration) -> Result<String, askama::Error> {
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
|
use crate::config::{Config, RenderConfig};
|
||||||
|
use crate::post::PostMetadata;
|
||||||
use color_eyre::eyre::{self, Context};
|
use color_eyre::eyre::{self, Context};
|
||||||
use scc::HashMap;
|
use scc::HashMap;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
use crate::config::{Config, RenderConfig};
|
|
||||||
use crate::post::PostMetadata;
|
|
||||||
|
|
||||||
/// do not persist cache if this version number changed
|
/// do not persist cache if this version number changed
|
||||||
pub const CACHE_VERSION: u16 = 2;
|
pub const CACHE_VERSION: u16 = 2;
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
|
use std::collections::BTreeSet;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use axum::http::HeaderValue;
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use color_eyre::eyre::{self, Context};
|
use color_eyre::eyre::{self, Context};
|
||||||
use fronma::parser::{parse, ParsedData};
|
use fronma::parser::{parse, ParsedData};
|
||||||
|
use serde::Deserialize;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::AsyncReadExt;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
@ -13,9 +18,40 @@ use tracing::{error, info, warn};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::markdown_render::render;
|
use crate::markdown_render::render;
|
||||||
use crate::post::cache::{load_cache, Cache, CACHE_VERSION};
|
use crate::post::cache::{load_cache, Cache, CACHE_VERSION};
|
||||||
use crate::post::{FrontMatter, PostError, PostManager, PostMetadata, RenderStats};
|
use crate::post::{PostError, PostManager, PostMetadata, RenderStats, ReturnedPost};
|
||||||
use crate::systemtime_as_secs::as_secs;
|
use crate::systemtime_as_secs::as_secs;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct FrontMatter {
|
||||||
|
pub title: String,
|
||||||
|
pub description: String,
|
||||||
|
pub author: String,
|
||||||
|
pub icon: Option<String>,
|
||||||
|
pub created_at: Option<DateTime<Utc>>,
|
||||||
|
pub modified_at: Option<DateTime<Utc>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub tags: BTreeSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrontMatter {
|
||||||
|
pub fn into_full(
|
||||||
|
self,
|
||||||
|
name: String,
|
||||||
|
created: Option<SystemTime>,
|
||||||
|
modified: Option<SystemTime>,
|
||||||
|
) -> PostMetadata {
|
||||||
|
PostMetadata {
|
||||||
|
name,
|
||||||
|
title: self.title,
|
||||||
|
description: self.description,
|
||||||
|
author: self.author,
|
||||||
|
icon: self.icon,
|
||||||
|
created_at: self.created_at.or_else(|| created.map(|t| t.into())),
|
||||||
|
modified_at: self.modified_at.or_else(|| modified.map(|t| t.into())),
|
||||||
|
tags: self.tags.into_iter().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
pub struct MarkdownPosts<C>
|
pub struct MarkdownPosts<C>
|
||||||
where
|
where
|
||||||
C: Deref<Target = Config>,
|
C: Deref<Target = Config>,
|
||||||
|
@ -219,8 +255,10 @@ where
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let post = self.get_post(&name).await?;
|
let post = self.get_post(&name).await?;
|
||||||
if filter(&post.0, &post.1) {
|
if let ReturnedPost::Rendered(meta, content, stats) = post
|
||||||
posts.push(post);
|
&& filter(&meta, &content)
|
||||||
|
{
|
||||||
|
posts.push((meta, content, stats));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,39 +266,66 @@ where
|
||||||
Ok(posts)
|
Ok(posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_post(&self, name: &str) -> Result<(PostMetadata, String, RenderStats), PostError> {
|
async fn get_post(&self, name: &str) -> Result<ReturnedPost, PostError> {
|
||||||
let start = Instant::now();
|
if self.config.markdown_access && name.ends_with(".md") {
|
||||||
let path = self.config.dirs.posts.join(name.to_owned() + ".md");
|
let path = self.config.dirs.posts.join(name);
|
||||||
|
|
||||||
let stat = match tokio::fs::metadata(&path).await {
|
let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(err) => match err.kind() {
|
Err(err) => match err.kind() {
|
||||||
io::ErrorKind::NotFound => {
|
io::ErrorKind::NotFound => {
|
||||||
if let Some(cache) = self.cache.as_ref() {
|
if let Some(cache) = self.cache.as_ref() {
|
||||||
cache.remove(name).await;
|
cache.remove(name).await;
|
||||||
|
}
|
||||||
|
return Err(PostError::NotFound(name.to_string()));
|
||||||
}
|
}
|
||||||
return Err(PostError::NotFound(name.to_string()));
|
_ => return Err(PostError::IoError(err)),
|
||||||
}
|
},
|
||||||
_ => return Err(PostError::IoError(err)),
|
};
|
||||||
},
|
|
||||||
};
|
|
||||||
let mtime = as_secs(&stat.modified()?);
|
|
||||||
|
|
||||||
if let Some(cache) = self.cache.as_ref()
|
let mut buf = Vec::with_capacity(4096);
|
||||||
&& let Some(hit) = cache.lookup(name, mtime, &self.config.render).await
|
|
||||||
{
|
file.read_to_end(&mut buf).await?;
|
||||||
Ok((
|
|
||||||
hit.metadata,
|
Ok(ReturnedPost::Raw(
|
||||||
hit.rendered,
|
buf,
|
||||||
RenderStats::Cached(start.elapsed()),
|
HeaderValue::from_static("text/plain"),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
let (metadata, rendered, stats) = self.parse_and_render(name.to_string(), path).await?;
|
let start = Instant::now();
|
||||||
Ok((
|
let path = self.config.dirs.posts.join(name.to_owned() + ".md");
|
||||||
metadata,
|
|
||||||
rendered,
|
let stat = match tokio::fs::metadata(&path).await {
|
||||||
RenderStats::ParsedAndRendered(start.elapsed(), stats.0, stats.1),
|
Ok(value) => value,
|
||||||
))
|
Err(err) => match err.kind() {
|
||||||
|
io::ErrorKind::NotFound => {
|
||||||
|
if let Some(cache) = self.cache.as_ref() {
|
||||||
|
cache.remove(name).await;
|
||||||
|
}
|
||||||
|
return Err(PostError::NotFound(name.to_string()));
|
||||||
|
}
|
||||||
|
_ => return Err(PostError::IoError(err)),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let mtime = as_secs(&stat.modified()?);
|
||||||
|
|
||||||
|
if let Some(cache) = self.cache.as_ref()
|
||||||
|
&& let Some(hit) = cache.lookup(name, mtime, &self.config.render).await
|
||||||
|
{
|
||||||
|
Ok(ReturnedPost::Rendered(
|
||||||
|
hit.metadata,
|
||||||
|
hit.rendered,
|
||||||
|
RenderStats::Cached(start.elapsed()),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
let (metadata, rendered, stats) =
|
||||||
|
self.parse_and_render(name.to_string(), path).await?;
|
||||||
|
Ok(ReturnedPost::Rendered(
|
||||||
|
metadata,
|
||||||
|
rendered,
|
||||||
|
RenderStats::ParsedAndRendered(start.elapsed(), stats.0, stats.1),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,47 +1,15 @@
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod markdown_posts;
|
pub mod markdown_posts;
|
||||||
|
|
||||||
use std::collections::BTreeSet;
|
use std::time::Duration;
|
||||||
use std::time::{Duration, SystemTime};
|
|
||||||
|
|
||||||
|
use axum::http::HeaderValue;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::error::PostError;
|
use crate::error::PostError;
|
||||||
pub use crate::post::markdown_posts::MarkdownPosts;
|
pub use crate::post::markdown_posts::MarkdownPosts;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct FrontMatter {
|
|
||||||
pub title: String,
|
|
||||||
pub description: String,
|
|
||||||
pub author: String,
|
|
||||||
pub icon: Option<String>,
|
|
||||||
pub created_at: Option<DateTime<Utc>>,
|
|
||||||
pub modified_at: Option<DateTime<Utc>>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub tags: BTreeSet<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FrontMatter {
|
|
||||||
pub fn into_full(
|
|
||||||
self,
|
|
||||||
name: String,
|
|
||||||
created: Option<SystemTime>,
|
|
||||||
modified: Option<SystemTime>,
|
|
||||||
) -> PostMetadata {
|
|
||||||
PostMetadata {
|
|
||||||
name,
|
|
||||||
title: self.title,
|
|
||||||
description: self.description,
|
|
||||||
author: self.author,
|
|
||||||
icon: self.icon,
|
|
||||||
created_at: self.created_at.or_else(|| created.map(|t| t.into())),
|
|
||||||
modified_at: self.modified_at.or_else(|| modified.map(|t| t.into())),
|
|
||||||
tags: self.tags.into_iter().collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct PostMetadata {
|
pub struct PostMetadata {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
@ -54,13 +22,18 @@ pub struct PostMetadata {
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub enum RenderStats {
|
pub enum RenderStats {
|
||||||
Cached(Duration),
|
Cached(Duration),
|
||||||
// format: Total, Parsed in, Rendered in
|
// format: Total, Parsed in, Rendered in
|
||||||
ParsedAndRendered(Duration, Duration, Duration),
|
ParsedAndRendered(Duration, Duration, Duration),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)] // Raw will be returned very rarely
|
||||||
|
pub enum ReturnedPost {
|
||||||
|
Rendered(PostMetadata, String, RenderStats),
|
||||||
|
Raw(Vec<u8>, HeaderValue),
|
||||||
|
}
|
||||||
|
|
||||||
pub trait PostManager {
|
pub trait PostManager {
|
||||||
async fn get_all_post_metadata(
|
async fn get_all_post_metadata(
|
||||||
&self,
|
&self,
|
||||||
|
@ -97,10 +70,13 @@ pub trait PostManager {
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
async fn get_post_metadata(&self, name: &str) -> Result<PostMetadata, PostError> {
|
async fn get_post_metadata(&self, name: &str) -> Result<PostMetadata, PostError> {
|
||||||
self.get_post(name).await.map(|(meta, ..)| meta)
|
match self.get_post(name).await? {
|
||||||
|
ReturnedPost::Rendered(metadata, ..) => Ok(metadata),
|
||||||
|
ReturnedPost::Raw(..) => Err(PostError::NotFound(name.to_string())),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_post(&self, name: &str) -> Result<(PostMetadata, String, RenderStats), PostError>;
|
async fn get_post(&self, name: &str) -> Result<ReturnedPost, PostError>;
|
||||||
|
|
||||||
async fn cleanup(&self);
|
async fn cleanup(&self);
|
||||||
}
|
}
|
||||||
|
|
4
static/main.js
Normal file
4
static/main.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
for (let el of document.querySelectorAll(".date-rfc3339")) {
|
||||||
|
let date = new Date(Date.parse(el.textContent));
|
||||||
|
el.textContent = date.toLocaleString();
|
||||||
|
}
|
|
@ -84,6 +84,35 @@ div.post {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
display: grid;
|
||||||
|
/*grid-template-columns: auto auto auto;
|
||||||
|
grid-template-rows: auto auto;*/
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > :not(.value)::after {
|
||||||
|
content: ":";
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > .value {
|
||||||
|
margin-left: 1em;
|
||||||
|
text-align: end;
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > .created {
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > .modified {
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > .tags {
|
||||||
|
grid-row: 3;
|
||||||
|
}
|
||||||
|
|
||||||
/* BEGIN cool effect everyone liked */
|
/* BEGIN cool effect everyone liked */
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
|
@ -9,6 +9,9 @@
|
||||||
<meta property="og:description" content="{{ description }}" />
|
<meta property="og:description" content="{{ description }}" />
|
||||||
<title>{{ title }}</title>
|
<title>{{ title }}</title>
|
||||||
<link rel="stylesheet" href="/static/style.css" />
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
|
{% if js %}
|
||||||
|
<script src="/static/main.js" defer></script>
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|
|
@ -1,19 +1,31 @@
|
||||||
|
{% macro span_date(value) %}
|
||||||
|
<span class="{%- match df -%}
|
||||||
|
{% when DateFormat::RFC3339 %}
|
||||||
|
date-rfc3339
|
||||||
|
{% when DateFormat::Strftime(_) %}
|
||||||
|
{%- endmatch -%}">{{ value|date(df) }}</span>
|
||||||
|
{% endmacro %}
|
||||||
{% macro table(post) %}
|
{% macro table(post) %}
|
||||||
|
<div class="table">
|
||||||
{% match post.created_at %}
|
{% match post.created_at %}
|
||||||
{% when Some(created_at) %}
|
{% when Some(created_at) %}
|
||||||
written: {{ created_at|date }}<br />
|
<div class="created">written</div>
|
||||||
|
<div class="created value">{% call span_date(created_at) %}</div>
|
||||||
{% when None %}
|
{% when None %}
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
{% match post.modified_at %}
|
{% match post.modified_at %}
|
||||||
{% when Some(modified_at) %}
|
{% when Some(modified_at) %}
|
||||||
last modified: {{ modified_at|date }}<br />
|
<div class="modified">last modified</div>
|
||||||
|
<div class="modified value">{% call span_date(modified_at) %}</div>
|
||||||
{% when None %}
|
{% when None %}
|
||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
|
|
||||||
{% if !post.tags.is_empty() %}
|
{% if !post.tags.is_empty() %}
|
||||||
tags:
|
<div class="tags">tags</div>
|
||||||
|
<div class="tags value">
|
||||||
{% for tag in post.tags %}
|
{% for tag in post.tags %}
|
||||||
<a href="/?tag={{ tag }}" title="view all posts with this tag">{{ tag }}</a>
|
<a href="/?tag={{ tag }}" title="view all posts with this tag">{{ tag }}</a>
|
||||||
{% endfor %}<br />
|
{% endfor %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
|
@ -2,20 +2,21 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<head>
|
<meta charset="UTF-8" />
|
||||||
<meta charset="UTF-8" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="description" content="{{ meta.title }}" />
|
||||||
<meta name="description" content="{{ meta.title }}" />
|
<meta property="og:title" content="{{ meta.title }}" />
|
||||||
<meta property="og:title" content="{{ meta.title }}" />
|
<meta property="og:description" content="{{ meta.description }}" />
|
||||||
<meta property="og:description" content="{{ meta.description }}" />
|
{% match meta.icon %} {% when Some with (url) %}
|
||||||
{% match meta.icon %} {% when Some with (url) %}
|
<meta property="og:image" content="{{ url }}" />
|
||||||
<meta property="og:image" content="{{ url }}" />
|
<link rel="shortcut icon" href="{{ url }}" />
|
||||||
<link rel="shortcut icon" href="{{ url }}" />
|
{% when None %} {% endmatch %}
|
||||||
{% when None %} {% endmatch %}
|
<title>{{ meta.title }}</title>
|
||||||
<title>{{ meta.title }}</title>
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
<link rel="stylesheet" href="/static/style.css" />
|
<link rel="stylesheet" href="/static/post.css" />
|
||||||
<link rel="stylesheet" href="/static/post.css" />
|
{% if js %}
|
||||||
</head>
|
<script src="/static/main.js" defer></script>
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
@ -24,11 +25,9 @@
|
||||||
<span class="post-author">- by {{ meta.author }}</span>
|
<span class="post-author">- by {{ meta.author }}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="post-desc">{{ meta.description }}</p>
|
<p class="post-desc">{{ meta.description }}</p>
|
||||||
<div class="" post>
|
<div class="post">
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<div>
|
|
||||||
{% call macros::table(meta) %}
|
{% call macros::table(meta) %}
|
||||||
</div>
|
|
||||||
<a href="/posts/{{ meta.name }}">link</a><br />
|
<a href="/posts/{{ meta.name }}">link</a><br />
|
||||||
<a href="/">back to home</a>
|
<a href="/">back to home</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue