forked from slonk/bingus-blog
Compare commits
No commits in common. "514d4e033d8cf8783bb50cc93fcae48537da6fb8" and "cf85c60c1cdd1d5b80cdb8775273fcbc95a03a91" have entirely different histories.
514d4e033d
...
cf85c60c1c
10 changed files with 100 additions and 319 deletions
115
Cargo.lock
generated
115
Cargo.lock
generated
|
@ -128,19 +128,6 @@ dependencies = [
|
|||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atom_syndication"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "571832dcff775e26562e8e6930cd483de5587301d40d3a3b85d532b6383e15a7"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"derive_builder",
|
||||
"diligent-date-parser",
|
||||
"never",
|
||||
"quick-xml 0.30.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.2.0"
|
||||
|
@ -303,7 +290,6 @@ dependencies = [
|
|||
"comrak",
|
||||
"console-subscriber",
|
||||
"fronma",
|
||||
"rss",
|
||||
"scc",
|
||||
"serde",
|
||||
"syntect",
|
||||
|
@ -314,7 +300,6 @@ dependencies = [
|
|||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
|
@ -591,30 +576,12 @@ version = "1.4.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "322ef0094744e63628e6f0eb2295517f79276a5b342a4c2ff3042566ca181d4e"
|
||||
|
||||
[[package]]
|
||||
name = "diligent-date-parser"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6cf7fe294274a222363f84bcb63cdea762979a0443b4cf1f4f8fd17c86b1182"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "entities"
|
||||
version = "1.0.1"
|
||||
|
@ -951,16 +918,6 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.3"
|
||||
|
@ -1104,12 +1061,6 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "never"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
@ -1257,7 +1208,7 @@ dependencies = [
|
|||
"base64",
|
||||
"indexmap 2.2.6",
|
||||
"line-wrap",
|
||||
"quick-xml 0.31.0",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
|
@ -1315,16 +1266,6 @@ dependencies = [
|
|||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.30.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
|
@ -1417,18 +1358,6 @@ version = "0.8.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
||||
|
||||
[[package]]
|
||||
name = "rss"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7b2c77eb4450d7d5f98df52c381cd6c4e19b75dad9209a9530b85a44510219a"
|
||||
dependencies = [
|
||||
"atom_syndication",
|
||||
"derive_builder",
|
||||
"never",
|
||||
"quick-xml 0.30.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
|
@ -1723,21 +1652,6 @@ dependencies = [
|
|||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.37.0"
|
||||
|
@ -2015,45 +1929,18 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode_categories"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
|
|
|
@ -37,7 +37,6 @@ comrak = { version = "0.22.0", features = [
|
|||
], default-features = false }
|
||||
console-subscriber = { version = "0.2.0", optional = true }
|
||||
fronma = "0.2.0"
|
||||
rss = "2.0.7"
|
||||
scc = { version = "2.1.0", features = ["serde"] }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
syntect = "5.2.0"
|
||||
|
@ -57,5 +56,4 @@ tower-http = { version = "0.5.2", features = [
|
|||
], default-features = false }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
url = { version = "2.5.0", features = ["serde"] }
|
||||
zstd = { version = "0.13.1", default-features = false }
|
||||
|
|
|
@ -11,7 +11,7 @@ blazingly fast markdown blog software written in rust memory safe
|
|||
|
||||
## TODO
|
||||
|
||||
- [x] RSS
|
||||
- [ ] RSS
|
||||
- [x] finish writing this document
|
||||
- [x] document config
|
||||
- [ ] extend syntect options
|
||||
|
@ -27,7 +27,6 @@ blazingly fast markdown blog software written in rust memory safe
|
|||
- [x] clean up imports and require less features
|
||||
- [ ] improve home page
|
||||
- [x] tags
|
||||
- [ ] multi-language support
|
||||
- [x] be blazingly fast
|
||||
- [x] 100+ MiB binary size
|
||||
|
||||
|
@ -40,11 +39,6 @@ title = "bingus-blog" # title of the website
|
|||
description = "blazingly fast markdown blog software written in rust memory safe" # description of the website
|
||||
raw_access = true # allow users to see the raw markdown of a post
|
||||
|
||||
[rss]
|
||||
enable = false # serve an rss field under /feed.xml
|
||||
# this may be a bit resource intensive
|
||||
link = "https://..." # public url of the blog, required if rss is enabled
|
||||
|
||||
[dirs]
|
||||
posts = "posts" # where posts are stored
|
||||
media = "media" # directory served under /media/
|
||||
|
|
|
@ -6,7 +6,6 @@ use color_eyre::eyre::{bail, Context, Result};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tracing::{error, info};
|
||||
use url::Url;
|
||||
|
||||
use crate::ranged_i128_visitor::RangedI128Visitor;
|
||||
|
||||
|
@ -51,12 +50,6 @@ pub struct DirsConfig {
|
|||
pub media: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RssConfig {
|
||||
pub enable: bool,
|
||||
pub link: Url,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
|
@ -64,7 +57,6 @@ pub struct Config {
|
|||
pub description: String,
|
||||
pub raw_access: bool,
|
||||
pub num_posts: usize,
|
||||
pub rss: RssConfig,
|
||||
pub dirs: DirsConfig,
|
||||
pub http: HttpConfig,
|
||||
pub render: RenderConfig,
|
||||
|
@ -78,16 +70,6 @@ impl Default for Config {
|
|||
description: "blazingly fast markdown blog software written in rust memory safe".into(),
|
||||
raw_access: true,
|
||||
num_posts: 5,
|
||||
// i have a love-hate relationship with serde
|
||||
// it was engimatic at first, but then i started actually using it
|
||||
// writing my own serialize and deserialize implementations.. spending
|
||||
// a lot of time in the docs trying to understand each and every option..
|
||||
// now with this knowledge i can do stuff like this! (see rss field)
|
||||
// and i'm proud to say that it still makes 0 sense.
|
||||
rss: RssConfig {
|
||||
enable: false,
|
||||
link: Url::parse("http://example.com").unwrap(),
|
||||
},
|
||||
dirs: Default::default(),
|
||||
http: Default::default(),
|
||||
render: Default::default(),
|
||||
|
|
|
@ -53,10 +53,6 @@ pub type AppResult<T> = Result<T, AppError>;
|
|||
pub enum AppError {
|
||||
#[error("failed to fetch post: {0}")]
|
||||
PostError(#[from] PostError),
|
||||
#[error("rss is disabled")]
|
||||
RssDisabled,
|
||||
#[error(transparent)]
|
||||
UrlError(#[from] url::ParseError),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for AppError {
|
||||
|
@ -79,8 +75,7 @@ impl IntoResponse for AppError {
|
|||
PostError::NotFound(_) => StatusCode::NOT_FOUND,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
AppError::RssDisabled => StatusCode::FORBIDDEN,
|
||||
AppError::UrlError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
//_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
(
|
||||
status_code,
|
||||
|
|
96
src/main.rs
96
src/main.rs
|
@ -18,13 +18,11 @@ use std::time::Duration;
|
|||
|
||||
use askama_axum::Template;
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::http::{header, Request};
|
||||
use axum::http::Request;
|
||||
use axum::response::{IntoResponse, Redirect, Response};
|
||||
use axum::routing::{get, Router};
|
||||
use axum::Json;
|
||||
use color_eyre::eyre::{self, Context};
|
||||
use error::AppError;
|
||||
use rss::{Category, ChannelBuilder, ItemBuilder};
|
||||
use serde::Deserialize;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpListener;
|
||||
|
@ -59,8 +57,8 @@ struct IndexTemplate {
|
|||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "post.html")]
|
||||
struct PostTemplate {
|
||||
#[template(path = "view_post.html")]
|
||||
struct ViewPostTemplate {
|
||||
meta: PostMetadata,
|
||||
rendered: String,
|
||||
rendered_in: RenderStats,
|
||||
|
@ -80,7 +78,7 @@ async fn index(
|
|||
) -> AppResult<IndexTemplate> {
|
||||
let posts = state
|
||||
.posts
|
||||
.get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_ref())
|
||||
.get_max_n_posts_with_optional_tag_sorted(query.num_posts, query.tag.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(IndexTemplate {
|
||||
|
@ -96,73 +94,12 @@ async fn all_posts(
|
|||
) -> AppResult<Json<Vec<PostMetadata>>> {
|
||||
let posts = state
|
||||
.posts
|
||||
.get_max_n_post_metadata_with_optional_tag_sorted(query.num_posts, query.tag.as_ref())
|
||||
.get_max_n_posts_with_optional_tag_sorted(query.num_posts, query.tag.as_ref())
|
||||
.await?;
|
||||
|
||||
Ok(Json(posts))
|
||||
}
|
||||
|
||||
async fn rss(
|
||||
State(state): State<ArcState>,
|
||||
Query(query): Query<QueryParams>,
|
||||
) -> AppResult<Response> {
|
||||
if !state.config.rss.enable {
|
||||
return Err(AppError::RssDisabled);
|
||||
}
|
||||
|
||||
let posts = state
|
||||
.posts
|
||||
.get_all_posts_filtered(|metadata, _| {
|
||||
!query
|
||||
.tag
|
||||
.as_ref()
|
||||
.is_some_and(|tag| !metadata.tags.contains(tag))
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut channel = ChannelBuilder::default();
|
||||
channel
|
||||
.title(&state.config.title)
|
||||
.link(state.config.rss.link.to_string())
|
||||
.description(&state.config.description);
|
||||
//TODO: .language()
|
||||
|
||||
for (metadata, content, _) in posts {
|
||||
channel.item(
|
||||
ItemBuilder::default()
|
||||
.title(metadata.title)
|
||||
.description(metadata.description)
|
||||
.author(metadata.author)
|
||||
.categories(
|
||||
metadata
|
||||
.tags
|
||||
.into_iter()
|
||||
.map(|tag| Category {
|
||||
name: tag,
|
||||
domain: None,
|
||||
})
|
||||
.collect::<Vec<Category>>(),
|
||||
)
|
||||
.pub_date(metadata.created_at.map(|date| date.to_rfc2822()))
|
||||
.content(content)
|
||||
.link(
|
||||
state
|
||||
.config
|
||||
.rss
|
||||
.link
|
||||
.join(&format!("/posts/{}", metadata.name))?
|
||||
.to_string(),
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
let body = channel.build().to_string();
|
||||
drop(channel);
|
||||
|
||||
Ok(([(header::CONTENT_TYPE, "text/xml")], body).into_response())
|
||||
}
|
||||
|
||||
async fn post(State(state): State<ArcState>, Path(name): Path<String>) -> AppResult<Response> {
|
||||
if name.ends_with(".md") && state.config.raw_access {
|
||||
let mut file = tokio::fs::OpenOptions::new()
|
||||
|
@ -176,7 +113,7 @@ async fn post(State(state): State<ArcState>, Path(name): Path<String>) -> AppRes
|
|||
Ok(([("content-type", "text/plain")], buf).into_response())
|
||||
} else {
|
||||
let post = state.posts.get_post(&name).await?;
|
||||
let page = PostTemplate {
|
||||
let page = ViewPostTemplate {
|
||||
meta: post.0,
|
||||
rendered: post.1,
|
||||
rendered_in: post.2,
|
||||
|
@ -313,7 +250,6 @@ async fn main() -> eyre::Result<()> {
|
|||
)
|
||||
.route("/posts/:name", get(post))
|
||||
.route("/posts", get(all_posts))
|
||||
.route("/feed.xml", get(rss))
|
||||
.nest_service("/static", ServeDir::new("static").precompressed_gzip())
|
||||
.nest_service("/media", ServeDir::new("media"))
|
||||
.layer(
|
||||
|
@ -380,24 +316,30 @@ async fn main() -> eyre::Result<()> {
|
|||
}
|
||||
|
||||
// write cache to file
|
||||
let config = &state.config;
|
||||
let posts = &state.posts;
|
||||
let AppState { config, posts } = Arc::<AppState>::try_unwrap(state).unwrap_or_else(|state| {
|
||||
warn!("couldn't unwrap Arc over AppState, more than one strong reference exists for Arc. cloning instead");
|
||||
// TODO: only do this when persistence is enabled
|
||||
// first check config from inside the arc, then try unwrap
|
||||
AppState::clone(state.as_ref())
|
||||
});
|
||||
if config.cache.enable
|
||||
&& config.cache.persistence
|
||||
&& let Some(cache) = posts.cache()
|
||||
&& let Some(cache) = posts.into_cache()
|
||||
{
|
||||
let path = &config.cache.file;
|
||||
let serialized = bitcode::serialize(cache).context("failed to serialize cache")?;
|
||||
let serialized = bitcode::serialize(&cache).context("failed to serialize cache")?;
|
||||
let mut cache_file = tokio::fs::File::create(path)
|
||||
.await
|
||||
.with_context(|| format!("failed to open cache at {}", path.display()))?;
|
||||
let compression_level = config.cache.compression_level;
|
||||
if config.cache.compress {
|
||||
let cache_file = cache_file.into_std().await;
|
||||
tokio::task::spawn_blocking(move || {
|
||||
std::io::Write::write_all(
|
||||
&mut zstd::stream::write::Encoder::new(cache_file, compression_level)?
|
||||
.auto_finish(),
|
||||
&mut zstd::stream::write::Encoder::new(
|
||||
cache_file,
|
||||
config.cache.compression_level,
|
||||
)?
|
||||
.auto_finish(),
|
||||
&serialized,
|
||||
)
|
||||
})
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::config::RenderConfig;
|
|||
use crate::post::PostMetadata;
|
||||
|
||||
/// do not persist cache if this version number changed
|
||||
pub const CACHE_VERSION: u16 = 2;
|
||||
pub const CACHE_VERSION: u16 = 1;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct CacheValue {
|
||||
|
@ -111,7 +111,6 @@ impl Cache {
|
|||
let old_size = self.0.len();
|
||||
let mut i = 0;
|
||||
|
||||
// TODO: multithread
|
||||
self.0
|
||||
.retain_async(|k, v| {
|
||||
if get_mtime(k).is_some_and(|mtime| mtime == v.mtime) {
|
||||
|
|
|
@ -5,6 +5,7 @@ use std::io;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
|
||||
use askama::Template;
|
||||
use chrono::{DateTime, Utc};
|
||||
use fronma::parser::{parse, ParsedData};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -62,6 +63,14 @@ pub struct PostMetadata {
|
|||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
use crate::filters;
|
||||
#[derive(Template)]
|
||||
#[template(path = "post.html")]
|
||||
struct Post<'a> {
|
||||
pub meta: &'a PostMetadata,
|
||||
pub rendered_markdown: String,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub enum RenderStats {
|
||||
Cached(Duration),
|
||||
|
@ -118,7 +127,12 @@ impl PostManager {
|
|||
let parsing = parsing_start.elapsed();
|
||||
|
||||
let before_render = Instant::now();
|
||||
let post = render(body, &self.config);
|
||||
let rendered_markdown = render(body, &self.config);
|
||||
let post = Post {
|
||||
meta: &metadata,
|
||||
rendered_markdown,
|
||||
}
|
||||
.render()?;
|
||||
let rendering = before_render.elapsed();
|
||||
|
||||
if let Some(cache) = self.cache.as_ref() {
|
||||
|
@ -137,7 +151,7 @@ impl PostManager {
|
|||
Ok((metadata, post, (parsing, rendering)))
|
||||
}
|
||||
|
||||
pub async fn get_all_post_metadata_filtered(
|
||||
pub async fn list_posts(
|
||||
&self,
|
||||
filter: impl Fn(&PostMetadata) -> bool,
|
||||
) -> Result<Vec<PostMetadata>, PostError> {
|
||||
|
@ -186,53 +200,21 @@ impl PostManager {
|
|||
Ok(posts)
|
||||
}
|
||||
|
||||
pub async fn get_all_posts_filtered(
|
||||
&self,
|
||||
filter: impl Fn(&PostMetadata, &str) -> bool,
|
||||
) -> Result<Vec<(PostMetadata, String, RenderStats)>, PostError> {
|
||||
let mut posts = Vec::new();
|
||||
|
||||
let mut read_dir = fs::read_dir(&self.dir).await?;
|
||||
while let Some(entry) = read_dir.next_entry().await? {
|
||||
let path = entry.path();
|
||||
let stat = fs::metadata(&path).await?;
|
||||
|
||||
if stat.is_file() && path.extension().is_some_and(|ext| ext == "md") {
|
||||
let name = path
|
||||
.clone()
|
||||
.file_stem()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let post = self.get_post(&name).await?;
|
||||
if filter(&post.0, &post.1) {
|
||||
posts.push(post);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(posts)
|
||||
}
|
||||
|
||||
pub async fn get_max_n_post_metadata_with_optional_tag_sorted(
|
||||
pub async fn get_max_n_posts_with_optional_tag_sorted(
|
||||
&self,
|
||||
n: Option<usize>,
|
||||
tag: Option<&String>,
|
||||
) -> Result<Vec<PostMetadata>, PostError> {
|
||||
let mut posts = self
|
||||
.get_all_post_metadata_filtered(|metadata| {
|
||||
!tag.is_some_and(|tag| !metadata.tags.contains(tag))
|
||||
})
|
||||
.list_posts(|metadata| !tag.is_some_and(|tag| !metadata.tags.contains(tag)))
|
||||
.await?;
|
||||
// we still want some semblance of order if created_at is None so sort by mtime as well
|
||||
posts.sort_unstable_by_key(|metadata| metadata.modified_at.unwrap_or_default());
|
||||
posts.sort_by_key(|metadata| metadata.created_at.unwrap_or_default());
|
||||
posts.reverse();
|
||||
posts.sort_unstable_by_key(|metadata| metadata.created_at.unwrap_or_default());
|
||||
|
||||
if let Some(n) = n {
|
||||
posts.truncate(n);
|
||||
posts = Vec::from(&posts[posts.len().saturating_sub(n)..]);
|
||||
}
|
||||
|
||||
posts.reverse();
|
||||
Ok(posts)
|
||||
}
|
||||
|
||||
|
@ -275,8 +257,8 @@ impl PostManager {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn cache(&self) -> Option<&Cache> {
|
||||
self.cache.as_ref()
|
||||
pub fn into_cache(self) -> Option<Cache> {
|
||||
self.cache
|
||||
}
|
||||
|
||||
pub async fn cleanup(&self) {
|
||||
|
|
|
@ -1,52 +1,16 @@
|
|||
{%- import "macros.askama" as macros -%}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="{{ meta.title }}" />
|
||||
<meta property="og:title" content="{{ meta.title }}" />
|
||||
<meta property="og:description" content="{{ meta.description }}" />
|
||||
{% match meta.icon %} {% when Some with (url) %}
|
||||
<meta property="og:image" content="{{ url }}" />
|
||||
<link rel="shortcut icon" href="{{ url }}" />
|
||||
{% when None %} {% endmatch %}
|
||||
<title>{{ meta.title }}</title>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
<link rel="stylesheet" href="/static/post.css" />
|
||||
</head>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1 class="post-title">
|
||||
{{ meta.title }}
|
||||
<span class="post-author">- by {{ meta.author }}</span>
|
||||
</h1>
|
||||
<p class="post-desc">{{ meta.description }}</p>
|
||||
<div class="" post>
|
||||
<!-- prettier-ignore -->
|
||||
<div>
|
||||
{% call macros::table(meta) %}
|
||||
</div>
|
||||
<a href="/posts/{{ meta.name }}">link</a><br />
|
||||
<a href="/">back to home</a>
|
||||
</div>
|
||||
<hr />
|
||||
{{ rendered|escape("none") }}
|
||||
</main>
|
||||
<!-- prettier-ignore -->
|
||||
<footer>
|
||||
{% match rendered_in %}
|
||||
{% when RenderStats::ParsedAndRendered(total, parsing, rendering) %}
|
||||
<span class="tooltipped" title="parsing took {{ parsing|duration }}">parsed</span> and
|
||||
<span class="tooltipped" title="rendering took {{ rendering|duration }}">rendered</span> in {{ total|duration }}
|
||||
{% when RenderStats::Cached(total) %}
|
||||
retrieved from cache in {{ total|duration }}
|
||||
{% endmatch %}
|
||||
{% if markdown_access %}
|
||||
- <a href="/posts/{{ meta.name }}.md">view raw</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
<h1 class="post-title">
|
||||
{{ meta.title }}
|
||||
<span class="post-author">- by {{ meta.author }}</span>
|
||||
</h1>
|
||||
<p class="post-desc">{{ meta.description }}</p>
|
||||
<p>
|
||||
<!-- prettier-ignore -->
|
||||
<div>
|
||||
{% call macros::table(meta) %}
|
||||
</div>
|
||||
<a href="/posts/{{ meta.name }}">link</a><br />
|
||||
<a href="/">back to home</a>
|
||||
</p>
|
||||
<hr />
|
||||
{{ rendered_markdown|escape("none") }}
|
||||
|
|
38
templates/view_post.html
Normal file
38
templates/view_post.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
<meta name="description" content="{{ meta.title }}" />
|
||||
<meta property="og:title" content="{{ meta.title }}" />
|
||||
<meta property="og:description" content="{{ meta.description }}" />
|
||||
{% match meta.icon %} {% when Some with (url) %}
|
||||
<meta property="og:image" content="{{ url }}" />
|
||||
<link rel="shortcut icon" href="{{ url }}" />
|
||||
{% when None %} {% endmatch %}
|
||||
<title>{{ meta.title }}</title>
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
<link rel="stylesheet" href="/static/post.css" />
|
||||
</head>
|
||||
</head>
|
||||
<body>
|
||||
<main>{{ rendered|escape("none") }}</main>
|
||||
<!-- prettier-ignore -->
|
||||
<footer>
|
||||
{% match rendered_in %}
|
||||
{% when RenderStats::ParsedAndRendered(total, parsing, rendering) %}
|
||||
<span class="tooltipped" title="parsing took {{ parsing|duration }}">parsed</span> and
|
||||
<span class="tooltipped" title="rendering took {{ rendering|duration }}">rendered</span> in {{ total|duration }}
|
||||
{% when RenderStats::Cached(total) %}
|
||||
retrieved from cache in {{ total|duration }}
|
||||
{% endmatch %}
|
||||
{% if markdown_access %}
|
||||
- <a href="/posts/{{ meta.name }}.md">view raw</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue